diff --git a/.github/workflows/ruby-ci.yml b/.github/workflows/ruby-ci.yml new file mode 100644 index 00000000000..6a208f2f7f5 --- /dev/null +++ b/.github/workflows/ruby-ci.yml @@ -0,0 +1,44 @@ +name: CI + +on: + pull_request: + branches: + - '**' + push: + branches: + - master + +jobs: + build: + name: Ruby ${{ matrix.version }} ${{ matrix.gemfile }} + runs-on: ubuntu-latest + env: + BUNDLE_GEMFILE: ${{ matrix.gemfile }} + strategy: + matrix: + version: + - 3.1 + gemfile: + - gemfiles/Gemfile.rails50 + - gemfiles/Gemfile.rails51 + - gemfiles/Gemfile.rails52 + - gemfiles/Gemfile.rails60 + - gemfiles/Gemfile.rails_master + exclude: + - version: 2.6 + gemfile: gemfiles/Gemfile.rails_master + - version: 2.5 + gemfile: gemfiles/Gemfile.rails_master + steps: + - uses: actions/checkout@v2 + + - name: Set up Ruby ${{ matrix.version }} + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.version }} + bundler-cache: true + + - name: Test + run: bundle exec rake test + - name: Linter + run: bundle exec rubocop 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 80efd64708b..d1272af4d1a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -15,4 +15,158 @@ AllCops: - "lib/active_merchant/billing/gateways/paypal_express.rb" - "vendor/**/*" ExtraDetails: false - TargetRubyVersion: 2.3 + TargetRubyVersion: 3.1 + +# Active Merchant gateways are not amenable to length restrictions +Metrics/ClassLength: + Enabled: false + +Metrics/ModuleLength: + Enabled: false + +Layout/ParameterAlignment: + EnforcedStyle: with_fixed_indentation + +Layout/DotPosition: + EnforcedStyle: trailing + +Layout/CaseIndentation: + EnforcedStyle: end + +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 +Gemspec/RequireMFA: # new in 1.23 + Enabled: false +Layout/LineEndStringConcatenationIndentation: # new in 1.18 + Enabled: true +Lint/AmbiguousOperatorPrecedence: # new in 1.21 + Enabled: true +Lint/AmbiguousRange: # new in 1.19 + Enabled: true +Lint/EmptyInPattern: # new in 1.16 + Enabled: true +Lint/IncompatibleIoSelectWithFiberScheduler: # new in 1.21 + Enabled: true +Lint/RequireRelativeSelfPath: # new in 1.22 + Enabled: true +Lint/UselessRuby2Keywords: # new in 1.23 + Enabled: true +Naming/BlockForwarding: # new in 1.24 + Enabled: false +Security/IoMethods: # new in 1.22 + Enabled: true +Style/FileRead: # new in 1.24 + Enabled: true +Style/FileWrite: # new in 1.24 + Enabled: true +Style/InPatternThen: # new in 1.16 + Enabled: true +Style/MapToHash: # new in 1.24 + Enabled: false +Style/MultilineInPatternThen: # new in 1.16 + Enabled: true +Style/NestedFileDirname: # new in 1.26 + Enabled: true +Style/NumberedParameters: # new in 1.22 + Enabled: true +Style/NumberedParametersLimit: # new in 1.22 + Enabled: true +Style/OpenStructUse: # new in 1.23 + Enabled: false +Style/QuotedSymbols: # new in 1.16 + Enabled: true +Style/RedundantSelfAssignmentBranch: # new in 1.19 + Enabled: true +Style/SelectByRegexp: # new in 1.22 + Enabled: false diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index f00fcb20eb8..a9338fe8526 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,873 +1,134 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2018-06-26 11:27:33 -0400 using RuboCop version 0.57.2. +# on 2018-11-20 16:45:49 -0500 using RuboCop version 0.60.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: Include, TreatCommentsAsGroupSeparators. -# Include: **/*.gemspec -Gemspec/OrderedDependencies: - Exclude: - - 'activemerchant.gemspec' - -# Offense count: 139 +# Offense count: 1828 # Cop supports --auto-correct. # Configuration parameters: EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. # SupportedHashRocketStyles: key, separator, table # SupportedColonStyles: key, separator, table # SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit -Layout/AlignHash: - Enabled: false - -# Offense count: 275 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, IndentationWidth. -# SupportedStyles: with_first_parameter, with_fixed_indentation -Layout/AlignParameters: - Enabled: false - -# Offense count: 113 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, IndentOneStep, IndentationWidth. -# SupportedStyles: case, end -Layout/CaseIndentation: - Enabled: false - -# Offense count: 426 -# Cop supports --auto-correct. -Layout/ClosingHeredocIndentation: - Enabled: false - -# Offense count: 35 -# Cop supports --auto-correct. -Layout/ClosingParenthesisIndentation: - Enabled: false - -# Offense count: 20 -# Cop supports --auto-correct. -Layout/CommentIndentation: - Exclude: - - 'lib/active_merchant/billing/gateways/firstdata_e4.rb' - - 'lib/active_merchant/billing/gateways/ogone.rb' - - 'lib/active_merchant/billing/gateways/opp.rb' - - 'lib/active_merchant/billing/gateways/payflow_express.rb' - - 'test/remote/gateways/remote_itransact_test.rb' - - 'test/remote/gateways/remote_opp_test.rb' - - 'test/remote/gateways/remote_paypal_test.rb' - - 'test/remote/gateways/remote_world_net_test.rb' - - 'test/unit/gateways/eway_managed_test.rb' - - 'test/unit/gateways/opp_test.rb' - -# Offense count: 4 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyleAlignWith, AutoCorrect, Severity. -# SupportedStylesAlignWith: start_of_line, def -Layout/DefEndAlignment: - Exclude: - - 'test/remote/gateways/remote_moneris_test.rb' - - 'test/remote/gateways/remote_pay_conex_test.rb' - - 'test/remote/gateways/remote_payflow_uk_test.rb' - - 'test/unit/gateways/optimal_payment_test.rb' - -# Offense count: 513 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: leading, trailing -Layout/DotPosition: - Enabled: false - -# Offense count: 24 -# Cop supports --auto-correct. -Layout/ElseAlignment: - Exclude: - - 'lib/active_merchant/billing/gateway.rb' - - 'lib/active_merchant/billing/gateways/authorize_net.rb' - - 'lib/active_merchant/billing/gateways/balanced.rb' - - 'lib/active_merchant/billing/gateways/braintree_blue.rb' - - 'lib/active_merchant/billing/gateways/card_stream.rb' - - 'lib/active_merchant/billing/gateways/clearhaus.rb' - - 'lib/active_merchant/billing/gateways/firstdata_e4.rb' - - 'lib/active_merchant/billing/gateways/moneris.rb' - - 'lib/active_merchant/billing/gateways/moneris_us.rb' - - 'lib/active_merchant/billing/gateways/pac_net_raven.rb' - - 'lib/active_merchant/billing/gateways/payflow.rb' - - 'lib/active_merchant/billing/gateways/trust_commerce.rb' - - 'lib/active_merchant/billing/response.rb' - -# Offense count: 11 -# Cop supports --auto-correct. -Layout/EmptyLineAfterMagicComment: - Exclude: - - 'lib/active_merchant/billing/gateways/ogone.rb' - - 'lib/active_merchant/billing/gateways/redsys.rb' - - 'test/remote/gateways/remote_banwire_test.rb' - - 'test/remote/gateways/remote_barclays_epdq_extra_plus_test.rb' - - 'test/remote/gateways/remote_finansbank_test.rb' - - 'test/remote/gateways/remote_ogone_test.rb' - - 'test/remote/gateways/remote_webpay_test.rb' - - 'test/remote/gateways/remote_wirecard_test.rb' - - 'test/unit/gateways/finansbank_test.rb' - - 'test/unit/gateways/usa_epay_advanced_test.rb' - - 'test/unit/gateways/wirecard_test.rb' - -# Offense count: 66 -# Cop supports --auto-correct. -# Configuration parameters: AllowAdjacentOneLineDefs, NumberOfEmptyLines. -Layout/EmptyLineBetweenDefs: - Enabled: false - -# Offense count: 97 -# Cop supports --auto-correct. -Layout/EmptyLines: - Enabled: false - -# Offense count: 49 -# Cop supports --auto-correct. -Layout/EmptyLinesAroundAccessModifier: - Enabled: false - -# Offense count: 2 -# Cop supports --auto-correct. -Layout/EmptyLinesAroundArguments: - Exclude: - - 'test/unit/gateways/cardknox_test.rb' - - 'test/unit/gateways/usa_epay_transaction_test.rb' - -# Offense count: 15 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: empty_lines, no_empty_lines -Layout/EmptyLinesAroundBlockBody: - Exclude: - - 'lib/active_merchant/billing/gateways/cc5.rb' - - 'lib/active_merchant/billing/gateways/data_cash.rb' - - 'lib/active_merchant/billing/gateways/efsnet.rb' - - 'lib/active_merchant/billing/gateways/payflow_express.rb' - - 'lib/active_merchant/billing/gateways/telr.rb' - - 'test/remote/gateways/remote_paystation_test.rb' - - 'test/remote/gateways/remote_realex_test.rb' - - 'test/unit/gateways/cardknox_test.rb' - - 'test/unit/gateways/usa_epay_transaction_test.rb' - -# Offense count: 165 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines, beginning_only, ending_only -Layout/EmptyLinesAroundClassBody: - Enabled: false - -# Offense count: 10 -# Cop supports --auto-correct. -Layout/EmptyLinesAroundExceptionHandlingKeywords: - Exclude: - - 'lib/active_merchant/billing/gateways/barclaycard_smartpay.rb' - - 'lib/active_merchant/billing/gateways/cecabank.rb' - - 'lib/active_merchant/billing/gateways/exact.rb' - - 'lib/active_merchant/billing/gateways/pay_conex.rb' - - 'lib/active_merchant/billing/gateways/pin.rb' - - 'lib/active_merchant/billing/gateways/wepay.rb' - - 'lib/active_merchant/billing/gateways/worldpay.rb' - - 'lib/active_merchant/billing/gateways/worldpay_online_payments.rb' - - 'lib/active_merchant/connection.rb' - - 'test/unit/gateways/worldpay_test.rb' - -# Offense count: 64 -# Cop supports --auto-correct. -Layout/EmptyLinesAroundMethodBody: - Enabled: false - -# Offense count: 18 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines -Layout/EmptyLinesAroundModuleBody: - Exclude: - - 'lib/active_merchant/billing/credit_card_formatting.rb' - - 'lib/active_merchant/billing/gateways/beanstream/beanstream_core.rb' - - 'lib/active_merchant/billing/gateways/efsnet.rb' - - 'lib/active_merchant/billing/gateways/ezic.rb' - - 'lib/active_merchant/billing/gateways/linkpoint.rb' - - 'lib/active_merchant/billing/gateways/mastercard.rb' - - 'lib/active_merchant/billing/gateways/moneris.rb' - - 'lib/active_merchant/billing/gateways/moneris_us.rb' - - 'lib/active_merchant/billing/gateways/payment_express.rb' - - 'lib/active_merchant/billing/gateways/quickpay/quickpay_common.rb' - - 'lib/active_merchant/billing/gateways/quickpay/quickpay_v10.rb' - - 'lib/active_merchant/billing/gateways/sage_pay.rb' - - 'lib/active_merchant/billing/gateways/trans_first.rb' - - 'lib/active_merchant/billing/gateways/usa_epay_transaction.rb' - - 'lib/active_merchant/posts_data.rb' - -# Offense count: 40 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyleAlignWith, AutoCorrect, Severity. -# SupportedStylesAlignWith: keyword, variable, start_of_line -Layout/EndAlignment: - Enabled: false - -# Offense count: 191 -# Cop supports --auto-correct. -# Configuration parameters: AllowForAlignment, ForceEqualSignAlignment. -Layout/ExtraSpacing: - Enabled: false - -# Offense count: 122 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, IndentationWidth. -# SupportedStyles: consistent, consistent_relative_to_receiver, special_for_inner_method_call, special_for_inner_method_call_in_parentheses -Layout/FirstParameterIndentation: - Enabled: false - -# Offense count: 13 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, IndentationWidth. -# SupportedStyles: special_inside_parentheses, consistent, align_brackets -Layout/IndentArray: - Exclude: - - 'lib/active_merchant/billing/gateways/firstdata_e4.rb' - - 'test/remote/gateways/remote_linkpoint_test.rb' - - 'test/remote/gateways/remote_payflow_express_test.rb' - - 'test/unit/gateways/cyber_source_test.rb' - - 'test/unit/gateways/paypal_express_test.rb' - -# Offense count: 253 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, IndentationWidth. -# SupportedStyles: special_inside_parentheses, consistent, align_braces -Layout/IndentHash: - Enabled: false - -# Offense count: 387 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: auto_detection, squiggly, active_support, powerpack, unindent -Layout/IndentHeredoc: - Enabled: false - -# Offense count: 97 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: normal, rails -Layout/IndentationConsistency: - Enabled: false - -# Offense count: 197 -# Cop supports --auto-correct. -# Configuration parameters: Width, IgnoredPatterns. -Layout/IndentationWidth: - Enabled: false - -# Offense count: 2 -# Cop supports --auto-correct. -Layout/LeadingBlankLines: - Exclude: - - 'lib/active_merchant/billing/gateways/quickpay/quickpay_common.rb' - - 'test/unit/gateways/quickpay_test.rb' - -# Offense count: 68 -# Cop supports --auto-correct. -Layout/LeadingCommentSpace: - Enabled: false - -# Offense count: 6 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: symmetrical, new_line, same_line -Layout/MultilineArrayBraceLayout: - Exclude: - - 'lib/active_merchant/billing/gateways/optimal_payment.rb' - - 'test/remote/gateways/remote_linkpoint_test.rb' - - 'test/remote/gateways/remote_orbital_test.rb' - - 'test/remote/gateways/remote_payflow_express_test.rb' - -# Offense count: 42 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: symmetrical, new_line, same_line -Layout/MultilineHashBraceLayout: - Enabled: false - -# Offense count: 234 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: symmetrical, new_line, same_line -Layout/MultilineMethodCallBraceLayout: - Enabled: false - -# Offense count: 34 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, IndentationWidth. -# SupportedStyles: aligned, indented, indented_relative_to_receiver -Layout/MultilineMethodCallIndentation: - Exclude: - - 'lib/active_merchant/billing/gateways/balanced.rb' - - 'lib/active_merchant/billing/gateways/ogone.rb' - - 'lib/active_merchant/billing/gateways/realex.rb' - - 'lib/active_merchant/billing/gateways/sage.rb' - - 'lib/active_merchant/billing/gateways/telr.rb' - - 'test/unit/connection_test.rb' - - 'test/unit/gateways/braintree_blue_test.rb' - - 'test/unit/gateways/jetpay_v2_test.rb' - - 'test/unit/gateways/wirecard_test.rb' - - 'test/unit/network_connection_retries_test.rb' - -# Offense count: 35 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, IndentationWidth. -# SupportedStyles: aligned, indented -Layout/MultilineOperationIndentation: - Exclude: - - 'lib/active_merchant/billing/credit_card_methods.rb' - - 'lib/active_merchant/billing/gateways/commercegate.rb' - - 'lib/active_merchant/billing/gateways/iridium.rb' - - 'lib/active_merchant/billing/gateways/moneris.rb' - - 'lib/active_merchant/billing/gateways/moneris_us.rb' - - 'lib/active_merchant/billing/gateways/orbital.rb' - - 'lib/active_merchant/billing/gateways/redsys.rb' - - 'test/unit/gateways/barclays_epdq_extra_plus_test.rb' - - 'test/unit/gateways/braintree_blue_test.rb' - - 'test/unit/gateways/ogone_test.rb' - - 'test/unit/gateways/skip_jack_test.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -Layout/RescueEnsureAlignment: - Exclude: - - 'lib/active_merchant/billing/gateways/checkout_v2.rb' - - 'lib/active_merchant/billing/gateways/mundipagg.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -Layout/SpaceAfterColon: - Exclude: - - 'test/remote/gateways/remote_micropayment_test.rb' - - 'test/remote/gateways/remote_payu_latam_test.rb' - -# Offense count: 315 -# Cop supports --auto-correct. -Layout/SpaceAfterComma: - Enabled: false - -# Offense count: 3 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyleInsidePipes. -# SupportedStylesInsidePipes: space, no_space -Layout/SpaceAroundBlockParameters: - Exclude: - - 'lib/active_merchant/billing/gateways/payscout.rb' - - 'test/unit/country_test.rb' - -# Offense count: 638 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: space, no_space -Layout/SpaceAroundEqualsInParameterDefault: - Enabled: false - -# Offense count: 105 -# Cop supports --auto-correct. -Layout/SpaceAroundKeyword: - Enabled: false - -# Offense count: 802 -# Cop supports --auto-correct. -# Configuration parameters: AllowForAlignment. -Layout/SpaceAroundOperators: - Enabled: false - -# Offense count: 182 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces. -# SupportedStyles: space, no_space -# SupportedStylesForEmptyBraces: space, no_space -Layout/SpaceBeforeBlockBraces: - Enabled: false - -# Offense count: 39 -# Cop supports --auto-correct. -Layout/SpaceBeforeComma: - Exclude: - - 'lib/active_merchant/billing/gateways/cyber_source.rb' - - 'lib/active_merchant/billing/gateways/ideal/ideal_base.rb' - - 'lib/active_merchant/billing/gateways/instapay.rb' - - 'lib/active_merchant/billing/gateways/net_registry.rb' - - 'lib/active_merchant/billing/gateways/optimal_payment.rb' - - 'lib/active_merchant/billing/gateways/psigate.rb' - - 'lib/active_merchant/billing/gateways/sage_pay.rb' - - 'test/remote/gateways/remote_eway_test.rb' - - 'test/remote/gateways/remote_payflow_express_test.rb' - - 'test/remote/gateways/remote_paypal_express_test.rb' - - 'test/remote/gateways/remote_paypal_test.rb' - - 'test/unit/gateways/checkout_test.rb' - - 'test/unit/gateways/paypal_express_test.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -Layout/SpaceBeforeComment: - Exclude: - - 'test/remote/gateways/remote_usa_epay_advanced_test.rb' - -# Offense count: 16 -# Cop supports --auto-correct. -# Configuration parameters: AllowForAlignment. -Layout/SpaceBeforeFirstArg: - Exclude: - - 'lib/active_merchant/billing/gateways/checkout.rb' - - 'lib/active_merchant/billing/gateways/cyber_source.rb' - - 'lib/active_merchant/billing/gateways/s5.rb' - - 'test/remote/gateways/remote_braintree_orange_test.rb' - - 'test/remote/gateways/remote_payflow_test.rb' - - 'test/unit/gateways/bogus_test.rb' - - 'test/unit/gateways/instapay_test.rb' - - 'test/unit/gateways/quickpay_v10_test.rb' - - 'test/unit/gateways/quickpay_v4to7_test.rb' - -# Offense count: 118 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBrackets. -# SupportedStyles: space, no_space, compact -# SupportedStylesForEmptyBrackets: space, no_space -Layout/SpaceInsideArrayLiteralBrackets: - Enabled: false - -# Offense count: 12 -# Cop supports --auto-correct. -Layout/SpaceInsideArrayPercentLiteral: - Exclude: - - 'lib/active_merchant/billing/gateways/migs/migs_codes.rb' - -# Offense count: 345 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters. -# SupportedStyles: space, no_space -# SupportedStylesForEmptyBraces: space, no_space -Layout/SpaceInsideBlockBraces: - Enabled: false - -# Offense count: 1186 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces. -# SupportedStyles: space, no_space, compact -# SupportedStylesForEmptyBraces: space, no_space -Layout/SpaceInsideHashLiteralBraces: - Enabled: false - -# Offense count: 116 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: space, no_space -Layout/SpaceInsideParens: - Enabled: false - -# Offense count: 115 -# Cop supports --auto-correct. -Layout/SpaceInsidePercentLiteralDelimiters: - Enabled: false - -# Offense count: 35 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBrackets. -# SupportedStyles: space, no_space -# SupportedStylesForEmptyBrackets: space, no_space -Layout/SpaceInsideReferenceBrackets: - Exclude: - - 'lib/active_merchant/billing/gateways/blue_pay.rb' - - 'lib/active_merchant/billing/gateways/cams.rb' - - 'lib/active_merchant/billing/gateways/ideal/ideal_base.rb' - - 'lib/active_merchant/billing/gateways/metrics_global.rb' - - 'lib/active_merchant/billing/gateways/sage_pay.rb' - - 'lib/active_merchant/billing/gateways/secure_pay.rb' - - 'lib/active_merchant/billing/gateways/transact_pro.rb' - - 'test/remote/gateways/remote_ideal_rabobank_test.rb' - - 'test/remote/gateways/remote_wirecard_test.rb' - -# Offense count: 3 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: space, no_space -Layout/SpaceInsideStringInterpolation: - Exclude: - - 'lib/active_merchant/billing/gateways/garanti.rb' - - 'lib/active_merchant/billing/gateways/trust_commerce.rb' - - 'test/unit/gateways/worldpay_test.rb' - -# Offense count: 27 -# Cop supports --auto-correct. -# Configuration parameters: IndentationWidth. -Layout/Tab: - Exclude: - - 'lib/active_merchant/billing/gateways/braintree_blue.rb' - - 'lib/active_merchant/billing/gateways/efsnet.rb' - - 'lib/active_merchant/billing/gateways/pagarme.rb' - - 'lib/active_merchant/billing/gateways/trust_commerce.rb' - - 'test/remote/gateways/remote_orbital_test.rb' - -# Offense count: 57 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: final_newline, final_blank_line -Layout/TrailingBlankLines: - Enabled: false - -# Offense count: 357 -# Cop supports --auto-correct. -# Configuration parameters: AllowInHeredoc. -Layout/TrailingWhitespace: - Enabled: false - -# Offense count: 4 -Lint/AmbiguousBlockAssociation: - Exclude: - - 'test/test_helper.rb' - - 'test/unit/gateways/gateway_test.rb' - - 'test/unit/gateways/worldpay_test.rb' - -# Offense count: 31 -Lint/AmbiguousRegexpLiteral: +Layout/HashAlignment: Enabled: false -# Offense count: 148 +# Offense count: 150 # Configuration parameters: AllowSafeAssignment. Lint/AssignmentInCondition: Enabled: false -# Offense count: 8 -Lint/DuplicateMethods: - Exclude: - - 'test/remote/gateways/remote_litle_certification_test.rb' - - 'test/remote/gateways/remote_mercury_certification_test.rb' - - 'test/remote/gateways/remote_modern_payments_test.rb' - - 'test/remote/gateways/remote_netaxept_test.rb' - - 'test/remote/gateways/remote_verifi_test.rb' - -# Offense count: 1 -Lint/EmptyWhen: - Exclude: - - 'lib/active_merchant/billing/gateways/usa_epay_advanced.rb' - # Offense count: 3 Lint/FormatParameterMismatch: Exclude: - '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' -# Offense count: 1 -Lint/IneffectiveAccessModifier: - Exclude: - - 'lib/active_merchant/network_connection_retries.rb' - -# Offense count: 1 -Lint/NestedMethodDefinition: - Exclude: - - 'test/unit/gateways/blue_pay_test.rb' - -# Offense count: 4 -Lint/ParenthesesAsGroupedExpression: - Exclude: - - 'lib/active_merchant/billing/gateways/trans_first_transaction_express.rb' - - 'test/remote/gateways/remote_payment_express_test.rb' - - 'test/remote/gateways/remote_worldpay_test.rb' - # Offense count: 1 Lint/RescueException: Exclude: - 'lib/active_merchant/billing/gateways/quantum.rb' -# Offense count: 3 -# Cop supports --auto-correct. -Lint/ScriptPermission: - Exclude: - - 'lib/active_merchant/billing/avs_result.rb' - - 'lib/active_merchant/billing/gateways/skip_jack.rb' - - 'test/test_helper.rb' - -# Offense count: 11 -# Cop supports --auto-correct. -Lint/StringConversionInInterpolation: - Exclude: - - 'lib/active_merchant/billing/gateways/adyen.rb' - - 'lib/active_merchant/billing/gateways/dibs.rb' - - 'lib/active_merchant/billing/gateways/pay_junction.rb' - - 'lib/active_merchant/billing/gateways/skip_jack.rb' - - 'lib/active_merchant/billing/gateways/usa_epay_advanced.rb' - - 'test/remote/gateways/remote_authorize_net_cim_test.rb' - -# Offense count: 1 -Lint/UnderscorePrefixedVariableName: - Exclude: - - 'lib/active_merchant/billing/gateways/ogone.rb' - -# Offense count: 1453 -# Cop supports --auto-correct. -# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. -Lint/UnusedBlockArgument: - Enabled: false - -# Offense count: 283 +# Offense count: 284 # Cop supports --auto-correct. # Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods. Lint/UnusedMethodArgument: Enabled: false -# Offense count: 1 -# Configuration parameters: ContextCreatingMethods, MethodCreatingMethods. -Lint/UselessAccessModifier: - Exclude: - - 'lib/active_merchant/network_connection_retries.rb' - -# Offense count: 68 -Lint/UselessAssignment: - Enabled: false - -# Offense count: 1409 +# Offense count: 1418 Metrics/AbcSize: Max: 192 # Offense count: 26 # Configuration parameters: CountComments, ExcludedMethods. +# ExcludedMethods: refine Metrics/BlockLength: Max: 54 -# Offense count: 12 +# Offense count: 9 # Configuration parameters: CountBlocks. Metrics/BlockNesting: Max: 6 -# Offense count: 489 -# Configuration parameters: CountComments. -Metrics/ClassLength: - Max: 2135 - # Offense count: 175 Metrics/CyclomaticComplexity: Max: 36 -# Offense count: 1741 -# Configuration parameters: CountComments. +# Offense count: 1793 +# Configuration parameters: CountComments, ExcludedMethods. Metrics/MethodLength: Max: 163 - -# Offense count: 5 -# Configuration parameters: CountComments. -Metrics/ModuleLength: - Max: 383 + IgnoredMethods: + - 'setup' # Offense count: 2 # Configuration parameters: CountKeywordArgs. Metrics/ParameterLists: Max: 6 -# Offense count: 126 -Metrics/PerceivedComplexity: - Max: 32 - -# Offense count: 6 -Naming/AccessorMethodName: - Exclude: - - 'lib/active_merchant/billing/gateways/usa_epay_advanced.rb' - - 'test/remote/gateways/remote_authorize_net_cim_test.rb' - - 'test/unit/gateways/authorize_net_cim_test.rb' - -# Offense count: 1 -Naming/ConstantName: - Exclude: - - 'test/test_helper.rb' - -# Offense count: 45 -# Configuration parameters: EnforcedStyle. -# SupportedStyles: lowercase, uppercase -Naming/HeredocDelimiterCase: - Exclude: - - 'test/unit/gateways/authorize_net_test.rb' - - 'test/unit/gateways/card_stream_test.rb' - - 'test/unit/gateways/hps_test.rb' - - 'test/unit/gateways/litle_test.rb' - - 'test/unit/gateways/moneris_test.rb' - -# Offense count: 85 -# Configuration parameters: Blacklist. -# Blacklist: (?-mix:(^|\s)(EO[A-Z]{1}|END)(\s|$)) -Naming/HeredocDelimiterNaming: - Enabled: false - -# Offense count: 1 -Naming/MemoizedInstanceVariableName: - Exclude: - - 'lib/active_merchant/billing/compatibility.rb' - -# Offense count: 15 -# Configuration parameters: EnforcedStyle. -# SupportedStyles: snake_case, camelCase -Naming/MethodName: - Exclude: - - 'lib/active_merchant/billing/gateways/card_connect.rb' - - 'lib/active_merchant/billing/gateways/latitude19.rb' - - 'lib/active_merchant/billing/gateways/qbms.rb' - - 'test/remote/gateways/remote_card_connect_test.rb' - - 'test/remote/gateways/remote_card_stream_test.rb' - - 'test/remote/gateways/remote_fat_zebra_test.rb' - - 'test/remote/gateways/remote_sage_pay_test.rb' - - 'test/unit/gateways/sage_pay_test.rb' - -# Offense count: 5 -# Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist, MethodDefinitionMacros. -# NamePrefix: is_, has_, have_ -# NamePrefixBlacklist: is_, has_, have_ -# NameWhitelist: is_a? -# MethodDefinitionMacros: define_method, define_singleton_method -Naming/PredicateName: - Exclude: - - 'spec/**/*' - - 'lib/active_merchant/billing/gateways/authorize_net.rb' - - 'lib/active_merchant/billing/gateways/payeezy.rb' - - 'lib/active_merchant/billing/gateways/paymill.rb' - - 'lib/active_merchant/billing/gateways/redsys.rb' - - 'lib/active_merchant/billing/gateways/sage_pay.rb' - -# Offense count: 14 -# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. -# AllowedNames: io, id, to, by, on, in, at -Naming/UncommunicativeMethodParamName: - Exclude: - - 'lib/active_merchant/billing/gateways/blue_snap.rb' - - 'lib/active_merchant/billing/gateways/cyber_source.rb' - - 'lib/active_merchant/billing/gateways/iridium.rb' - - 'lib/active_merchant/billing/gateways/latitude19.rb' - - 'lib/active_merchant/billing/gateways/merchant_ware.rb' - - 'lib/active_merchant/billing/gateways/quantum.rb' - - 'lib/active_merchant/billing/gateways/worldpay_online_payments.rb' - - 'test/unit/gateways/paypal/paypal_common_api_test.rb' - - 'test/unit/gateways/realex_test.rb' - -# Offense count: 51 -# Configuration parameters: EnforcedStyle. -# SupportedStyles: snake_case, camelCase -Naming/VariableName: - Exclude: - - 'lib/active_merchant/billing/gateways/cyber_source.rb' - - 'lib/active_merchant/billing/gateways/ideal/ideal_base.rb' - - 'lib/active_merchant/billing/gateways/iridium.rb' - - 'lib/active_merchant/billing/gateways/latitude19.rb' - - 'lib/active_merchant/billing/gateways/optimal_payment.rb' - - 'lib/active_merchant/billing/gateways/quantum.rb' - - 'lib/active_merchant/billing/gateways/worldpay_online_payments.rb' - - 'test/remote/gateways/remote_authorize_net_test.rb' - - 'test/remote/gateways/remote_card_stream_test.rb' - - 'test/remote/gateways/remote_worldpay_test.rb' - - 'test/unit/gateways/card_stream_test.rb' - - 'test/unit/gateways/worldpay_online_payments_test.rb' - -# Offense count: 11 -# Configuration parameters: EnforcedStyle. -# SupportedStyles: snake_case, normalcase, non_integer -Naming/VariableNumber: - Exclude: - - 'lib/active_merchant/billing/gateways/merchant_partners.rb' - - 'lib/active_merchant/billing/gateways/mercury.rb' - - 'lib/active_merchant/billing/gateways/orbital.rb' - - 'test/remote/gateways/remote_paypal_test.rb' - - 'test/unit/gateways/merchant_ware_test.rb' - - 'test/unit/gateways/merchant_ware_version_four_test.rb' - - 'test/unit/gateways/orbital_test.rb' - - 'test/unit/gateways/paypal/paypal_common_api_test.rb' - -# Offense count: 6 -# Cop supports --auto-correct. -Performance/Casecmp: - Exclude: - - 'lib/active_merchant/billing/gateways/ebanx.rb' - - 'lib/active_merchant/billing/gateways/eway_managed.rb' - - 'lib/active_merchant/billing/gateways/itransact.rb' - - 'lib/active_merchant/billing/gateways/paystation.rb' - - 'lib/active_merchant/billing/gateways/redsys.rb' - - 'lib/active_merchant/country.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -Performance/CompareWithBlock: - Exclude: - - 'lib/active_merchant/billing/gateways/skip_jack.rb' - -# Offense count: 13 -# Cop supports --auto-correct. -Performance/InefficientHashSearch: - Exclude: - - 'lib/active_merchant/billing/credit_card.rb' - - 'lib/active_merchant/billing/gateways/inspire.rb' - - 'lib/active_merchant/billing/gateways/smart_ps.rb' - - 'lib/active_merchant/billing/gateways/usa_epay_advanced.rb' - - 'test/unit/gateways/payscout_test.rb' - -# Offense count: 8 -# Cop supports --auto-correct. -Performance/RangeInclude: - Exclude: - - 'lib/active_merchant/billing/check.rb' - - 'lib/active_merchant/billing/credit_card_methods.rb' - - 'lib/active_merchant/billing/gateways/blue_pay.rb' - - 'lib/active_merchant/billing/gateways/blue_snap.rb' - - 'lib/active_merchant/billing/gateways/cashnet.rb' - - 'lib/active_merchant/billing/gateways/moneris.rb' - - 'lib/active_merchant/billing/gateways/moneris_us.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -Performance/RedundantBlockCall: - Exclude: - - 'test/unit/gateways/nab_transact_test.rb' - -# Offense count: 4 -# Cop supports --auto-correct. -Performance/RedundantMatch: - Exclude: - - 'lib/active_merchant/billing/gateways/opp.rb' - - 'test/unit/gateways/payu_latam_test.rb' - -# Offense count: 59 -# Cop supports --auto-correct. -# Configuration parameters: MaxKeyValuePairs. -Performance/RedundantMerge: - Enabled: false +# Offense count: 129 +Metrics/PerceivedComplexity: + Max: 33 -# Offense count: 1 -# Cop supports --auto-correct. -Performance/ReverseEach: +# Offense count: 6 +Naming/AccessorMethodName: Exclude: - - 'lib/active_merchant/billing/gateways/trust_commerce.rb' + - 'lib/active_merchant/billing/gateways/usa_epay_advanced.rb' + - 'test/remote/gateways/remote_authorize_net_cim_test.rb' + - 'test/unit/gateways/authorize_net_cim_test.rb' -# Offense count: 12 -# Cop supports --auto-correct. -Performance/StringReplacement: +# Offense count: 15 +# Configuration parameters: EnforcedStyle. +# SupportedStyles: snake_case, camelCase +Naming/MethodName: Exclude: - - 'lib/active_merchant/billing/compatibility.rb' - 'lib/active_merchant/billing/gateways/card_connect.rb' - - 'lib/active_merchant/billing/gateways/firstdata_e4.rb' - - 'lib/active_merchant/billing/gateways/ideal/ideal_base.rb' + - 'lib/active_merchant/billing/gateways/latitude19.rb' + - 'lib/active_merchant/billing/gateways/qbms.rb' + - 'test/remote/gateways/remote_card_connect_test.rb' + - 'test/remote/gateways/remote_card_stream_test.rb' + - 'test/remote/gateways/remote_fat_zebra_test.rb' + - 'test/remote/gateways/remote_sage_pay_test.rb' + - 'test/unit/gateways/sage_pay_test.rb' + +# Offense count: 14 +# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. +# AllowedNames: io, id, to, by, on, in, at, ip, db +Naming/MethodParameterName: + Exclude: + - 'lib/active_merchant/billing/gateways/blue_snap.rb' + - 'lib/active_merchant/billing/gateways/cyber_source.rb' + - 'lib/active_merchant/billing/gateways/iridium.rb' + - 'lib/active_merchant/billing/gateways/latitude19.rb' - 'lib/active_merchant/billing/gateways/merchant_ware.rb' - - 'lib/active_merchant/billing/gateways/merchant_ware_version_four.rb' - - 'lib/active_merchant/billing/gateways/orbital.rb' - - 'lib/active_merchant/billing/gateways/quickbooks.rb' - - 'lib/active_merchant/billing/gateways/quickpay/quickpay_v10.rb' - - 'lib/active_merchant/billing/gateways/realex.rb' - - 'test/unit/gateways/nab_transact_test.rb' + - 'lib/active_merchant/billing/gateways/quantum.rb' + - 'lib/active_merchant/billing/gateways/worldpay_online_payments.rb' + - 'test/unit/gateways/paypal/paypal_common_api_test.rb' + - 'test/unit/gateways/realex_test.rb' -# Offense count: 11 -# Cop supports --auto-correct. -Security/YAMLLoad: +# Offense count: 49 +# Configuration parameters: EnforcedStyle. +# SupportedStyles: snake_case, camelCase +Naming/VariableName: Exclude: - - 'test/test_helper.rb' - - 'test/unit/fixtures_test.rb' - - 'test/unit/gateways/firstdata_e4_test.rb' - - 'test/unit/gateways/payeezy_test.rb' + - 'lib/active_merchant/billing/gateways/cyber_source.rb' + - 'lib/active_merchant/billing/gateways/iridium.rb' + - 'lib/active_merchant/billing/gateways/latitude19.rb' + - 'lib/active_merchant/billing/gateways/optimal_payment.rb' + - 'lib/active_merchant/billing/gateways/quantum.rb' + - 'lib/active_merchant/billing/gateways/worldpay_online_payments.rb' + - 'test/remote/gateways/remote_authorize_net_test.rb' + - 'test/remote/gateways/remote_card_stream_test.rb' + - 'test/remote/gateways/remote_worldpay_test.rb' + - 'test/unit/gateways/card_stream_test.rb' + - 'test/unit/gateways/worldpay_online_payments_test.rb' # Offense count: 2 # Configuration parameters: EnforcedStyle. @@ -877,36 +138,6 @@ Style/AccessModifierDeclarations: - 'test/unit/gateways/metrics_global_test.rb' - 'test/unit/gateways/optimal_payment_test.rb' -# Offense count: 11 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: prefer_alias, prefer_alias_method -Style/Alias: - Exclude: - - 'lib/active_merchant/billing/gateways/beanstream.rb' - - 'lib/active_merchant/billing/gateways/braintree_blue.rb' - - 'lib/active_merchant/billing/gateways/inspire.rb' - - 'lib/active_merchant/billing/gateways/migs.rb' - - 'lib/active_merchant/billing/gateways/smart_ps.rb' - - 'lib/active_merchant/billing/gateways/spreedly_core.rb' - - 'lib/active_merchant/post_data.rb' - - 'test/unit/gateways/bpoint_test.rb' - - 'test/unit/gateways/paymentez_test.rb' - -# Offense count: 12 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: always, conditionals -Style/AndOr: - Exclude: - - 'lib/active_merchant/billing/gateways/beanstream/beanstream_core.rb' - - 'lib/active_merchant/billing/gateways/eway.rb' - - 'lib/active_merchant/billing/gateways/iridium.rb' - - 'lib/active_merchant/billing/gateways/pac_net_raven.rb' - - 'lib/active_merchant/billing/gateways/smart_ps.rb' - - 'lib/active_merchant/billing/gateways/stripe.rb' - - 'lib/active_merchant/billing/gateways/webpay.rb' - # Offense count: 47 # Configuration parameters: AllowedChars. Style/AsciiComments: @@ -925,31 +156,16 @@ Style/AsciiComments: - 'test/remote/gateways/remote_data_cash_test.rb' - 'test/remote/gateways/remote_nab_transact_test.rb' -# Offense count: 1 -# Cop supports --auto-correct. -Style/Attr: - Exclude: - - 'test/unit/gateways/forte_test.rb' - -# Offense count: 3 +# Offense count: 2 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle. # SupportedStyles: percent_q, bare_percent Style/BarePercentLiterals: Exclude: - - 'test/unit/gateways/clearhaus_test.rb' - 'test/unit/gateways/eway_rapid_test.rb' - 'test/unit/gateways/orbital_test.rb' -# Offense count: 3 -# Cop supports --auto-correct. -Style/BlockComments: - Exclude: - - 'test/remote/gateways/remote_barclays_epdq_extra_plus_test.rb' - - 'test/remote/gateways/remote_netpay_test.rb' - - 'test/remote/gateways/remote_payu_in_test.rb' - -# Offense count: 75 +# Offense count: 77 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, ProceduralMethods, FunctionalMethods, IgnoredMethods. # SupportedStyles: line_count_based, semantic, braces_for_chaining @@ -959,13 +175,6 @@ Style/BlockComments: Style/BlockDelimiters: Enabled: false -# Offense count: 443 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: braces, no_braces, context_dependent -Style/BracesAroundHashParameters: - Enabled: false - # Offense count: 2 Style/CaseEquality: Exclude: @@ -981,7 +190,7 @@ Style/ClassAndModuleChildren: - 'test/unit/gateways/optimal_payment_test.rb' - 'test/unit/gateways/realex_test.rb' -# Offense count: 35 +# Offense count: 30 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle. # SupportedStyles: is_a?, kind_of? @@ -1012,16 +221,39 @@ Style/ColonMethodCall: - 'lib/active_merchant/billing/gateways/nmi.rb' - 'test/unit/gateways/quickpay_v4to7_test.rb' -# Offense count: 6 +# Offense count: 5 # Cop supports --auto-correct. # Configuration parameters: Keywords. # Keywords: TODO, FIXME, OPTIMIZE, HACK, REVIEW Style/CommentAnnotation: - Exclude: - - 'lib/active_merchant/billing/gateways/authorize_net_cim.rb' - - '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: @@ -1032,7 +264,7 @@ Style/CommentedKeyword: - 'test/remote/gateways/remote_cardknox_test.rb' - 'test/unit/gateways/cardknox_test.rb' -# Offense count: 23 +# Offense count: 22 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions. # SupportedStyles: assign_to_condition, assign_inside_condition @@ -1040,6 +272,7 @@ Style/ConditionalAssignment: Enabled: false # Offense count: 7 +# Configuration parameters: AllowCoercion. Style/DateTime: Exclude: - 'test/remote/gateways/remote_first_pay_test.rb' @@ -1048,14 +281,7 @@ Style/DateTime: - 'test/unit/gateways/orbital_test.rb' - 'test/unit/gateways/paypal/paypal_common_api_test.rb' -# Offense count: 2 -# Cop supports --auto-correct. -Style/DefWithParentheses: - Exclude: - - 'lib/active_merchant/billing/gateways/latitude19.rb' - - 'lib/active_merchant/billing/gateways/pagarme.rb' - -# Offense count: 210 +# Offense count: 211 Style/Documentation: Enabled: false @@ -1130,7 +356,7 @@ Style/EmptyMethod: - 'test/unit/gateways/world_net_test.rb' - 'test/unit/gateways/worldpay_online_payments_test.rb' -# Offense count: 24 +# Offense count: 23 # Cop supports --auto-correct. Style/Encoding: Enabled: false @@ -1151,6 +377,7 @@ Style/ExpandPathArguments: - 'test/test_helper.rb' # Offense count: 11 +# Cop supports --auto-correct. # Configuration parameters: EnforcedStyle. # SupportedStyles: each, for Style/For: @@ -1162,7 +389,7 @@ Style/For: - 'lib/active_merchant/billing/gateways/usa_epay_transaction.rb' - 'test/remote/gateways/remote_orbital_test.rb' -# Offense count: 96 +# Offense count: 97 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle. # SupportedStyles: format, sprintf, percent @@ -1173,19 +400,9 @@ 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' - -# Offense count: 677 + Enabled: false + +# Offense count: 679 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle. # SupportedStyles: when_needed, always, never @@ -1202,18 +419,20 @@ Style/GlobalVars: - 'test/unit/gateways/finansbank_test.rb' - 'test/unit/gateways/garanti_test.rb' -# Offense count: 192 +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: Enabled: false -# Offense count: 7424 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. -# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys -Style/HashSyntax: - Enabled: false - # Offense count: 6 Style/IdenticalConditionalBranches: Exclude: @@ -1221,7 +440,7 @@ Style/IdenticalConditionalBranches: - 'lib/active_merchant/billing/gateways/litle.rb' - 'lib/active_merchant/billing/gateways/payu_latam.rb' -# Offense count: 15 +# Offense count: 14 Style/IfInsideElse: Exclude: - 'lib/active_merchant/billing/credit_card.rb' @@ -1235,16 +454,10 @@ Style/IfInsideElse: - 'lib/active_merchant/billing/gateways/skip_jack.rb' - 'lib/active_merchant/billing/gateways/worldpay_online_payments.rb' -# Offense count: 128 -# Cop supports --auto-correct. -Style/IfUnlessModifier: - Enabled: false - -# Offense count: 3 +# Offense count: 1 Style/IfUnlessModifierOfIfUnless: Exclude: - 'lib/active_merchant/billing/gateways/merchant_e_solutions.rb' - - 'lib/active_merchant/posts_data.rb' # Offense count: 2 # Cop supports --auto-correct. @@ -1253,56 +466,41 @@ 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: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: line_count_dependent, lambda, literal -Style/Lambda: - Exclude: - - 'lib/active_merchant/billing/gateways/orbital.rb' - -# Offense count: 15 -# Cop supports --auto-correct. -Style/LineEndConcatenation: - Exclude: - - 'lib/active_merchant/billing/gateways/commercegate.rb' - - 'lib/active_merchant/billing/gateways/pay_hub.rb' - - 'lib/active_merchant/billing/gateways/swipe_checkout.rb' - - 'test/unit/gateways/barclays_epdq_extra_plus_test.rb' - - 'test/unit/gateways/ogone_test.rb' - -# Offense count: 31 +# Offense count: 32 # Cop supports --auto-correct. # Configuration parameters: IgnoredMethods. Style/MethodCallWithoutArgsParentheses: Enabled: false -# Offense count: 4 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: require_parentheses, require_no_parentheses, require_no_parentheses_except_multiline -Style/MethodDefParentheses: - Exclude: - - 'lib/active_merchant/billing/gateways/cyber_source.rb' - - 'test/unit/gateways/ideal_rabobank_test.rb' - - 'test/unit/gateways/quickpay_v10_test.rb' - -# Offense count: 626 +# Offense count: 656 Style/MultilineBlockChain: Enabled: false -# Offense count: 20 +# Offense count: 15 # Cop supports --auto-correct. Style/MultilineIfModifier: - Enabled: false + Exclude: + - 'lib/active_merchant/billing/compatibility.rb' + - 'lib/active_merchant/billing/gateways/authorize_net_cim.rb' + - 'lib/active_merchant/billing/gateways/bank_frick.rb' + - 'lib/active_merchant/billing/gateways/cenpos.rb' + - 'lib/active_merchant/billing/gateways/efsnet.rb' + - 'lib/active_merchant/billing/gateways/eway.rb' + - 'lib/active_merchant/billing/gateways/flo2cash.rb' + - 'lib/active_merchant/billing/gateways/itransact.rb' + - 'lib/active_merchant/billing/gateways/monei.rb' + - 'lib/active_merchant/billing/gateways/optimal_payment.rb' + - 'lib/active_merchant/billing/gateways/payflow/payflow_common_api.rb' + - 'lib/active_merchant/billing/gateways/psigate.rb' + - 'lib/active_merchant/billing/gateways/realex.rb' -# Offense count: 7 +# Offense count: 6 # Cop supports --auto-correct. Style/MultilineIfThen: Exclude: - 'lib/active_merchant/billing/gateways/eway_managed.rb' - - 'lib/active_merchant/connection.rb' # Offense count: 4 Style/MultilineTernaryOperator: @@ -1317,12 +515,12 @@ Style/MultipleComparison: Exclude: - 'lib/active_merchant/billing/gateways/worldpay_online_payments.rb' -# Offense count: 516 +# Offense count: 530 # Cop supports --auto-correct. Style/MutableConstant: Enabled: false -# Offense count: 21 +# Offense count: 17 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle. # SupportedStyles: both, prefix, postfix @@ -1330,7 +528,6 @@ Style/NegatedIf: Exclude: - 'lib/active_merchant/billing/credit_card.rb' - 'lib/active_merchant/billing/gateways/adyen.rb' - - 'lib/active_merchant/billing/gateways/beanstream/beanstream_core.rb' - 'lib/active_merchant/billing/gateways/itransact.rb' - 'lib/active_merchant/billing/gateways/iveri.rb' - 'lib/active_merchant/billing/gateways/ogone.rb' @@ -1340,12 +537,11 @@ Style/NegatedIf: - 'lib/support/ssl_verify.rb' - 'test/remote/gateways/remote_paypal_test.rb' -# Offense count: 3 +# Offense count: 1 # Cop supports --auto-correct. Style/NestedModifier: Exclude: - 'lib/active_merchant/billing/gateways/merchant_e_solutions.rb' - - 'lib/active_merchant/posts_data.rb' # Offense count: 2 # Cop supports --auto-correct. @@ -1364,14 +560,15 @@ Style/Next: - 'lib/active_merchant/billing/gateways/authorize_net.rb' - 'lib/support/outbound_hosts.rb' -# Offense count: 6 +# Offense count: 5 # Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. +# SupportedStyles: predicate, comparison Style/NilComparison: Exclude: - 'lib/active_merchant/billing/gateways/card_stream.rb' - 'lib/active_merchant/billing/gateways/litle.rb' - 'lib/active_merchant/billing/gateways/telr.rb' - - 'lib/active_merchant/billing/gateways/usa_epay_advanced.rb' - 'test/unit/gateways/braintree_blue_test.rb' - 'test/unit/gateways/stripe_test.rb' @@ -1390,7 +587,7 @@ Style/Not: - 'test/test_helper.rb' - 'test/unit/gateways/braintree_blue_test.rb' -# Offense count: 20 +# Offense count: 19 # Cop supports --auto-correct. # Configuration parameters: EnforcedOctalStyle. # SupportedOctalStyles: zero_with_o, zero_only @@ -1410,15 +607,15 @@ Style/NumericLiteralPrefix: - 'test/unit/gateways/opp_test.rb' - 'test/unit/gateways/pay_junction_v2_test.rb' -# Offense count: 446 +# Offense count: 466 # Cop supports --auto-correct. # Configuration parameters: Strict. Style/NumericLiterals: MinDigits: 17 -# Offense count: 40 +# Offense count: 41 # Cop supports --auto-correct. -# Configuration parameters: AutoCorrect, EnforcedStyle. +# Configuration parameters: AutoCorrect, EnforcedStyle, IgnoredMethods. # SupportedStyles: predicate, comparison Style/NumericPredicate: Enabled: false @@ -1434,13 +631,7 @@ Style/OrAssignment: Style/ParallelAssignment: Enabled: false -# Offense count: 29 -# Cop supports --auto-correct. -# Configuration parameters: AllowSafeAssignment, AllowInMultilineConditions. -Style/ParenthesesAroundCondition: - Enabled: false - -# Offense count: 877 +# Offense count: 879 # Cop supports --auto-correct. # Configuration parameters: PreferredDelimiters. Style/PercentLiteralDelimiters: @@ -1455,7 +646,7 @@ Style/PerlBackrefs: - 'lib/support/outbound_hosts.rb' - 'test/unit/gateways/payu_in_test.rb' -# Offense count: 94 +# Offense count: 96 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle. # SupportedStyles: short, verbose @@ -1469,62 +660,25 @@ Style/Proc: - 'test/unit/credit_card_methods_test.rb' - 'test/unit/gateways/nab_transact_test.rb' -# Offense count: 33 +# Offense count: 31 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle. # SupportedStyles: compact, exploded Style/RaiseArgs: Enabled: false -# Offense count: 12 -# Cop supports --auto-correct. -Style/RandomWithOffset: - Exclude: - - 'lib/active_merchant/billing/gateways/borgun.rb' - - 'test/remote/gateways/remote_beanstream_test.rb' - - 'test/remote/gateways/remote_braintree_orange_test.rb' - - 'test/remote/gateways/remote_inspire_test.rb' - - 'test/remote/gateways/remote_merchant_ware_test.rb' - - 'test/remote/gateways/remote_merchant_ware_version_four_test.rb' - - 'test/remote/gateways/remote_pay_junction_test.rb' - - 'test/unit/gateways/beanstream_test.rb' - -# Offense count: 21 -# Cop supports --auto-correct. -Style/RedundantBegin: - Enabled: false - -# Offense count: 3 -# Cop supports --auto-correct. -Style/RedundantConditional: - Exclude: - - 'lib/active_merchant/billing/gateways/pin.rb' - - 'lib/active_merchant/billing/gateways/usa_epay_advanced.rb' - - 'lib/active_merchant/billing/gateways/worldpay_online_payments.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -Style/RedundantException: - Exclude: - - 'lib/active_merchant/billing/gateway.rb' - -# Offense count: 27 -# Cop supports --auto-correct. -Style/RedundantParentheses: - Enabled: false - -# Offense count: 87 +# Offense count: 86 # Cop supports --auto-correct. # Configuration parameters: AllowMultipleReturnValues. Style/RedundantReturn: Enabled: false -# Offense count: 173 +# Offense count: 179 # Cop supports --auto-correct. Style/RedundantSelf: Enabled: false -# Offense count: 1178 +# Offense count: 1209 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, AllowInnerSlashes. # SupportedStyles: slashes, percent_r, mixed @@ -1554,13 +708,6 @@ Style/RescueStandardError: Exclude: - 'lib/active_merchant/billing/base.rb' -# Offense count: 33 -# Cop supports --auto-correct. -# Configuration parameters: ConvertCodeThatCanStartToReturnNil, Whitelist. -# Whitelist: present?, blank?, presence, try, try! -Style/SafeNavigation: - Enabled: false - # Offense count: 2 # Cop supports --auto-correct. Style/SelfAssignment: @@ -1587,68 +734,6 @@ Style/SingleLineMethods: Exclude: - 'test/unit/gateways/paypal/paypal_common_api_test.rb' -# Offense count: 3 -# Cop supports --auto-correct. -# Configuration parameters: . -# SupportedStyles: use_perl_names, use_english_names -Style/SpecialGlobalVars: - EnforcedStyle: use_perl_names - -# Offense count: 31 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: single_quotes, double_quotes -Style/StringLiteralsInInterpolation: - Enabled: false - -# Offense count: 307 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, MinSize. -# SupportedStyles: percent, brackets -Style/SymbolArray: - Enabled: false - -# Offense count: 9 -# Cop supports --auto-correct. -# Configuration parameters: IgnoredMethods. -# IgnoredMethods: respond_to, define_method -Style/SymbolProc: - Exclude: - - 'lib/active_merchant/billing/compatibility.rb' - - 'lib/active_merchant/billing/gateways/braintree_blue.rb' - - 'lib/active_merchant/billing/gateways/cenpos.rb' - - 'lib/active_merchant/billing/gateways/checkout_v2.rb' - - 'lib/active_merchant/billing/gateways/creditcall.rb' - - 'lib/active_merchant/billing/gateways/mercury.rb' - - 'lib/active_merchant/billing/gateways/payflow/payflow_response.rb' - - 'lib/active_merchant/billing/gateways/psl_card.rb' - - 'lib/active_merchant/billing/gateways/realex.rb' - -# Offense count: 15 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, AllowSafeAssignment. -# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex -Style/TernaryParentheses: - Exclude: - - 'lib/active_merchant/billing/gateways/beanstream/beanstream_core.rb' - - 'lib/active_merchant/billing/gateways/blue_snap.rb' - - 'lib/active_merchant/billing/gateways/borgun.rb' - - 'lib/active_merchant/billing/gateways/braintree_blue.rb' - - 'lib/active_merchant/billing/gateways/card_stream.rb' - - 'lib/active_merchant/billing/gateways/litle.rb' - - 'lib/active_merchant/billing/gateways/moneris_us.rb' - - 'lib/active_merchant/billing/gateways/ogone.rb' - - 'lib/active_merchant/billing/gateways/orbital.rb' - - 'lib/active_merchant/billing/gateways/payeezy.rb' - - 'lib/active_merchant/billing/gateways/visanet_peru.rb' - -# Offense count: 27 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyleForMultiline. -# SupportedStylesForMultiline: comma, consistent_comma, no_comma -Style/TrailingCommaInArguments: - Enabled: false - # Offense count: 7 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyleForMultiline. @@ -1656,70 +741,17 @@ Style/TrailingCommaInArguments: Style/TrailingCommaInArrayLiteral: Exclude: - 'lib/active_merchant/billing/credit_card_methods.rb' - - 'test/remote/gateways/remote_payflow_express_test.rb' - 'test/unit/gateways/netaxept_test.rb' - 'test/unit/gateways/usa_epay_transaction_test.rb' -# Offense count: 155 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyleForMultiline. -# SupportedStylesForMultiline: comma, consistent_comma, no_comma -Style/TrailingCommaInHashLiteral: - Enabled: false - -# Offense count: 36 -# Cop supports --auto-correct. -# Configuration parameters: AllowNamedUnderscoreVariables. -Style/TrailingUnderscoreVariable: - Enabled: false - -# Offense count: 26 -# Cop supports --auto-correct. -Style/UnneededInterpolation: - Exclude: - - 'lib/active_merchant/billing/base.rb' - - 'lib/active_merchant/billing/compatibility.rb' - - 'lib/active_merchant/billing/gateways/banwire.rb' - - 'lib/active_merchant/billing/gateways/cardknox.rb' - - 'lib/active_merchant/billing/gateways/conekta.rb' - - 'lib/active_merchant/billing/gateways/digitzs.rb' - - 'lib/active_merchant/billing/gateways/eway_managed.rb' - - 'lib/active_merchant/billing/gateways/first_giving.rb' - - 'lib/active_merchant/billing/gateways/merchant_one.rb' - - 'lib/active_merchant/billing/gateways/openpay.rb' - - 'lib/active_merchant/billing/gateways/pay_junction_v2.rb' - - 'lib/active_merchant/billing/gateways/safe_charge.rb' - - 'lib/active_merchant/billing/gateways/telr.rb' - - 'lib/support/gateway_support.rb' - - 'test/remote/gateways/remote_cyber_source_test.rb' - -# Offense count: 54 -# Cop supports --auto-correct. -Style/UnneededPercentQ: - Exclude: - - 'test/unit/gateways/clearhaus_test.rb' - - 'test/unit/gateways/forte_test.rb' - - 'test/unit/gateways/merchant_warrior_test.rb' - - 'test/unit/gateways/nmi_test.rb' - - 'test/unit/gateways/orbital_test.rb' - - 'test/unit/gateways/payex_test.rb' - - 'test/unit/gateways/paymentez_test.rb' - - 'test/unit/gateways/world_net_test.rb' - -# Offense count: 117 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, MinSize, WordRegex. -# SupportedStyles: percent, brackets -Style/WordArray: - Enabled: false - -# Offense count: 33 +# Offense count: 34 # Cop supports --auto-correct. Style/ZeroLengthPredicate: Enabled: false -# Offense count: 9190 +# Offense count: 9321 # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # URISchemes: http, https -Metrics/LineLength: - Max: 2484 +Layout/LineLength: + Max: 2602 + diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 726411a1e7e..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,24 +0,0 @@ -language: ruby -sudo: false -cache: bundler - -rvm: -- 2.5 -- 2.4 -- 2.3 - -gemfile: -- Gemfile.rails52 -- Gemfile.rails51 -- Gemfile.rails50 -- Gemfile.rails42 - -matrix: - include: - - rvm: 2.3 - gemfile: Gemfile.rails42 - -notifications: - email: - on_success: never - on_failure: always diff --git a/CHANGELOG b/CHANGELOG index 30baf9a4d7c..6ffa6ede58e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,1781 @@ + = ActiveMerchant CHANGELOG == HEAD +* Mercury, TransFirst: Repair gateways following updates to `rexml` [aenand] #5206 +* NMI: Fix Decrypted indicator for Google/Apple pay [javierpedrozaing] #5196 +* FlexCharge: add more descriptives error messages [gasb150] #5199 +* Braintree: Updates to Paypal Integration [almalee24] #5190 +* Stripe and Stripe PI: Add metadata and order_id for refund and void [yunnydang] #5204 +* CommerceHub: Update test url [DustinHaefele] #5211 +* Adyen: Fix billing address empty string error [yunnydang] #5208 +* Elavon: Update sending CVV for MIT transactions [almalee24] #5210 +* Adyen: Fix NT integration [jherreraa] #5155 +* HPS: Update NetworkTokenizationCreditCard flow [almalee24] #5178 +* Braintree: Support override_application_id [aenand] #5194 +* Decidir: Pass CVV for NT [almalee24] #5205 +* NMI: Add customer vault fields [yunnydang] #5215 +* CheckoutV2: Add inquire method [almalee24] #5209 +* Decidir & Braintree: Scrub cryptogram and card number [almalee24] #5220 +* Naranja: Update valid number check to include luhn10 [DustinHaefele] #5217 +* Cybersource: Add apple_pay with discover. [DustinHaefele] #5213 +* MercadoPago: Add idempotency key field [yunnydang] #5229 +* Adyen: Update split refund method [yunnydang] #5218 +* Adyen: Remove raw_error_message [almalee24] #5202 +* Elavon: Remove old Stored Credential method [almalee24] #5219 +* PayTrace: Update MultiResponse for Capture [almalee24] #5203 +* Adyen: Add support for Pan Only GooglePay [almalee24] #5221 +* Decidir: Send extra fields for tokenized NT transactions [sinourain] #5224 +* StripePI: Skip add_network_token_cryptogram_and_eci method to accept ApplePay recurring payments [sinourain] #5212 +* Decidir: Fix scrub method after update NT fields [sinourain] #5241 +* Cybersource and Cybersource Rest: Update card type code for Carnet cards [rachelkirk] #5235 +* Stripe PI: Add challenge as valid value for request_three_d_secure [jcreiff] #5238 +* StripePI: Add metadata for GooglePay FPAN [almalee24] #5242 +* Paypal: Add inquire method [almalee24] #5231 +* Adyen: Enable multiple legs within airline data [jcreiff] #5249 +* SafeCharge: Add card holder verification fields [yunnydang] #5252 +* Iveri: Add AuthorisationReversal for Auth Void [almalee24] #5233 +* Stripe PI: Update Stored Credentials [almalee24] #5236 +* Checkout V2: Update stored credential options function [jherreraa] #5239 +* Ebanx: Add support for Stored Credentials [almalee24] #5243 +* Shift4: Update response parsing to account for hostresponse [jcreiff] #5261 +* NMI: Add `industry_indicator` field [naashton] #5264 +* Nuvei: Add 3DS GS [gasb150] #5247 +* Rapyd: Add idempotency for all endpoints [almalee24] #5255 +* Decidir: Map error code -1 to processing_error [Buitragox] #5257 +* SumUp: Append partner_id to checkout_reference [naashton] #5272 +* Adyen: Update shopperInteraction [almalee24] #5273 +* CenPOS: Add test_url [jcreiff] #5274 +* Worldpay: Fix stored credentials issue [jherreraa] #5267 +* StripePI: Update authorization for failed Payment Intents [almalee24] #5260 +* Worldpay: Add support for recurring Apple Pay [kenderbolivart] #5628 +* Cybersource Rest: Add support for recurring Apple Pay [bdcano] #5270 +* Cybersource Rest: Update message and error_code [almalee24] #5276 +* Paysafe: Add support for `external_initial_transaction_id` [rachelkirk] #5291 +* Worldpay: Add customStringFields [jcreiff] #5284 +* Airwallex: truncate descriptor field to 32 characters [jcreiff] #5292 +* DLocal: Add the description field for refund [yunnydang] #5296 +* Worldpay: Add support for Worldpay decrypted apple pay and google pay [dustinhaefele] #5271 +* Orbital: Update alternate_ucaf_flow [almalee24] #5282 +* Adyen: Remove cryptogram flag [almalee24] #5300 +* Cecabank: Include Apple Pay and Google Pay for recurring payments [gasb150] #5295 +* DLocal: Add X-Dlocal-Payment-Source to header [almalee24] #5281 +* StripePI: Update to retrieve_setup_intent and headers [almalee24] #5283 +* Upgrade rexml to 3.3.8 [raymzag] #5245 +* Nuvei: Add partial approval feature [javierpedrozaing] #5250 +* Nuvei: Add ACH support [javierpedrozaing] #5269 +* Nuvei: Add GSF for verify method [javierpedrozaing] #5278 +* Nuvei: Add Google and Apple pay [javierpedrozaing] #5289 +* Cybersource and Cybersource Rest: Add the MCC field [yunnydang] #5301 +* Adyen: Add the manual_capture field [yunnydang] #5310 +* Versapay: First Implementation [gasb150] #5288 +* Adyen: Enable GSF recurring_detail_reference on other transactions [rubenmarindev] #5285 +* Adyen: Update skip_mpi_data [almalee24] #5306 +* Paysafe: Update fields in standalonecredits [almalee24] #5293 +* GlobalCollect: Add support for $0 Auth [almalee24] #5303 +* Versapay: Store and Unstore transactions [gasb150] #5315 +* Nuvei: Add 3DS Global [javierpedrozaing] #5308 +* StripePI: Store the three_d_secure_usage field [yunnydang] #5321 +* StripePI: Last4 From Payment Method [naashton] #5322 +* New Card Type: Patagonia365 [gasb150] #5265 +* Decidir: Patagonia365 Card Type Mapping [naashton] #5324 +* Ebanx: Add network token support [Buitragox] #5263 +* DLocal: Add the optional country field override [yunnydang] #5326 +* Nuvei: Adding account founding transaction [javierpedrozaing] #5307 +* Nuvei: Add card holder name verification params [javierpedrozaing] #5312 +* Stripe: Remove StripePaymenToken & ApplePayPaymentToken [almalee24] #5304 +* HPS: Update API urls [jherreraa] #5313 +* MIT: Change test URL [Buitragox] #5335 +* Nuvei: Fix NTID stored credentials [javierpedrozaing] #5334 +* StripePI: Fix the store three_d_secure_usage field [yunnydang] #5338 +* Litle: Success codes added to valid responses [jherreraa] #5339 +* CommerceHub: Update Production URL [almalee24] #5340 +* CommerceHub: Add Network Token support [almalee24] #5331 +* Worldpay: Update Stored Credentials [almalee24] #5330 +* Update Rubocop 1.26.0 [almalee24] #5325 +* RedsysREST: Add Network Tokens [gasb150] #5333 +* DLocal: Update the success_from for Void [almalee24] #5337 +* Braintree: Add support for stored credentials with Apple Pay [bdcano] #5336 +* FlexCharge: Update homePage url [javierpedrozaing] #5351 +* Nuvei: Fix send savePM in false by default [javierpedrozaing] #5353 +* Decidir and DecicirPlus: Add the wallet_id field [yunnydang] #5354 +* Worldpay: Update where to pass shopperIPAddress [almalee24] #5348 +* Braintree: Account for BraintreeError [almalee24] #5346 +* Worldpay: Fix stored credentials unscheduled reason type [Buitragox] #5352 +* Worldpay: Worldpay: Idempotency key fix [jherreraa] #5359 +* Hi Pay: Don't add 3ds when :three_ds_2 is missing [Buitragox] #5355 +* SecurePayAU: Send order ID for payments with stored card [dacook] #3979 +* Orbital: Add XSD version testing to unit test [almalee24] #5375 +* Priority: fix for bin lookup [Buitragox] #5366 +* CommerceHub: Update merchantInvoiceNumber & merchantTransactionId [almalee24] #5374 +* VersaPay: refactor authorization from structure [gasb150] #5363 +* VersaPay: Improve Message Error and Error Mapping [gasb150] #5357 +* CheckoutV2: Add metadata to Credit [almalee24] #5328 +* Normalize API versions for Adyen, Braintree & GlobalCollect [Buitragox] #5371 +* SafeCharge: Add Australia & Canada as supported countries [almalee24] #5377 +* RedsysRest: Improve authorization from in Redsys [gasb150] #5372 +* Ebanx: Add the notification_url field [yunnydang] #5388 +* Ebanx: Add optional payment_type_code override [jcreiff] #5370 +* Ebanx: Update list of supported countries [jcreiff] #5387 +* Add alternate spelling for Vietnam [jcreiff] #5386 +* Checkout v2: add l2/l3 [gasb150] #5385 +* Worldpay: Update passing 3DS data for NT [almalee24] #5389 +* Ebanx: Add the merchant_payment_code override [yunnydang] #5394 +* Credorax: Add AFT fields [yunnydang] #5390 +* Cashnet: Update max_retries to 1 [almalee24] #5393 +* Cybersource: Update 3DS fields [almalee24] #5396 +* Airwallex: Update authorization_from method to include payment intent ID on refunds [yunnydang] #5400 +* New Credit Card Sol: Add new bin set for Sol credit card [javierpedrozaing] #5380 +* Credit Card Sol: Update card name to `tarjeta_sol` instead of `sol` [javierpedrozaing] #5404 + +== Version 1.137.0 (August 2, 2024) +* Unlock dependency on `rexml` to allow fixing a CVE (#5181). +* 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 +* Braintree: Update card verfification payload if billing address fields are not present [yunnydang] #5142 +* DLocal: Update the phone and ip fields [yunnydang] #5143 +* CheckoutV2: Add support for risk data fields [yunnydang] #5147 +* Pin Payments: Add new 3DS params mentioned in Pin Payments docs [hudakh] #4720 +* RedsysRest: Add support for stored credentials & 3DS exemptions [jherreraa] #5132 +* CheckoutV2: Truncate the reference id for amex transactions [yunnydang] #5151 +* CommerceHub: Add billing address name override [yunnydang] #5157 +* StripePI: Add optional ability for 3DS exemption on verify calls [yunnydang] #5160 +* CyberSource: Update stored credentials [sinourain] #5136 +* Orbital: Update to accept UCAF Indicator GSF [almalee24] #5150 +* CyberSource: Add addtional invoiceHeader fields [yunnydang] #5161 +* MerchantWarrior: Update phone, email, ip and store ID [almalee24] #5158 +* Credorax: Update 3DS version mapping [almalee24] #5159 +* Add Maestro card bins [yunnydang] #5172 +* Braintree: Remove stored credential v1 [almalee24] #5175 +* Braintree Blue: Pass overridden mid into client token for GS 3DS [sinourain] #5166 +* Moneris: Update crypt_type for 3DS [almalee24] #5162 +* CheckoutV2: Update 3DS message & error code [almalee24] #5177 +* DecicirPlus: Update error_message to add safety navigator [almalee24] #5187 +* Elavon: Add updated stored credential version [almalee24] #5170 +* Adyen: Add header fields to response body [yunnydang] #5184 +* Stripe and Stripe PI: Add header fields to response body [yunnydang] #5185 +* Nuvei: Add support for Stored Credentials [javierpedrozaing] #5186 +* Addition of shopper ip address when a network token transaction occurs [rubenmarindev] #5262 + +== 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 +* Worldpay: Add transaction inquire request [molbrown] #4592 +* Alelo: Adding homologation changes [heavyblade] #4590 +* Adyen: Map standard error codes for `processing_error`, `config_error`, `invalid_amount`, and `incorrect_address` [ajawadmirza] #4593 +* MerchantE: Add support for recurring transactions [naashton] #4594 +* CyberSource: Add support for `discount_management_indicator`, `purchase_tax_amount`, `installment_total_amount`, and `installment_annual_interest_rate` fields. [rachelkirk] #4595 +* Shift4: Remove `customer` from refund and `clerk` from store requests [ajawadmirza] #4596 +* TransFirst Transaction Express: Update xml prefixing to be compatible with Nokogiri 1.13.4 [dsmcclain] #4582 +* Iveri: Adding support for external MPI 3DS2 [heavyblade] #4598 +* Credorax: Pass Network Transaction ID on MIT [jherreraa] #4600 +* iVeri: Remove schema validation on Live requests [curiousepic] #4606 +* Beanstream: Adding Third Party 3DS fields [heavyblade] #4602 +* Borgun: Add support for 3DS preauth [ajawadmirza] #4603 +* Cardstream: Add third party 3ds2 support [sainterman] #4570 +* Accept both formats of Canadian routing numbers [molbrown] #4568 +* DLocal: Add support for `original_order_id` [rachelkirk] #4605 +* CheckoutV2: Add support for `merchant_initiated_transaction_id` [rachelkirk] #4611 +* CardConnect: Add stored credential & pass any valid `ecomind` field [ajawadmirza] #4609 +* Alelo: Trigger access token refresh on 404 [curiousepic] #4614 +* DLocal: Add Network Tokens [gasb150] #4608 +* Redsys: enable NTID generation with zero-value verify [jcreiff] #4615 +* IPG: Add support for passing in `store_id` on transactions [aenand] #4619 +* Adyen: Field support for Level 2 and level 3 information [sainterman] #4617 +* Add alternate alpha2 country code for Kosovo [jcreiff] #4622 +* CyberSource: Add support for several fields [rachelkirk] #4623 +* Reach: adding gateway [cristian] #4618 +* Orbital: integration improvements [molbrown] #4626 +* iVeri: add new url [almalee24] #4630 +* Payeezy: Enable Apple Pay support [naashton] #4631 +* Payeezy: Scrub Cryptogram [naashton] #4633 +* Checkout: Fix for `[:source][:stored]` in stored credentials [marioarranzr] #4629 +* Mundipagg: send authorization_secret_key on all transaction types [edgarv09] #4635 +* CommerceHub: Add new gateway [naashton] #4640 +* CyberSource: Update installment data method [rachelkirk] #4642 +* Element: fix bug with billing address email [jcreiff] #4644 +* Openpay: set URL by merchant country [edgarv09] #4637 +* Alelo: Improving credentials refresh process [heavyblade] #4616 +* Decidir: Add transaction inquire request [almalee24] #4649 +* EBANX: add soft_descriptor field [jcreiff] #4658 +* CommerceHub: Add Apple Pay and Google Pay [gasb150] #4648 +* Global Collect & Alelo: Fixing year dependent failing tests [heavyblade] #4665 +* Moneris: Add Google Pay [sinourain] #4666 +* Global Collect: Add transaction inquire request [almalee24] #4669 +* Stripe PI: Add Level 3 support [almalee24] #4673 +* Braintree: return additional processor response [jcreiff] #4653 +* Payeezy: name from `billing_address` on `purchase` [naashton] #4674 +* Stripe: add reverse_transfer to void transactions [jcreiff] #4668 +* Global Collect: fix bug on transaction inquire request [almalee24] #4676 +* Credorax: Support google pay and apple pay [edgarv09] #4661 +* Plexo: Add support for 5 new credit card brands (passcard, edenred, anda, tarjeta-d, sodexo) [edgarv09] #4652 +* Authorize.net: Google pay token support [sainterman] #4659 +* Credorax: Add support for Network Tokens [jherreraa] #4679 +* Stripe PI: use MultiResponse in create_setup_intent [jcreiff] #4683 +* Credorax: Correct NTID logic for MIT transactions [aenand] #4686 +* Adyen: Add support for `skip_mpi_data` flag [rachelkirk] #4654 +* Add Canadian Institution Numbers [jcreiff] #4687 +* Tns: update test URL [almalee24] #4698 +* TrustCommerce: Update `authorization_from` to handle `store` response [jherreraa] #4691 +* TrustCommerce: Verify feature added [jherreraa] #4692 +* Rapyd: Add customer object to transactions [javierpedrozaing] #4664 +* CybersourceRest: Add new gateway with authorize and purchase [heavyblade] #4690 +* Litle: Add prelive_url option [aenand] #4710 +* CommerceHub: Fixing verify status and prevent tokenization [heavyblade] #4716 +* Payeezy: Update Stored Credentials [almalee24] #4711 +* CheckoutV2: Add store/unstore [gasb150] #4712 +* CybersourceREST - Refund | Credit [sinourain] #4700 +* Braintree - Add Paypal custom fields [yunnydang] #4713 +* BlueSnap - Add descriptor phone number field [yunnydang] #4717 +* Braintree - Update transaction hash to include processor_authorization_code [yunnydang] #4718 +* CyberSourceRest: Add apple pay, google pay [gasb150] #4708 +* CybersourceREST - Void | Verify [sinourain] #4695 +* Credorax: Set default ECI values for token transactions [sainterman] #4693 +* CyberSourceRest: Add ACH Support [edgarv09] #4722 +* CybersourceREST: Add capture request [heavyblade] #4726 +* Paymentez: Add transaction inquire request [aenand] #4729 +* Ebanx: Add transaction inquire request [almalee24] #4725 +* Ebanx: Add support for Elo & Hipercard [almalee24] #4702 +* CheckoutV2: Add Idempotency key support [yunnydang] #4728 +* Adyen: Add support for shopper_statement field for capture [yunnydang] #4736 +* CheckoutV2: Update idempotency_key name [yunnydang] #4737 +* Payeezy: Enable external 3DS [jherreraa] #4715 +* Ebanx: Remove default email [aenand] #4747 +* CyberSourceRest: Add stored credentials support [jherreraa] #4707 +* Payeezy: Add `last_name` for `add_network_tokenization` [naashton] #4743 +* Stripe PI: Tokenize payment method at Stripe for `verify` [aenand] #4748 +* Kushki: Add support for the months and deferred fields [yunnydang] #4752 +* Borgun: Update TrCurrencyExponent for 3DS transactions with `ISK` [aenand] #4751 +* CyberSourceRest: Add gateway specific fields handling [jherreraa] #4746 +* IPG: Improve error handling [heavyblade] #4753 +* Shift4: Handle access token failed calls [heavyblade] #4745 +* Bogus: Add verify functionality [willemk] #4749 +* Litle: Update successful_from method [almalee24] #4765 + +== Version 1.127.0 (September 20th, 2022) +* BraintreeBlue: Add venmo profile_id [molbrown] #4512 +* Maestro: Adding missing BIN ranges [bradbroge] #4423 +* Simetrik: Fix integer and float types, update scrub method [rachelkirk] #4405 +* Credorax: Convert country codes for `recipient_country_code` field [ajawadmirza] #4408 +* BlueSnap: Correctly parse `refund-transaction-id` [dsmcclain] #4411 +* Worldpay: Add level II and level III data [javierpedrozaing] #4393 +* Worldpay: extract `issuer_response_code` and `issuer_response_description` from gateway response [dsmcclain] #4412 +* Vantiv: Support `duplicate` field read from saleResponse.duplicate attr [mashton] #4413 +* Ogone: Add support for 3dsv2 [gasb150] #4410 +* BlueSnap: Add support for stored credentials [ajawadmirza] #4414 +* Monei: Add support for `lang` field [drkjc] #4421 +* Wompi: Redirect `refund` to `void` [drkjc] #4424 +* Rapyd: 3DS Support [naashton] #4422 +* Adyen: Update API version [jherreraa] #4418 +* Ogone: Updated home gateway URL [gasb150] #4419 +* Credorax: Update url gateway and credit cards [javierpedrozaing] #4417 +* Kushki: Pass extra_taxes with USD [therufs] #4426 +* DLocal: fix bug with `X-Idempotency-Key` header [dsmcclain] #4431 +* DLocal: Mark support for additional countries [gasb150] #4427 +* Rapyd: Additional Fields [naashton] #4434 +* Braintree: Return generated client token [BritneyS] #4416 +* Simetrik: Update `audience` field [simetrik-frank] #4433 +* CyberSource: Add bank account payment method support [heavyblade] #4428 +* Rapyd: Zero Dollar Auth [naashton] #4435 +* Rapyd: Scrub ACH [naashton] #4436 +* VisaNet Peru: Update `purchase_number` [rachelkirk] #4437 +* CardConnect: Add support for 3ds V2 [javierpedrozaing] #4429 +* Rapyd: Support `store` and `unstore` [naashton] #4439 +* Orbital: Update API version to 9.0 [gasb150] #4440 +* Plexo: Add `meta_data` fields and reorder amount object in response [ajawadmirza] #4441 +* Plexo: Change field name from `meta_data` to `metadata` [ajawadmirza] #4443 +* Simetrik: Update `vat` to be in cents [simetrik-frank] #4425 +* Cybersource: Handle Amex cryptograms [heavyblade] #4445 +* Rapyd: Pass fields to `refund` and `store` [naashton] #4449 +* VPOS: Allow reuse of encryption key [therufs] #4450 +* Orbital: Add `payment_action_ind` field and refund through credit card to support tandem implementation [ajawadmirza] #4420 +* Airwallex: Send `referrer_data` on setup transactions [drkjc] #4453 +* Adyen and StripPI: Updated error messaging [mbreenlyles] #4454 +* Airwallex: Update `referrer_data` field [drkjc] #4455 +* Simetrik: Update `order_id` and `description` to be top level fields [simetrik-frank] #4451 +* Plexo: Update `ip`, `description`, and `email` fields request format and scrub method to not filter cardholder name and reference id [ajawadmirza] #4457 +* Plexo: Update `verify` implementation and add `verify_amount` field [ajawadmirza] #4462 +* Vanco: Update `purchase` to complete a purchase transaction with an existing session id [BritneyS] #4461 +* Authorize.net: Allow custom verify_amount and validate it [jherreraa] #4464 +* Shift4: Add gateway adapter [ali-hassan] #4415 +* Rapyd: Correctly add `billing_address` [naashton] #4465 +* Credorax: Update processor response messages [jcreiff] #4466 +* Shift4: add `customer_reference`, `destination_postal_code`, `product_descriptors` fields and core refactoring [ajawadmirza] #4469 +* Paypal Express: Add checkout status to response object [mbreenlyles] #4467 +* Shift4: Scrub security code [naashton] #4470 +* Shift4: Update `cardOnFile` transaction requests [ajawadmirza] #4471 +* Plexo: Update `success_from` definition [ajawadmirza] #4468 +* Rapyd: Un-nest the payment urls [naashton] #4472 +* Paypal Express: Correct naming mistake for accessor [mbreenlyles] #4473 +* GlobalCollect: Enable Google Pay and Apple Pay [gasb150] #4388 +* Shift4: $0 auth [naashton] #4474 +* CyberSource: Updatie API version to 1.198 and fix 3DS test [cristian] #4456 +* Shift4: add `store` method, `present` field in card, and to pass amount in cents [ajawadmirza] #4475 +* Shift4: add `3ds2` implementation [ajawadmirza] #4476 +* Shift4: update `success_from` definition to consider response code [ajawadmirza] #4477 +* Rapyd: Customer Object [naashton] #4478 +* Shift4: Verify Endopint Fix [naashton] #4479 +* CheckoutV2: Scrub cryptogram and credit card number [ajawadmirza] #4488 +* CheckoutV2: Add `3ds.status` field to send status of 3DS flow of all 3DS transactions [BritneyS] #4492 +* CheckoutV2: Add `challenge_indicator`, `exemption`, `authorization_type`, `processing_channel_id`, and `capture_type` fields [ajawadmirza] #4482 +* Add `mada` card type and associated BINs; add support for `mada` in CheckoutV2 gateway [dsmcclain] #4486 +* Authorize.net: Refactor custom verify amount handling [jherreraa] #4485 +* EBANX: Change amount for Colombia [flaaviaa] #4481 +* Worldpay: Update `required_status_message` and `message_from` methods for response. [rachelkirk] #4493 +* CheckoutV2: Add support for transactions through OAuth [ajawadmirza] #4483 +* Vanco: Update unit test to remove remote call to gateway [ajawadmirza] #4497 +* Shift4: remove support for 3ds2 [ajawadmirza] #4503 +* Rapyd: Add support for stored credential [ajawadmirza] #4487 +* MerchantE: Update `store` and add `verify` method [ajawadmirza] #4507 +* Shift4: Add default `numericId`, add `InterfaceVersion`, `InterfaceName`, and `CompanyName` header fields, change date time format and allow merchant time zone [ajawadmirza] #4509 +* BraintreeBlue: Add support for partial capture [aenand] #4515 +* Rapyd: Change key name to `network_transaction_id` [ajawadmirza] #4514 +* CyberSource: Handle unsupported Network Token brands [heavyblade] #4500 +* Ingenico(Global Collect): Add support for `payment_product_id` [rachelkirk] #4521 +* Adyen: Add network transaction id to store call [jcreiff] #4522 +* Worldpay: Add machine cookie to subsequent calls during 3DS challenge [mbreenlyles] #4513* +* Shift4: Scrub `securityCode` fix [naashton] #4524 +* Credorax: Update `OpCode` for credit transactions [dsmcclain] #4279 +* CheckoutV2: Add `credit` method [ajawadmirza] #4490 +* Stripe Payment Intents: Add `options` for retrieve_setup_intent [aenand] #4529 +* CheckoutV2: Send payment id via `incremental_authorization` field [ajawadmirza] #4518 +* Shift4: Add card `present` field, use previous transaction authorization for capture, and hardcode header values [ajawadmirza] #4528 +* Orbital: Remove `DPANInd` field for RC transactions [ajawadmirza] #4502 +* EBANX: Add Spreedly tag to payment body [flaaviaa] #4527 +* Shift4: Add `expiration_date` field for refund transactions [ajawadmirza] #4532 +* Improve handling of AVS and CVV Results in Multiresponses [gasb150] #4516 +* Airwallex: Add `skip_3ds` field for create payment transactions [ajawadmirza] #4534 +* Shift4: Typo correction for `initial_transaction` [ajawadmirza] #4537 +* Rapyd: Pass Customer ID and fix `add_token` method [naashton] #4538 +* Shift4: If no timezone is sent on transactions, the code uses the hours and minutes as a timezone offset [ali-hassan] #4536 +* Priority: Add support for general credit and updating cvv and zip [priorityspreedly] #4517 +* Worldpay: Update actions for generated message in `required_status_message` method [rachelkirk] #4530 +* Adyen: Modify handling of countryCode for ACH [jcreiff] #4543 +* CardConnect: update api end-point urls [heavyblade] #4541 +* Vantiv(Litle): Add support for `fraudFilterOverride` field [rachelkirk] #4544 +* Stripe: Add shipping address [jcreiff] #4539 +* PayuLatam: Add extra1, extra2, extra3 fields [jcreiff] #4550 +* Paysafe: Add fundingTransaction object [jcreiff] #4552 +* MerchantE: Add tests for `moto_ecommerce_ind` field [ajawadmirza] #4554 +* Plexo: Update `purchase` method, add flags for header fields, add new fields `billing_address`, `identification_type`, `identification_value`, and `cardholder_birthdate` [ajawadmirza] #4540 +* Rapyd: Remove `BR`, `MX`, and `US` from supported countries [ajawadmirza] #4558 +* Stripe Payment Intents: fix bug with billing address email [jcreiff] #4556 +* Shift4: Add customer to `purchase` & `store` and remove transaction from `store` [ajawadmirza] #4557 +* MerchantE: only add `moto_commerce_ind` to request if it is present [ajawadmirza] #4560 +* Add BpPlus card type along with custom validation logic [dsmcclain] #4559 +* PayTrace: Support ACH implementation for new endpoints and request body [ajawadmirza] #4545 +* Rapyd: No force capture for ACH [naashton] #4562 +* Shift4: Applied checks on Shift4 Time/Timezone offset [ali-hassan] #4561 +* Alelo: Add gateway [heavyblade] #4555 +* Wompi: Allow partial refund amount on void_sync [jcreiff] #4535 +* Shift4: Timezone Offset [naashton] #4566 +* MerchantE: `recurring_pmt_num` and `recurring_pmt_count` fields [ali-hassan] #4553 +* Orbital: Add South African Rand to supported currencies [molbrown] #4569 +* Orbital: Fix CardSecValInd [molbrown] #4563 +* Shift4: Add `usage_indicator`, `indicator`, `scheduled_indicator`, and `transaction_id` fields [ajawadmirza] #4564 +* Shift4: Retrieve `access_token` once [naashton] #4572 +* Redsys: Update Base64 encryption handling for secret key [jcreiff] #4565 +* Shift4: refuse `postalCode` when its null [ajawadmirza] #4574 +* Plexo: Update param key to `refund_type` [ajawadmirza] #4575 +* Shift4: Update request params for `verify`, `capture`, and `refund` [ajawadmirza] #4577 +* CyberSource: Add support for `sec_code` [rachelkirk] #4581 +* BraintreeBlue: Correctly vault payment method token for PayPal Checkout with Vault [almalee24] #4579 +* BpPlus: Allow spaces in card number [ajawadmirza] #4585 +* Shift4: Decline referral transactions and parse message for internal server errors [ajawadmirza] #4583 +* Litle: Update homepage_url [gasb150] #4491 +* Priority: Update credential handling [therufs] #4571 +* Shift4: Fix authorization and remove `entryMode` from verify and store transactions [ajawadmirza] #4589 + +== Version 1.126.0 (April 15th, 2022) +* Moneris: Add 3DS MPI field support [esmitperez] #4373 +* StripePI: Add ability to change payment_method_type to confirm_intent [aenand] #4300 +* GlobalCollect: Improve support for Naranja and Cabal card types [dsmcclain] #4286 +* Payflow: Add support for stored credentials [ajawadmirza] #4277 +* Orbital: Don't void $0 auths for Verify [javierpedrozaing] #2487 +* StripePI: Enable Apple Pay and Google Pay payment methods [gasb150] #4252 +* PaySafe: Update `unstore` method and authorization for redact [ajawadmirza] #4294 +* CyberSource: Add `national_tax_indicator` fields in authorize and purchase [ajawadmirza] #4299 +* NMI: Update gateway credentials to accept security_key [javierpedrozaing] #4302 +* PaySafe: Fix commit for `unstore` method [ajawadmirza] #4303 +* Ebanx: Add support for `order_number` field [ali-hassan] #4304 +* BlueSnap: Add support for `idempotency_key` field [drkjc] #4305 +* Paymentez: Update `capture` method to verify by otp for pending transactions [ajawadmirza] #4267 +* BlueSnap: Update refund request and endpoint along with merchant transaction support [ajawadmirza] #4307 +* DecidirPlus: Added `authorize`, `capture`, `void`, and `verify` methods [ajawadmirza] #4284 +* Paymentez: Fix `authorize` to call `purchase` for otp flow [ajawadmirza] #4310 +* Orbital: Indicate support for network tokenization [dsmcclain] #4309 +* IPG: remove `uruguay` from supported countries [ajawadmirza] #4311 +* Decidir: Add sub_payments sub-fields to gateway [meagabeth] #4315 +* Priority: Add additional fields to purchase and capture requests [dsmcclain] #4301 +* DecidirPlus: Added `unstore` method [ajawadmirza] #4317 +* Decidir & Decidir Plus: Revise handling of `sub_payment` sub-fields [meagabeth] #4318 +* DecidirPlus: Update `unstore` implementation to get token from params [ajawadmirza] #4320 +* CyberSource: Add option for zero amount verify [gasb150] #4313 +* PayU Latam: Refactor `message_from` method, fix failing remote tests [rachelkirk] #4326 +* Adyen: Add currencies with three decimals places [gasb150] #4322 +* GlobalCollect: Stregthen success criteria for void action [peteroas] #4324 +* Priority Payment Systems - Clean up/refactor gateway file and tests [ali-hassan] #4327 +* SafeCharge: change `verify` to send 0 amount [dsmcclain] #4332 +* DLocal: add support for `force_type` field [dsmcclain] #4336 +* Barclaycard SmartPay: Support more nonstandard currencies [jherreraa] #4335 +* DecidirPlus: `name_override` option on `store` [naashton] #4338 +* Priority: Update `add_purchases_data` to return if `options[:purchases]` is empty [drkjc] #4349 +* Stripe PI: update `shipping` field to `shipping_address` [ajawadmirza] #4347 +* DecidirPlus: Handle `payment_method_id` by `card_brand` [naashton] #4350 +* DecidirPlus: `debit` and `payment_method_id` fields [naashton] #4351 +* Adyen: Include Application ID in adyen authorize and purchase transactions [peteroas] #4343 +* Priority: Add support for `replay_id` field [drkjc] #4352 +* Stripe PI: standardize `shipping_address` fields [dsmcclain] #4355 +* Airwallex: support gateway [therufs] #4342 +* Litle: Translate google_pay as android_pay [javierpedrozaing] #4331 +* Braintree: Add ACH support for store [cristian] #4285 +* Simetrik: Add support for Simetrik gateway [simetrik-frank] #4339 +* EBANX: Change amount for Mexico and Chile [flaaviaa] #4337 +* DecidirPlus: Add `establishment_name`, `aggregate_data`, `sub_payments`, `card_holder_identification_type`, `card_holder_identification_number`, `card_door_number`, and `card_holder_birthday` fields [ajawadmirza] #4361 +* DecidirPlus: Update `error_code_from` to get error reason id [ajawadmirza] #4364 +* Dlocal: Add three_ds mpi support [cristian] #4345 +* Stripe PI: Add `request_three_d_secure` field for `create_setup_intent` [aenand] #4365 +* Adyen: Add `verify_amount` field for verify [ajawadmirza] #4369 +* Stripe PI: Pass options for tokenizing Apple/Google Pay [gasb150] #4368 +* Dlocal: Format 3DS mpi enrollment data correctly [cristian] #4371 +* Airwallex: QA fixes for option handling [therufs] #4367 +* CardConnect: Fixed duplicate(concat) Address sent - card_connect is concat. address1 and 2 causing a AVS error [ahmirza] #4362 +* CyberSource: Remove Pinless Debit Transaction Functionality [peteroas] #4370 +* Litle: Add support for Level 2 and 3 enhanced data [curiousepic] #4360 +* Rapyd: Add gateway support [meagabeth] #4372 +* CyberSource: Update and fix test coverage [peteroas] #4374 +* Airwallex: QA fixes for address and create_setup_intent handling [therufs] #4377 +* Airwallex: add `descriptor` field and update logic for sending `request_id` and `merchant_order_id` [dsmcclain] #4379 +* Visanet Peru: use timestamp instead of random for purchaseNumber [therufs] #4093 +* Orbital: add `verify_amount` field [ajawadmirza] #4376 +* Credorax: add `recipient_street_address`, `recipient_city`, `recipient_province_code`, and `recipient_country_code` fields [ajawadmirza] #4384 +* Airwallex: add support for stored credentials [drkjc] #4382 +* Rapyd: Add metadata and ewallet_id options [naashton] #4387 +* Priority: Add additional fields to request and refactor gateway integration [dsmcclain] #4383 +* Rapyd: Update `type` option to `pm_type` [naashton] #4391 +* Conekta: Fix remote test [javierpedrozaing] #4386 +* NMI: Update post URL [jherreraa] #4380 +* Multiple Gateways: Resolve when/case bug [naashton] #4399 +* Airwallex: Add 3DS MPI support [drkjc] #4395 +* Add Cartes Bancaires card bin ranges [leahriffell] #4398 +* Airwallex: Add support for `original_transaction_id` field [drkjc] #4401 +* Securion Pay: Pass external 3DS data [jherreraa] #4404 +* Airwallex: Update Stored Credentials testing, remove support for `original_transaction_id` field [drkjc] 4407 + +== Version 1.125.0 (January 20, 2022) +* Wompi: support gateway [therufs] #4173 +* Stripe Payment Intents: Add setup_purchase [aenand] #4178 +* Ipg: Add new gateway [ajawadmirza] #4171 +* Worldpay: Adding support for google pay and apple pay [cristian] #4180 +* Worldpay: Adding scrubbing for network token transactions [cristian] #4181 +* SafeCharge: Add sg_NotUseCVV field [ajawadmirza] #4177 +* PayULatam: Correctly map maestro and condensa card types [dsmcclain] #4182 +* StripePaymentIntents: Refactor response for setup_purchase [aenand] #4183 +* Wompi: cast error messages to JSON [therufs] #4186 +* NMI: Omit initial_transaction_id for CIT [aenand] #4189 +* Priority: Support Priority Payment Systems gateway [jessiagee] #4166 +* GlobalCollect: Support for Lodging Data [naashton] #4190 +* IPG: Add support for sub-merchant and recurring type fields [ajawadmirza] # 4188 +* Wompi: Support `installments` option [therufs] #4192 +* Stripe PI: add support for `fulfillment_date` and `event_type` [dsmcclain] #4193 +* Paysafe: Adjust logic for sending 3DS field [meagabeth] #4194 +* Priority: Fix unit test cases [ajawadmirza] #4195 +* EBANX: New Gateway Specific Receiver [spreedly-kledoux] #4198 +* Wompi: Don't send CVV field if no CVV provided [therufs] #4199 +* Worldpay: cleaning order_id according to worldpay rules [cristian] #4197 +* Paysafe: Concatenate credentials for headers [meagabeth] #4201 +* Stripe Payment Intents: Add metadata to setup_purchase [aenand] #4202 +* Priority: Add gateway standard changes [ajawadmirza] #4200 +* IPG: Add support for payment by token [ajawadmirza] #4191 +* Element (Vantiv Express): Add support for general credit [dsmcclain] #4203 +* Worldpay: Update supported countries list, currencies [jherreraa] #4207 +* StripePI: Adding countries available. [gasb150] #4208 +* Orbital: Adding google pay payment tests for Orbital. [ajawadmirza] #4205 +* Bug: Fixing supported countries method when there is inheritance involved [cristian] #4211 +* Mundipagg: Update success method [ajawadmirza] #4210 +* Worldpay: Add support for Visa Direct Fast Funds Credit [dsmcclain] #4212 +* Paysafe: Add support for stored credentials [meagabeth] #4214 +* Worldpay: Adding missing countries to supported countries [cristian] #4213 +* Update institution numbers for Canadian banks [therufs] #4216 +* Worldpay: Set default eCommerce indicator for EMVCO network tokens [shasum] #4215 +* Update handling routing numbers for Canadian banks [therufs] #4217 +* Stripe: API version updated [jherreraa] #4209 +* Mercado Pago: Update verify method [ajawadmirza] #4219 +* DLocal: Set API Version [gasb150] #4222 +* Wompi: Add support for Authorize and Capture [rachelkirk] #4218 +* Priority: Update source and billing address checks [jessiagee] #4220 +* Pin Payments: Add support for `diners_club`, `discover`, and `jcb` cardtypes [montdidier] #4142 +* USA ePay: Add store method [ajawadmirza] #4224 +* IPG: Quick fix to remove warning [ajawadmirza] #4225 +* Remove YAML warning on load_fixtures_method [jherreraa] #4226 +* Worldpay: Add support for tokenizing payment methods with transaction identifiers [dsmcclain] #4227 +* USA ePay: Update implementation to send valid authorization [ajawadmirza] #4231 +* USA ePay: Add store test, update authorize param [jessiagee] #4232 +* Stripe: Update destination test account [jherreraa] #4234 +* Add skip_response option on request check for commit stubs [cristian] #4223 +* Pin Payments: Add support for `void` and New Zealand to supported countries. [montdidier] #4144 +* Wompi: Update authorization in `capture` method. [rachelkirk] #4238 +* IPG: Update authorization to support `store` method token. [ajawadmirza] #4233 +* Paymentez: Update card mappings [ajawadmirza] #4237 +* Priority: Update parsing for error messages [jessiagee] #4245 +* GlobalCollect: Support for Airline Data [naashton] #4187 +* IPG: Add `tpv_error_code` and `tpv_error_msg` fields [ajawadmirza] #4241 +* StripePI: Set restriction for Apple/Google Pay [jherreraa] #4247 +* Cashnet: support multiple itemcodes and amounts [peteroas] #4243 +* IPG: Send default currency in `verify` and two digit `ExpMonth` [ajawadmirza] #4244 +* Stripe: Add remote tests set up to avoid exceed the max external accounts limit [jherreraa] #4239 +* Stripe: Add support for `radar_options: skip_rules` [dsmcclain] #4250 +* CyberSource: Add `user_po`, `taxable`, `national_tax_indicator`, `tax_amount`, and `national_tax` fields [ajawadmirza] #4251 +* Kushki: Add support for `metadata` [rachelkirk] #4253 +* IPG: Add `redact` operation [ajawadmirza] #4254 +* Wompi: Update sandbox and production endpoints [rachelkirk] #4255 +* Orbital: Add `sca_merchant_initiated` operation [ajawadmirza] #4256 +* Cashnet: convert amounts to integers for proper gateway handling [peteroas] #2207 +* PayTrace: Add `unstore` operation [ajawadmirza] #4262 +* Decidir Plus: Add gateway adapter [naashton] #4264 +* CheckoutV2: Add support for Apple Pay and Google Pay tokens [AMHOL] #4235 +* Decidir Plus: Update payment reference [naashton] #4271 +* Paysafe: Update redact method [meagabeth] #4269 +* CyberSource: Add `line_items` field in authorize method [ajawadmirza] #4268 +* CheckoutV2: Support processing channel and marketplace sub entity ID [AMHOL] #4236 +* Elavon: `third_party_token` bug fix [rachelkirk] #4273 +* Decidir Plus: Add `sub_payments` field [naashton] #4274 +* Pin Payments: Add `unstore` support [montdidier] #4276 +* Orbital: Add support for $0 verify [javierpedrozaing] #4275 +* Update inline documentation with all supported cardtypes [ali-hassan] #4283 +* PayWay: Update endpoints, response code [jessiagee] #4281 +* CyberSource: Add `line_items` for purchase [ajawadmirza] #4282 +* Payflow Pro: Add `stored_credential` fields [ajawadmirza] #4277 +* Decidir Plus: Add `fraud_detection` fields [naashton] #4289 + +== Version 1.124.0 (October 28th, 2021) +* Worldpay: Add Support for Submerchant Data on Worldpay [almalee24] #4147 +* dlocal: Add device_id and ip to payer object and add additional_data [aenand] #4116 +* Adyen: Add network tokenization support to Adyen gateway [mymir] #4101 +* Adyen: Add ACH Support [almalee24] #4105 +* Moka: Support 3DS endpoint and update test url [dsmcclain] #4110 +* Paysafe: Adjust profile data [meagabeth] #4112 +* Stripe Payment Intents: Add support for claim_without_transaction_id field [BritneyS] #4111 +* Mit: Add New Gateway [EsporaInfra] #3820 +* Routex: add card type [rachelkirk] #4115 +* Orbital: Scrub Payment Cryptogram [naashton] #4121 +* Paysafe: Add support for airline fields [meagabeth] #4120 +* Stripe and Stripe PI: Add Radar Session Option [tatsianaclifton] #4119 +* PayArc: Fix billing address nil and phone_number issues [dsmcclain] #4114 +* Routex: Update BIN numbers [rachelkirk] #4123 +* UnionPay: Add Stripe's UnionPay test card to UnionPay BIN range #4122 +* GlobalCollect: Support URL override [naashton] #4127 +* PayConex: scrub bank account info from transcripts [mbreenlyles] #4128 +* Moka: Remove additional transaction data from subsequent calls [naashton] #4129 +* Moka: Ensure CvcNumber can be an empty string [jessiagee] #4130 +* Maestro: Allow more card lengths for Luhnless bins [therufs] #4131 +* Paysafe: Update supported countries [meagabeth] #4135 +* Paysafe: Update field mapping for split_pay [meagabeth] #4136 +* SafeCharge: Add handling for non-fractional currencies [dsmcclain] #4137 +* CardStream: Support passing country_code in request [dsmcclain] #4139 +* Adyen: Adjust phone number mapping [aenand] #4138 +* Mit: Change how parameters are converted to JSON [tatsianaclifton] #4140 +* Stripe: Add account_number to scrubbing [aenand] #4145 +* Stripe PI: add name on card to billing_details [dsmcclain] #4146 +* TrustCommerce: Scrub bank account info [mbreenlyles] #4149 +* TransFirst: Scrub account number [aenand] #4152 +* Paysafe: Update supported countries list [meagabeth] #4154 +* dLocal: Update supported countries list [mbreenlyles] #4155 +* SafeCharge: Add support for email field in capture [rachelkirk] #4153 +* Paysafe: Remove invalid code [meagabeth] #4156 +* NMI: Add descriptor fields [ajawadmirza] #4157 +* Authorize.net: Add tests for scrubbing banking account info (in addition to BluePay, BridgePay, Forte, HPS, and Vanco Gateways)[aenand] #4159 +* Moka: Send refund amount with decimal [dsmcclain] #4160 +* GlobalCollect: Append URI to the URL [naashton] #4162 +* Adyen: Add application info fields [aenand] #4163 +* Adyen: Send NTID from stored cred hash [curiousepic] #4164 +* Payflow: use proper case for 3DS 2.x element names [bbraschi] #4113 +* Realex: Add support for stored credentials [dsmcclain] #4170 +* Moka: Add support for InstallmentNumber field [dsmcclain] #4172 +* Payflow: include AuthenticationStatus for 3DS 2.x [bbraschi] #4168 + +== Version 1.123.0 (September 10th, 2021) +* Paysafe: Add gateway integration [meagabeth] #4085 +* Elavon: Support recurring transactions with stored credentials [cdmackeyfree] #4086 +* Orbital: Truncate three_d_secure[:version] [carrigan] #4087 +* Credorax: Determine ISK decimal by datetime [curiousepic] #4088 +* Moka: support new gateway type [dsmcclain] #4089 +* Paymentez: Add more_info field [reblevins] #4091 +* Worldpay: Support $0 auth [therufs] #4092 +* Elavon: Support recurring transactions with token, revert stored credentials recurring [cdmackeyfree] #4089 +* SafeCharge(Nuvei): Add support for product_id [rachelkirk] #4095 +* NMI: Change cardholder_auth 3DS field population [carrigan] #4094 +* Synchrony: add card type [therufs] #4096 +* Maestro: support BINs without Luhn check [therufs] #4097 +* Maestro: support BINs [therufs] #4098 +* Redsys: Route MIT Exemptions to webservice endpoint [curiousepic] #4081 +* Adyen: Update Classic Integration API to v64 and Recurring API to v49 [almalee24] #4090 +* Payeezy: support soft_descriptor and merchant_ref [cdmackeyfree] #4099 +* Elavon: add ssl_token field [cdmackeyfree] #4100 +* Credorax: Remove special logic for ISK [curiousepic] #4102 +* UnionPay: Pull UnionPay's 62* BIN ranges out of Discover's #4103 +* Monei: Update Creation of Billing Details [tatsianaclifton] #4107 +* Monei: Typo Correction on Billing Details [tatsianaclifton] #4108 +* Paysafe: Add support for 3DS [meagabeth] #4109 + +== Version 1.122.0 (August 3rd, 2021) +* Orbital: Correct success logic for refund [tatsianaclifton] #4014 +* usaepay: Added pin gateway setting [DustinHaefele] #4026 +* MercadoPago: Added external_reference, more payer object options, and metadata field [DustinHaefele] #4020 +* Element: Add duplicate_override_flag [almalee24] #4012 +* PayTrace: Support gateway [meagabeth] #3985 +* vPOS: Support credit + refund [therufs] #3998 +* PayArc: Support gateway [senthil-code] #3974 +* NMI: Support cardholder_auth field for 3DS2 [cdmackeyfree] #4002 +* Confiable: Support cardtype [therufs] #4004 +* Maestro: Add BIN [therufs] #4003 +* PayULatam: Ensure phone number is pulled from shipping_address correctly [dsmcclain] #4005 +* SafeCharge: Add challenge_preference for 3DS [klaiv] #3999 +* Adyen: Pass networkTxReference in all transactions [naashton] #4006 +* Adyen: Ensure correct transaction reference is selected [dsmcclain] #4007 +* PayTrace: Support level_3_data fields [meagabeth] #4008 +* BluePay: Add support for Stored Credentials [dsmcclain] #4009 +* Orbital: Add support for SCARecurringPayment [jessiagee] #4010 +* Braintree: Support recurring_first and moto reasons [curiousepic] #4013 +* PayTrace: Adjust capture method [meagabeth] #4015 +* BarclaysEpdqExtraPlus: updated custom_eci test + remote tests [yyapuncich] #4022 +* CyberSource: Add customerID field [deemeyers] #4025 +* CyberSource: Adjust Auth [naashton] #3956 +* Valid Canadian Institution Numbers [naashton] #4024 +* PayTrace: Adjust purchase and capture methods to handle MultiResponse scenarios [meagabeth] #4027 +* Payflow: Add support for MERCHDESCR field [rachelkirk] #4028 +* PayTrace: Support $0 authorize in verify method [meagabeth] #4030 +* PayArc: Add error_code in response [cdm-83] #4021 +* Update bank routing account validation check [jessiagee] #4029 +* Kushki: Add 'contactDetails' fields [mbreenlyles] #4033 +* Adyen: Truncating order_id and remote test [yyapuncich] #4036 +* CyberSource: Allow string content for Ignore AVS/CVV flags [curiousepic] #4043 +* Decidir: Update validation error message handling [arbianchi] #4042 +* Authorize.net: Remove cardholderAuthentication for non-3DS transactions [BritneyS] #4045 +* BlueSnap: Handle 429 errors [britth] #4044 +* Orbital: Update unit test files [meagabeth] #4046 +* Orbital: Strip null characters from responses [britth] #4041 +* Merchant Warrior: Handle invalid XML responses [arbianchi] #4047 +* Braintree: Fix NoMethodError for failed card verification [molbrown] #4048 +* Worldpay: Accepting 3DS1 and 3DS2 authentication data from external MPI [chandan-PS] #4017 +* PayArc: Currency and parameters updates [jessiagee] #4051 +* Elavon: Add support for special characters [mbreenlyles] #4049 +* PayArc: Formatting CC month, adding tax_rate, removing default void reason [jessiagee] #4053 +* Kushki: Add support for fullResponse field [rachelkirk] #4057 +* Element: Add support for `MerchantDescriptor` field [BritneyS] #4058 +* PayArc: Added email and phone to credit and charge [jessiagee] #4056 +* Mundipagg: Added support for 'authentication_secret_key' for 'api_key' overwrite [DustinHaefele] #4059 +* Payflow: Raise an error if store method is called [dsmcclain] #4066 +* Monei: JSON API implementation [jimmyn] #3613 +* Maestro: Update BINs [therufs] #4067 +* Monei: Change domain to monei.com [jimmyn] #4068 +* Spreedly: Support gateway_specific_response_fields in response params [abarrak] #4064 +* Payeezy: Add support for `add_soft_descriptors` [rachelkirk] #4069 +* Stripe Payment Intents: Add support for network_transaction_id field [cdmackeyfree] #4060 +* Worldpay: Support 'CAPTURED' response for authorize transactions [naashton] #4070 +* Ingenico (Global Collect): New idempotence key header [BritneyS] #4073 +* PayTrace: Adjust handling of line_items subfields [meagabeth] #4074 +* Worldpay: Correct Expiration Year Format [tatsianaclifton] #4076 +* Monei: Improve Scrub Regex [tatsianaclifton] #4072 +* Payflow: add THREEDSVERSION and DSTRANSACTIONID when present [bbraschi] #4075 +* CT Payments: update remote tests [cdmackeyfree] #3947 +* Orbital: Ensure full e-check scrubbing [mbreenlyles] #4079 + +== Version 1.121 (June 8th, 2021) +* Braintree: Lift restriction on gem version to allow for backwards compatibility [naashton] #3993 +* Payment Express/Windcave: Send amount on verify calls [cdmackeyfree] #3995 +* Orbital: Use billing_address name as fallback [curiousepic] #3966 +* vPOS: handle shop_process_id correctly [therufs] #3996 +* Checkout v2: Support metadata field [saschakala] #3992 +* Adyen: Support networkTxReference field [naashton] #3997 +* Paypal Express: Enable PayPal express reference transaction request to send merchant session id [janees-e] #3994 + +== Version 1.120.0 (May 28th, 2021) +* Braintree: Bump required braintree gem version to 3.0.1 +* Stripe PI: ensure `setup_future_sage` and `off_session` work when using SetupIntents. +* Orbital: Update commit to accept retry_logic in params [jessiagee] #3890 +* Orbital: Update remote 3DS tests [jessiagee] #3892 +* Mercado Pago: support Creditel card type [therufs] #3893 +* Payeezy: Update error mapping [meagabeth] #3896 +* HPS: Add support for stored_credential [cdmackeyfree] #3894 +* Orbital: Ensure payment_detail sends for ECP [jessiagee] #3899 +* Payeezy: Update `error_code_from` method [meagabeth] #3900 +* Worldpay: Add support for `statementNarrative` field [meagabeth] #3901 +* Mercado Pago: Give ability to pass capture option in authorize txn field [naashton] #3897 +* Orbital: Ensure correct fields sent in refund [jessiagee] #3903 +* WorldPay: remove some defaults in billing address [carrigan] #3902 +* Adyen: Support for General Credit [naashton] #3904 +* Worldpay: reintroduce address1 and city defaults [carrigan] #3905 +* Stripe: ensure potentially nested data is scrubbed #3907 +* Stripe PI: Send Validate on Payment Method Attach [tatsianaclifton] #3909 +* Adyen: Update handling of authorization returned from gateway [meagabeth] #3910 +* Update gateway templates for Rubocop compliance [therufs] #3912 #3895 +* Orbital: Send AVSname for all eCheck transactions [jessiagee] #3911 +* Litle: update support of customerId field [cdmackeyfree] #3913 +* Payment Express: fix signature for `verify` [therufs] #3914 +* Forte: Send xdata fields [dsmcclain] #3915 +* PaywayDotCom: Add New Gateway [DanAtPayway] #3898 +* Orbital: Remove unnecessary requirements [jessiagee] #3917 +* SafeCharge (Nuvei): Add network tokenization support [DStoyanoff] #3847 +* Stripe PI: Enhance testing of SetupIntents API #3908 +* SafeCharge (Nuvei): Fix NT related bug [jimilpatel24] #3921 +* Worldpay: Only override cardholdername for 3ds tests [curiousepic] #3918 +* Orbital: Add support for general credit [meagabeth] #3922 +* Banco Sabadell: Ensure sca_exemption field is used #3923 +* Redsys: Refactor XML character escape logic #3925 +* HPS: Strip zip codes of non-alphanumeric characters [dsmcclain] #3926 +* Orbital: $0 PreNote using authorize for eCheck force_capture [jessiagee] #3927 +* Worldpay: synchronous response changes [naashton] #3928 +* PaywayDotCom: Add more thorough scrubbing [tatsianaclifton] #3929 +* Remove CONTRIBUTING.md and update README.md to reflect new repository wiki [dsmcclain] #3930 +* Qvalent: Add customer_reference_number [fredo-] #3931 +* Orbital: Add 'ND' ECPActionCode to $0 Prenote Check [jessiagee] #3935 +* Checkout: Add support for stored_credential [meagabeth] #3934 +* Credorax: Add support for 3ds_reqchallengeind [dsmcclain] #3936 +* Adyen: cancelOrRefund endpoint when passed as option [naashton] #3937 +* Qvalent: Add customer reference number FIX [fredo-] #3939 +* Orbital: Pass line_items in capture [jessiagee] #3941 +* BraintreeBlue: Add support for $0 auth verification [meagabeth] #3944 +* JCB: Add additional BIN ranges [dsmcclain] #3946 +* vPOS: Support new gateway type [therufs] #3906 +* Braintree: Add support for AVS and CVV results in $0 credit card verification transactions [meagabeth] #3951 +* Braintree: Return cvv_code and avs_code in response [meagabeth] #3952 +* vPOS: Stringify values [therufs] #3954 +* Payeezy: Send level2 fields [dsmcclain] #3953 +* Credorax: adjust logic for sending 3ds shipping address fields [dsmcclain] #3959 +* Orbital: Ensure ECP always sends AVSName [jessiagee] #3963 +* Orbital: Add middle name to EWSMiddleName for ECP [jessiagee] #3962 +* Support Canadian Bank Accounts [naashton] #3964 +* Windcave/Payment Express: Add support for AvsAction and EnableAVSData fields [meagabeth] #3967 +* CyberSource: Update XML tag for merchantDefinedData [meagabeth] #3969 +* Elavon: Send ssl_vendor_id field [dsmcclain] #3972 +* Credorax: Add support for `echo` field [meagabeth] #3973 +* Worldpay: support cancelOrRefund via options [therufs] #3975 +* Payeezy: support general credit [cdmackeyfree] #3977 +* Ripley and Hipercard: Add BIN ranges [naashton] #3978 +* Adyen: Default card holder name for credit cards [shasum] #3980 +* PaywayDotCom: make `source_id` a required field [dsmcclain] # 3981 +* Qvalent: remove `pem_password` from required credentials [dsmcclain] #3982 +* Authorize.net: Fix stored credentials [tatsianaclifton] #3971 +* CyberSource: Add support for multiple new fields [dsmcclain] #3984 +* CASHNet: Update gateway adapter [dsmcclain] #3986 +* Elavon: Send `ssl_vendor_id` field via options on gateway initialization [dsmcclain] #3989 + +== Version 1.119.0 (February 9th, 2021) +* Payment Express: support verify/validate [therufs] #3874 +* GlobalCollect: Truncate address fields [meagabeth] #3878 +* Litle: Truncate address fields [meagabeth] #3877 +* Netbanx: Add-customer-information(name,email,IP)-to-a-transaction [rockyhakjoong] #3754 +* Netbanx: Adjust the avs and cvv return code in shopify [rockyhakjoong] #3833 +* Decidir: Improve error mapping [meagabeth] #3875 +* Worldpay: support `skip_capture` [therufs] #3879 +* Redsys: Add new response code text [britth] #3880 +* Orbital: Update ECP details to use payment source [jessiagee] #3881 +* Alelo: Add additional BIN ranges [meagabeth] #3882 +* HPS: Update Add support for general credit [naashton] #3885 +* Elavon: Fix issue with encoding data sent in the request [naashton] #3865 +* Orbital: Update ECP to use EWS verification [jessiagee] #3886 +* Eway: Add 3ds field when do direct payment [GavinSun9527] #3860 +* Support Creditel cardtype [therufs] #3883 +* Elavon: Remove ampersand char from fields [naashton] #3891 + +== Version 1.118.0 (January 22nd, 2021) +* Worldpay: Add support for challengeWindowSize [carrigan] #3823 +* Adyen: Update capitalization on subMerchantId field [cdmackeyfree] #3824 +* Maestro and Elo: Update BIN ranges [meagabeth] #3822 +* HPS: Truncate invoice numbers that are too long [curiousepic] #3825 +* Pass network_transaction_id attribute in Response [therufs] #3815 +* Elavon: support standardized stored credentials [therufs] #3816 +* Decidir: update fraud_detection field [cdmackeyfree] #3829 +* Paymentez: Add Olimpica cardtype [meagabeth] #3831 +* SafeCharge: 3DS external MPI data refinements [curiousepic] #3821 +* Credorax: Add support for 3DS Adviser [meagabeth] #3834 +* Adyen: Support subMerchant data [mymir][therufs] #3835 +* Decidir: add device_unique_identifier to card data [cdmackeyfree] #3839 +* BraintreeBlue: add support for account_type field [jimilpatel24] #3840 +* Redsys: Add support for stored_credential [meagabeth] #3844 +* Redsys: add_payment method solution [meagabeth] #3845 +* Stripe Payment Intents: Add support for error_on_requires_action option [tatsianaclifton] #3846 +* Add 3DS 2.0 values to paypal [nebdil] #3285 +* Redsys: Update Mpi Fields [tatsianaclifton] #3855 +* Paypal: Update AuthStatus3ds MPI field [curiousepic] #3857 +* Orbital: Update 3DS support for Mastercard [meagabeth] #3850 +* Payeezy: Support standardized stored credentials [therufs] #3861 +* CyberSource: Update `billing_address` override [meagabeth] #3862 +* Paymentez: Add 3DS MPI field support [carrigan] #3856 +* BlueSnap: Add support `fraud-session-id` field [meagabeth] #3863 +* BlueSnap: Update handling of `transaction-fraud-info` fields [meagabeth] #3866 +* Payeezy: Allow no stored credential transaction id [therufs] #3868 +* Orbital: eCheck processing added [ajawadmirza] #3870 +* FirsdataE4V27: Fixes some apple pay transaction issues [pi3r] #3872 + +== Version 1.117.0 (November 13th) +* Checkout V2: Pass attempt_n3d along with 3ds enabled [naashton] #3805 +* GlobalCollect: Add support for Third-party 3DS2 data [molbrown] #3801 +* Authorize.net: Pass stored credentials [therufs] #3804 +* Authorize.net: Don't pass isFirstRecurringPayment [therufs] #3805 +* Litle: Add support for general credit transactions [naashton] #3807 +* Redsys: Add 3DS2 Integration Support [esmitperez] #3794 +* Cybersource: Use firstname/lastname from address instead of the payment method [pi3r] #3798 +* Add MPI functionality for SafeCharge gateway [daniel] #3809 +* SafeCharge: Standardize MPI fields [curiousepic] #3809 +* Credorax: Adds AMEX to supported cards and adds 1A error code [LinTrieu] #3792 +* Stripe PI: Pass external 3DS auth data [curiousepic] #3811 +* Credorax: Allow 3DS1 normalized pass-through, ease version matching [britth] #3812 +* Redsys: Redsys: Harden 3DS v1/v2 check for External MPI [esmitperez] #3814 +* Add card types for Stripe, Worldpay, Checkout.com [LinTrieu] #3810 +* ActiveMerchant::Billing::Response: Include `network_transaction_id` attribute [therufs] #3815 + +== Version 1.116.0 (October 28th) +* Remove Braintree specific version dependency [pi3r] #3800 + +== Version 1.115.0 (October 27th) +* Checkout v2: $0 Auth on gateway [jessiagee] #3762 +* Adyen: Safely add execute_threeds: false [curiousepic] #3756 +* RuboCop: Fix Layout/SpaceAroundEqualsInParameterDefault [leila-alderman] #3720 +* iATS: Allow email to be passed outside of the billing_address context [naashton] #3750 +* Orbital: Don't pass xid for transactions using network tokens [britth] #3757 +* Forte: Add service_fee_amount field [meagabeth] #3751 +* WorldPay: Add support for idempotency_key[cdmackeyfree] #3759 +* Orbital: Handle line_tot key as a string [naashton] #3760 +* RuboCop: Fix Lint/UnusedMethodArgument [leila-alderman] #3721 +* RuboCop: Fix Naming/MemoizedInstanceVariableName [leila-alderman] #3722 +* RuboCop: Fix Style/BlockComments [leila-alderman] #3729 +* Checkout V2: Move to single-transaction Purchases [curiousepic] #3761 +* RuboCop: Fix Naming/ConstantName [leila-alderman] #3723 +* Orbital: Fix schema errors [britth] #3766 +* Checkout V2: Start testing via amount code [curiousepic] #3767 +* CyberSource: Don't include empty `mdd_` fields [arbianchi] #3758 +* RuboCop: Fix Naming/VariableNumber [leila-alderman] #3725 +* Update BIN ranges for Elo cardtype [cdmackeyfree] #3769 +* Orbital: Resolve CardIndicators issue [meagabeth] #3771 +* Adyen: Add subMerchant fields [naashton] #3772 +* PayPal Express: reduce param requirements [shasum] #3773 +* PayU Latam: Support partial refunds [leila-alderman] #3774 +* RuboCop: Fix Style/Alias [leila-alderman] #3727 +* Stripe PI: Allow `on_behalf_of` to be passed alone #3776 +* RuboCop: Fix Performance/RedundantMatch [leila-alderman] #3765 +* RuboCop: Fix Layout/MultilineMethodCallBraceLayout [leila-alderman] #3763 +* NMI: Add standardized 3DS fields [meagabeth] #3775 +* Mundipagg: Add support for SubMerchant fields [meagabeth] #3779 +* Stripe Payment Intents: Add request_three_d_secure option [molbrown] #3787 +* Decidir: Add support for csmdds fields [naashton] #3786 +* RuboCop: Fix Performance/StringReplacement [leila-alderman] #3782 +* RuboCop: Fix Naming/HeredocDelimiterCase & Naming [leila-alderman] #3781 +* BlueSnap: Add address fields to contact info [naashton] #3777 +* RuboCop: Fix Layout/SpaceInsideHashLiteralBraces [leila-alderman] #3780 +* RuboCop: Fix Style/AndOr [leila-alderman] #3783 +* Checkout V2: Support ability to pass attempt_n3d 3ds field [naashton] #3788 +* Elavon: Upgrade to `processxml.do` [therufs] #3784 +* Checkout V2: Support for attempt_n3d 3DS field [naashton] #3790 +* Elavon: Strip ampersands [therufs] #3795 +* Paybox: Add support for 3DS 1.0 values [jcpaybox] #3335 +* Decidir: Add additional fraud_detection options [cdmackeyfree] #3812 + +== Version 1.114.0 +* BlueSnap: Add address1,address2,phone,shipping_* support #3749 +* BlueSnap: Protect against `nil` metadata [carrigan] #3752 +* Cybersource: [CyberSource] Ensure the default address doesn't override `ActionController::Parameters` [pi3r] #3755 + +== Version 1.113.0 +* Orbital: Add cardIndicators field [meagabeth] #3734 +* Openpay: Add Colombia to supported countries [molbrown] #3740 +* Mercado Pago: Update Device Id Header field [cdmackeyfree] #3741 +* RuboCop: Fix Style/TrailingCommaInHashLiteral [leila-alderman] #3718 +* RuboCop: Fix Naming/PredicateName [leila-alderman] #3724 +* RuboCop: Fix Style/Attr [leila-alderman] #3728 +* Payflow: Use application_id to set buttonsource [britth] #3737 +* HPS: Enable refunds using capture transaction [britth] #3738 +* Quickbooks: Omit empty strings in address [leila-alderman] #3743 +* BlueSnap: Add transactionMetaData support #3745 +* Orbital: Fix typo in PC3DtlLineTot field [naashton] #3736 +* Credorax: Send first and last name parameters for CFT transactions [britth] #3748 +* Orbital: Update CardIndicators field to fix bug [meagabeth] #3746 +* CyberSource: Always send default address [leila-alderman] #3747 +* Netbanx: Reject partial refund on pending status [rockyhakjoong] #3735 + +== Version 1.112.0 +* Cybersource: add `maestro` and `diners_club` eci brand mapping [bbraschi] #3708 +* Cybersource: Ensure Partner Solution Id placement conforms to schema [britth] #3715 +* Adyen: Adyen: Pass `subMerchantId` as `additionalData` [naashton] #3714 +* Litle: Omit checkNum when nil [leila-alderman] #3719 +* 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 +* Realex: Change 3DSecure v1 message_version to a valid format [shuhala] #3702 +* Ingenico/ GlobalCollect: Add field for installments [cdmackeyfree] #3707 +* Cybersource: do not send 3DS fields if 'cavv` is missing and `commerceIndicator` is inferred [bbraschi] #3712 + +== Version 1.110.0 +* FirstData e4 v27+: Strip linebreaks from address [curiousepic] #3693 +* Adyen: Change shopper_email to email and shopper_ip to ip [rikterbeek] #3675 +* FirstData e4 v27+ Fix strip_line_breaks method [carrigan] #3695 +* Cybersource: Set authorization on the response even when in fraud review [pi3r] #3701 +* Cybersource: Add fields to override stored creds [leila-alderman] #3689 +* Cybersource: Conditionally find stored credentials [therufs] #3696 #3697 +* Cybersource: Update logic to send cavv as xid for 3DS2 [douglas] #3699 +* Credorax: Default 3ds_browsercolordepth to 32 when passed as 30 [britth] #3700 + +== Version 1.109.0 +* Remove reference to `Billing::Integrations` [pi3r] #3692 +* DLocal: Handle nil address1 [molbrown] #3661 +* Braintree: Add travel and lodging fields [leila-alderman] #3668 +* Stripe: strict_encode64 api key [britth] #3672 +* Stripe PI: Implement verify action [leila-alderman] #3662 +* Stripe, Stripe Payment Intents: Update supported countries [britth] #3684 +* Forte: Use underscore for unused arguments in test [wsmoak] #3605 +* Add Alia card type [therufs] #3673 +* Element: Fix unit tests [leila-alderman] #3676 +* PayU Latam: Fix store method [ccarruitero] #2590 +* Adyen: Allow for executeThreeD to be passed as false [naashton] #3681 +* WorldPay: Fix handling of `state` field for 3DS transactions [chinhle23] #3687 +* Alia: Skip Luhn validation [therufs] #3673 +* Diners Club: support 16 digit card numbers [therufs] #3682 +* Cybersource: Update supported countries [britth] #3683 +* Cybersource: pass reconciliation_id [therufs] #3688 +* RuboCop: Fix Style/SpecialGlobalVars [leila-alderman] #3669 +* RuboCop: Fix Style/StringLiteralsInInterpolation [leila-alderman] #3670 +* RuboCop: Fix Layout/HeredocIndentation [leila-alderman] #3685 +* RuboCop: Fix Gemspec/OrderedDependencies [leila-alderman] #3679 +* RuboCop: Fix Style/TrailingUnderscoreVariable [leila-alderman] #3663 +* RuboCop: Fix Style/WordArray [leila-alderman] #3664 +* RuboCop: Fix Style/SymbolArray [leila-alderman] #3665 +* Mercado-Pago: Notification url GSF [cdmackeyfree] #3678 +* Credorax: Update logic for setting 3ds_homephonecountry [britth] #3691 + +== Version 1.108.0 (Jun 9, 2020) +* Cybersource: Send cavv as xid is xid is missing [pi3r] #3658 +* Forte: Change default sec_code value to PPD [molbrown] #3653 +* Elavon: Add merchant initiated unscheduled field [leila-alderman] #3647 +* Decidir: Add aggregate data fields [leila-alderman] #3648 +* Vantiv: Vantiv(Element): add option to send terminal id in transactions [cdmackeyfree] #3654 +* Update supported Ruby and Rails versions [leila-alderman] #3656 +* CI: Drop unused sudo: false Travis directive [olleolleolle] #3616 +* PayU Latam: Prevent blank country in billing_address [britth] #3657 +* DLocal: Fix address field names [molbrown] #3651 + +== Version 1.107.4 (Jun 2, 2020) +* Elavon: Implement true verify action [leila-alderman] #3610 +* Vantiv Express: Implement true verify [leila-alderman] #3617 +* Litle: Pass expiration data for basis payment method [therufs] #3606 +* Stripe Payment Intents: Error handling and backwards compatibility within refund [britth] #3627 +* HPS: Prevent errors when account_type or account_holder_type are nil [britth] #3628 +* D Local: Handle invalid country code errors [curiousepic] #3626 +* Stripe Payment Intents: Utilize execute_threed flag to determine success [britth] #3625 +* Elavon: Add Level 3 fields [leila-alderman] #3632 +* CyberSource: Stored Credential fixes [curiousepic] #3624 +* CyberSource: Fix invalid and missing field tests [curiousepic] #3634 +* CyberSource: Pass stored credentials with purchase [curiousepic] #3636 +* Mercado Pago: Add payment_method_option_id field [schwarzgeist] #3635 +* Stripe: Provide error when attempting an authorize with ACH [britth] #3633 +* EBANX: Send original order id as merchant_payment_code metadata [miguelxpn] #3637 +* Element: Add card_present_code field [schwarzgeist] #3623 +* Orbital: Add support for Level 3 fields [leila-alderman] #3639 +* Firstdata: Strip newline characters from address [bittercoder] #3643 +* Forte: add sec_code attribute for echeck [wsmoak] #3640 + +== Version 1.107.3 (May 8, 2020) +* Realex: Ignore IPv6 unsupported addresses [elfassy] #3622 +* Cybersource: Set partnerSolutionID after the business rules, fixes 500 error [pi3r] #3621 + +== Version 1.107.2 (May 7, 2020) +* Cybersource: Send a specific card brand commerceIndicator for 3DS [pi3r] #3620 +* Cybersource: Send application_id as partnerSolutionID [pi3r] #3620 +* Iridium: Localize zero-decimal currencies [chinhle23] #3587 +* iVeri: Fix `verify` action [chinhle23] #3588 +* Ixopay: Properly support three-decimal currencies [chinhle23] #3589 +* Kushki: support `auth` and `capture` [therufs] #3591 +* PaymentExpress: Update references to Windcave to reflect rebranding [britth] #3595 +* Decidir: Improve handling of error responses from the gateway [naashton] #3594 +* CyberSource: Added support for MerchantInformation CyberSource-specific fields [apfranzen] #3592 +* ePay: Send unique order ids for remote tests [curiousepic] #3593 +* Checkout V2: Send more informative error messages for 4xx errors [britth] #3601 +* Elavon: Add ssl_dynamic_dba field [apfranzen] #3600 +* iATS Payments: Update gateway to v3 and add support for additional GSFs [naashton] #3599 +* Remove deprecated `rubyforge_project` attribute and tidy up unit test output [fatcatt316] #3598 +* Elavon: Cleanup inadvertant field removal (avs_address) in #3600 [apfranzen] #3602 +* EBANX: Fix transaction amount for verify transaction [miguelxpn] #3603 +* iATS Payments: Update gateway to accept `email`, `phone`, and `country` fields [naashton] #3607 +* Braintree: Fix response for failed refunds when falling back to voids [jasonwebster] #3608 +* Worldpay: Fix response for failed refunds when falling back to voids [jasonwebster] #3609 +* iATS Payments: Add support for Customer Code payment method [molbrown] #3611 +* HPS: Add Google Pay support [MSmedal] #3597 +* Adyen: Parse appropriate message for 3DS2 authorization calls [britth] #3619 +* CyberSource: Add error details response fields [schwarzgeist] #3629 + +== Version 1.107.1 (Apr 1, 2020) +* Add `allowed_push_host` to gemspec [mdeloupy] + +== Version 1.107.0 (Apr 1, 2020) +* Stripe Payment Intents: Early return failed `payment_methods` response [chinhle23] #3570 +* Borgun: Support `passengerItineraryData` [therufs] #3572 +* Ingenico GlobalCollect: support optional `requires_approval` field [fatcatt316] #3571 +* CenPOS: Update failing remote tests [britth] #3575 +* Realex: Update remote tests [britth] #3576 +* FirstData e4 v27: Properly tag stored credential initiation field in request [britth] #3578 +* Orbital: Fix stored credentials [chinhle23] #3579 +* Acapture(Opp): Update gateway credentials [molbrown] #3574 +* Ingenico GlobalCollect: support `requires_approval` field [fatcatt316] #3577 +* CyberSource: Fix `void` for `purchase` transactions [chinhle23] #3581 +* Checkout V2: Begin to add support for using network tokens for transactions. [arbianchi] #3580 +* Opp: Update remote test fixtures [ccarruitero] #3582 +* Optimal Payment: Add support for store [britth] #3585 +* SecurePay Australia : Update test URL (#3586) + +== Version 1.106.0 (Mar 10, 2020) +* PayJunctionV2: Send billing address in `auth` and `purchase` transactions [naashton] #3538 +* Adyen: Fix some remote tests [curiousepic] #3541 +* Redsys: Properly escape cardholder name and description fields in 3DS requests [britth] #3537 +* RuboCop: Fix Style/HashSyntax [leila-alderman] #3540 +* Paypal: Fix OrderTotal elements in `add_payment_details` [chinhle23] #3544 +* Stripe Payment Intents: Add tests for "Idempotency-Key" header [fatcatt316] #3542 +* Paypal: Fix RuboCop Style/HashSyntax violations [chinhle23] #3547 +* Rubocop corrections for space around operators [cdmackeyfree] #3543 +* Fat Zebra: Add `is_billing` in post for `store` call [chinhle23] #3551 +* SafeCharge: Adds four supported countries [carrigan] #3550 +* Ixopay: Support stored credentials [leila-alderman] #3549 +* BlueSnap: Adds localized currency support [carrigan] #3552 +* CheckoutV2: Use status as message for 3DS txns in progress [britth] #3545 +* Stripe Payment Intents: Prevent idempotency key errors for compound actions [britth] #3554 +* Adyen: Add tests for voiding with idempotency keys [jknipp] #3553 +* Fat Zebra: Fix `store` call [chinhle23] #3556 +* Update README to include Adyen [haolime] #3452 +* PayJunctionv2: Fix billing address fields [leila-alderman] #3557 +* Adyen: Fail unexpected 3DS responses [curiousepic] #3546 +* Merchant Warrior: Add support for setting soft descriptors [daBayrus] #3558 +* Adyen: Fix stored credentials [chinhle23] #3560 +* Update BIN ranges for Alelo and Maestro cards [leila-alderman] #3559 +* EBANX: Fix declines if order id is bigger than 40 chars [miguelxpn] #3563 +* Moneris US: Remove gateway [chinhle23] #3561 +* Decidir: Decidir: Improving the response message when encountering errors [naashton] #3564 +* PayBox: Added USERTrust RSA Certification Authority and Sectigo RSA Organization Validation Secure Server CA [baldowl] #3567 + +== Version 1.105.0 (Feb 20, 2020) +* Credorax: Fix `3ds_transtype` setting in post [chinhle23] #3531 +* Bambora Apac: Send void amount in options [leila-alderman] #3532 +* RuboCop: Fix Layout/IndentHash [leila-alderman] #3529 +* Stripe: Add connected account support [Carrigan] #3535 +* Redsys: Update scrub method to account for 3DS error responses [britth] #3534 +* Authorize.Net: Pass `account_type` to `check` payment types [chinhle23] #3530 +* Merchant Warrior: Send void amount in options [leila-alderman] #3525 +* Stripe: Add support for `statement_descriptor_suffix` field [Carrigan] #3528 +* Decidir: Add support for fraud_detection, site_id, and establishment_name [fatcatt316] #3527 +* HPS: support eCheck [therufs] #3500 +* EBANX: Add metadata information in post [miguelxpn] #3522 +* Worldpay: Add `riskData` GSF [fatcatt316] #3514 +* EBANX: Fix `scrub` [chinhle23] #3521 +* Worldpay: Remove unnecessary .tag! methods [leila-alderman] #3519 +* BPoint: Remove amount from void requests [leila-alderman] #3518 +* Authorize.net: Trim supported countries to AU, CA, US [fatcatt316] #3516 +* Credorax: Allow optional 3DS 2 fields [jeremywrowe] #3515 +* Stripe: Remove outdated 'customer options' deprecation [alexdunae] #3401 +* Added support for fraud review in CyberSource gateway [greg-burgoon] #3536 + +== Version 1.104.0 (Jan 29, 2020) +* Adyen: add `recurring_contract_type` GSF [therufs] #3460 +* Credorax: Only pass `3ds_version` parameter when required [britth] #3458 +* EBANX: Include Peru in supported countries [Ruanito] #3443 +* Bluesnap: include fraud data in response message [therufs] #3459 +* Ingenico GlobalCollect: support `airline_data` and related GSFs [therufs] #3461 +* Add UnionPay card type [leila-alderman] #3464 +* Braintree: Fix add_credit_card_to_customer in Store [molbrown] #3466 +* EBANX: Default to not send amount on capture [chinhle23] #3463 +* Latitude19: Convert money format to dollars [molbrown] #3468 +* Adyen: Fix response success for unstore [kheang] #3470 +* CyberSource: add several GSFs [therufs] #3465 +* Adyen: add `recurring_contract_type` GSF to auth [therufs] #3471 +* Stripe Payment Intents: Use localized_amount on capture [molbrown] #3475 +* dLocal: Add support for installments [kdelemme] #3456 +* Merchant Warrior: Add void operation [leila-alderman] #3474 +* Decidir: Update payment method IDs [leila-alderman] #3476 +* Adyen: Add delivery address [leila-alderman] #3477 +* Authorize.net: Correctly parse direct_response field with quotation marks [britth] #3479 +* Decidir: Add debit card payment method IDs [leila-alderman] #3480 +* CyberSource: Add issuer data+MDD to credit & void [leila-alderman] #3481 +* Credorax: add `authorization_type` and `multiple_capture_count` GSFs [therufs] #3478 +* CardStream: use localized_amount to correctly support zero-decimal currencies [britth] #3473 +* EBANX: Add additional data in post [Ruanito] #3482 +* Credorax: Omit phone when nil [leila-alderman] #3490 +* TransFirst TrExp: Remove hyphens from zip [leila-alderman] #3483 +* Mundipagg: Return acquirer code as the error code [leila-alderman] #3492 +* Braintree Blue: Remove customer hash when using a payment_method_nonce #3495 +* Credorax: Update non-standard currencies list [chinhle23] #3499 +* Redsys: Update production URL [britth] #3505 +* Moneris: include AVS and CoF fields when storing vault records [alexdunae] #3446 +* Moneris: Add support for temporary vault storage [alexdunae] #3446 +* Clearhaus: Update currencies without fractions list [chinhle23] #3506 +* Merchant Warrior: Add recurringFlag to purchase & authorize [carrigan] #3504 +* CardConnect: Remove domain port validation [leila-alderman] #3494 +* Paymentez: Correct refund and void message parsing [carrigan] #3509 +* Mercado Pago: Add taxes and net_amount gateway specific fields [carrigan] #3512 +* Moneris: use dedicated card_verification methods [alexdunae] #3428 +* Authorize.net: Trim down supported countries [fatcatt316] #3511 +* Stripe: Add support for `statement_descriptor_suffix` field [carrigan] #3528 +* Stripe: Add connected account support [carrigan] #3535 + +== Version 1.103.0 (Dec 2, 2019) +* Quickbooks: Mark transactions that returned `AuthorizationFailed` as failures [britth] #3447 +* Credorax: Add referral CFT transactions [leila-alderman] #3432 +* DLocal: Updates for version 2.1 [molbrown] #3449 +* CyberSource: Send MDD on capture [leila-alderman] #3453 +* Ixopay: Include extra_data gateway specific field [therufs] #3450 +* CyberSource: Fix XML error on capture [leila-alderman] #3454 +* Adyen: Add gateway specific field for splits [leila-alderman] #3448 +* Adyen: Add `unstore` and `storeToken` actions with '/Recurring' endpoint [deedeelavinder][davidsantoso] #3438 +* Barclaycard Smartpay: Add functionality to set 3DS exemptions via API [britth] #3457 +* Use null@cybersource.com when option[:email] is an empty string [pi3r] #3462 + +== Version 1.102.0 (Nov 14, 2019) +* Quickbooks: Make token refresh optional with allow_refresh flag [britth] #3419 +* Paymentez: Update supported countries [curiousepic] #3425 +* Ixopay: Add new gateway [jasonxp] #3426 +* Ixopay: Add support for currency option to refund method #3433 +* Ixopay: Remove default callback URL #3436 +* Ixopay: Refactor capture #3431 +* Update supported countries list. Add currencies without fractions / with 3 decimal places #3424 +* RuboCop: Fix Layout/EndAlignment [leila-alderman] #3427 +* RuboCop: Fix Layout/ExtraSpacing [leila-alderman] #3429 +* RuboCop: Fix Layout/MultilineOperationIndentation [leila-alderman] #3439 +* Worldpay: Update logic to set cardholderName for 3DS transactions [britth] #3444 +* Adopt new enrolled key for 3DS1 transactions. enrolled contains the 3… #3442 + +== Version 1.101.0 (Nov 4, 2019) +* Add UYI to list of currencies without fractions [curiousepic] #3416 +* Quickbooks: Add OAuth 2.0 support and void action [britth] #3397 +* Credorax: Stop always sending r1 parameter [molbrown] #3415 +* Rubocop: Layout/RescueEnsureAlignment fix [leila-alderman] #3411 +* CyberSource: Send issuer data on capture [leila-alderman] #3404 +* Rubocop: Style/IfUnlessModifier [nfarve] #3390 +* Redsys: Updates to parse method for non-3DS responses [britth] #3391 +* Netbanx: Add 3DS2 Support [Jujhar] #3394 + +== Version 1.100.0 (Oct 16, 2019) +* Stripe: Restore non-auto capture behaviour for card present transactions [PatrickFang] #3258 +* Revert "Revert "Worldpay: Switch to Nokogiri"" [curiousepic] #3373 +* Adyen: Fix `authorise3d` message for refusals [jeremywrowe] #3374 +* Redsys: Set authorization field for 3DS transactions [britth] #3377 +* Adyen: Add capture_delay_hours GSF [therufs] #3376 +* Credorax: Add support for stored credentials [chinhle23] #3375 +* BlueSnap: Add remote tests for Cabal and Naranja [leila-alderman] #3382 +* WorldPay: Add Cabal and Naranja remote tests [leila-alderman] #3378 +* Rubocop: Indentions [nfarve] #3383 +* Worldpay: Handle parse errors gracefully [curiousepic] #3380 +* BluePay: Add ability to pass doc_type in refunds and credits [britth] #3386 +* Stripe Payment Intents: Fix fallback for Store [waaux] #3343 +* Update Securionpay supported countries [hossamhossny] #2472 +* Visanet Peru: Add amount argument to Capture [curiousepic] #3389 +* Rubocop: Layout/MultilineHashBraceLayout [nfarve] #3385 +* CardConnect: Always include additional_data in purchase [therufs] #3387 +* CardConnect: Add user_fields GSF [therufs] #3388 +* Moneris: Add support for stored credentials [chinhle23] #3384 + +== Version 1.99.0 (Sep 26, 2019) +* Adyen: Add functionality to set 3DS exemptions via API [britth] #3331 +* Adyen: Send "NA" instead of "N/A" [leila-alderman] #3339 +* Stripe Payment Intents: Set application fee or transfer amount on capture [britth] #3340 +* TNS: Support Europe endpoint [curiousepic] #3346 +* Redsys: Add 3DS support to gateway [britth] #3336 +* Worldpay: Allow multiple refunds per authorization [jknipp] #3349 +* MercadoPago: Add remote and unit tests for Naranja card [hdeters] #3345 +* CyberSource: Pass commerce indicator if present [curiousepic] #3350 +* Worldpay: Add 3DS2 Support [nfarve] #3344 +* Credorax: Add 3DS 2.0 [nfarve] #3342 +* TNS: Update verison and support pay mode [curiousepic] #3355 +* Stripe: Add supported countries [therufs] #3358 +* Stripe Payment Intents: Add supported countries [therufs] #3359 +* Mundipagg: Append error messages to the message response field [jasonxp] #3353 +* Redsys: Add ability to pass sca_exemption and moto fields to request exemptions [britth] #3354 +* Credorax: Add A Mandatory 3DS field [nfarve] #3360 +* CyberSource: Support 3DS2 pass-through fields [curiousepic] #3363 +* Credorax: Add support for MOTO flagging [britth] #3366 +* Credorax: Enable selecting a processor [leila-alderman] #3302 +* Adyen: Add Cabal card [leila-alderman] #3361 +* Decidir: Add remote tests for Cabal and Naranja [leila-alderman] #3337 +* Payflow: Pass correct field in Status for 3DS in Payflow [nebdil] #3362 +* CyberSource: Use 3DS hash for enrolled field [curiousepic] #3371 + +== Version 1.98.0 (Sep 9, 2019) +* Stripe Payment Intents: Add new gateway [britth] #3290 +* Stripe: Send cardholder name and address when creating sources for 3DS 1.0 [jknipp] #3300 +* Checkout_v2: Support for native 3DS2.0 [nfarve] #3303 +* Adds new Maestro BINs [tanyajajodia] #3305 +* eWAY Rapid: If no address is available, default to the name associated with the payment method when setting the Customer fields [jasonxp] #3306 +* eWAY Rapid: Fix a bug in which the email was not set in Customer fields if no address was provided [jasonxp] #3306 +* eWAY Rapid: Support both `phone` and `phone_number` fields under the `shipping_address` option [jasonxp] #3306 +* PayU Latam: Add support for the `merchant_buyer_id` field in the `options` and `buyer` hashes [jasonxp] #3308 +* Update Braintree Gem [curiousepic] #3311 +* Fat Zebra: Send metadata for purchase and authorize [montdidier] #3101 +* TrustCommerce: Add support for custom fields [jasonxp] #3313 +* Stripe Payment Intents: Support option fields `transfer_destination` and `transfer_amount` and remove `transfer_data` hash [britth] #3317 +* Barclaycard Smartpay: Add support for `shopperStatement` gateway-specific field [jasonxp] #3319 +* Stripe Payment Intents: Add support for billing_details on payment methods [britth] #3320 +* BlueSnap: add standardized 3DS 2 auth fields [bayprogrammer] #3318 +* Barclaycard Smartpay: Add app based 3DS requests for auth and purchase [britth] #3327 +* Stripe Payment Intents, Checkout V2: Add support for `MOTO` flagging [britth] #3323 +* Braintree Blue: Adding 3DS2 passthru support [molbrown] #3328 +* Global Collect: Add Cabal card [leila-alderman] #3310 +* WorldPay: Add Cabal card [leila-alderman] #3316 +* Decidir: Add Cabal card [leila-alderman] #3322 +* PayU Latam: Add Cabal card [leila-alderman] #3324 +* dLocal: Add Cabal card [leila-alderman] #3325 +* BlueSnap: Add Cabal card [leila-alderman] #3326 +* Adyen: added 3DS support through external [rikterbeek] #3294 +* Worldpay: Add support for MOTO flagging [britth] #3329 +* ePay: 3DS support [AllaWLie] #3321 +* Checkout.com: added options[:metadata][:manual_entry] support for MOTO transactions [filipebarcos] #3330 + +== Version 1.97.0 (Aug 15, 2019) +* CyberSource: Add issuer `additionalData` gateway-specific field [jasonxp] #3296 +* PayU Latam: Add Naranja card type [hdeters] #3299 +* Adyen: Add app based 3DS requests for auth and purchase [jeremywrowe] #3298 +* MercadoPago: Add Cabal card type [leila-alderman] #3295 +* MONEI: Add external MPI 3DS 1 support [jimmyn] #3292 +* Bambora formerly Beanstream: Pass card owner when storing tokenized cards [alexdunae] #3006 +* Realex: Prevent error calculating `refund_hash` or `credit_hash` when the secret is nil [jasonxp] #3291 +* Orbital: Add external MPI support for 3DS1 [pi3r] #3261 +* Paymill: Add currency and amount to store requests [jasonxp] #3289 +* Realex: Re-implement credit as general credit [leila-alderman] #3280 +* Braintree Blue: Support for stored credentials [hdeters] #3286 +* CardConnect: Move domain from gateway specific to gateway field [hdeters] #3283 + +== Version 1.96.0 (Jul 26, 2019) +* Bluesnap: Omit state codes for unsupported countries [therufs] #3229 +* Adyen: Pass updateShopperStatement, industryUsage [curiousepic] #3233 +* TransFirst Transaction Express: Fix blank address2 values [britth] #3231 +* WorldPay: Add support for store method [bayprogrammer] #3232 +* Adyen: Support for additional AVS code mapping [jknipp] #3236 +* Adyen: Update message for AVS result code 'A' to generically cover postal code mismatches [jknipp] #3237 +* CyberSource: Update CyberSource SOAP documentation link [vince-smith] #3204 +* USAePay: Handle additional error codes and add default error code [estelendur] #3167 +* Braintree: Add `skip_avs` and `skip_cvv` gateway specific fields [leila-alderman] #3241 +* NAB Transact: Update periodic test url [mengqing] #3177 +* NMI: Add level 3 gateway-specific fields tax, shipping, and ponumber [jasonxp] #3239 +* Checkout V2: Update stored card flag [curiousepic] #3247 +* NMI: Add support for stored credentials [bayprogrammer] #3243 +* Spreedly: Consolidate API requests and support bank accounts [lancecarlson] #3105 +* BPoint: Hook up merchant_reference and CRN fields [curiousepic] #3249 +* Checkout V2: Stop sending phone number to Checkout V2 integration [filipebarcos] #3248 +* Barclaycard Smartpay: Add support for 3DS2 [britth] #3251 +* Adyen: Add support for non-fractional currencies [molbrown] #3257 +* Decidir: Add new gateway [jknipp] #3254 +* Checkout V2: Reapply Update stored card flag [curiousepic] +* CyberSource: Update supported countries [molbrown] #3260 +* Credorax: Update supported countries [molbrown] #3260 +* Kushki: Update supported countries [molbrown] #3260 +* Paypal: Update supported countries [molbrown] #3260 +* BlueSnap: Send amount in capture requests [jknipp] #3262 +* Mundipagg: Add Alelo card support [jasonxp] #3255 +* Adyen: Remove temporary amount modification for non-fractional currencies [molbrown] #3263 +* Adyen: Set blank state to N/A [therufs] #3252 +* MiGS: Add tx_source gateway specific field [leila-alderman] #3264 +* NMI: Correct password scrubber to scrub symbols [hdeters] #3267 +* Global Collect: Only add name if present [curiousepic] #3268 +* HPS: Add Apple Pay raw cryptogram support [slogsdon] #3209 +* CardConnect: Fix parsing of level 3 fields [hdeters] #3273 +* TrustCommerce: Support void after purchase [jknipp] #3265 +* Payflow: Support arbitrary level 2 + level 3 fields [therufs] #3272 +* BlueSnap: Default to not send amount on capture [molbrown] #3270 +* Spreedly: extra fields, remove extraneous check [montdidier] #3102 #3281 +* Cecabank: Update encryption to SHA2 [leila-alderman] #3278 +* Checkout V2: Fix 3DS 1&2 integration [nicolas-maalouf-cko] #3240 +* Credorax: add 3DS2 MPI auth data support [bayprogrammer] #3274 +* Add Kosovo to the list of countries [AnotherJoSmith] #3226 +* Realex: Adds 3DS 1&2 support through external MPI [filipebarcos] #3284 +* PayPal: Adds 3DS 1 support through external MPI [nebdil] #3279 + +== Version 1.95.0 (May 23, 2019) +* Adyen: Constantize version to fix subdomains [curiousepic] #3228 +* Qvalent: Adds support for standard stored credential framework [molbrown] #3227 +* Cybersource: Send tokenization data when card is :master [pi3r] #3230 + +== Version 1.94.0 (May 21, 2019) +* Mundipagg: Fix number lengths for both VR and Sodexo [dtykocki] #3195 +* Stripe: Support show and list webhook endpoints [jknipp] #3196 +* CardConnect: Add frontendid parameter to requests [gcatlin] #3198 +* Adyen: Correct formatting of Billing Address [nfarve] #3200 +* Stripe: Stripe: Show payment source [jknipp] #3202 +* Checkout V2: Checkout V2: Correct success criteria [curiousepic] #3205 +* Adyen: Add normalized hash of 3DS 2.0 data fields from web browsers [davidsantoso] #3207 +* Stripe: Do not attempt application fee refund if refund was not successful [jasonwebster] #3206 +* Elavon: Send transaction_currency if currency is provided [gcatlin] #3201 +* Elavon: Multi-currency support [jknipp] #3210 +* Adyen: Support preAuths and Synchronous Adjusts [curiousepic] #3212 +* WorldPay: Support Unknown Card Type [tanyajajodia] #3213 +* Mundipagg: Make gateway_affiliation_id an option [curiousepic] #3219 +* CyberSource: Adds Elo Card Type [tanyajajodia] #3220 +* CyberSource: Support standalone credit for cards [curiousepic] #3225 + +== Version 1.93.0 (April 18, 2019) +* Stripe: Do not consider a refund unsuccessful if only refunding the fee failed [jasonwebster] #3188 +* Stripe: Fix webhook creation for connected account [jknipp] #3193 +* Adyen: Upgrade to v40 API version [davidsantoso] #3192 + +== Version 1.92.0 (April 8, 2019) +* BluePay: Send customer IP address when provided [jknipp] #3149 +* PaymentExpress: Use ip field for client_info field [jknipp] #3150 +* Bambora Asia-Pacific: Adds Store [molbrown] #3147 +* Orbital: Pass normalized stored credential fields [curiousepic] #3148 +* Adds Elo card type in general and specifically to Adyen [deedeelavinder] #3153 +* Mercado Pago: Adds Elo card type [deedeelavinder] #3156 +* Litle: Add support for stored credentials [bayprogrammer] #3155 +* Adyen: Correctly process risk_data option [bayprogrammer] #3161 +* Paymentez: Adds Elo card type [deedeelavinder] #3162 +* WorldPay: Adds Elo card type [deedeelavinder] #3163 +* Adyen: Idempotency for non-purchase requests [molbrown] #3164 +* FirstData e4 v27: Support v28 url and stored creds [curiousepic] #3165 +* WorldPay: Fix element order for 3DS + stored cred [bayprogrammer] #3172 +* Braintree: Add risk data to returned response [jknipp] #3169 +* Adyen: Support idempotency on purchase [molbrown] #3168 +* Adyen: Pass phone, statement, device_fingerprint [curiousepic] #3178 +* Adyen: Fix adding phone from billing address [curiousepic] #3179 +* Fix partial or missing address exceptions [molbrown] #3180 +* Adyen: Update to support normalized stored credential fields [molbrown] #3182 +* VisaNet Peru: Always include DSC_COD_ACCION [bayprogrammer] #3174 +* Adyen: Support adjust action [curiousepic] #3190 +* CyberSource: Add support for stored credentials [therufs] #3185 + +== Version 1.91.0 (February 22, 2019) +* WorldPay: Pull CVC and AVS Result from Response [nfarve] #3106 +* Worldpay: Add AVS and CVC Mapping [nfarve] #3107 +* Paymentez: Fixes extra_params field [molbrown] #3108 +* Improved support for account_type using Check class's account_type instead [lancecarlson] #3097 +* USA Epay: Allow quantity to be passed and check custom fields [lancecarlson] #3090 +* Fix usaepay transaction invoice [lancecarlson] #3093 +* Adyen: Handles blank state address field [molbrown] #3113 +* Braintree: Send all country fields [curiousepic] #3112 +* Braintree: Account for empty string countries [curiousepic] #3115 +* Orbital: Support for stored credentials framework [jknipp] #3117 +* Openpay: Fix for marking successful transaction(s) as failed [jknipp] #3121 +* Braintree: Adds support for transaction_source [molbrown] #3120 +* Moneris: Remove redundant card on file guard clause [davidsantoso] #3123 +* Switch order of Romania country codes [molbrown] #3125 +* Blue Snap: Supports Level 2/3 data [molbrown] #3126 +* Blue Snap: Support personal_identification_number [jknipp] #3128 +* ProPay: Send 9 digit zip code without dash [molbrown] #3129 +* Adyen: Extend AVS code mappings [therufs] #3119 +* NMI: Add customer id to authorization on store [curiousepic] #3130 +* Trans First Express: Don't pass blank name field [curiousepic] #3133 +* TrustCommerce: Send full name on ACH transactions [jknipp] #3132 +* Qvalent: Map CVV Result to responses [curiousepic] #3135 +* Card Connect: Handle 401s as responses [curiousepic] #3137 +* Worldpay: Introduce normalized stored credential options [davidsantoso] #3134 +* Worldpay: Adjust use of normalized stored credentials hash [davidsantoso] #3139 +* Adyen: Enable Dynamic 3DS [molbrown] #3138 +* Fat Zebra: Support voids [curiousepic] #3142 +* Blue Snap: Support ACH/ECP payments [jknipp] #3143 +* Blue Snap: Fix Card-on-File field typo [jknipp] #3143 +* Add Bambora gateway [InfraRuby] #3145 +* Bambora Asia-Pacific: Updates Gateway [molbrown] #3145 +* PaymentExpress: Support ClientInfo field [jknipp] #3131 +* Pin Payments: Concatenate card and customer tokens when storing card [therufs] #3144 +* Update Discover regex to allow card numbers longer than 16 digits [prashcr] #3146 +* Merrco partial refunds fix [payfirma1] #3141 + +== Version 1.90.0 (January 8, 2019) +* Mercado Pago: Support "gateway" processing mode [curiousepic] #3087 +* Braintree: Update gem to latest version [curiousepic] #3091 +* Adyen: Pass arbitrary riskData fields [curiousepic] #3089 +* Worldpay: Fix cookie header name [curiousepic] #3099 +* Paymentez: Adds support for extra_params optional field [molbrown] #3095 +* Braintree Blue: Support Level 2 and 3 data fields [curiousepic] #3094 +* Braintree Blue: Refactor line_items field [curiousepic] #3100 +* TrustCommerce: Use `application_id` [nfarve] #3103 +* Stripe: Add 3DS Support [nfarve] #3086 +* Cecabank: Append error text to message [therufs] #3127 + +== Version 1.89.0 (December 17, 2018) +* Worldpay: handle Visa and MasterCard payouts differently [bpollack] #3068 +* QuickPay: update supported countries [ta] #3049 +* WorldPay: set cardholder name to "3D" for 3DS transactions [bpollack] #3071 +* Authorize.Net: Support refunds for bank accounts [nfarve] #3063 +* Stripe: support specifying a reason for refunds [yosukehasumi] #3056 +* Paybox Direct: add support for XPF currency [adam-stead] #2938 +* TrustCommerce: Add ACH Ability [nfarve] #3073 +* Payeezy: Support $0 for verify transactions [molbrown] #3074 +* USA ePay: add support for recurring transactions, custom fields, and line items [lancecarlson] #3069 +* Add dLocal gateway [curiousepic] #3709 +* dLocal: Require secret_key [curiousepic] #3080 +* Adyen: Implement 3DS [nfarve] #3076 +* Adyen: Add 3DS Fix [nfarve] #3081 +* Payeezy: Add `stored_credentials` [nfarve] #3083 +* Fix CVC validation for 0 length CVC [filipebarcos] #3082 +* NMI: Supports vendor_id and processor_id fields [molbrown] #3085 + +== Version 1.88.0 (November 30, 2018) +* Added ActiveSupport/Rails master support [Edouard-chin] #3065 + +== Version 1.87.0 (November 29, 2018) +* Barclaycard Smartpay: Improves Error Handling [deedeelavinder] #3026 +* Braintree: Fix passing phone-only billing address [curiousepic] #3025 +* Litle: Capitalize check account type [curiousepic] #3028 +* Braintree: Account for nil billing address fields [curiousepic] #3029 +* Realex: Add verify [kheang] #3030 +* Braintree: Actually account for nil address fields [curiousepic] #3032 +* Mercado Pago: do not infer card type [bpollack] #3038 +* Credorax: allow sending submerchant ID (h3 parameter) [bpollack] #3040 +* Worldpay: Pass stored credential option fields [curiousepic] #3041 +* Make behavior of nil CC numbers more consistent [guaguasi] #3010 +* Moneris: Adds Credential on File logic [deedeelavinder] #3042 +* Adyen: Return AVS and CVC Result [nfarve] #3044 +* Paymentez: Supports phone field, does not send if empty [molbrown] #3043 +* Braintree: Account for nil address with existing customer [curiousepic] #3047 +* Optimal Payment: Add verify capabilities #3052 +* Moneris: Allows cof_enabled gateway to process non-cof transactions [deedeelavinder] #3051 +* Cenpos: update supported countries [bpollack] #3055 +* CyberSource: update supported countries [bpollack] #3055 +* MiGS: update supported countries [bpollack] #3055 +* Clearhaus: update submission data format [bpollack] #3053 +* Forte: Allow void on capture [nfarve] #3059 + +== Version 1.86.0 (October 26, 2018) +* UsaEpayTransaction: Support UMcheckformat option for echecks [dtykocki] #3002 +* Global Collect: handle internal server errors [molbrown] #3005 +* Barclaycard Smartpay: allow third-party payouts for credits [bpollack] #3009 +* RuboCop: AlignHash [nfarve] #3004 +* Beanstream: Switch `recurringPayment` flag from boolean to integer [dtykocki] #3011 +* Update Swipe HQ endpoint [bdewater] #3013 +* Braintree: Adds device_data [deedeelavinder] #3012 +* Payflow Express: Add phone to returned Response [filipebarcos] #3003 +* Authorize.Net: Pass some level 3 fields [curiousepic] #3022 +* Add state to the netbanx payload [Girardvjonathan] #3024 + +== Version 1.85.0 (September 28, 2018) +* Authorize.Net: Support custom delimiter for cim [curiousepic] #3001 + +== Version 1.84.0 (September 27, 2018) +* PayU Latam: support partial captures [bpollack] #2974 +* Braintree: Reflect correct test mode in Braintree responses [elfassy] #2980 +* FirstPay: Expose error code [curiousepic] #2979 +* Barclaycard Smartpay: Pass device_fingerprint when specified [dtykocki] #2981 +* Komoju: remove no-longer-relevant sandbox URL [miyazawadegica] #2987 +* [POSSIBLE BREAKAGE] Determine credit cards via functions [bpollack] #2983 +* Drop support for Laser cards [bpollack] #2983 +* Improve Maestro card detection [bpollack] #2983 +* Add ROU alpha3 code for Romania [dtykocki] #2989 +* [POSSIBLE BREAKAGE] Drop support for Solo and Switch cards [bpollack] #2991 +* Add support for Carnet cards [bpollack] #2992 +* Stripe: support a reason for voiding a transaction [whitby3001] #2378 +* Payeezy: Add reversal_id in support of timeout reversals [dtykocki] #2997 +* Stripe: support Level 3 transaction fields [bpollack] #2996 +* Conekta: support Carnet cards [bpollack] #2999 +* Openpay: support Carnet cards [bpollack] #2999 +* Adyen: Add support for GooglePay [dtykocki] #2971 + +== Version 1.83.0 (August 30, 2018) +* CT Payment: Update How Address is Passed [nfarve] #2960 +* Adyen: Add RecurringProcessingModel [nfarve] #2951 +* Optimal Payments: update country list [bpollack] #2961 +* Ebanx: update sandbox and production URLs [vnbrs] #2949 +* Ebanx: support additional countries [vnbrs] #2950 +* Gateway generator: fix a typo that would cause the script to crash [bpollack] #2962 +* Clearhaus: use $0 for verify transactions [bpollack] #2964 +* Global Collect: properly handle partial captures [bpollack] #2967 +* Braintree: Add support for GooglePay [dtykocki] #2966 +* Adyen: allow overriding card brands [bpollack] #2968 +* Adyen: allow custom routing [bpollack] #2969 +* First Pay: Adds scrubbing [deedeelavinder] #2972 + +== Version 1.82.0 (August 13, 2018) +* FirstData: add support for WalletProviderID in v27 gateway [bpollack] #2946 +* BlueSnap: Handle 403 responses [curiousepic] #2948 +* BlueSnap: Add StoreCard Field [nfarve] #2953 +* Worldpay: support installments [bpollack] #2957 +* Paymentez: support partial refunds [bpollack] #2959 +* Payflow: allow support for partial captures [pi3r] #2952 + +== Version 1.81.0 (July 30, 2018) +* GlobalCollect: Don't overwrite contactDetails [curiousepic] #2915 +* Pin Payments: Pass reference for statement desc [curiousepic] #2919 +* FirstData: introduce v27 gateway [shasum] #2912 +* Stripe: Fix contactless magstripe support [abhiin1947] #2917 +* ANET: Expose full response code [curiousepic] #2924 +* Global Collect: Fix customer data field structure [curiousepic] #2929 +* Adyen: Set Default Name for Apple Pay Transactions [nfarve] #2930 +* Beanstream: Update to use api key with login credentials [nfarve] #2934 +* CT Payments: Fix a typo in the live URL scheme [bpollack] #2936 +* CyberSource: Don't throw exceptions on HTML responses [bpollack] #2937 +* CyberSource: Remove extraneous parameter blocking echecks [chriscz] #2861 +* FirstPay: Update Fields For Recurring Payments [nfarve] #2940 +* Remove unused handle_response method [bl] #2309 +* Barclaycard Smartpay: bump API version to v30 [bpollack] #2941 +* Safecharge: Remove duplicate supported country [curiousepic] +* Payflow Express: Use SHIPTONAME instead of `full_name` for shipping address [filipebarcos] #2945 + +== Version 1.80.0 (July 4, 2018) * Default SSL min_version to TLS 1.1 to comply with June 30 PCI DSS deadline [bdewater] #2909 * Paymentez: return a Result object even when the upstream server 500s [bpollack] #2871 * Drop support for Ruby versions older than 2.3 [bpollack] #2863 @@ -32,6 +1807,7 @@ * Redsys: Fix payments with cc token [Leonardo Diez] #2586 * Redsys: Missing cardnumber params in xml_signed_fields [nerburish] #2628 * Bogus: allow authorizing with a tokenized card [Azdaroth] #2703 +* CT Payment: Add new gateway [nfarve] #2911 == Version 1.79.3 (January 26, 2023) * Fix cert file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 7d7989b171d..00000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,40 +0,0 @@ -# Contributing guidelines - -We gladly accept bugfixes and new gateways. Please follow the guidelines here to ensure your work is accepted. - -## New Gateways - -Please see the [ActiveMerchant Guide to Contributing a new Gateway](https://github.com/activemerchant/active_merchant/wiki/contributing) for information on adding a new gateway to ActiveMerchant. - -## Issues & Bugfixes - -### Reporting issues - -When filing a new Issue: - -- Please make clear in the subject what gateway the issue is about. -- Include the version of ActiveMerchant, Ruby, ActiveSupport, and Nokogiri you are using. - -### Pull request guidelines - -When submitting a pull request to resolve an issue: - -1. [Fork it](http://github.com/activemerchant/active_merchant/fork) and clone your new repo -2. Create a branch (`git checkout -b my_awesome_feature`) -3. Commit your changes (`git add my/awesome/file.rb; git commit -m "Added my awesome feature"`) -4. Push your changes to your fork (`git push origin my_awesome_feature`) -5. Open a [Pull Request](https://github.com/activemerchant/active_merchant/pulls) - -## Gateway Placement within Shopify - -Placement within Shopify is available by invitation only at this time. - -## Version/Release Management - -Contributors don't need to worry about versions, this is something Committers do at important milestones: - -1. Check the [semantic versioning page](http://semver.org) for info on how to version the new release. -2. Update the `ActiveMerchant::VERSION` constant in **lib/active_merchant/version.rb**. -3. Add a `CHANGELOG` entry for the new release with the date -4. Tag the release commit on GitHub: `bundle exec rake tag_release` -5. Release the gem to rubygems using ShipIt diff --git a/Gemfile b/Gemfile index c1d71bab576..26fef7a9866 100644 --- a/Gemfile +++ b/Gemfile @@ -1,10 +1,15 @@ source 'https://rubygems.org' gemspec -gem 'jruby-openssl', :platforms => :jruby -gem 'rubocop', '~> 0.57.2' +gem 'jruby-openssl', platforms: :jruby +gem 'rubocop', '~> 1.26.0', require: false group :test, :remote_test do # gateway-specific dependencies, keeping these gems out of the gemspec - gem 'braintree', '~> 2.78' + gem 'braintree', '>= 4.14.0' + gem 'concurrent-ruby', '1.3.4' + gem 'jose', '~> 1.2.0' + gem 'jwe' + gem 'mechanize' + gem 'timecop' end diff --git a/README.md b/README.md index 5fbec7885ae..c3f26f99bae 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Active Merchant -[![Build Status](https://travis-ci.org/activemerchant/active_merchant.png?branch=master)](https://travis-ci.org/activemerchant/active_merchant) -[![Code Climate](https://codeclimate.com/github/activemerchant/active_merchant.png)](https://codeclimate.com/github/activemerchant/active_merchant) +[![Build Status](https://github.com/activemerchant/active_merchant/workflows/CI/badge.svg?branch=master)](https://github.com/activemerchant/active_merchant/actions?query=workflow%3ACI) +[![Code Climate](https://codeclimate.com/github/activemerchant/active_merchant.svg)](https://codeclimate.com/github/activemerchant/active_merchant) Active Merchant is an extraction from the ecommerce system [Shopify](http://www.shopify.com). Shopify's requirements for a simple and unified API to access dozens of different payment @@ -17,7 +17,7 @@ from an ever-growing set of contributors. See [GettingStarted.md](GettingStarted.md) if you want to learn more about using Active Merchant in your applications. -If you'd like to contribute to Active Merchant, please start with our [contribution guide](CONTRIBUTING.md). +If you'd like to contribute to Active Merchant, please start with our [Contribution Guide](https://github.com/activemerchant/active_merchant/wiki/Contributing). ## Installation @@ -81,17 +81,24 @@ if credit_card.validate.empty? end ``` +## Contributing + For more in-depth documentation and tutorials, see [GettingStarted.md](GettingStarted.md) and the [API documentation](http://www.rubydoc.info/github/activemerchant/active_merchant/). +Emerging ActiveMerchant 3DS conventions are documented in the [Contributing](https://github.com/activemerchant/active_merchant/wiki/Contributing#3ds-options) +guide and [Standardized 3DS Fields](https://github.com/activemerchant/active_merchant/wiki/Standardized-3DS-Fields) guide of the wiki. + ## Supported Payment Gateways -The [ActiveMerchant Wiki](http://github.com/activemerchant/active_merchant/wikis) contains a [table of features supported by each gateway](http://github.com/activemerchant/active_merchant/wikis/gatewayfeaturematrix). +The [ActiveMerchant Wiki](https://github.com/activemerchant/active_merchant/wikis) contains a [table of features supported by each gateway](https://github.com/activemerchant/active_merchant/wiki/Gateway-Feature-Matrix). +* [Adyen](https://www.adyen.com/) - US, AT, AU, BE, BG, BR, CH, CY, CZ, DE, DK, EE, ES, FI, FR, GB, GI, GR, HK, HU, IE, IS, IT, LI, LT, LU, LV, MC, MT, MX, NL, NO, PL, PT, RO, SE, SG, SK, SI * [Authorize.Net CIM](http://www.authorize.net/) - US -* [Authorize.Net](http://www.authorize.net/) - AD, AT, AU, BE, BG, CA, CH, CY, CZ, DE, DK, ES, FI, FR, GB, GB, GI, GR, HU, IE, IT, LI, LU, MC, MT, NL, NO, PL, PT, RO, SE, SI, SK, SM, TR, US, VA +* [Authorize.Net](http://www.authorize.net/) - AU, CA, US * [Axcess MS](http://www.axcessms.com/) - AD, AT, BE, BG, BR, CA, CH, CY, CZ, DE, DK, EE, ES, FI, FO, FR, GB, GI, GR, HR, HU, IE, IL, IM, IS, IT, LI, LT, LU, LV, MC, MT, MX, NL, NO, PL, PT, RO, RU, SE, SI, SK, TR, US, VA * [Balanced](https://www.balancedpayments.com/) - US +* [Bambora Asia-Pacific](http://www.bambora.com/) - AU, NZ * [Bank Frick](http://www.bankfrickacquiring.com/) - LI, US * [Banwire](http://www.banwire.com/) - MX * [Barclays ePDQ Extra Plus](http://www.barclaycard.co.uk/business/accepting-payments/epdq-ecomm/) - GB @@ -108,7 +115,7 @@ The [ActiveMerchant Wiki](http://github.com/activemerchant/active_merchant/wikis * [Cecabank](http://www.ceca.es/es/) - ES * [Cenpos](https://www.cenpos.com/) - AD, AI, AG, AR, AU, AT, BS, BB, BE, BZ, BM, BR, BN, BG, CA, HR, CY, CZ, DK, DM, EE, FI, FR, DE, GR, GD, GY, HK, HU, IS, IN, IL, IT, JP, LV, LI, LT, LU, MY, MT, MX, MC, MS, NL, PA, PL, PT, KN, LC, MF, VC, SM, SG, SK, SI, ZA, ES, SR, SE, CH, TR, GB, US, UY * [CAMS: Central Account Management System](https://www.centralams.com/) - US -* [Checkout.com](https://www.checkout.com/) - AT, BE, BG, CY, CZ, DE, DK, EE, ES, FI, FR, GR, HR, HU, IE, IS, IT, LI, LT, LU, LV, MT, MU, NL, NO, PL, PT, RO, SE, SI, SK, US +* [Checkout.com](https://www.checkout.com/) - 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 * [Clearhaus](https://www.clearhaus.com) - AD, AT, BE, BG, CH, CY, CZ, DE, DK, EE, ES, FI, FO, FR, GB, GL, GR, HR, HU, IE, IS, IT, LI, LT, LU, LV, MT, NL, NO, PL, PT, RO, SE, SI, SK * [Commercegate](http://www.commercegate.com/) - AD, AT, AX, BE, BG, CH, CY, CZ, DE, DK, ES, FI, FR, GB, GG, GI, GR, HR, HU, IE, IM, IS, IT, JE, LI, LT, LU, LV, MC, MT, NL, NO, PL, PT, RO, SE, SI, SK, VA * [Conekta](https://conekta.io) - MX @@ -153,9 +160,8 @@ The [ActiveMerchant Wiki](http://github.com/activemerchant/active_merchant/wikis * [Metrics Global](http://www.metricsglobal.com) - US * [MasterCard Internet Gateway Service (MiGS)](http://mastercard.com/mastercardsps) - AU, AE, BD, BN, EG, HK, ID, IN, JO, KW, LB, LK, MU, MV, MY, NZ, OM, PH, QA, SA, SG, TT, VN * [Modern Payments](http://www.modpay.com) - US -* [MONEI](http://www.monei.net/) - AD, AT, BE, BG, CA, CH, CY, CZ, DE, DK, EE, ES, FI, FO, FR, GB, GI, GR, HU, IE, IL, IS, IT, LI, LT, LU, LV, MT, NL, NO, PL, PT, RO, SE, SI, SK, TR, US, VA +* [MONEI](http://www.monei.com/) - AD, AT, BE, BG, CA, CH, CY, CZ, DE, DK, EE, ES, FI, FO, FR, GB, GI, GR, HU, IE, IL, IS, IT, LI, LT, LU, LV, MT, NL, NO, PL, PT, RO, SE, SI, SK, TR, US, VA * [Moneris](http://www.moneris.com/) - CA -* [Moneris (US)](http://www.monerisusa.com/) - US * [MoneyMovers](http://mmoa.us/) - US * [NAB Transact](http://transact.nab.com.au) - AU * [NELiX TransaX](https://www.nelixtransax.com/) - US @@ -179,7 +185,7 @@ The [ActiveMerchant Wiki](http://github.com/activemerchant/active_merchant/wikis * [Paybox Direct](http://www.paybox.com/) - FR * [Payeezy](https://developer.payeezy.com/) - CA, US * [Payex](http://payex.com/) - DK, FI, NO, SE -* [PaymentExpress](http://www.paymentexpress.com/) - AU, CA, DE, ES, FR, GB, HK, IE, MY, NL, NZ, SG, US, ZA +* [Windcave (formerly PaymentExpress)](https://www.windcave.com/) - AU, CA, DE, ES, FR, GB, HK, IE, MY, NL, NZ, SG, US, ZA * [PAYMILL](https://paymill.com) - AD, AT, BE, BG, CH, CY, CZ, DE, DK, EE, ES, FI, FO, FR, GB, GI, GR, HU, IE, IL, IS, IT, LI, LT, LU, LV, MT, NL, NO, PL, PT, RO, SE, SI, SK, TR, VA * [PayPal Express Checkout](https://www.paypal.com/webapps/mpp/express-checkout) - US, CA, SG, AU * [PayPal Express Checkout (UK)](https://www.paypal.com/uk/webapps/mpp/express-checkout) - GB @@ -192,14 +198,14 @@ The [ActiveMerchant Wiki](http://github.com/activemerchant/active_merchant/wikis * [Paystation](http://paystation.co.nz) - NZ * [Pay Way](http://www.payway.com.au) - AU * [PayU India](https://www.payu.in/) - IN -* [Pin Payments](http://www.pin.net.au/) - AU +* [Pin Payments](http://www.pinpayments.com/) - AU * [Plug'n Pay](http://www.plugnpay.com/) - US * [Psigate](http://www.psigate.com/) - CA * [PSL Payment Solutions](http://www.paymentsolutionsltd.com/) - GB * [QuickBooks Merchant Services](http://payments.intuit.com/) - US * [QuickBooks Payments](http://payments.intuit.com/) - US * [Quantum Gateway](http://www.quantumgateway.com) - US -* [QuickPay](http://quickpay.net/) - DE, DK, ES, FI, FR, FO, GB, IS, NO, SE +* [QuickPay](http://quickpay.net/) - AT, BE, BG, CY, CZ, DE, DK, EE, ES, FI, FR, GB, GR, HR, HU, IE, IS, IT, LI, LT, LU, LV, MT, NL, NO, PL, PT, RO, SE SI, SK * [Qvalent](https://www.qvalent.com/) - AU * [Raven](http://www.deepcovelabs.com/raven) - AI, AN, AT, AU, BE, BG, BS, BZ, CA, CH, CR, CY, CZ, DE, DK, DM, DO, EE, EL, ES, FI, FR, GB, GG, GI, HK, HR, HU, IE, IL, IM, IN, IT, JE, KN, LI, LT, LU, LV, MH, MT, MY, NL, NO, NZ, PA, PE, PH, PL, PT, RO, RS, SC, SE, SG, SI, SK, UK, US, VG, ZA * [Realex](http://www.realexpayments.com/) - IE, GB, FR, BE, NL, LU, IT @@ -211,7 +217,7 @@ The [ActiveMerchant Wiki](http://github.com/activemerchant/active_merchant/wikis * [SecureNet](http://www.securenet.com/) - US * [SecurePay](http://www.securepay.com/) - US, CA, GB, AU * [SecurePayTech](http://www.securepaytech.com/) - NZ -* [SecurionPay](https://securionpay.com/) - AD, AE, AF, 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, BR, BS, BT, BV, BW, BY, BZ, CA, CC, CD, CF, CG, CH, CI, CK, CL, CM, CN, CO, CR, CU, CV, CW, CX, CY, CZ, DE, DJ, DK, DM, DO, DZ, EC, EE, EG, EH, ER, ES, ET, FI, FJ, FK, FM, FO, FR, GA, GB, GD, GE, GF, GG, GH, GI, GL, GM, GN, GP, GQ, GR, GS, GT, GU, GW, GY, HK, HM, HN, HR, HT, HU, ID, IE, IL, IM, IN, IO, IQ, IR, IS, IT, JE, JM, JO, JP, KE, KG, KH, KI, KM, KN, KP, KR, KW, KY, KZ, LA, LB, LC, LI, LK, LR, LS, LT, LU, LV, LY, MA, MC, MD, ME, MF, MG, MH, MK, ML, MM, MN, MO, MP, MQ, MR, MS, MT, MU, MV, MW, MX, MY, MZ, NA, NC, NE, NF, NG, NI, NL, NO, NP, NR, NU, NZ, OM, PA, PE, PF, PG, PH, PK, PL, PM, PN, PR, PS, PT, PW, PY, QA, RE, RO, RS, RU, RW, SA, SB, SC, SD, SE, SG, SH, SI, SJ, SK, SL, SM, SN, SO, SR, ST, SV, SY, SZ, TC, TD, TF, TG, TH, TJ, TK, TL, TM, TN, TO, TR, TT, TV, TW, TZ, UA, UG, UM, US, UY, UZ, VA, VC, VE, VG, VI, VN, VU, WF, WS, YE, YT, ZA, ZM, ZW +* [SecurionPay](https://securionpay.com/) - AD, BE, BG, CH, CY, CZ, DE, DK, EE, ES, FI, FO, FR, GI, GL, GR, GS, GT, HR, HU, IE, IS, IT, LI, LR, LT, LU, LV, MC, MT, MU, MV, MW, NL, NO, PL, RO, SE, SI * [SkipJack](http://www.skipjack.com/) - US, CA * [SoEasyPay](http://www.soeasypay.com/) - US, CA, AT, BE, BG, HR, CY, CZ, DK, EE, FI, FR, DE, GR, HU, IE, IT, LV, LT, LU, MT, NL, PL, PT, RO, SK, SI, ES, SE, GB, IS, NO, CH * [Spreedly](https://spreedly.com) - AD, AE, AT, AU, BD, BE, BG, BN, CA, CH, CY, CZ, DE, DK, EE, EG, ES, FI, FR, GB, GI, GR, HK, HU, ID, IE, IL, IM, IN, IS, IT, JO, KW, LB, LI, LK, LT, LU, LV, MC, MT, MU, MV, MX, MY, NL, NO, NZ, OM, PH, PL, PT, QA, RO, SA, SE, SG, SI, SK, SM, TR, TT, UM, US, VA, VN, ZA @@ -221,6 +227,7 @@ The [ActiveMerchant Wiki](http://github.com/activemerchant/active_merchant/wikis * [Transact Pro](https://www.transactpro.lv/business/online-payments-acceptance) - US * [TransFirst](http://www.transfirst.com/) - US * [Transnational](http://www.tnbci.com/) - US +* [Trexle](https://trexle.com) - AD, AE, AT, AU, BD, BE, BG, BN, CA, CH, CY, CZ, DE, DK, EE, EG, ES, FI, FR, GB, GI, GR, HK, HU, ID, IE, IL, IM, IN, IS, IT, JO, KW, LB, LI, LK, LT, LU, LV, MC, MT, MU, MV, MX, MY, NL, NO, NZ, OM, PH, PL, PT, QA, RO, SA, SE, SG, SI, SK, SM, TR, TT, UM, US, VA, VN, ZA * [TrustCommerce](http://www.trustcommerce.com/) - US * [USA ePay](http://www.usaepay.com/) - US * [Vanco Payment Solutions](http://vancopayments.com/) - US @@ -239,4 +246,4 @@ Functionality or APIs that are deprecated will be marked as such. Deprecated fun ## Ruby and Rails compatibility policies -Because Active Merchant is a payment library, it needs to take security seriously. For this reason, Active Merchant guarantees compatibility only with actively supported versions of Ruby and Rails. At the time of this writing, that means that Ruby 2.3+ and Rails 4.2+ are supported. +Because Active Merchant is a payment library, it needs to take security seriously. For this reason, Active Merchant guarantees compatibility only with actively supported versions of Ruby and Rails. At the time of this writing, that means that Ruby 2.5+ and Rails 5.0+ are supported. diff --git a/Rakefile b/Rakefile index 1aa5657c126..2a4b5d39097 100644 --- a/Rakefile +++ b/Rakefile @@ -1,4 +1,4 @@ -$:.unshift File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift File.expand_path('../lib', __FILE__) require 'active_merchant/version' begin @@ -24,20 +24,21 @@ task :tag_release do end desc 'Run the unit test suite' -task :default => 'test:local' -task :test => 'test:units' +task default: 'test:units' +task test: 'test:units' RuboCop::RakeTask.new namespace :test do Rake::TestTask.new(:units) do |t| + ENV['RUNNING_UNIT_TESTS'] = 'true' t.pattern = 'test/unit/**/*_test.rb' t.libs << 'test' - t.verbose = true + t.verbose = false end desc 'Run all tests that do not require network access' - task :local => ['test:units', 'rubocop'] + task local: ['test:units', 'rubocop'] Rake::TestTask.new(:remote) do |t| t.pattern = 'test/remote/**/*_test.rb' diff --git a/activemerchant.gemspec b/activemerchant.gemspec index 031b34b2576..ed70f374f7d 100644 --- a/activemerchant.gemspec +++ b/activemerchant.gemspec @@ -1,4 +1,4 @@ -$:.push File.expand_path('../lib', __FILE__) +$LOAD_PATH.push File.expand_path('../lib', __FILE__) require 'active_merchant/version' Gem::Specification.new do |s| @@ -12,22 +12,26 @@ Gem::Specification.new do |s| s.author = 'Tobias Luetke' s.email = 'tobi@leetsoft.com' s.homepage = 'http://activemerchant.org/' - s.rubyforge_project = 'activemerchant' - s.required_ruby_version = '>= 2.3' + s.required_ruby_version = '>= 3.1' s.files = Dir['CHANGELOG', 'README.md', 'MIT-LICENSE', 'CONTRIBUTORS', 'lib/**/*', 'vendor/**/*'] s.require_path = 'lib' + s.metadata['allowed_push_host'] = 'https://rubygems.org' + s.has_rdoc = true if Gem::VERSION < '1.7.0' - s.add_dependency('activesupport', '>= 4.2', '< 6.x') - s.add_dependency('i18n', '>= 0.6.9') + s.add_dependency('activesupport', '>= 4.2') 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.3', '>= 3.3.8') + s.add_development_dependency('mocha', '~> 1') + s.add_development_dependency('pry') + s.add_development_dependency('pry-byebug') s.add_development_dependency('rake') s.add_development_dependency('test-unit', '~> 3') - s.add_development_dependency('mocha', '~> 1') s.add_development_dependency('thor') end diff --git a/circle.yml b/circle.yml index 5d2a53e5abb..d9438f7d281 100644 --- a/circle.yml +++ b/circle.yml @@ -1,6 +1,6 @@ machine: ruby: - version: '2.3.0' + version: '3.1.0' dependencies: cache_directories: diff --git a/gemfiles/Gemfile.rails42 b/gemfiles/Gemfile.rails42 new file mode 100644 index 00000000000..8d11bec617c --- /dev/null +++ b/gemfiles/Gemfile.rails42 @@ -0,0 +1,3 @@ +eval_gemfile '../Gemfile' + +gem 'activesupport', '~> 4.2.0' diff --git a/gemfiles/Gemfile.rails50 b/gemfiles/Gemfile.rails50 new file mode 100644 index 00000000000..ce57bebccbe --- /dev/null +++ b/gemfiles/Gemfile.rails50 @@ -0,0 +1,3 @@ +eval_gemfile '../Gemfile' + +gem 'activesupport', '~> 5.0.0' diff --git a/gemfiles/Gemfile.rails51 b/gemfiles/Gemfile.rails51 new file mode 100644 index 00000000000..a352b24eaa8 --- /dev/null +++ b/gemfiles/Gemfile.rails51 @@ -0,0 +1,3 @@ +eval_gemfile '../Gemfile' + +gem 'activesupport', '~> 5.1.0' diff --git a/gemfiles/Gemfile.rails52 b/gemfiles/Gemfile.rails52 new file mode 100644 index 00000000000..c6c439fce53 --- /dev/null +++ b/gemfiles/Gemfile.rails52 @@ -0,0 +1,3 @@ +eval_gemfile '../Gemfile' + +gem 'activesupport', '~> 5.2.0.rc1' diff --git a/gemfiles/Gemfile.rails60 b/gemfiles/Gemfile.rails60 new file mode 100644 index 00000000000..c9289b875eb --- /dev/null +++ b/gemfiles/Gemfile.rails60 @@ -0,0 +1,3 @@ +eval_gemfile '../Gemfile' + +gem 'activesupport', '~> 6.0.0' diff --git a/gemfiles/Gemfile.rails_master b/gemfiles/Gemfile.rails_master new file mode 100644 index 00000000000..f2e894b53f2 --- /dev/null +++ b/gemfiles/Gemfile.rails_master @@ -0,0 +1,3 @@ +eval_gemfile '../Gemfile' + +gem 'activesupport', '~>7.2.1' diff --git a/generators/gateway/gateway_generator.rb b/generators/gateway/gateway_generator.rb index d20ad21343c..27dda8aa510 100644 --- a/generators/gateway/gateway_generator.rb +++ b/generators/gateway/gateway_generator.rb @@ -38,7 +38,7 @@ def fixtures_file end def next_identifier - fixtures = (YAML.load(File.read(fixtures_file)).keys + [identifier]).uniq.sort + fixtures = (YAML.safe_load(File.read(fixtures_file), [], [], true).keys + [identifier]).uniq.sort fixtures[fixtures.sort.index(identifier)+1] end end diff --git a/generators/gateway/templates/gateway.rb b/generators/gateway/templates/gateway.rb index f9f986176f8..0fce705c279 100644 --- a/generators/gateway/templates/gateway.rb +++ b/generators/gateway/templates/gateway.rb @@ -1,24 +1,24 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class <%= class_name %>Gateway < Gateway self.test_url = 'https://example.com/test' self.live_url = 'https://example.com/live' self.supported_countries = ['US'] self.default_currency = 'USD' - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'http://www.example.net/' self.display_name = 'New Gateway' STANDARD_ERROR_CODE_MAPPING = {} - def initialize(options={}) + def initialize(options = {}) requires!(options, :some_credential, :another_credential) super end - def purchase(money, payment, options={}) + def purchase(money, payment, options = {}) post = {} add_invoice(post, money, options) add_payment(post, payment) @@ -28,7 +28,7 @@ def purchase(money, payment, options={}) commit('sale', post) end - def authorize(money, payment, options={}) + def authorize(money, payment, options = {}) post = {} add_invoice(post, money, options) add_payment(post, payment) @@ -38,19 +38,19 @@ def authorize(money, payment, options={}) commit('authonly', post) end - def capture(money, authorization, options={}) + def capture(money, authorization, options = {}) commit('capture', post) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) commit('refund', post) end - def void(authorization, options={}) + def void(authorization, options = {}) commit('void', post) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -67,19 +67,16 @@ def scrub(transcript) private - def add_customer_data(post, options) - end + def add_customer_data(post, options); end - def add_address(post, creditcard, options) - end + def add_address(post, creditcard, options); end def add_invoice(post, money, options) post[:amount] = amount(money) post[:currency] = (options[:currency] || currency(money)) end - def add_payment(post, payment) - end + def add_payment(post, payment); end def parse(body) {} @@ -94,24 +91,20 @@ def commit(action, parameters) message_from(response), response, authorization: authorization_from(response), - avs_result: AVSResult.new(code: response["some_avs_response_key"]), - cvv_result: CVVResult.new(response["some_cvv_response_key"]), + avs_result: AVSResult.new(code: response['some_avs_response_key']), + cvv_result: CVVResult.new(response['some_cvv_response_key']), test: test?, error_code: error_code_from(response) ) end - def success_from(response) - end + def success_from(response); end - def message_from(response) - end + def message_from(response); end - def authorization_from(response) - end + def authorization_from(response); end - def post_data(action, parameters = {}) - end + def post_data(action, parameters = {}); end def error_code_from(response) unless success_from(response) diff --git a/generators/gateway/templates/gateway_test.rb b/generators/gateway/templates/gateway_test.rb index 03f699b9641..38d9f0817e1 100644 --- a/generators/gateway/templates/gateway_test.rb +++ b/generators/gateway/templates/gateway_test.rb @@ -31,38 +31,27 @@ def test_failed_purchase assert_equal Gateway::STANDARD_ERROR_CODE[:card_declined], response.error_code end - def test_successful_authorize - end + def test_successful_authorize; end - def test_failed_authorize - end + def test_failed_authorize; end - def test_successful_capture - end + def test_successful_capture; end - def test_failed_capture - end + def test_failed_capture; end - def test_successful_refund - end + def test_successful_refund; end - def test_failed_refund - end + def test_failed_refund; end - def test_successful_void - end + def test_successful_void; end - def test_failed_void - end + def test_failed_void; end - def test_successful_verify - end + def test_successful_verify; end - def test_successful_verify_with_failed_void - end + def test_successful_verify_with_failed_void; end - def test_failed_verify - end + def test_failed_verify; end def test_scrub assert @gateway.supports_scrubbing? @@ -72,19 +61,19 @@ def test_scrub private def pre_scrubbed - %q( + ' Run the remote tests for this gateway, and then put the contents of transcript.log here. - ) + ' end def post_scrubbed - %q( + ' Put the scrubbed contents of transcript.log here after implementing your scrubbing function. Things to scrub: - Credit card number - CVV - Sensitive authentication details - ) + ' end def successful_purchase_response @@ -98,30 +87,21 @@ def successful_purchase_response ) end - def failed_purchase_response - end + def failed_purchase_response; end - def successful_authorize_response - end + def successful_authorize_response; end - def failed_authorize_response - end + def failed_authorize_response; end - def successful_capture_response - end + def successful_capture_response; end - def failed_capture_response - end + def failed_capture_response; end - def successful_refund_response - end + def successful_refund_response; end - def failed_refund_response - end + def failed_refund_response; end - def successful_void_response - end + def successful_void_response; end - def failed_void_response - end + def failed_void_response; end end diff --git a/generators/gateway/templates/remote_gateway_test.rb b/generators/gateway/templates/remote_gateway_test.rb index 08262a97c93..dbe34a57225 100644 --- a/generators/gateway/templates/remote_gateway_test.rb +++ b/generators/gateway/templates/remote_gateway_test.rb @@ -22,8 +22,8 @@ def test_successful_purchase def test_successful_purchase_with_more_options options = { order_id: '1', - ip: "127.0.0.1", - email: "joe@example.com" + ip: '127.0.0.1', + email: 'joe@example.com' } response = @gateway.purchase(@amount, @credit_card, options) @@ -56,7 +56,7 @@ def test_partial_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture end @@ -79,7 +79,7 @@ def test_partial_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount-1, purchase.authorization) + assert refund = @gateway.refund(@amount - 1, purchase.authorization) assert_success refund end @@ -143,5 +143,4 @@ def test_transcript_scrubbing assert_scrubbed(@credit_card.verification_value, transcript) assert_scrubbed(@gateway.options[:password], transcript) end - end diff --git a/lib/active_merchant.rb b/lib/active_merchant.rb index 06ab2f0d19d..5634caae807 100644 --- a/lib/active_merchant.rb +++ b/lib/active_merchant.rb @@ -50,9 +50,9 @@ require 'active_merchant/country' module ActiveMerchant - def self.deprecated(message, caller=Kernel.caller[1]) + def self.deprecated(message, caller = Kernel.caller[1]) warning = caller + ': ' + message - if(respond_to?(:logger) && logger.present?) + if respond_to?(:logger) && logger.present? logger.warn(warning) else warn(warning) diff --git a/lib/active_merchant/billing.rb b/lib/active_merchant/billing.rb index ea3108597c8..998e9c27ddf 100644 --- a/lib/active_merchant/billing.rb +++ b/lib/active_merchant/billing.rb @@ -1,4 +1,5 @@ require 'active_merchant/errors' +require 'active_merchant/versionable' require 'active_merchant/billing/avs_result' require 'active_merchant/billing/cvv_result' @@ -13,3 +14,4 @@ require 'active_merchant/billing/response' require 'active_merchant/billing/gateways' require 'active_merchant/billing/gateway' +require 'active_merchant/billing/three_d_secure_eci_mapper' diff --git a/lib/active_merchant/billing/apple_pay_payment_token.rb b/lib/active_merchant/billing/apple_pay_payment_token.rb index a3e7e98388a..0943cd8f2d7 100644 --- a/lib/active_merchant/billing/apple_pay_payment_token.rb +++ b/lib/active_merchant/billing/apple_pay_payment_token.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class ApplePayPaymentToken < PaymentToken # This is a representation of the token object specified here: # https://developer.apple.com/library/ios/documentation/PassKit/Reference/PKPaymentToken_Ref/ diff --git a/lib/active_merchant/billing/avs_result.rb b/lib/active_merchant/billing/avs_result.rb index 527c3efa119..c4bcf939dda 100644 --- a/lib/active_merchant/billing/avs_result.rb +++ b/lib/active_merchant/billing/avs_result.rb @@ -1,17 +1,15 @@ -#!ruby19 # encoding: utf-8 module ActiveMerchant - module Billing + module Billing # Implements the Address Verification System - # https://www.wellsfargo.com/downloads/pdf/biz/merchant/visa_avs.pdf + # https://www.cybersource.com/developers/other_resources/quick_references/avs_results/. # http://en.wikipedia.org/wiki/Address_Verification_System - # http://apps.cybersource.com/library/documentation/dev_guides/CC_Svcs_IG/html/app_avs_cvn_codes.htm#app_AVS_CVN_codes_7891_48375 - # http://imgserver.skipjack.com/imgServer/5293710/AVS%20and%20CVV2.pdf # http://www.emsecommerce.net/avs_cvv2_response_codes.htm + # https://www.cardfellow.com/blog/address-verification-service-avs/ class AVSResult MESSAGES = { - 'A' => 'Street address matches, but 5-digit and 9-digit postal code do not match.', + 'A' => 'Street address matches, but postal code does not match.', 'B' => 'Street address matches, but postal code not verified.', 'C' => 'Street address and postal code do not match.', 'D' => 'Street address and postal code match.', @@ -24,7 +22,7 @@ class AVSResult 'K' => 'Card member\'s name matches but billing address and billing postal code do not match.', 'L' => 'Card member\'s name and billing postal code match, but billing address does not match.', 'M' => 'Street address and postal code match.', - 'N' => 'Street address and postal code do not match.', + 'N' => 'Street address and postal code do not match. For American Express: Card member\'s name, street address and postal code do not match.', 'O' => 'Card member\'s name and billing address match, but billing postal code does not match.', 'P' => 'Postal code matches, but street address not verified.', 'Q' => 'Card member\'s name, billing address, and postal code match. Shipping information verified but chargeback protection not guaranteed.', @@ -38,60 +36,59 @@ class AVSResult 'Y' => 'Street address and 5-digit postal code match.', 'Z' => 'Street address does not match, but 5-digit postal code matches.' } - + # Map vendor's AVS result code to a postal match code POSTAL_MATCH_CODE = { - 'Y' => %w( D H F H J L M P Q V W X Y Z ), - 'N' => %w( A C K N O ), - 'X' => %w( G S ), - nil => %w( B E I R T U ) + 'Y' => %w(D H F H J L M P Q V W X Y Z), + 'N' => %w(A C K N O), + 'X' => %w(G S), + nil => %w(B E I R T U) }.inject({}) do |map, (type, codes)| codes.each { |code| map[code] = type } map end - + # Map vendor's AVS result code to a street match code STREET_MATCH_CODE = { - 'Y' => %w( A B D H J M O Q T V X Y ), - 'N' => %w( C K L N W Z ), - 'X' => %w( G S ), - nil => %w( E F I P R U ) + 'Y' => %w(A B D H J M O Q T V X Y), + 'N' => %w(C K L N W Z), + 'X' => %w(G S), + nil => %w(E F I P R U) }.inject({}) do |map, (type, codes)| codes.each { |code| map[code] = type } map end - + attr_reader :code, :message, :street_match, :postal_match - + def self.messages MESSAGES end - + def initialize(attrs) attrs ||= {} - + @code = attrs[:code].upcase unless attrs[:code].blank? @message = self.class.messages[code] - + if attrs[:street_match].blank? @street_match = STREET_MATCH_CODE[code] - else + else @street_match = attrs[:street_match].upcase end - + if attrs[:postal_match].blank? @postal_match = POSTAL_MATCH_CODE[code] - else + else @postal_match = attrs[:postal_match].upcase end end - + def to_hash { 'code' => code, 'message' => message, 'street_match' => street_match, - 'postal_match' => postal_match - } + 'postal_match' => postal_match } end end end diff --git a/lib/active_merchant/billing/base.rb b/lib/active_merchant/billing/base.rb index 2248e6f9c39..31c8b9c4a21 100644 --- a/lib/active_merchant/billing/base.rb +++ b/lib/active_merchant/billing/base.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: module Base GATEWAY_MODE_DEPRECATION_MESSAGE = 'Base#gateway_mode is deprecated in favor of Base#mode and will be removed in a future version' @@ -39,19 +39,6 @@ def self.gateway(name) end end - # Return the matching integration module - # You can then get the notification from the module - # * bogus: Bogus - Does nothing (for testing) - # * chronopay: Chronopay - # * paypal: Paypal - # - # chronopay = ActiveMerchant::Billing::Base.integration('chronopay') - # notification = chronopay.notification(raw_post) - # - def self.integration(name) - Billing::Integrations.const_get("#{name.to_s.downcase}".camelize) - end - # A check to see if we're in test mode def self.test? mode == :test diff --git a/lib/active_merchant/billing/check.rb b/lib/active_merchant/billing/check.rb index 561f7b54615..35c6f233389 100644 --- a/lib/active_merchant/billing/check.rb +++ b/lib/active_merchant/billing/check.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # The Check object is a plain old Ruby object, similar to CreditCard. It supports validation # of necessary attributes such as checkholder's name, routing and account numbers, but it is # not backed by any database. @@ -13,6 +13,16 @@ class Check < Model # Used for Canadian bank accounts attr_accessor :institution_number, :transit_number + # Canadian Institution Numbers + # Partial list found here: https://en.wikipedia.org/wiki/Routing_number_(Canada) + CAN_INSTITUTION_NUMBERS = %w( + 001 002 003 004 006 010 016 030 039 117 127 177 219 245 260 269 270 308 + 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 618 842 + ) + def name @name ||= "#{first_name} #{last_name}".strip end @@ -29,19 +39,15 @@ def name=(value) def validate errors = [] - [:name, :routing_number, :account_number].each do |attr| + %i[name routing_number account_number].each do |attr| errors << [attr, 'cannot be empty'] if empty?(self.send(attr)) end errors << [:routing_number, 'is invalid'] unless valid_routing_number? - if(!empty?(account_holder_type) && !%w[business personal].include?(account_holder_type.to_s)) - errors << [:account_holder_type, 'must be personal or business'] - end + errors << [:account_holder_type, 'must be personal or business'] if !empty?(account_holder_type) && !%w[business personal].include?(account_holder_type.to_s) - if(!empty?(account_type) && !%w[checking savings].include?(account_type.to_s)) - errors << [:account_type, 'must be checking or savings'] - end + errors << [:account_type, 'must be checking or savings'] if !empty?(account_type) && !%w[checking savings].include?(account_type.to_s) errors_hash(errors) end @@ -53,25 +59,64 @@ def type def credit_card? false end + + def network_token? + false + end + + def mobile_wallet? + false + end + + def encrypted_wallet? + false + end + + def valid_routing_number? + digits = routing_number.to_s.split('').map(&:to_i).select { |d| (0..9).cover?(d) } + case digits.size + when 9 + return checksum(digits) == 0 || CAN_INSTITUTION_NUMBERS.include?(routing_number[1..3]) + when 8 + return CAN_INSTITUTION_NUMBERS.include?(routing_number[5..7]) + end + + false + end + # Routing numbers may be validated by calculating a checksum and dividing it by 10. The # formula is: # (3(d1 + d4 + d7) + 7(d2 + d5 + d8) + 1(d3 + d6 + d9))mod 10 = 0 # See http://en.wikipedia.org/wiki/Routing_transit_number#Internal_checksums - def valid_routing_number? - digits = routing_number.to_s.split('').map(&:to_i).select{|d| (0..9).include?(d)} + def checksum(digits) + ((3 * (digits[0] + digits[3] + digits[6])) + + (7 * (digits[1] + digits[4] + digits[7])) + + (digits[2] + digits[5] + digits[8])) % 10 + end + + # Always return MICR-formatted routing number for Canadian routing numbers, US routing numbers unchanged + def micr_format_routing_number + digits = routing_number.to_s.split('').map(&:to_i).select { |d| (0..9).cover?(d) } case digits.size when 9 - checksum = ((3 * (digits[0] + digits[3] + digits[6])) + - (7 * (digits[1] + digits[4] + digits[7])) + - (digits[2] + digits[5] + digits[8])) % 10 - case checksum - when 0 - true + if checksum(digits) == 0 + return routing_number else - false + return routing_number[4..8] + routing_number[1..3] end - else - false + when 8 + return routing_number + end + end + + # Always return electronic-formatted routing number for Canadian routing numbers, US routing numbers unchanged + def electronic_format_routing_number + digits = routing_number.to_s.split('').map(&:to_i).select { |d| (0..9).cover?(d) } + case digits.size + when 9 + return routing_number + when 8 + return '0' + routing_number[5..7] + routing_number[0..4] end end end diff --git a/lib/active_merchant/billing/compatibility.rb b/lib/active_merchant/billing/compatibility.rb index b32cb6ca4f4..fcd14928b40 100644 --- a/lib/active_merchant/billing/compatibility.rb +++ b/lib/active_merchant/billing/compatibility.rb @@ -28,10 +28,8 @@ def self.deprecated def self.humanize(lower_case_and_underscored_word) result = lower_case_and_underscored_word.to_s.dup result.gsub!(/_id$/, '') - result.gsub!(/_/, ' ') - result.gsub(/([a-z\d]*)/i) { |match| - match.downcase - }.gsub(/^\w/) { $&.upcase } + result.tr!('_', ' ') + result.gsub(/([a-z\d]*)/i, &:downcase).gsub(/^\w/) { Regexp.last_match(0).upcase } end end end @@ -58,12 +56,12 @@ def valid? private def internal_errors - @errors ||= Errors.new + @internal_errors ||= Errors.new end class Errors < Hash def initialize - super(){|h, k| h[k] = []} + super() { |h, k| h[k] = [] } end alias count size @@ -77,7 +75,7 @@ def []=(key, value) end def empty? - all?{|k, v| v && v.empty?} + all? { |_k, v| v&.empty? } end def on(field) @@ -92,17 +90,18 @@ 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 result = [] self.each do |key, messages| - next unless(messages && !messages.empty?) + next unless messages && !messages.empty? + if key == 'base' - result << "#{messages.first}" + result << messages.first.to_s else result << "#{Compatibility.humanize(key)} #{messages.first}" end @@ -114,7 +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 5a808b6b5b5..a0acc78c09f 100644 --- a/lib/active_merchant/billing/credit_card.rb +++ b/lib/active_merchant/billing/credit_card.rb @@ -2,8 +2,8 @@ require 'date' require 'active_merchant/billing/model' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # A +CreditCard+ object represents a physical credit card, and is capable of validating the various # data associated with these. # @@ -15,12 +15,35 @@ module Billing #:nodoc: # * American Express # * Diner's Club # * JCB - # * Switch - # * Solo # * Dankort # * Maestro # * Forbrugsforeningen - # * Laser + # * Sodexo + # * Vr + # * Carnet + # * Synchrony + # * Routex + # * Elo + # * Alelo + # * Cabal + # * Naranja + # * UnionPay + # * Alia + # * Olimpica + # * Creditel + # * Confiable + # * Mada + # * BpPlus + # * Passcard + # * Edenred + # * Anda + # * Creditos directos (Tarjeta D) + # * Panal + # * Verve + # * Tuya + # * UATP + # * Patagonia365 + # * Tarjeta Sol # # 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 @@ -50,6 +73,8 @@ module Billing #:nodoc: class CreditCard < Model include CreditCardMethods + BRANDS_WITH_SPACES_IN_NUMBER = %w(bp_plus) + class << self # Inherited, but can be overridden w/o changing parent's value attr_accessor :require_verification_value @@ -65,7 +90,7 @@ class << self attr_reader :number def number=(value) - @number = (empty?(value) ? value : value.to_s.gsub(/[^\d]/, '')) + @number = (empty?(value) ? value : filter_number(value)) end # Returns or sets the expiry month for the card. @@ -88,12 +113,35 @@ def number=(value) # * +'american_express'+ # * +'diners_club'+ # * +'jcb'+ - # * +'switch'+ - # * +'solo'+ # * +'dankort'+ # * +'maestro'+ # * +'forbrugsforeningen'+ - # * +'laser'+ + # * +'sodexo'+ + # * +'vr'+ + # * +'carnet'+ + # * +'synchrony'+ + # * +'routex'+ + # * +'elo'+ + # * +'alelo'+ + # * +'cabal'+ + # * +'naranja'+ + # * +'union_pay'+ + # * +'alia'+ + # * +'olimpica'+ + # * +'creditel'+ + # * +'confiable'+ + # * +'mada'+ + # * +'bp_plus'+ + # * +'passcard'+ + # * +'edenred'+ + # * +'anda'+ + # * +'tarjeta-d'+ + # * +'panal'+ + # * +'verve'+ + # * +'tuya'+ + # * +'uatp'+ + # * +'patagonia_365'+ + # * +'tarjeta_sol'+ # # Or, if you wish to test your implementation, +'bogus'+. # @@ -121,10 +169,6 @@ def brand=(value) # @return [String] attr_accessor :last_name - # Required for Switch / Solo cards - attr_reader :start_month, :start_year - attr_accessor :issue_number - # Returns or sets the card verification value. # # This attribute is optional but recommended. The verification value is @@ -188,7 +232,7 @@ def requires_verification_value? 'contactless' => 'Data was read by a Contactless EMV kernel. Issuer script results are not available.', 'contactless_magstripe' => 'Contactless data was read with a non-EMV protocol.', 'contact' => 'Data was read using the EMV protocol. Issuer script results may follow.', - 'contact_quickchip' => 'Data was read by the Quickchip EMV kernel. Issuer script results are not available.', + 'contact_quickchip' => 'Data was read by the Quickchip EMV kernel. Issuer script results are not available.' } # Returns the ciphertext of the card's encrypted PIN. @@ -254,7 +298,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 @@ -263,7 +307,7 @@ def #{m}=(v) v.to_i end end - ) + RUBY end def verification_value? @@ -303,8 +347,7 @@ def validate errors_hash( errors + - validate_card_brand_and_number + - validate_switch_or_solo_attributes + validate_card_brand_and_number ) end @@ -320,9 +363,34 @@ def emv? icc_data.present? end + def allow_spaces_in_card?(number = nil) + BRANDS_WITH_SPACES_IN_NUMBER.include?(self.class.brand?(self.number || number)) + end + + def network_token? + false + end + + def mobile_wallet? + false + end + + def encrypted_wallet? + false + end + private - def validate_essential_attributes #:nodoc: + def filter_number(value) + regex = if allow_spaces_in_card?(value) + /[^\d ]/ + else + /[^\d]/ + end + value.to_s.gsub(regex, '') + end + + def validate_essential_attributes # :nodoc: errors = [] if self.class.requires_name? @@ -330,7 +398,7 @@ def validate_essential_attributes #:nodoc: errors << [:last_name, 'cannot be empty'] if last_name.blank? end - if(empty?(month) || empty?(year)) + if empty?(month) || empty?(year) errors << [:month, 'is required'] if empty?(month) errors << [:year, 'is required'] if empty?(year) else @@ -339,19 +407,17 @@ def validate_essential_attributes #:nodoc: if expired? errors << [:year, 'expired'] else - errors << [:year, 'is not a valid year'] if !valid_expiry_year?(year) + errors << [:year, 'is not a valid year'] if !valid_expiry_year?(year) end end errors end - def validate_card_brand_and_number #:nodoc: + def validate_card_brand_and_number # :nodoc: errors = [] - if !empty?(brand) - errors << [:brand, 'is invalid'] if !CreditCard.card_companies.keys.include?(brand) - end + errors << [:brand, 'is invalid'] if !empty?(brand) && !CreditCard.card_companies.include?(brand) if empty?(number) errors << [:number, 'is required'] @@ -359,69 +425,44 @@ 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 - def validate_verification_value #:nodoc: + def validate_verification_value # :nodoc: errors = [] if verification_value? - unless valid_card_verification_value?(verification_value, brand) - errors << [:verification_value, "should be #{card_verification_value_length(brand)} digits"] - end - elsif requires_verification_value? + errors << [:verification_value, "should be #{card_verification_value_length(brand)} digits"] unless valid_card_verification_value?(verification_value, brand) + elsif requires_verification_value? && !valid_card_verification_value?(verification_value, brand) errors << [:verification_value, 'is required'] end errors end - def validate_switch_or_solo_attributes #:nodoc: - errors = [] - - if %w[switch solo].include?(brand) - valid_start_month = valid_month?(start_month) - valid_start_year = valid_start_year?(start_year) - - if((!valid_start_month || !valid_start_year) && !valid_issue_number?(issue_number)) - if empty?(issue_number) - errors << [:issue_number, 'cannot be empty'] - errors << [:start_month, 'is invalid'] if !valid_start_month - errors << [:start_year, 'is invalid'] if !valid_start_year - else - errors << [:issue_number, 'is invalid'] if !valid_issue_number?(issue_number) - end - end - end - - errors - end - - class ExpiryDate #:nodoc: + class ExpiryDate # :nodoc: attr_reader :month, :year + def initialize(month, year) @month = month.to_i @year = year.to_i end - def expired? #:nodoc: + def expired? # :nodoc: Time.now.utc > expiration end - def expiration #:nodoc: - begin - Time.utc(year, month, month_days, 23, 59, 59) - rescue ArgumentError - Time.at(0).utc - end + def expiration # :nodoc: + Time.utc(year, month, month_days, 23, 59, 59) + rescue ArgumentError + Time.at(0).utc end private + def month_days - mdays = [nil,31,28,31,30,31,30,31,31,30,31,30,31] + mdays = [nil, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] mdays[2] = 29 if Date.leap?(year) mdays[month] end diff --git a/lib/active_merchant/billing/credit_card_formatting.rb b/lib/active_merchant/billing/credit_card_formatting.rb index 65078d04e6f..1788f024e99 100644 --- a/lib/active_merchant/billing/credit_card_formatting.rb +++ b/lib/active_merchant/billing/credit_card_formatting.rb @@ -1,11 +1,14 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: module CreditCardFormatting - 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" @@ -14,9 +17,10 @@ def format(number, option) return '' if number.blank? case option - when :two_digits then sprintf('%.2i', number.to_i)[-2..-1] - when :four_digits then sprintf('%.4i', number.to_i)[-4..-1] - else number + when :two_digits then sprintf('%.2i', number.to_i)[-2..-1] + when :four_digits then sprintf('%.4i', number.to_i)[-4..-1] + when :four_digits_year then number.to_s.length == 2 ? '20' + number.to_s : format(number, :four_digits) + else number end end end diff --git a/lib/active_merchant/billing/credit_card_methods.rb b/lib/active_merchant/billing/credit_card_methods.rb index e61ee859070..ae70f909066 100644 --- a/lib/active_merchant/billing/credit_card_methods.rb +++ b/lib/active_merchant/billing/credit_card_methods.rb @@ -1,24 +1,66 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +require 'set' + +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # Convenience methods that can be included into a custom Credit Card object, such as an ActiveRecord based Credit Card object. module CreditCardMethods - CARD_COMPANIES = { - 'visa' => /^4\d{12}(\d{3})?(\d{3})?$/, - 'master' => /^(5[1-5]\d{4}|677189|222[1-9]\d{2}|22[3-9]\d{3}|2[3-6]\d{4}|27[01]\d{3}|2720\d{2})\d{10}$/, - 'discover' => /^(6011|65\d{2}|64[4-9]\d)\d{12}|(62\d{14})$/, - 'american_express' => /^3[47]\d{13}$/, - 'diners_club' => /^3(0[0-5]|[68]\d)\d{11}$/, - 'jcb' => /^35(28|29|[3-8]\d)\d{12}$/, - 'switch' => /^6759\d{12}(\d{2,3})?$/, - 'solo' => /^6767\d{12}(\d{2,3})?$/, - 'dankort' => /^5019\d{12}$/, - 'maestro' => /^(5[06-8]|6\d)\d{10,17}$/, - 'forbrugsforeningen' => /^600722\d{10}$/, - 'laser' => /^(6304|6706|6709|6771(?!89))\d{8}(\d{4}|\d{6,7})?$/, - 'sodexo' => /^(606071|603389|606070|606069|606068|600818)\d{8}$/, - 'vr' => /^(627416|637036)\d{8}$/ + CARD_COMPANY_DETECTORS = { + 'visa' => ->(num) { num =~ /^4\d{12}(\d{3})?(\d{3})?$/ }, + 'master' => ->(num) { num&.size == 16 && in_bin_range?(num.slice(0, 6), MASTERCARD_RANGES) }, + 'elo' => ->(num) { num&.size == 16 && in_bin_range?(num.slice(0, 6), ELO_RANGES) }, + 'cabal' => ->(num) { num&.size == 16 && in_bin_range?(num.slice(0, 8), CABAL_RANGES) }, + 'alelo' => ->(num) { num&.size == 16 && in_bin_range?(num.slice(0, 6), ALELO_RANGES) }, + 'discover' => ->(num) { num =~ /^(6011|65\d{2}|64[4-9]\d)\d{12,15}$/ }, + 'american_express' => ->(num) { num =~ /^3[47]\d{13}$/ }, + 'naranja' => ->(num) { num&.size == 16 && in_bin_range?(num.slice(0, 6), NARANJA_RANGES) }, + 'diners_club' => ->(num) { num =~ /^3(0[0-5]|[68]\d)\d{11,16}$/ }, + 'jcb' => ->(num) { num&.size == 16 && in_bin_range?(num.slice(0, 4), JCB_RANGES) }, + 'dankort' => ->(num) { num =~ /^5019\d{12}$/ }, + 'maestro' => lambda { |num| + (12..19).cover?(num&.size) && ( + in_bin_range?(num.slice(0, 6), MAESTRO_RANGES) || + MAESTRO_BINS.any? { |bin| num.slice(0, bin.size) == bin } + ) + }, + 'maestro_no_luhn' => ->(num) { num =~ /^(501080|501081|501082)\d{6,13}$/ }, + 'forbrugsforeningen' => ->(num) { num =~ /^600722\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) }, + 'carnet' => lambda { |num| + num&.size == 16 && ( + in_bin_range?(num.slice(0, 6), CARNET_RANGES) || + CARNET_BINS.any? { |bin| num.slice(0, bin.size) == bin } + ) + }, + 'cartes_bancaires' => ->(num) { num&.size == 16 && in_bin_range?(num.slice(0, 6), CARTES_BANCAIRES_RANGES) }, + 'olimpica' => ->(num) { num =~ /^636853\d{10}$/ }, + 'creditel' => ->(num) { num =~ /^601933\d{10}$/ }, + 'confiable' => ->(num) { num =~ /^560718\d{10}$/ }, + 'synchrony' => ->(num) { num =~ /^700600\d{10}$/ }, + '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}$/ }, + '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}$/ }, + 'patagonia_365' => ->(num) { num =~ /^504656\d{10}$/ }, + 'tarjeta_sol' => ->(num) { num =~ /^504639\d{10}$/ } } + SODEXO_NO_LUHN = ->(num) { num =~ /^(505864|505865)\d{10}$/ } + # http://www.barclaycard.co.uk/business/files/bin_rules.pdf ELECTRON_RANGES = [ [400115], @@ -39,12 +81,244 @@ 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), + ] + + CARNET_BINS = Set.new( + %w[ + 286900 502275 606333 627535 636318 636379 639388 + 639484 639559 50633601 50633606 58877274 62753500 + 60462203 60462204 588772 + ] + ) + + CARTES_BANCAIRES_RANGES = [ + (507589..507590), + (507593..507595), + [507597], + [560408], + [581752], + (585402..585405), + (585501..585505), + (585577..585582) + ] + + # https://www.mastercard.us/content/dam/mccom/global/documents/mastercard-rules.pdf, page 73 + MASTERCARD_RANGES = [ + (222100..272099), + (510000..559999), + [605272], + [606282], + [637095], + [637568], + (637599..637600), + [637609], + ] + + MAESTRO_BINS = Set.new( + %w[ 500057 + 501018 501043 501045 501047 501049 501051 501072 501075 501083 501087 501089 501095 + 501500 501623 + 501879 502113 502120 502121 502301 + 503175 503337 503645 503670 + 504310 504338 504363 504533 504587 504620 504738 504781 504910 + 505616 + 507001 507002 507004 507082 507090 + 560014 560565 561033 + 572402 572610 572626 + 576904 + 578614 + 581149 + 585274 585697 + 586509 + 588729 588792 + 589244 589407 589471 589605 589633 589647 589671 589916 + 590043 590206 590263 590265 590278 590361 590362 590379 590393 590590 + 591235 591420 591481 591620 591770 591948 591994 + 592024 592161 592184 592186 592201 592384 592393 592528 592566 592704 592735 592879 592884 + 593074 593264 593272 593355 593496 593556 593589 593666 593709 593825 593963 593994 + 594184 594409 594468 594475 594581 594665 594691 594710 594874 594968 + 595355 595364 595532 595547 595561 595568 595743 595929 + 596245 596289 596399 596405 596590 596608 596645 596646 596791 596808 596815 596846 + 597077 597094 597143 597370 597410 597765 597855 597862 + 598053 598054 598395 598585 598793 598794 598815 598835 598838 598880 598889 + 599000 599069 599089 599148 599191 599310 599741 599742 599867 + 601070 601452 601628 601638 + 602648 + 603326 603450 603689 + 604983 + 606126 + 608710 + 627339 627453 627454 627973 + 636117 636380 636422 636502 636639 + 637046 637529 637568 637600 637756 + 639130 639229 639350 + 690032] + ) + + # https://www.mastercard.us/content/dam/mccom/global/documents/mastercard-rules.pdf, page 79 + MAESTRO_RANGES = [ + (500032..500033), + (501015..501016), + (501020..501021), + (501023..501029), + (501038..501041), + (501053..501058), + (501060..501063), + (501066..501067), + (501091..501092), + (501104..501105), + (501107..501108), + (501104..501105), + (501107..501109), + (501800..501899), + (502000..502099), + (503800..503899), + (561200..561269), + (561271..561299), + (561320..561356), + (581700..581751), + (581753..581800), + (589300..589399), + (589998..591259), + (591261..596770), + (596772..598744), + (598746..599999), + (600297..600314), + (600316..600335), + (600337..600362), + (600364..600382), + (601232..601254), + (601256..601276), + (601640..601652), + (601689..601700), + (602011..602048), + [602050], + (630400..630499), + (639000..639099), + (670000..679999), + ] + + # https://dev.elo.com.br/apis/tabela-de-bins, download csv from left sidebar + ELO_RANGES = [ + 506707..506708, 506715..506715, 506717..506722, 506724..506736, 506739..506743, + 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, 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. + # The BINs beginning with the digit 4 overlap with Visa's range of valid card numbers. + # By placing the 'alelo' entry in CARD_COMPANY_DETECTORS below the 'visa' entry, we + # identify these cards as Visa. This works because transactions with such cards will + # run on Visa rails. + ALELO_RANGES = [ + 402588..402588, 404347..404347, 405876..405876, 405882..405882, 405884..405884, + 405886..405886, 430471..430471, 438061..438061, 438064..438064, 470063..470066, + 496067..496067, 506699..506704, 506706..506706, 506713..506714, 506716..506716, + 506749..506750, 506752..506752, 506754..506756, 506758..506767, 506770..506771, + 506773..506773, 509015..509019, 509880..509882, 509884..509885, 509887..509887, + 509987..509992 + ] + + CABAL_RANGES = [ + 60420100..60440099, + 58965700..58965799, + 60352200..60352299, + 65027200..65027299, + 65008700..65008700, + 65090000..65090099 + ] + + MADA_RANGES = [ + 504300..504300, 506968..506968, 508160..508160, 585265..585265, 588848..588848, + 588850..588850, 588982..588983, 589005..589005, 589206..589206, 604906..604906, + 605141..605141, 636120..636120, 968201..968209, 968211..968211 + ] + + NARANJA_RANGES = [ + 589562..589562 + ] + + # https://www.discoverglobalnetwork.com/content/dam/discover/en_us/dgn/pdfs/IPP-VAR-Enabler-Compliance.pdf + UNIONPAY_RANGES = [ + 62000000..62000000, 62178570..62178570, 62212600..62379699, 62400000..62699999, 62820000..62889999, + 81000000..81099999, 81100000..81319999, 81320000..81519999, 81520000..81639999, 81640000..81719999 + ] + + JCB_RANGES = [ + 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 + def self.in_bin_range?(number, ranges) + bin = number.to_i + ranges.any? do |range| + range.include?(bin) + end + end + def valid_month?(month) - (1..12).include?(month.to_i) + (1..12).cover?(month.to_i) end def credit_card? @@ -52,7 +326,7 @@ def credit_card? end def valid_expiry_year?(year) - (Time.now.year..Time.now.year + 20).include?(year.to_i) + (Time.now.year..Time.now.year + 20).cover?(year.to_i) end def valid_start_year?(year) @@ -77,7 +351,14 @@ def valid_card_verification_value?(cvv, brand) end def card_verification_value_length(brand) - brand == 'american_express' ? 4 : 3 + case brand + when 'american_express' + 4 + when 'maestro' + 0 + else + 3 + end end def valid_issue_number?(number) @@ -98,47 +379,28 @@ module ClassMethods # - http://www.beachnet.com/~hstiles/cardtype.html def valid_number?(number) valid_test_mode_card_number?(number) || - valid_card_number_length?(number) && - valid_card_number_characters?(number) && - valid_checksum?(number) + (valid_card_number_length?(number) && + valid_card_number_characters?(brand?(number), number) && + valid_by_algorithm?(brand?(number), number)) end - # Regular expressions for the known card companies. - # - # References: - # - http://en.wikipedia.org/wiki/Credit_card_number - # - http://www.barclaycardbusiness.co.uk/information_zone/processing/bin_rules.html def card_companies - CARD_COMPANIES + CARD_COMPANY_DETECTORS.keys end # Returns a string containing the brand of card from the list of known information below. - # Need to check the cards in a particular order, as there is some overlap of the allowable ranges - #-- - # TODO Refactor this method. We basically need to tighten up the Maestro Regexp. - # - # Right now the Maestro regexp overlaps with the MasterCard regexp (IIRC). If we can tighten - # things up, we can boil this whole thing down to something like... - # - # def brand?(number) - # return 'visa' if valid_test_mode_card_number?(number) - # card_companies.find([nil]) { |brand, regexp| number =~ regexp }.first.dup - # end - # def brand?(number) return 'bogus' if valid_test_mode_card_number?(number) - card_companies.reject { |c,p| c == 'maestro' }.each do |company, pattern| - return company.dup if number =~ pattern + CARD_COMPANY_DETECTORS.each do |company, func| + return company.dup if func.call(number) end - return 'maestro' if number =~ card_companies['maestro'] - return nil end def electron?(number) - return false unless [16, 19].include?(number.length) + return false unless [16, 19].include?(number&.length) # don't recalculate for each range bank_identification_number = first_digits(number).to_i @@ -154,11 +416,13 @@ def type?(number) end def first_digits(number) - number.to_s.slice(0,6) + number&.slice(0, 6) || '' end def last_digits(number) - number.to_s.length <= 4 ? number : number.to_s.slice(-4..-1) + return '' if number.nil? + + number.length <= 4 ? number : number.slice(-4..-1) end def mask(number) @@ -177,20 +441,46 @@ def matching_type?(number, brand) private - def valid_card_number_length?(number) #:nodoc: - number.to_s.length >= 12 + def valid_card_number_length?(number) # :nodoc: + return false if number.nil? + + number.length >= 12 end - def valid_card_number_characters?(number) #:nodoc: - !number.to_s.match(/\D/) + def valid_card_number_characters?(brand, number) # :nodoc: + return false if number.nil? + return number =~ /\A[0-9 ]+\Z/ if brand == 'bp_plus' + + !number.match(/\D/) end - def valid_test_mode_card_number?(number) #:nodoc: + def valid_test_mode_card_number?(number) # :nodoc: ActiveMerchant::Billing::Base.test? && - %w[1 2 3 success failure error].include?(number.to_s) + %w[1 2 3 success failure error].include?(number) + end + + def sodexo_no_luhn?(numbers) + SODEXO_NO_LUHN.call(numbers) + end + + def valid_by_algorithm?(brand, numbers) # :nodoc: + case brand + when 'naranja' + valid_naranja_algo?(numbers) || valid_luhn?(numbers) + when 'creditel' + valid_creditel_algo?(numbers) + when 'alia', 'confiable', 'maestro_no_luhn', 'anda', 'tarjeta-d', 'hipercard' + true + when 'sodexo' + sodexo_no_luhn?(numbers) ? true : valid_luhn?(numbers) + when 'bp_plus', 'passcard', 'edenred' + valid_luhn_non_zero_check_digit?(numbers) + else + valid_luhn?(numbers) + end end - ODD_LUHN_VALUE = { + BYTES_TO_DIGITS = { 48 => 0, 49 => 1, 50 => 2, @@ -204,7 +494,7 @@ def valid_test_mode_card_number?(number) #:nodoc: nil => 0 }.freeze - EVEN_LUHN_VALUE = { + BYTES_TO_DIGITS_DOUBLED = { 48 => 0, # 0 * 2 49 => 2, # 1 * 2 50 => 4, # 2 * 2 @@ -214,28 +504,67 @@ def valid_test_mode_card_number?(number) #:nodoc: 54 => 3, # 6 * 2 - 9 55 => 5, # etc ... 56 => 7, - 57 => 9, + 57 => 9 }.freeze # Checks the validity of a card number by use of the Luhn Algorithm. # Please see http://en.wikipedia.org/wiki/Luhn_algorithm for details. # This implementation is from the luhn_checksum gem, https://github.com/zendesk/luhn_checksum. - def valid_checksum?(numbers) #:nodoc: + def valid_luhn?(numbers) # :nodoc: sum = 0 odd = true - numbers.reverse.bytes.each do |number| + numbers.reverse.bytes.each do |bytes| if odd odd = false - sum += ODD_LUHN_VALUE[number] + sum += BYTES_TO_DIGITS[bytes] else odd = true - sum += EVEN_LUHN_VALUE[number] + sum += BYTES_TO_DIGITS_DOUBLED[bytes] end end sum % 10 == 0 end + + def valid_luhn_with_check_digit?(numbers, check_digit) + sum = 0 + + doubler = true + + numbers.reverse.bytes.each do |bytes| + doubler ? sum += BYTES_TO_DIGITS_DOUBLED[bytes] : sum += BYTES_TO_DIGITS[bytes] + doubler = !doubler + end + + (10 - (sum % 10)) % 10 == check_digit.to_i + end + + def valid_luhn_non_zero_check_digit?(numbers) + return valid_luhn?(numbers.delete(' ')) if numbers[5] == ' ' + + check_digit = numbers[-1] + luhn_payload = numbers.delete(' ').chop + valid_luhn_with_check_digit?(luhn_payload, check_digit) + end + + # Checks the validity of a card number by use of specific algorithms + def valid_naranja_algo?(numbers) # :nodoc: + num_array = numbers.to_s.chars.map(&:to_i) + multipliers = [4, 3, 2, 7, 6, 5, 4, 3, 2, 7, 6, 5, 4, 3, 2] + num_sum = num_array[0..14].zip(multipliers).map { |a, b| a * b }.reduce(:+) + intermediate = 11 - (num_sum % 11) + final_num = intermediate > 9 ? 0 : intermediate + final_num == num_array[15] + end + + def valid_creditel_algo?(numbers) # :nodoc: + num_array = numbers.to_s.chars.map(&:to_i) + multipliers = [5, 4, 3, 2, 1, 9, 8, 7, 6, 5, 4, 3, 2, 1, 9] + num_sum = num_array[0..14].zip(multipliers).map { |a, b| a * b }.reduce(:+) + final_num = num_sum % 10 + final_num == num_array[15] + end end end end diff --git a/lib/active_merchant/billing/cvv_result.rb b/lib/active_merchant/billing/cvv_result.rb index 2fbbbb83c4b..e96eaf0889f 100644 --- a/lib/active_merchant/billing/cvv_result.rb +++ b/lib/active_merchant/billing/cvv_result.rb @@ -4,7 +4,6 @@ module Billing # http://www.bbbonline.org/eExport/doc/MerchantGuide_cvv2.pdf # Check additional codes from cybersource website class CVVResult - MESSAGES = { 'D' => 'CVV check flagged transaction as suspicious', 'I' => 'CVV failed data validation check', diff --git a/lib/active_merchant/billing/gateway.rb b/lib/active_merchant/billing/gateway.rb index d6596a810e9..7dd6da50098 100644 --- a/lib/active_merchant/billing/gateway.rb +++ b/lib/active_merchant/billing/gateway.rb @@ -2,8 +2,8 @@ require 'net/https' require 'active_merchant/billing/response' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # # == Description # The Gateway class is the base class for all ActiveMerchant gateway implementations. @@ -55,8 +55,7 @@ module Billing #:nodoc: class Gateway include PostsData include CreditCardFormatting - - DEBIT_CARDS = [ :switch, :solo ] + include Versionable CREDIT_DEPRECATION_MESSAGE = 'Support for using credit to refund existing transactions is deprecated and will be removed from a future release of ActiveMerchant. Please use the refund method instead.' RECURRING_DEPRECATION_MESSAGE = 'Recurring functionality in ActiveMerchant is deprecated and will be removed in a future version. Please contact the ActiveMerchant maintainers if you have an interest in taking ownership of a separate gem that continues support for it.' @@ -82,22 +81,23 @@ class Gateway # as network tokenization. STANDARD_ERROR_CODE = { - :incorrect_number => 'incorrect_number', - :invalid_number => 'invalid_number', - :invalid_expiry_date => 'invalid_expiry_date', - :invalid_cvc => 'invalid_cvc', - :expired_card => 'expired_card', - :incorrect_cvc => 'incorrect_cvc', - :incorrect_zip => 'incorrect_zip', - :incorrect_address => 'incorrect_address', - :incorrect_pin => 'incorrect_pin', - :card_declined => 'card_declined', - :processing_error => 'processing_error', - :call_issuer => 'call_issuer', - :pickup_card => 'pick_up_card', - :config_error => 'config_error', - :test_mode_live_card => 'test_mode_live_card', - :unsupported_feature => 'unsupported_feature', + incorrect_number: 'incorrect_number', + invalid_number: 'invalid_number', + invalid_expiry_date: 'invalid_expiry_date', + invalid_cvc: 'invalid_cvc', + expired_card: 'expired_card', + incorrect_cvc: 'incorrect_cvc', + incorrect_zip: 'incorrect_zip', + incorrect_address: 'incorrect_address', + incorrect_pin: 'incorrect_pin', + card_declined: 'card_declined', + processing_error: 'processing_error', + call_issuer: 'call_issuer', + pickup_card: 'pick_up_card', + config_error: 'config_error', + test_mode_live_card: 'test_mode_live_card', + unsupported_feature: 'unsupported_feature', + invalid_amount: 'invalid_amount' } cattr_reader :implementations @@ -125,8 +125,9 @@ def generate_unique_id class_attribute :supported_cardtypes self.supported_cardtypes = [] + # This default list of currencies without fractions are from https://en.wikipedia.org/wiki/ISO_4217 class_attribute :currencies_without_fractions, :currencies_with_three_decimal_places - self.currencies_without_fractions = %w(BIF BYR CLP CVE DJF GNF ISK JPY KMF KRW PYG RWF UGX VND VUV XAF XOF XPF) + self.currencies_without_fractions = %w(BIF BYR CLP CVE DJF GNF ISK JPY KMF KRW PYG RWF UGX UYI VND VUV XAF XOF XPF) self.currencies_with_three_decimal_places = %w() class_attribute :homepage_url @@ -157,15 +158,13 @@ def self.card_brand(source) def self.supported_countries=(country_codes) country_codes.each do |country_code| - unless ActiveMerchant::Country.find(country_code) - raise ActiveMerchant::InvalidCountryCodeError, "No country could be found for the country #{country_code}" - end + raise ActiveMerchant::InvalidCountryCodeError, "No country could be found for the country #{country_code}" unless ActiveMerchant::Country.find(country_code) end @supported_countries = country_codes.dup end def self.supported_countries - @supported_countries ||= [] + @supported_countries ||= (self.superclass.supported_countries || []) end def supported_countries @@ -195,32 +194,42 @@ def supports_scrubbing? end def scrub(transcript) - raise RuntimeError.new('This gateway does not support scrubbing.') + raise 'This gateway does not support scrubbing.' end def supports_network_tokenization? false end + def add_fields_to_post_if_present(post, options, fields) + fields.each do |field| + add_field_to_post_if_present(post, options, field) + end + end + + def add_field_to_post_if_present(post, options, field) + post[field] = options[field] if options[field] + end + protected # :nodoc: all def normalize(field) case field - when 'true' then true - when 'false' then false - when '' then nil - when 'null' then nil - else field + when 'true' then true + when 'false' then false + when '' then nil + when 'null' then nil + else field end end def user_agent @@ua ||= JSON.dump({ - :bindings_version => ActiveMerchant::VERSION, - :lang => 'ruby', - :lang_version => "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})", - :platform => RUBY_PLATFORM, - :publisher => 'active_merchant' + bindings_version: ActiveMerchant::VERSION, + lang: 'ruby', + lang_version: "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})", + platform: RUBY_PLATFORM, + publisher: 'active_merchant' }) end @@ -242,16 +251,16 @@ def name def amount(money) return nil if money.nil? - cents = if money.respond_to?(:cents) - ActiveMerchant.deprecated 'Support for Money objects is deprecated and will be removed from a future release of ActiveMerchant. Please use an Integer value in cents' - money.cents - else - money - end - if money.is_a?(String) - raise ArgumentError, 'money amount must be a positive Integer in cents.' - end + cents = + if money.respond_to?(:cents) + ActiveMerchant.deprecated 'Support for Money objects is deprecated and will be removed from a future release of ActiveMerchant. Please use an Integer value in cents' + money.cents + else + money + end + + raise ArgumentError, 'money amount must be a positive Integer in cents.' if money.is_a?(String) if self.money_format == :cents cents.to_s @@ -272,6 +281,7 @@ def localized_amount(money, currency) amount = amount(money) return amount unless non_fractional_currency?(currency) || three_decimal_currency?(currency) + if non_fractional_currency?(currency) if self.money_format == :cents sprintf('%.0f', amount.to_f / 100) @@ -293,6 +303,7 @@ def currency(money) def truncate(value, max_size) return nil unless value + value.to_s[0, max_size] end @@ -305,9 +316,13 @@ def split_names(full_name) [first_name, last_name] end - def requires_start_date_or_issue_number?(credit_card) - return false if card_brand(credit_card).blank? - DEBIT_CARDS.include?(card_brand(credit_card).to_sym) + 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) @@ -316,7 +331,7 @@ def requires!(hash, *params) raise ArgumentError.new("Missing required parameter: #{param.first}") unless hash.has_key?(param.first) valid_options = param[1..-1] - raise ArgumentError.new("Parameter: #{param.first} must be one of #{valid_options.to_sentence(:words_connector => 'or')}") unless valid_options.include?(hash[param.first]) + raise ArgumentError.new("Parameter: #{param.first} must be one of #{valid_options.to_sentence(words_connector: 'or')}") unless valid_options.include?(hash[param.first]) else raise ArgumentError.new("Missing required parameter: #{param}") unless hash.has_key?(param) end diff --git a/lib/active_merchant/billing/gateways/adyen.rb b/lib/active_merchant/billing/gateways/adyen.rb index afea91c45ee..d11ce91092e 100644 --- a/lib/active_merchant/billing/gateways/adyen.rb +++ b/lib/active_merchant/billing/gateways/adyen.rb @@ -1,90 +1,186 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class AdyenGateway < Gateway - # we recommend setting up merchant-specific endpoints. # https://docs.adyen.com/developers/api-manual#apiendpoints - self.test_url = 'https://pal-test.adyen.com/pal/servlet/Payment/v18' - self.live_url = 'https://pal-live.adyen.com/pal/servlet/Payment/v18' + self.test_url = 'https://pal-test.adyen.com/pal/servlet/' + self.live_url = 'https://pal-live.adyen.com/pal/servlet/' - self.supported_countries = ['AT','AU','BE','BG','BR','CH','CY','CZ','DE','DK','EE','ES','FI','FR','GB','GI','GR','HK','HU','IE','IS','IT','LI','LT','LU','LV','MC','MT','MX','NL','NO','PL','PT','RO','SE','SG','SK','SI','US'] + self.supported_countries = %w(AT AU BE BG BR CH CY CZ DE DK EE ES FI FR GB GI GR HK HU IE IS IT LI LT LU LV MC MT MX NL NO PL PT RO SE SG SK SI US) self.default_currency = 'USD' - self.supported_cardtypes = [:visa, :master, :american_express, :diners_club, :jcb, :dankort, :maestro, :discover] + self.currencies_without_fractions = %w(CVE DJF GNF IDR JPY KMF KRW PYG RWF UGX VND VUV XAF XOF XPF) + self.currencies_with_three_decimal_places = %w(BHD IQD JOD KWD LYD OMR TND) + self.supported_cardtypes = %i[visa master american_express diners_club jcb dankort maestro discover elo naranja cabal unionpay patagonia_365 tarjeta_sol] self.money_format = :cents self.homepage_url = 'https://www.adyen.com/' self.display_name = 'Adyen' + version 'v68', :payment_api + version 'v68', :payout_api + version 'v68', :recurring_api + STANDARD_ERROR_CODE_MAPPING = { + '0' => STANDARD_ERROR_CODE[:processing_error], + '10' => STANDARD_ERROR_CODE[:config_error], + '100' => STANDARD_ERROR_CODE[:invalid_amount], '101' => STANDARD_ERROR_CODE[:incorrect_number], '103' => STANDARD_ERROR_CODE[:invalid_cvc], + '104' => STANDARD_ERROR_CODE[:incorrect_address], '131' => STANDARD_ERROR_CODE[:incorrect_address], '132' => STANDARD_ERROR_CODE[:incorrect_address], '133' => STANDARD_ERROR_CODE[:incorrect_address], '134' => STANDARD_ERROR_CODE[:incorrect_address], - '135' => STANDARD_ERROR_CODE[:incorrect_address], + '135' => STANDARD_ERROR_CODE[:incorrect_address] } - def initialize(options={}) + def initialize(options = {}) requires!(options, :username, :password, :merchant_account) @username, @password, @merchant_account = options.values_at(:username, :password, :merchant_account) super end - def purchase(money, payment, options={}) - MultiResponse.run do |r| - r.process{authorize(money, payment, options)} - r.process{capture(money, r.authorization, options)} + def purchase(money, payment, options = {}) + if options[:execute_threed] || options[:threed_dynamic] + authorize(money, payment, options) + else + MultiResponse.run do |r| + r.process { authorize(money, payment, options) } + r.process { capture(money, r.authorization, capture_options(options)) } + end end end - def authorize(money, payment, options={}) + def authorize(money, payment, options = {}) requires!(options, :order_id) post = init_post(options) add_invoice(post, money, options) - add_payment(post, payment) + add_payment(post, payment, options) add_extra_data(post, payment, options) - add_shopper_interaction(post, payment, options) + add_stored_credentials(post, payment, options) add_address(post, options) add_installments(post, options) if options[:installments] - commit('authorise', post) + add_3ds(post, options) + add_3ds_authenticated_data(post, options) + add_splits(post, options) + add_recurring_contract(post, options, payment) + add_network_transaction_reference(post, options) + add_application_info(post, options) + add_level_2_data(post, options) + add_level_3_data(post, options) + add_data_airline(post, options) + add_data_lodging(post, options) + add_metadata(post, options) + add_recurring_detail_reference(post, options) + add_fund_source(post, options) + add_fund_destination(post, options) + post[:localizedShopperStatement] = options[:localized_shopper_statement] if options[:localized_shopper_statement] + commit('authorise', post, options) end - def capture(money, authorization, options={}) + def capture(money, authorization, options = {}) post = init_post(options) add_invoice_for_modification(post, money, options) add_reference(post, authorization, options) - commit('capture', post) + add_splits(post, options) + add_network_transaction_reference(post, options) + add_shopper_statement(post, options) + commit('capture', post, options) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) post = init_post(options) add_invoice_for_modification(post, money, options) - add_original_reference(post, authorization, options) - commit('refund', post) + add_reference(post, authorization, options) + add_splits(post, options) + add_network_transaction_reference(post, options) + add_shopper_statement(post, options) + commit('refund', post, options) + end + + def credit(money, payment, options = {}) + 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 + add_recurring_detail_reference(post, options) + commit(action, post, options) end - def void(authorization, options={}) + def void(authorization, options = {}) post = init_post(options) + endpoint = options[:cancel_or_refund] ? 'cancelOrRefund' : 'cancel' add_reference(post, authorization, options) - commit('cancel', post) + add_network_transaction_reference(post, options) + commit(endpoint, post, options) end - def store(credit_card, options={}) + def adjust(money, authorization, options = {}) + post = init_post(options) + add_invoice_for_modification(post, money, options) + add_reference(post, authorization, options) + add_extra_data(post, nil, options) + add_recurring_detail_reference(post, options) + commit('adjustAuthorisation', post, options) + end + + def store(credit_card, options = {}) requires!(options, :order_id) post = init_post(options) add_invoice(post, 0, options) - add_payment(post, credit_card) + add_payment(post, credit_card, options) add_extra_data(post, credit_card, options) - add_recurring_contract(post, options) + add_stored_credentials(post, credit_card, options) add_address(post, options) - commit('authorise', post) + add_network_transaction_reference(post, options) + options[:recurring_contract_type] ||= 'RECURRING' + add_recurring_contract(post, options) + + action = options[:tokenize_only] ? 'storeToken' : 'authorise' + + initial_response = commit(action, post, options) + + if initial_response.success? && card_not_stored?(initial_response) + unsupported_failure_response(initial_response) + else + initial_response + end + end + + def unstore(options = {}) + requires!(options, :shopper_reference, :recurring_detail_reference) + post = {} + + add_shopper_reference(post, options) + add_merchant_account(post, options) + post[:recurringDetailReference] = options[:recurring_detail_reference] + + commit('disable', post, options) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) + amount = options[:verify_amount]&.to_i || 0 MultiResponse.run(:use_first_response) do |r| - r.process { authorize(0, credit_card, options) } + r.process { authorize(amount, credit_card, options) } + options[:idempotency_key] = nil r.process(:ignore_result) { void(r.authorization, options) } end end @@ -93,34 +189,308 @@ def supports_scrubbing? true end + def supports_network_tokenization? + true + end + def scrub(transcript) transcript. gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). - gsub(%r(("number\\?":\\?")[^"]*)i, '\1[FILTERED]'). - gsub(%r(("cvc\\?":\\?")[^"]*)i, '\1[FILTERED]'). - gsub(%r(("cavv\\?":\\?")[^"]*)i, '\1[FILTERED]') + gsub(%r(("number\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("cvc\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("cavv\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("bankLocationId\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("iban\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("bankAccountNumber\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]') end private + AVS_MAPPING = { + '0' => 'R', # Unknown + '1' => 'A', # Address matches, postal code doesn't + '2' => 'N', # Neither postal code nor address match + '3' => 'R', # AVS unavailable + '4' => 'E', # AVS not supported for this card type + '5' => 'U', # No AVS data provided + '6' => 'Z', # Postal code matches, address doesn't match + '7' => 'D', # Both postal code and address match + '8' => 'U', # Address not checked, postal code unknown + '9' => 'B', # Address matches, postal code unknown + '10' => 'N', # Address doesn't match, postal code unknown + '11' => 'U', # Postal code not checked, address unknown + '12' => 'B', # Address matches, postal code not checked + '13' => 'U', # Address doesn't match, postal code not checked + '14' => 'P', # Postal code matches, address unknown + '15' => 'P', # Postal code matches, address not checked + '16' => 'N', # Postal code doesn't match, address unknown + '17' => 'U', # Postal code doesn't match, address not checked + '18' => 'I', # Neither postal code nor address were checked + '19' => 'L', # Name and postal code matches. + '20' => 'V', # Name, address and postal code matches. + '21' => 'O', # Name and address matches. + '22' => 'K', # Name matches. + '23' => 'F', # Postal code matches, name doesn't match. + '24' => 'H', # Both postal code and address matches, name doesn't match. + '25' => 'T', # Address matches, name doesn't match. + '26' => 'N' # Neither postal code, address nor name matches. + } + + CVC_MAPPING = { + '0' => 'P', # Unknown + '1' => 'M', # Matches + '2' => 'N', # Does not match + '3' => 'P', # Not checked + '4' => 'S', # No CVC/CVV provided, but was required + '5' => 'U', # Issuer not certifed by CVC/CVV + '6' => 'P' # No CVC/CVV provided + } + NETWORK_TOKENIZATION_CARD_SOURCE = { 'apple_pay' => 'applepay', 'android_pay' => 'androidpay', - 'google_pay' => 'paywithgoogle' + 'google_pay' => 'googlepay' } def add_extra_data(post, payment, options) - post[:shopperEmail] = options[:shopper_email] if options[:shopper_email] - post[:shopperIP] = options[:shopper_ip] if options[:shopper_ip] - post[:shopperReference] = options[:shopper_reference] if options[:shopper_reference] - post[:fraudOffset] = options[:fraud_offset] if options[:fraud_offset] - post[:selectedBrand] = options[:selected_brand] || NETWORK_TOKENIZATION_CARD_SOURCE[payment.source.to_s] if payment.is_a?(NetworkTokenizationCreditCard) + 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[:selectedBrand] = options[:selected_brand] if options[:selected_brand] && !post[:selectedBrand] 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] + post[:additionalData][:authorisationType] = options[:authorisation_type] if options[:authorisation_type] + 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[:additionalData][:updateShopperStatement] = options[:update_shopper_statement] if options[:update_shopper_statement] + post[:additionalData][:manualCapture] = options[:manual_capture] if options[:manual_capture] + end + + def extract_and_transform(mapper, from) + mapper.each_with_object({}) do |key_map, hsh| + key, item_key = key_map[0], key_map[1] + hsh[key] = from[item_key.to_sym] + end + end + + def add_level_2_data(post, options) + return unless options[:level_2_data].present? + + mapper = { + 'enhancedSchemeData.totalTaxAmount': 'total_tax_amount', + 'enhancedSchemeData.customerReference': 'customer_reference' + } + post[:additionalData].merge!(extract_and_transform(mapper, options[:level_2_data])) + end + + def add_level_3_data(post, options) + return unless options[:level_3_data].present? + + mapper = { 'enhancedSchemeData.freightAmount': 'freight_amount', + 'enhancedSchemeData.destinationStateProvinceCode': 'destination_state_province_code', + 'enhancedSchemeData.shipFromPostalCode': 'ship_from_postal_code', + 'enhancedSchemeData.orderDate': 'order_date', + 'enhancedSchemeData.destinationPostalCode': 'destination_postal_code', + 'enhancedSchemeData.destinationCountryCode': 'destination_country_code', + 'enhancedSchemeData.dutyAmount': 'duty_amount' } + + post[:additionalData].merge!(extract_and_transform(mapper, options[:level_3_data])) + + item_detail_keys = %w[description product_code quantity unit_of_measure unit_price discount_amount total_amount commodity_code] + if options[:level_3_data][:items].present? + options[:level_3_data][:items].last(9).each.with_index(1) do |item, index| + mapper = item_detail_keys.each_with_object({}) do |key, hsh| + hsh["enhancedSchemeData.itemDetailLine#{index}.#{key.camelize(:lower)}"] = key + end + post[:additionalData].merge!(extract_and_transform(mapper, item)) + end + end + post[:additionalData].compact! + end + + def add_data_airline(post, options) + return unless options[:additional_data_airline] + + mapper = %w[ + agency_invoice_number + agency_plan_name + airline_code + airline_designator_code + boarding_fee + computerized_reservation_system + customer_reference_number + document_type + flight_date + ticket_issue_address + ticket_number + travel_agency_code + travel_agency_name + passenger_name + ].each_with_object({}) { |value, hash| hash["airline.#{value}"] = value } + + post[:additionalData].merge!(extract_and_transform(mapper, options[:additional_data_airline])) + + if options[:additional_data_airline][:leg].present? + leg_data = %w[ + carrier_code + class_of_travel + date_of_travel + depart_airport + depart_tax + destination_code + fare_base_code + flight_number + stop_over_code + ].each_with_object({}) { |value, hash| hash["airline.leg.#{value}"] = value } + + post[:additionalData].merge!(extract_and_transform(leg_data, options[:additional_data_airline][:leg])) + end + + # temporary duplication with minor modification (:legs array with nested hashes instead of a single :leg hash) + # this should preserve backward-compatibility with :leg logic above until it is deprecated/removed + if options[:additional_data_airline][:legs].present? + options[:additional_data_airline][:legs].each_with_index do |leg, number| + 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#{number + 1}.#{value}"] = value } + + post[:additionalData].merge!(extract_and_transform(leg_data, leg)) + end + 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) + post[:additionalData] ||= {} + post[:additionalData][:shopperStatement] = options[:shopper_statement] if options[:shopper_statement] + post[:additionalData][:localizedShopperStatement] = options[:localized_shopper_statement] if options[:localized_shopper_statement] + end + + def add_merchant_data(post, options) + post[:additionalData][:subMerchantID] = options[:sub_merchant_id] if options[:sub_merchant_id] + post[:additionalData][:subMerchantName] = options[:sub_merchant_name] if options[:sub_merchant_name] + post[:additionalData][:subMerchantStreet] = options[:sub_merchant_street] if options[:sub_merchant_street] + post[:additionalData][:subMerchantCity] = options[:sub_merchant_city] if options[:sub_merchant_city] + post[:additionalData][:subMerchantState] = options[:sub_merchant_state] if options[:sub_merchant_state] + post[:additionalData][:subMerchantPostalCode] = options[:sub_merchant_postal_code] if options[:sub_merchant_postal_code] + post[:additionalData][:subMerchantCountry] = options[:sub_merchant_country] if options[:sub_merchant_country] + post[:additionalData][:subMerchantTaxId] = options[:sub_merchant_tax_id] if options[:sub_merchant_tax_id] + post[:additionalData][:subMerchantMCC] = options[:sub_merchant_mcc] if options[:sub_merchant_mcc] + post[:additionalData] = post[:additionalData].merge(options[:sub_merchant_data]) if options[:sub_merchant_data] + end + + def add_risk_data(post, options) + if (risk_data = options[:risk_data]) + risk_data = risk_data.map { |k, v| ["riskdata.#{k}", v] }.to_h + post[:additionalData].merge!(risk_data) + end + end + + def add_splits(post, options) + return unless split_data = options[:splits] + + splits = [] + split_data.each do |split| + if split['amount'] + amount = {} + amount[:value] = split['amount']['value'] if split['amount']['value'] + amount[:currency] = split['amount']['currency'] if split['amount']['currency'] + end + + split_hash = { + type: split['type'], + reference: split['reference'] + } + split_hash[:amount] = amount unless amount.nil? + split_hash['account'] = split['account'] if split['account'] + splits.push(split_hash) + end + post[:splits] = splits + end + + def add_stored_credentials(post, payment, options) + add_shopper_interaction(post, payment, options) + add_recurring_processing_model(post, options) + end + + def add_merchant_account(post, options) + post[:merchantAccount] = options[:merchant_account] || @merchant_account + end + + def add_shopper_reference(post, options) + post[:shopperReference] = options[:shopper_reference] if options[:shopper_reference] end - def add_shopper_interaction(post, payment, options={}) - if (payment.respond_to?(:verification_value) && payment.verification_value) || payment.is_a?(NetworkTokenizationCreditCard) + def add_shopper_interaction(post, payment, options = {}) + if ecommerce_shopper_interaction?(payment, options) shopper_interaction = 'Ecommerce' else shopper_interaction = 'ContAuth' @@ -129,46 +499,115 @@ def add_shopper_interaction(post, payment, options={}) post[:shopperInteraction] = options[:shopper_interaction] || shopper_interaction end + def add_recurring_processing_model(post, options) + return unless options.dig(:stored_credential, :reason_type) || options[:recurring_processing_model] + + if options.dig(:stored_credential, :reason_type) == 'unscheduled' + if options.dig(:stored_credential, :initiator) == 'merchant' + recurring_processing_model = 'UnscheduledCardOnFile' + else + recurring_processing_model = 'CardOnFile' + end + else + recurring_processing_model = 'Subscription' + end + + post[:recurringProcessingModel] = options[:recurring_processing_model] || recurring_processing_model + end + def add_address(post, options) - return unless post[:card] && post[:card].kind_of?(Hash) + if address = options[:shipping_address] + post[:deliveryAddress] = {} + 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] = 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[:card][:billingAddress] = {} - post[:card][:billingAddress][:street] = address[:address1] || 'N/A' - post[:card][:billingAddress][:houseNumberOrName] = address[:address2] || 'N/A' - post[:card][:billingAddress][:postalCode] = address[:zip] if address[:zip] - post[:card][:billingAddress][:city] = address[:city] || 'N/A' - post[:card][:billingAddress][:stateOrProvince] = address[:state] if address[:state] - post[:card][:billingAddress][:country] = address[:country] if address[:country] + add_billing_address(post, options, address) end end + def add_billing_address(post, options, address) + address[:address1] = 'NA' if address[:address1].blank? + address[:address2] = 'NA' if address[:address2].blank? + + post[:billingAddress] = {} + post[:billingAddress][:street] = options[:address_override] == true ? address[:address2] : address[:address1] + post[:billingAddress][:houseNumberOrName] = options[:address_override] == true ? address[:address1] : address[:address2] + 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 = { - value: amount(money), - currency: options[:currency] || currency(money) + value: localized_amount(money, currency), + currency: } + post[:amount] = amount end def add_invoice_for_modification(post, money, options) + currency = options[:currency] || currency(money) amount = { - value: amount(money), - currency: options[:currency] || currency(money) + value: localized_amount(money, currency), + currency: } post[:modificationAmount] = amount end - def add_payment(post, payment) - if payment.is_a?(String) + def add_payment(post, payment, options, action = nil) + case payment + when String _, _, recurring_detail_reference = payment.split('#') post[:selectedRecurringDetailReference] = recurring_detail_reference - add_recurring_contract(post, options) + options[:recurring_contract_type] ||= 'RECURRING' + when Check + add_bank_account(post, payment, options, action) else - add_mpi_data_for_network_tokenization_card(post, payment) if payment.is_a?(NetworkTokenizationCreditCard) + add_network_tokenization_card(post, payment, options) if network_tokenization_payment?(payment, options) add_card(post, payment) end end + def network_tokenization_payment?(payment, options) + payment.is_a?(NetworkTokenizationCreditCard) || options[:wallet_type] == :google_pay + end + + def add_recurring_detail_reference(post, options) + post[:selectedRecurringDetailReference] = options[:recurring_detail_reference] if options[:recurring_detail_reference].present? + end + + def add_bank_account(post, bank_account, options, action) + bank = { + bankAccountNumber: bank_account.account_number, + ownerName: bank_account.name, + countryCode: options[:billing_address].try(:[], :country) + } + + action == 'refundWithData' ? bank[:iban] = bank_account.routing_number : bank[:bankLocationId] = bank_account.routing_number + + requires!(bank, :bankAccountNumber, :ownerName, :countryCode) + post[:bankAccount] = bank + end + def add_card(post, credit_card) card = { expiryMonth: credit_card.month, @@ -178,39 +617,105 @@ def add_card(post, credit_card) cvc: credit_card.verification_value } - card.delete_if{|k,v| v.blank? } + card.delete_if { |_k, v| v.blank? } + card[:holderName] ||= 'Not Provided' requires!(card, :expiryMonth, :expiryYear, :holderName, :number) 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] + + options + end + + def add_network_transaction_reference(post, options) + return unless ntid = options[:network_transaction_id] || options.dig(:stored_credential, :network_transaction_id) + + post[:additionalData] = {} unless post[:additionalData] + post[:additionalData][:networkTxReference] = ntid + end + def add_reference(post, authorization, options = {}) - _, psp_reference, _ = authorization.split('#') - post[:originalReference] = single_reference(authorization) || psp_reference + original_reference = authorization.split('#').reject(&:empty?).first + post[:originalReference] = original_reference end - def add_original_reference(post, authorization, options = {}) - original_psp_reference, _, _ = authorization.split('#') - post[:originalReference] = single_reference(authorization) || original_psp_reference + def add_network_tokenization_card(post, payment, options) + selected_brand = NETWORK_TOKENIZATION_CARD_SOURCE[options[:wallet_type]&.to_s || payment.source.to_s] + if selected_brand + post[:selectedBrand] = selected_brand + post[:additionalData] = {} unless post[:additionalData] + post[:additionalData]['paymentdatasource.type'] = selected_brand + post[:additionalData]['paymentdatasource.tokenized'] = options[:wallet_type] ? 'false' : 'true' if selected_brand == 'googlepay' + end + + return if skip_mpi_data?(payment, options) + + post[:mpiData] = { + authenticationResponse: 'Y', + directoryResponse: 'Y', + eci: payment.eci || '07' + } + + cryptogram_field = payment.try(:network_token?) ? :tokenAuthenticationVerificationValue : :cavv + post[:mpiData][cryptogram_field] = payment.payment_cryptogram end - def add_mpi_data_for_network_tokenization_card(post, payment) - post[:mpiData] = {} - post[:mpiData][:authenticationResponse] = 'Y' - post[:mpiData][:cavv] = payment.payment_cryptogram - post[:mpiData][:directoryResponse] = 'Y' - post[:mpiData][:eci] = payment.eci || '07' + def add_recurring_contract(post, options = {}, payment = nil) + return unless options[:recurring_contract_type] || payment.try(:network_token?) + + post[:recurring] ||= {} + post[:recurring][:contract] = options[:recurring_contract_type] if 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] + + if payment.try(:network_token?) + post[:recurring][:contract] = 'EXTERNAL' + post[:recurring][:tokenService] = case payment.brand + when 'visa' then 'VISATOKENSERVICE' + else 'MCTOKENSERVICE' + end + end end - def single_reference(authorization) - authorization if !authorization.include?('#') + def add_application_info(post, options) + post[:applicationInfo] ||= {} + add_external_platform(post, options) + add_merchant_application(post, options) end - def add_recurring_contract(post, options = {}) - recurring = { - contract: 'RECURRING' + def add_external_platform(post, options) + options.update(externalPlatform: application_id) if application_id + + return unless options[:externalPlatform] + + post[:applicationInfo][:externalPlatform] = { + name: options[:externalPlatform][:name], + version: options[:externalPlatform][:version] } + end - post[:recurring] = recurring + def add_merchant_application(post, options) + return unless options[:merchantApplication] + + post[:applicationInfo][:merchantApplication] = { + name: options[:merchantApplication][:name], + version: options[:merchantApplication][:version] + } end def add_installments(post, options) @@ -219,39 +724,178 @@ def add_installments(post, options) } end + def add_3ds(post, options) + if three_ds_2_options = options[:three_ds_2] + device_channel = three_ds_2_options[:channel] + if device_channel == 'app' + post[:threeDS2RequestData] = { deviceChannel: device_channel } + else + add_browser_info(three_ds_2_options[:browser_info], post) + post[:threeDS2RequestData] = { deviceChannel: device_channel, notificationURL: three_ds_2_options[:notification_url] } + end + + if options.has_key?(:execute_threed) + post[:additionalData][:executeThreeD] = options[:execute_threed] + post[:additionalData][:scaExemption] = options[:sca_exemption] if options[:sca_exemption] + end + else + return unless !options[:execute_threed].nil? || !options[:threed_dynamic].nil? + + post[:browserInfo] = { userAgent: options[:user_agent], acceptHeader: options[:accept_header] } if options[:execute_threed] || options[:threed_dynamic] + post[:additionalData] ||= {} + post[:additionalData][:executeThreeD] = options[:execute_threed] if !options[:execute_threed].nil? + end + end + + def add_3ds_authenticated_data(post, options) + if options[:three_d_secure] && options[:three_d_secure][:eci] && options[:three_d_secure][:xid] + add_3ds1_authenticated_data(post, options) + elsif options[:three_d_secure] + add_3ds2_authenticated_data(post, options) + end + end + + def add_3ds1_authenticated_data(post, options) + three_d_secure_options = options[:three_d_secure] + post[:mpiData] = { + cavv: three_d_secure_options[:cavv], + cavvAlgorithm: three_d_secure_options[:cavv_algorithm], + eci: three_d_secure_options[:eci], + xid: three_d_secure_options[:xid], + directoryResponse: three_d_secure_options[:enrolled], + authenticationResponse: three_d_secure_options[:authentication_response_status] + } + end + + def add_3ds2_authenticated_data(post, options) + three_d_secure_options = options[:three_d_secure] + # If the transaction was authenticated in a frictionless flow, send the transStatus from the ARes. + if three_d_secure_options[:authentication_response_status].nil? + authentication_response = three_d_secure_options[:directory_response_status] + else + authentication_response = three_d_secure_options[:authentication_response_status] + end + post[:mpiData] = { + threeDSVersion: three_d_secure_options[:version], + eci: three_d_secure_options[:eci], + cavv: three_d_secure_options[:cavv], + dsTransID: three_d_secure_options[:ds_transaction_id], + directoryResponse: three_d_secure_options[:directory_response_status], + authenticationResponse: authentication_response + } + 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] + post[:fundSource][:shopperEmail] = fund_source[:shopper_email] if fund_source[:shopper_email] + + 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_fund_destination(post, options) + return unless fund_destination = options[:fund_destination] + + post[:fundDestination] = {} + post[:fundDestination][:additionalData] = fund_destination[:additional_data] if fund_destination[:additional_data] + end + + def add_metadata(post, options = {}) + return unless options[:metadata] + + post[:metadata] ||= {} + post[:metadata].merge!(options[:metadata]) if options[:metadata] + end + + def add_header_fields(response) + return unless @response_headers.present? + + headers = {} + headers['response_headers'] = {} + headers['response_headers']['transient_error'] = @response_headers['transient-error'] if @response_headers['transient-error'] + + response.merge!(headers) + end + def parse(body) return {} if body.blank? - JSON.parse(body) + + response = JSON.parse(body) + add_header_fields(response) + response end - def commit(action, parameters) + # Override the regular handle response so we can access the headers + # set header fields and values so we can add them to the response body + def handle_response(response) + @response_headers = response.each_header.to_h if response.respond_to?(:header) + case response.code.to_i + when 200...300 + response.body + else + raise ResponseError.new(response) + end + end + + def commit(action, parameters, options) begin - raw_response = ssl_post("#{url}/#{action.to_s}", post_data(action, parameters), request_headers) + raw_response = ssl_post(url(action), post_data(action, parameters), request_headers(options)) response = parse(raw_response) rescue ResponseError => e raw_response = e.response.body response = parse(raw_response) end - success = success_from(action, response) + 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?, - error_code: success ? nil : error_code_from(response) + error_code: success ? nil : error_code_from(response), + network_transaction_id: network_transaction_id_from(response), + avs_result: AVSResult.new(code: avs_code_from(response)), + cvv_result: CVVResult.new(cvv_result_from(response)) ) + end + def avs_code_from(response) + AVS_MAPPING[response['additionalData']['avsResult'][0..1].strip] if response.dig('additionalData', 'avsResult') end - def url + def cvv_result_from(response) + CVC_MAPPING[response['additionalData']['cvcResult'][0]] if response.dig('additionalData', 'cvcResult') + end + + def endpoint(action) + case action + when 'disable', 'storeToken' + "Recurring/#{fetch_version(:recurring_api)}/#{action}" + when 'payout' + "Payout/#{fetch_version(:payout_api)}/#{action}" + else + "Payment/#{fetch_version(:payment_api)}/#{action}" + end + end + + def url(action) if test? - test_url + "#{test_url}#{endpoint(action)}" elsif @options[:subdomain] - "https://#{@options[:subdomain]}-pal-live.adyenpayments.com/pal/servlet/Payment/v18" + "https://#{@options[:subdomain]}-pal-live.adyenpayments.com/pal/servlet/#{endpoint(action)}" else - live_url + "#{live_url}#{endpoint(action)}" end end @@ -259,47 +903,74 @@ def basic_auth Base64.strict_encode64("#{@username}:#{@password}") end - def request_headers - { + def request_headers(options) + headers = { 'Content-Type' => 'application/json', 'Authorization' => "Basic #{basic_auth}" } + headers['Idempotency-Key'] = options[:idempotency_key] if options[:idempotency_key] + headers end - def success_from(action, response) + def success_from(action, response, options) + 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 case action.to_s - when 'authorise' - ['Authorised', 'Received', 'RedirectShopper'].include?(response['resultCode']) - when 'capture', 'refund', 'cancel' + when 'authorise', 'authorise3d' + %w[Authorised Received RedirectShopper].include?(response['resultCode']) + when 'capture', 'refund', 'cancel', 'cancelOrRefund' response['response'] == "[#{action}-received]" + when 'adjustAuthorisation' + response['response'] == 'Authorised' || response['response'] == '[adjustAuthorisation-received]' + when 'storeToken' + response['result'] == 'Success' + when 'disable' + 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 action.to_s == 'authorise' - response['response'] || response['message'] + def message_from(action, response, options = {}) + case action.to_s + when 'authorise', 'authorise3d', 'authorise3ds2' + authorize_message_from(response, options) + when 'payout' + response['refusalReason'] || response['resultCode'] || response['message'] + else + response['response'] || response['message'] || response['result'] || response['resultCode'] + end end - def authorize_message_from(response) - if response['refusalReason'] && response['additionalData'] && response['additionalData']['refusalReasonRaw'] - "#{response['refusalReason']} | #{response['additionalData']['refusalReasonRaw']}" + def authorize_message_from(response, options = {}) + 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['refusalReason'] || response['resultCode'] || response['message'] || response['result'] end end def authorization_from(action, parameters, response) return nil if response['pspReference'].nil? + recurring = response['additionalData']['recurring.recurringDetailReference'] if response['additionalData'] + recurring = response['recurringDetailReference'] if action == 'storeToken' + "#{parameters[:originalReference]}##{response['pspReference']}##{recurring}" end def init_post(options = {}) post = {} - post[:merchantAccount] = options[:merchant_account] || @merchant_account - post[:reference] = options[:order_id] if options[:order_id] + add_merchant_account(post, options) + post[:reference] = options[:order_id][0..79] if options[:order_id] post end @@ -308,7 +979,62 @@ def post_data(action, parameters = {}) end def error_code_from(response) - STANDARD_ERROR_CODE_MAPPING[response['errorCode']] + response.dig('additionalData', 'refusalReasonRaw').try(:match, /^([a-zA-Z0-9 ]{1,5})(?=:)/).try(:[], 1).try(:strip) || + STANDARD_ERROR_CODE_MAPPING[response['errorCode']] || + response['errorCode'] || + response['refusalReason'] + end + + def network_transaction_id_from(response) + response.dig('additionalData', 'networkTxReference') + end + + def add_browser_info(browser_info, post) + return unless browser_info + + post[:browserInfo] = { + acceptHeader: browser_info[:accept_header], + colorDepth: browser_info[:depth], + javaEnabled: browser_info[:java], + language: browser_info[:language], + screenHeight: browser_info[:height], + screenWidth: browser_info[:width], + timeZoneOffset: browser_info[:timezone], + userAgent: browser_info[:user_agent] + } + end + + def unsupported_failure_response(initial_response) + Response.new( + false, + 'Recurring transactions are not supported for this card type.', + initial_response.params, + authorization: initial_response.authorization, + test: initial_response.test, + error_code: initial_response.error_code, + avs_result: initial_response.avs_result, + cvv_result: initial_response.cvv_result[:code] + ) + end + + def card_not_stored?(response) + response.authorization ? response.authorization.split('#')[2].nil? : true + end + + def skip_mpi_data?(payment, options = {}) + return true if options[:shopper_interaction] == 'ContAuth' && options[:recurring_processing_model] == 'Subscription' + return true if options.dig(:stored_credential, :initiator) == 'merchant' && payment.is_a?(NetworkTokenizationCreditCard) + + # Skips adding the NT mpi data if it is explicitly skipped in options, or if it is MIT and not the initial transaction. + options[:wallet_type] || (!options.dig(:stored_credential, :initial_transaction) && options.dig(:stored_credential, :initiator) == 'merchant') + end + + def ecommerce_shopper_interaction?(payment, options) + return true if payment.is_a?(NetworkTokenizationCreditCard) && options.dig(:stored_credential, :initiator) != 'merchant' + return true unless (stored_credential = options[:stored_credential]) + + stored_credential[:initiator] == 'cardholder' || + (payment.respond_to?(:verification_value) && payment.verification_value && stored_credential[:initial_transaction]) end end end diff --git a/lib/active_merchant/billing/gateways/airwallex.rb b/lib/active_merchant/billing/gateways/airwallex.rb new file mode 100644 index 00000000000..e30bdeb0845 --- /dev/null +++ b/lib/active_merchant/billing/gateways/airwallex.rb @@ -0,0 +1,384 @@ +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class AirwallexGateway < Gateway + self.test_url = 'https://api-demo.airwallex.com/api/v1' + self.live_url = 'https://pci-api.airwallex.com/api/v1' + + # per https://www.airwallex.com/docs/online-payments__overview, cards are accepted in all EU countries + self.supported_countries = %w[AT AU BE BG CY CZ DE DK EE GR ES FI FR GB HK HR HU IE IT LT LU LV MT NL PL PT RO SE SG SI SK] + self.default_currency = 'AUD' + self.supported_cardtypes = %i[visa master] + + self.homepage_url = 'https://airwallex.com/' + self.display_name = 'Airwallex' + + ENDPOINTS = { + login: '/authentication/login', + setup: '/pa/payment_intents/create', + sale: '/pa/payment_intents/%{id}/confirm', + capture: '/pa/payment_intents/%{id}/capture', + refund: '/pa/refunds/create', + void: '/pa/payment_intents/%{id}/cancel' + } + + # Provided by Airwallex for testing purposes + TEST_NETWORK_TRANSACTION_IDS = { + visa: '123456789012345', + master: 'MCC123ABC0101' + } + + def initialize(options = {}) + requires!(options, :client_id, :client_api_key) + @client_id = options[:client_id] + @client_api_key = options[:client_api_key] + super + @access_token = options[:access_token] || setup_access_token + end + + def purchase(money, card, options = {}) + payment_intent_id = create_payment_intent(money, options) + post = { + 'request_id' => request_id(options), + 'merchant_order_id' => merchant_order_id(options) + } + add_card(post, card, options) + add_descriptor(post, options) + add_stored_credential(post, options) + add_return_url(post, options) + post['payment_method_options'] = { 'card' => { 'auto_capture' => false } } if authorization_only?(options) + + add_three_ds(post, options) + commit(:sale, post, payment_intent_id) + end + + def authorize(money, payment, options = {}) + # authorize is just a purchase w/o an auto capture + purchase(money, payment, options.merge({ auto_capture: false })) + end + + def capture(money, authorization, options = {}) + raise ArgumentError, 'An authorization value must be provided.' if authorization.blank? + + post = { + 'request_id' => request_id(options), + 'merchant_order_id' => merchant_order_id(options), + 'amount' => amount(money) + } + add_descriptor(post, options) + + commit(:capture, post, authorization) + end + + def refund(money, authorization, options = {}) + raise ArgumentError, 'An authorization value must be provided.' if authorization.blank? + + post = {} + post[:amount] = amount(money) + post[:payment_intent_id] = authorization + post[:request_id] = request_id(options) + post[:merchant_order_id] = merchant_order_id(options) + + commit(:refund, post) + end + + def void(authorization, options = {}) + raise ArgumentError, 'An authorization value must be provided.' if authorization.blank? + + post = {} + post[:request_id] = request_id(options) + post[:merchant_order_id] = merchant_order_id(options) + add_descriptor(post, options) + + commit(:void, post, authorization) + end + + def verify(credit_card, options = {}) + MultiResponse.run(:use_first_response) do |r| + r.process { authorize(100, credit_card, options) } + r.process(:ignore_result) { void(r.authorization, options) } + end + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(/(\\\"number\\\":\\\")\d+/, '\1[REDACTED]'). + gsub(/(\\\"cvc\\\":\\\")\d+/, '\1[REDACTED]') + end + + private + + def request_id(options) + options[:request_id] || generate_uuid + end + + def merchant_order_id(options) + options[:merchant_order_id] || options[:order_id] || generate_uuid + end + + def add_return_url(post, options) + post[:return_url] = options[:return_url] if options[:return_url] + end + + def generate_uuid + SecureRandom.uuid + end + + def setup_access_token + token_headers = { + 'Content-Type' => 'application/json', + 'x-client-id' => @client_id, + 'x-api-key' => @client_api_key + } + + 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) + endpoint = ENDPOINTS[action].to_s + endpoint = id.present? ? endpoint % { id: } : endpoint + base_url + endpoint + end + + def add_referrer_data(post) + post[:referrer_data] = { type: 'spreedly' } + end + + def create_payment_intent(money, options = {}) + post = {} + add_invoice(post, money, options) + add_order(post, options) + post[:request_id] = "#{request_id(options)}_setup" + post[:merchant_order_id] = merchant_order_id(options) + add_referrer_data(post) + add_descriptor(post, options) + post['payment_method_options'] = { 'card' => { 'risk_control' => { 'three_ds_action' => 'SKIP_3DS' } } } if options[:skip_3ds] + + response = commit(:setup, post) + raise ArgumentError.new(response.message) unless response.success? + + response.params['id'] + end + + def add_billing(post, card, options = {}) + return unless has_name_info?(card) + + billing = post['payment_method']['card']['billing'] || {} + billing['email'] = options[:email] if options[:email] + billing['phone'] = options[:phone] if options[:phone] + billing['first_name'] = card.first_name + billing['last_name'] = card.last_name + billing_address = options[:billing_address] + billing['address'] = build_address(billing_address) if has_required_address_info?(billing_address) + + post['payment_method']['card']['billing'] = billing + end + + def has_name_info?(card) + # These fields are required if billing data is sent. + card.first_name && card.last_name + end + + def has_required_address_info?(address) + # These fields are required if address data is sent. + return unless address + + address[:address1] && address[:country] + end + + def build_address(address) + return unless address + + address_data = {} # names r hard + address_data[:country_code] = address[:country] + address_data[:street] = address[:address1] + address_data[:city] = address[:city] if address[:city] # required per doc, not in practice + address_data[:postcode] = address[:zip] if address[:zip] + address_data[:state] = address[:state] if address[:state] + address_data + end + + def add_invoice(post, money, options) + post[:amount] = amount(money) + post[:currency] = (options[:currency] || currency(money)) + end + + def add_card(post, card, options = {}) + post['payment_method'] = { + 'type' => 'card', + 'card' => { + 'expiry_month' => format(card.month, :two_digits), + 'expiry_year' => card.year.to_s, + 'number' => card.number.to_s, + 'name' => card.name, + 'cvc' => card.verification_value, + 'brand' => card.brand + } + } + add_billing(post, card, options) + end + + def add_order(post, options) + return unless shipping_address = options[:shipping_address] + + physical_address = build_shipping_address(shipping_address) + first_name, last_name = split_names(shipping_address[:name]) + shipping = {} + shipping[:first_name] = first_name if first_name + shipping[:last_name] = last_name if last_name + shipping[:phone_number] = shipping_address[:phone_number] if shipping_address[:phone_number] + shipping[:address] = physical_address + post[:order] = { shipping: } + end + + def build_shipping_address(shipping_address) + address = {} + address[:city] = shipping_address[:city] + address[:country_code] = shipping_address[:country] + address[:postcode] = shipping_address[:zip] + address[:state] = shipping_address[:state] + address[:street] = shipping_address[:address1] + address + end + + def add_stored_credential(post, options) + return unless stored_credential = options[:stored_credential] + + external_recurring_data = post[:external_recurring_data] = {} + + case stored_credential.dig(:reason_type) + when 'recurring', 'installment' + external_recurring_data[:merchant_trigger_reason] = 'scheduled' + when 'unscheduled' + external_recurring_data[:merchant_trigger_reason] = 'unscheduled' + end + + external_recurring_data[:original_transaction_id] = test_mit?(options) ? test_network_transaction_id(post) : stored_credential.dig(:network_transaction_id) + external_recurring_data[:triggered_by] = stored_credential.dig(:initiator) == 'cardholder' ? 'customer' : 'merchant' + end + + def test_network_transaction_id(post) + case post['payment_method']['card']['brand'] + when 'visa' + TEST_NETWORK_TRANSACTION_IDS[:visa] + when 'master' + TEST_NETWORK_TRANSACTION_IDS[:master] + end + end + + def test_mit?(options) + test? && options.dig(:stored_credential, :initiator) == 'merchant' + end + + def add_three_ds(post, options) + return unless three_d_secure = options[:three_d_secure] + + pm_options = post.dig('payment_method_options', 'card') + + external_three_ds = { + 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:) : post['payment_method_options'] = { card: { external_three_ds: } } + end + + def format_three_ds_version(three_d_secure) + version = three_d_secure[:version].split('.') + + version.push('0') until version.length == 3 + version.join('.') + end + + 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] + } + else + { + cavv: three_d_secure[:cavv], + xid: three_d_secure[:xid] + } + end + end + + def authorization_only?(options = {}) + options.include?(:auto_capture) && options[:auto_capture] == false + end + + def add_descriptor(post, options) + post[:descriptor] = truncate(options[:description], 32) if options[:description] + end + + def parse(body) + JSON.parse(body) + end + + def commit(action, post, id = nil) + url = build_request_url(action, id) + + post_headers = { 'Authorization' => "Bearer #{@access_token}", 'Content-Type' => 'application/json' } + response = parse(ssl_post(url, post_data(post), post_headers)) + + Response.new( + success_from(response), + message_from(response), + response, + authorization: authorization_from(response), + avs_result: AVSResult.new(code: response.dig('latest_payment_attempt', 'authentication_data', 'avs_result')), + cvv_result: CVVResult.new(response.dig('latest_payment_attempt', 'authentication_data', 'cvc_code')), + test: test?, + error_code: error_code_from(response) + ) + end + + def handle_response(response) + case response.code.to_i + when 200...300, 400, 404 + response.body + else + raise ResponseError.new(response) + end + end + + def post_data(post) + post.to_json + end + + def success_from(response) + %w(REQUIRES_PAYMENT_METHOD SUCCEEDED RECEIVED REQUIRES_CAPTURE CANCELLED).include?(response['status']) + end + + def message_from(response) + response.dig('latest_payment_attempt', 'status') || response['status'] || response['message'] + end + + def authorization_from(response) + response.dig('latest_payment_attempt', 'payment_intent_id') || response.dig('payment_intent_id') + end + + def error_code_from(response) + response['provider_original_response_code'] || response['code'] unless success_from(response) + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/alelo.rb b/lib/active_merchant/billing/gateways/alelo.rb new file mode 100644 index 00000000000..0f53f6c18ba --- /dev/null +++ b/lib/active_merchant/billing/gateways/alelo.rb @@ -0,0 +1,274 @@ +require 'jose' + +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class AleloGateway < Gateway + class_attribute :prelive_url + + self.test_url = 'https://sandbox-api.alelo.com.br/alelo/sandbox/' + self.live_url = 'https://api.alelo.com.br/alelo/prd/' + self.prelive_url = 'https://api.homologacaoalelo.com.br/alelo/uat/' + + self.supported_countries = ['BR'] + self.default_currency = 'BRL' + self.supported_cardtypes = %i[visa master american_express discover] + + self.homepage_url = 'https://www.alelo.com.br' + self.display_name = 'Alelo' + + def initialize(options = {}) + requires!(options, :client_id, :client_secret) + super + end + + def purchase(money, payment, options = {}) + post = {} + add_order(post, options) + add_amount(post, money) + add_payment(post, payment) + add_geolocation(post, options) + add_extra_data(post, options) + + commit('capture/transaction', post, options) + end + + def refund(money, authorization, options = {}) + request_id = authorization.split('#').first + options[:http] = { method: :put, prevent_encrypt: true } + commit('capture/transaction/refund', { requestId: request_id }, options, :put) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + force_utf8(transcript.encode). + gsub(%r((Authorization: Bearer )[\w -]+), '\1[FILTERED]'). + gsub(%r((client_id=|Client-Id:)[\w -]+), '\1[FILTERED]\2'). + gsub(%r((client_secret=|Client-Secret:)[\w -]+), '\1[FILTERED]\2'). + gsub(%r((access_token\":\")[^\"]*), '\1[FILTERED]'). + gsub(%r((publicKey\":\")[^\"]*), '\1[FILTERED]') + end + + private + + def force_utf8(string) + return nil unless string + + # binary = string.encode('BINARY', invalid: :replace, undef: :replace, replace: '?') + string.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?') + end + + def add_amount(post, money) + post[:amount] = amount(money).to_f + end + + def add_order(post, options) + post[:requestId] = options[:order_id] + end + + def add_extra_data(post, options) + post.merge!({ + establishmentCode: options[:establishment_code], + playerIdentification: options[:player_identification], + captureType: '3', # send fixed value 3 to ecommerce + subMerchantCode: options[:sub_merchant_mcc], + externalTraceNumber: options[:external_trace_number] + }.compact) + end + + def add_geolocation(post, options) + return if options[:geo_latitude].blank? || options[:geo_longitude].blank? + + post.merge!(geolocation: { + latitude: options[:geo_latitude], + longitude: options[:geo_longitude] + }) + end + + def add_payment(post, payment) + post.merge!({ + cardNumber: payment.number, + cardholderName: payment.name, + expirationMonth: payment.month, + expirationYear: format(payment.year, :two_digits).to_i, + securityCode: payment.verification_value + }) + end + + def fetch_access_token + params = { + grant_type: 'client_credentials', + client_id: @options[:client_id], + client_secret: @options[:client_secret], + scope: '/capture' + } + + headers = { + 'Accept' => 'application/json', + 'Content-Type' => 'application/x-www-form-urlencoded' + } + + 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) + response = parse(ssl_get(url('capture/key'), request_headers(access_token))) + Response.new(true, response[:publicKey], response) + end + + def ensure_credentials(try_again = true) + multiresp = MultiResponse.new + access_token = @options[:access_token] + key = @options[:encryption_key] + uuid = @options[:encryption_uuid] + + if access_token.blank? + multiresp.process { fetch_access_token } + access_token = multiresp.message + key = nil + uuid = nil + end + + if key.blank? + multiresp.process { remote_encryption_key(access_token) } + key = multiresp.message + uuid = multiresp.params['uuid'] + end + + { + key:, + uuid:, + access_token:, + multiresp: multiresp.responses.present? ? multiresp : nil + } + rescue ActiveMerchant::OAuthResponseError => e + raise e + rescue ResponseError => e + # retry to generate a new access_token when the provided one is expired + raise e unless retry?(try_again, e, :access_token) + + @options.delete(:access_token) + @options.delete(:encryption_key) + ensure_credentials false + end + + def encrypt_payload(body, credentials, options) + key = OpenSSL::PKey::RSA.new(Base64.decode64(credentials[:key])) + jwk = JOSE::JWK.from_key(key) + alg_enc = { 'alg' => 'RSA-OAEP-256', 'enc' => 'A128CBC-HS256' } + + token = JOSE::JWE.block_encrypt(jwk, body.to_json, alg_enc).compact + + encrypted_body = { + token:, + uuid: credentials[:uuid] + } + + encrypted_body.to_json + end + + def parse(body) + JSON.parse(body, symbolize_names: true) + end + + def post_data(params) + params.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&') + end + + def commit(action, body, options, try_again = true) + credentials = ensure_credentials + payload = encrypt_payload(body, credentials, options) + + if options.dig :http, :method + payload = body.to_json if options.dig :http, :prevent_encrypt + response = parse ssl_request(options[:http][:method], url(action), payload, request_headers(credentials[:access_token])) + else + response = parse ssl_post(url(action), payload, request_headers(credentials[:access_token])) + end + + resp = Response.new( + success_from(action, response), + message_from(response), + response, + authorization: authorization_from(response, options), + test: test? + ) + + return resp unless credentials[:multiresp].present? + + multiresp = credentials[:multiresp] + resp.params.merge!({ + 'access_token' => credentials[:access_token], + 'encryption_key' => credentials[:key], + 'encryption_uuid' => credentials[:uuid] + }) + multiresp.process { resp } + + multiresp + rescue ActiveMerchant::OAuthResponseError => e + raise OAuthResponseError.new(e) + rescue ActiveMerchant::ResponseError => e + # Retry on a possible expired encryption key + if retry?(try_again, e, :encryption_key) + @options.delete(:encryption_key) + commit(action, body, options, false) + else + res = parse(e.response.body) + Response.new(false, res[:messageUser] || res[:error], res, test: test?) + end + end + + def 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' + response[:status] == 'ESTORNADA' + when 'capture/transaction' + response[:status] == 'CONFIRMADA' + else + false + end + end + + def message_from(response) + response[:messages] || response[:messageUser] + end + + def authorization_from(response, options) + [response[:requestId]].join('#') + end + + def url(action) + return prelive_url if @options[:url_override] == 'prelive' + + "#{test? ? test_url : live_url}#{action}" + end + + def request_headers(access_token) + { + 'Accept' => 'application/json', + 'X-IBM-Client-Id' => @options[:client_id], + 'X-IBM-Client-Secret' => @options[:client_secret], + 'Content-Type' => 'application/json', + 'Authorization' => "Bearer #{access_token}" + } + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/allied_wallet.rb b/lib/active_merchant/billing/gateways/allied_wallet.rb index a7f359849c1..5ec8ce7a547 100644 --- a/lib/active_merchant/billing/gateways/allied_wallet.rb +++ b/lib/active_merchant/billing/gateways/allied_wallet.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class AlliedWalletGateway < Gateway self.display_name = 'Allied Wallet' self.homepage_url = 'https://www.alliedwallet.com' @@ -9,15 +9,15 @@ class AlliedWalletGateway < Gateway self.supported_countries = ['US'] self.default_currency = 'USD' self.money_format = :dollars - self.supported_cardtypes = [:visa, :master, :american_express, :discover, - :diners_club, :jcb, :maestro] + self.supported_cardtypes = %i[visa master american_express discover + diners_club jcb maestro] - def initialize(options={}) + def initialize(options = {}) requires!(options, :site_id, :merchant_id, :token) super end - def purchase(amount, payment_method, options={}) + def purchase(amount, payment_method, options = {}) post = {} add_invoice(post, amount, options) add_payment_method(post, payment_method) @@ -26,7 +26,7 @@ def purchase(amount, payment_method, options={}) commit(:purchase, post) end - def authorize(amount, payment_method, options={}) + def authorize(amount, payment_method, options = {}) post = {} add_invoice(post, amount, options) add_payment_method(post, payment_method) @@ -35,7 +35,7 @@ def authorize(amount, payment_method, options={}) commit(:authorize, post) end - def capture(amount, authorization, options={}) + def capture(amount, authorization, options = {}) post = {} add_invoice(post, amount, options) add_reference(post, authorization, :capture) @@ -44,14 +44,14 @@ def capture(amount, authorization, options={}) commit(:capture, post) end - def void(authorization, options={}) + def void(authorization, options = {}) post = {} add_reference(post, authorization, :void) commit(:void, post) end - def refund(amount, authorization, options={}) + def refund(amount, authorization, options = {}) post = {} add_invoice(post, amount, options) add_reference(post, authorization, :refund) @@ -61,7 +61,7 @@ def refund(amount, authorization, options={}) commit(:refund, post) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -113,7 +113,7 @@ def add_customer_data(post, options) post[:city] = billing_address[:city] post[:state] = billing_address[:state] post[:countryId] = billing_address[:country] - post[:postalCode] = billing_address[:zip] + post[:postalCode] = billing_address[:zip] post[:phone] = billing_address[:phone] end end @@ -128,7 +128,6 @@ def add_reference(post, authorization, action) post[transactions[action]] = authorization end - ACTIONS = { purchase: 'SALE', authorize: 'AUTHORIZE', @@ -142,7 +141,8 @@ def commit(action, post) raw_response = ssl_post(url(action), post.to_json, headers) response = parse(raw_response) rescue ResponseError => e - raise unless(e.response.code.to_s =~ /4\d\d/) + raise unless e.response.code.to_s =~ /4\d\d/ + response = parse(e.response.body) end @@ -152,8 +152,8 @@ def commit(action, post) message_from(succeeded, response), response, authorization: response['id'], - :avs_result => AVSResult.new(code: response['avs_response']), - :cvv_result => CVVResult.new(response['cvv2_response']), + avs_result: AVSResult.new(code: response['avs_response']), + cvv_result: CVVResult.new(response['cvv2_response']), test: test? ) rescue JSON::ParserError @@ -183,7 +183,7 @@ def parse(body) def parse_element(response, node) if node.has_elements? - node.elements.each{|element| parse_element(response, element) } + node.elements.each { |element| parse_element(response, element) } else response[node.name.underscore.to_sym] = node.text end @@ -200,7 +200,6 @@ def message_from(succeeded, response) response['message'] || 'Unable to read error message' end end - end end end diff --git a/lib/active_merchant/billing/gateways/authorize_net.rb b/lib/active_merchant/billing/gateways/authorize_net.rb index 5db6708cb7e..bb8bbcdf590 100644 --- a/lib/active_merchant/billing/gateways/authorize_net.rb +++ b/lib/active_merchant/billing/gateways/authorize_net.rb @@ -8,10 +8,10 @@ class AuthorizeNetGateway < Gateway self.test_url = 'https://apitest.authorize.net/xml/v1/request.api' self.live_url = 'https://api2.authorize.net/xml/v1/request.api' - self.supported_countries = %w(AD AT AU BE BG CA CH CY CZ DE DK EE ES FI FR GB GI GR HU IE IL IS IT LI LT LU LV MC MT NL NO PL PT RO SE SI SK SM TR US VA) + self.supported_countries = %w(AU CA US) self.default_currency = 'USD' self.money_format = :dollars - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb, :maestro] + self.supported_cardtypes = %i[visa master american_express discover diners_club jcb maestro] self.homepage_url = 'http://www.authorize.net/' self.display_name = 'Authorize.Net' @@ -54,25 +54,25 @@ class AuthorizeNetGateway < Gateway '37' => STANDARD_ERROR_CODE[:invalid_expiry_date], '378' => STANDARD_ERROR_CODE[:invalid_cvc], '38' => STANDARD_ERROR_CODE[:expired_card], - '384' => STANDARD_ERROR_CODE[:config_error], + '384' => STANDARD_ERROR_CODE[:config_error] } MARKET_TYPE = { - :moto => '1', - :retail => '2' + moto: '1', + retail: '2' } DEVICE_TYPE = { - :unknown => '1', - :unattended_terminal => '2', - :self_service_terminal => '3', - :electronic_cash_register => '4', - :personal_computer_terminal => '5', - :airpay => '6', - :wireless_pos => '7', - :website => '8', - :dial_terminal => '9', - :virtual_terminal => '10' + unknown: '1', + unattended_terminal: '2', + self_service_terminal: '3', + electronic_cash_register: '4', + personal_computer_terminal: '5', + airpay: '6', + wireless_pos: '7', + website: '8', + dial_terminal: '9', + virtual_terminal: '10' } class_attribute :duplicate_window @@ -85,23 +85,21 @@ 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' - def initialize(options={}) + def initialize(options = {}) requires!(options, :login, :password) super end def purchase(amount, payment, options = {}) if payment.is_a?(String) - commit(:cim_purchase) do |xml| + commit(:cim_purchase, options) do |xml| add_cim_auth_purchase(xml, 'profileTransAuthCapture', amount, payment, options) end else @@ -111,9 +109,9 @@ def purchase(amount, payment, options = {}) end end - def authorize(amount, payment, options={}) + def authorize(amount, payment, options = {}) if payment.is_a?(String) - commit(:cim_authorize) do |xml| + commit(:cim_authorize, options) do |xml| add_cim_auth_purchase(xml, 'profileTransAuthOnly', amount, payment, options) end else @@ -123,7 +121,7 @@ def authorize(amount, payment, options={}) end end - def capture(amount, authorization, options={}) + def capture(amount, authorization, options = {}) if auth_was_for_cim?(authorization) cim_capture(amount, authorization, options) else @@ -131,12 +129,13 @@ def capture(amount, authorization, options={}) end end - def refund(amount, authorization, options={}) - response = if auth_was_for_cim?(authorization) - cim_refund(amount, authorization, options) - else - normal_refund(amount, authorization, options) - end + def refund(amount, authorization, options = {}) + response = + if auth_was_for_cim?(authorization) + cim_refund(amount, authorization, options) + else + normal_refund(amount, authorization, options) + end return response if response.success? return response unless options[:force_full_refund_if_unsettled] @@ -148,7 +147,7 @@ def refund(amount, authorization, options={}) end end - def void(authorization, options={}) + def void(authorization, options = {}) if auth_was_for_cim?(authorization) cim_void(authorization, options) else @@ -156,10 +155,8 @@ def void(authorization, options={}) end end - def credit(amount, payment, options={}) - if payment.is_a?(String) - raise ArgumentError, 'Reference credits are not supported. Please supply the original credit card or use the #refund method.' - end + def credit(amount, payment, options = {}) + raise ArgumentError, 'Reference credits are not supported. Please supply the original credit card or use the #refund method.' if payment.is_a?(String) commit(:credit) do |xml| add_order_id(xml, options) @@ -167,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) @@ -177,11 +174,29 @@ def credit(amount, payment, options={}) end end - def verify(credit_card, options = {}) + def verify(payment_method, options = {}) + amount = amount_for_verify(options) + MultiResponse.run(:use_first_response) do |r| - r.process { authorize(100, credit_card, options) } - r.process(:ignore_result) { void(r.authorization, options) } + r.process { authorize(amount, payment_method, options) } + r.process(:ignore_result) { void(r.authorization, options) } unless amount == 0 end + rescue ArgumentError => e + Response.new(false, e.message) + end + + def amount_for_verify(options) + return 100 unless options[:verify_amount].present? + + amount = options[:verify_amount] + raise ArgumentError.new 'verify_amount value must be an integer' unless (amount.is_a?(Integer) && !amount.negative?) || (amount.is_a?(String) && amount.match?(/^\d+$/) && !amount.to_i.negative?) + raise ArgumentError.new 'Billing address including zip code is required for a 0 amount verify' if amount.to_i.zero? && !validate_billing_address_values?(options) + + amount.to_i + end + + def validate_billing_address_values?(options) + options.dig(:billing_address, :zip).present? && options.dig(:billing_address, :address1).present? end def store(credit_card, options = {}) @@ -193,13 +208,13 @@ def store(credit_card, options = {}) end def unstore(authorization) - customer_profile_id, _, _ = split_authorization(authorization) + customer_profile_id, = split_authorization(authorization) delete_customer_profile(customer_profile_id) end def verify_credentials - response = commit(:verify_credentials) { } + response = commit(:verify_credentials) {} response.success? end @@ -222,13 +237,13 @@ def scrub(transcript) def supports_network_tokenization? card = Billing::NetworkTokenizationCreditCard.new({ - :number => '4111111111111111', - :month => 12, - :year => 20, - :first_name => 'John', - :last_name => 'Smith', - :brand => 'visa', - :payment_cryptogram => 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + number: '4111111111111111', + month: 12, + year: 20, + first_name: 'John', + last_name: 'Smith', + brand: 'visa', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' }) request = post_data(:authorize) do |xml| @@ -246,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) @@ -257,6 +272,10 @@ 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) end end @@ -268,15 +287,17 @@ 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 + add_extra_options_for_cim(xml, options) end def cim_capture(amount, authorization, options) - commit(:cim_capture) do |xml| + commit(:cim_capture, options) do |xml| add_order_id(xml, options) xml.transaction do xml.profileTransPriorAuthCapture do @@ -287,6 +308,7 @@ def cim_capture(amount, authorization, options) xml.transId(transaction_id_from(authorization)) end end + add_extra_options_for_cim(xml, options) end end @@ -309,9 +331,9 @@ def normal_capture(amount, authorization, options) end def cim_refund(amount, authorization, options) - transaction_id, card_number, _ = split_authorization(authorization) + transaction_id, card_number, = split_authorization(authorization) - commit(:cim_refund) do |xml| + commit(:cim_refund, options) do |xml| add_order_id(xml, options) xml.transaction do xml.profileTransRefund do @@ -324,20 +346,30 @@ def cim_refund(amount, authorization, options) xml.transId(transaction_id) end end + add_extra_options_for_cim(xml, options) end end def normal_refund(amount, authorization, options) - transaction_id, card_number, _ = split_authorization(authorization) + transaction_id, card_number, = split_authorization(authorization) commit(:refund) do |xml| xml.transactionRequest do xml.transactionType('refundTransaction') xml.amount(amount.nil? ? 0 : amount(amount)) xml.payment do - xml.creditCard do - xml.cardNumber(card_number || options[:card_number]) - xml.expirationDate('XXXX') + if options[:routing_number] + xml.bankAccount do + xml.accountType(options[:account_type]) + xml.routingNumber(options[:routing_number]) + xml.accountNumber(options[:account_number]) + xml.nameOnAccount(truncate("#{options[:first_name]} #{options[:last_name]}", 22)) + end + else + xml.creditCard do + xml.cardNumber(card_number || options[:card_number]) + xml.expirationDate('XXXX') + end end end xml.refTransId(transaction_id) @@ -355,13 +387,14 @@ def normal_refund(amount, authorization, options) end def cim_void(authorization, options) - commit(:cim_void) do |xml| + commit(:cim_void, options) do |xml| add_order_id(xml, options) xml.transaction do xml.profileTransVoid do xml.transId(transaction_id_from(authorization)) end end + add_extra_options_for_cim(xml, options) end end @@ -375,26 +408,34 @@ def normal_void(authorization, options) end end - def add_payment_source(xml, source, options, action = nil) - return unless source - 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) + def add_payment_method(xml, payment_method, options, action = nil) + return unless payment_method + + 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 + String(key).split('_').inject([]) { |buffer, e| buffer.push(buffer.empty? ? e : e.capitalize) }.join end def add_settings(xml, source, options) xml.transactionSettings do - if options[:recurring] + if options[:recurring] || subsequent_recurring_transaction?(options) xml.setting do xml.settingName('recurringBilling') xml.settingValue('true') @@ -465,12 +506,7 @@ def add_credit_card(xml, credit_card, action) xml.creditCard do xml.cardNumber(truncate(credit_card.number, 16)) xml.expirationDate(format(credit_card.month, :two_digits) + '/' + format(credit_card.year, :four_digits)) - if credit_card.valid_card_verification_value?(credit_card.verification_value, credit_card.brand) - xml.cardCode(credit_card.verification_value) - end - if credit_card.is_a?(NetworkTokenizationCreditCard) && action != :credit - xml.cryptogram(credit_card.payment_cryptogram) - end + xml.cardCode(credit_card.verification_value) if credit_card.valid_card_verification_value?(credit_card.verification_value, credit_card.brand) end end end @@ -478,7 +514,7 @@ def add_credit_card(xml, credit_card, action) def add_swipe_data(xml, credit_card) TRACKS.each do |key, regex| - if regex.match(credit_card.track_data) + if regex.match?(credit_card.track_data) @valid_track_data = true xml.payment do xml.trackData do @@ -490,24 +526,28 @@ def add_swipe_data(xml, credit_card) end def add_token_payment_method(xml, token, options) - customer_profile_id, customer_payment_profile_id, _ = split_authorization(token) + customer_profile_id, customer_payment_profile_id, = split_authorization(token) customer_profile_id = options[:customer_profile_id] if options[:customer_profile_id] customer_payment_profile_id = options[:customer_payment_profile_id] if options[:customer_payment_profile_id] xml.customerProfileId(customer_profile_id) 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 xml.marketType(options[:market_type] || MARKET_TYPE[:retail]) @@ -533,6 +573,7 @@ def valid_track_data def add_check(xml, check) xml.payment do xml.bankAccount do + xml.accountType(check.account_type) xml.routingNumber(check.routing_number) xml.accountNumber(check.account_number) xml.nameOnAccount(truncate(check.name, 22)) @@ -553,12 +594,16 @@ def add_customer_data(xml, payment_source, options) xml.customerIP(options[:ip]) unless empty?(options[:ip]) - xml.cardholderAuthentication do - three_d_secure = options.fetch(:three_d_secure, {}) - xml.authenticationIndicator( - options[:authentication_indicator] || three_d_secure[:eci]) - xml.cardholderAuthenticationValue( - options[:cardholder_authentication_value] || three_d_secure[:cavv]) + if !empty?(options.fetch(:three_d_secure, {})) || options[:authentication_indicator] || options[:cardholder_authentication_value] + xml.cardholderAuthentication do + three_d_secure = options.fetch(:three_d_secure, {}) + xml.authenticationIndicator( + options[:authentication_indicator] || three_d_secure[:eci] + ) + xml.cardholderAuthenticationValue( + options[:cardholder_authentication_value] || three_d_secure[:cavv] + ) + end end end @@ -569,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) @@ -578,21 +624,22 @@ 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 - def add_shipping_address(xml, options, root_node='shipTo') + def add_shipping_address(xml, options, root_node = 'shipTo') address = options[:shipping_address] || options[:address] return unless address xml.send(root_node) do - first_name, last_name = if address[:name] - split_names(address[:name]) - else - [address[:first_name], address[:last_name]] - end + first_name, last_name = + if address[:name] + split_names(address[:name]) + else + [address[:first_name], address[:last_name]] + end full_address = "#{address[:address1]} #{address[:address2]}".strip xml.firstName(truncate(first_name, 50)) unless empty?(first_name) @@ -604,7 +651,16 @@ def add_shipping_address(xml, options, root_node='shipTo') xml.zip(truncate(address[:zip], 20)) xml.country(truncate(address[:country], 60)) end + end + + def add_ship_from_address(xml, options, root_node = 'shipFrom') + address = options[:ship_from_address] + return unless address + xml.send(root_node) do + xml.zip(truncate(address[:zip], 20)) unless empty?(address[:zip]) + xml.country(truncate(address[:country], 60)) unless empty?(address[:country]) + end end def add_order_id(xml, options) @@ -616,6 +672,7 @@ def add_invoice(xml, transaction_type, options) xml.invoiceNumber(truncate(options[:order_id], 20)) xml.description(truncate(options[:description], 255)) xml.purchaseOrderNumber(options[:po_number]) if options[:po_number] && transaction_type.start_with?('profileTrans') + xml.summaryCommodityCode(truncate(options[:summary_commodity_code], 4)) if options[:summary_commodity_code] && !transaction_type.start_with?('profileTrans') end # Authorize.net API requires lineItems to be placed directly after order tag @@ -654,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) @@ -673,24 +740,47 @@ def add_po_number(xml, options) xml.poNumber(options[:po_number]) if options[:po_number] end + def add_extra_options_for_cim(xml, options) + xml.extraOptions("x_delim_char=#{options[:delimiter]}") if options[:delimiter] + end + + def add_processing_options(xml, options) + return unless options[:stored_credential] + + xml.processingOptions do + if options[:stored_credential][:initial_transaction] && options[:stored_credential][:reason_type] == 'recurring' + xml.isFirstRecurringPayment 'true' + elsif options[:stored_credential][:initial_transaction] + xml.isFirstSubsequentAuth 'true' + elsif options[:stored_credential][:initiator] == 'cardholder' + xml.isStoredCredentials 'true' + else + xml.isSubsequentAuth 'true' + end + end + end + + def add_subsequent_auth_information(xml, options) + return unless options.dig(:stored_credential, :initiator) == 'merchant' + + xml.subsequentAuthInformation do + xml.reason options[:stored_credential_reason_type_override] if options[:stored_credential_reason_type_override] + xml.originalNetworkTransId options[:stored_credential][:network_transaction_id] if options[:stored_credential][:network_transaction_id] + end + end + def create_customer_payment_profile(credit_card, options) - commit(:cim_store_update) do |xml| + commit(:cim_store_update, options) do |xml| 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 def create_customer_profile(credit_card, options) - commit(:cim_store) do |xml| + commit(:cim_store, options) do |xml| xml.profile do xml.merchantCustomerId(truncate(options[:merchant_customer_id], 20) || SecureRandom.hex(10)) xml.description(truncate(options[:description], 255)) unless empty?(options[:description]) @@ -700,20 +790,14 @@ 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 end def delete_customer_profile(customer_profile_id) - commit(:cim_store_delete_customer) do |xml| + commit(:cim_store_delete_customer, options) do |xml| xml.customerProfileId(customer_profile_id) end end @@ -728,13 +812,17 @@ def names_from(payment_source, address, options) end def state_from(address, options) - if ['US', 'CA'].include?(address[:country]) + if %w[US CA].include?(address[:country]) address[:state] || 'NC' else address[:state] || 'n/a' end end + def subsequent_recurring_transaction?(options) + options.dig(:stored_credential, :reason_type) == 'recurring' && !options.dig(:stored_credential, :initial_transaction) + end + def headers { 'Content-Type' => 'text/xml' } end @@ -743,17 +831,17 @@ def url test? ? test_url : live_url end - def parse(action, raw_response) - if is_cim_action?(action) || action == :verify_credentials - parse_cim(raw_response) + def parse(action, raw_response, options = {}) + if cim_action?(action) || action == :verify_credentials + parse_cim(raw_response, options) else parse_normal(action, raw_response) end end - def commit(action, &payload) - raw_response = ssl_post(url, post_data(action, &payload), headers) - response = parse(action, raw_response) + def commit(action, options = {}, &) + raw_response = ssl_post(url, post_data(action, &), headers) + response = parse(action, raw_response, options) avs_result_code = response[:avs_result_code].upcase if response[:avs_result_code] avs_result = AVSResult.new(code: STANDARD_AVS_CODE_MAPPING[avs_result_code]) @@ -767,15 +855,15 @@ def commit(action, &payload) response, authorization: authorization_from(action, response), test: test?, - avs_result: avs_result, - cvv_result: cvv_result, + avs_result:, + cvv_result:, fraud_review: fraud_review?(response), error_code: map_error_code(response[:response_code], response[:response_reason_code]) ) end end - def is_cim_action?(action) + def cim_action?(action) action.to_s.start_with?('cim') end @@ -797,7 +885,7 @@ def root_for(action) 'deleteCustomerProfileRequest' elsif action == :verify_credentials 'authenticateTestRequest' - elsif is_cim_action?(action) + elsif cim_action?(action) 'createCustomerProfileTransactionRequest' else 'createTransactionRequest' @@ -815,19 +903,19 @@ def parse_normal(action, body) doc = Nokogiri::XML(body) doc.remove_namespaces! - response = {action: action} + response = { action: } - response[:response_code] = if(element = doc.at_xpath('//transactionResponse/responseCode')) - (empty?(element.content) ? nil : element.content.to_i) - end + response[:response_code] = if (element = doc.at_xpath('//transactionResponse/responseCode')) + empty?(element.content) ? nil : element.content.to_i + end - if(element = doc.at_xpath('//errors/error')) + if (element = doc.at_xpath('//errors/error')) response[:response_reason_code] = element.at_xpath('errorCode').content[/0*(\d+)$/, 1] response[:response_reason_text] = element.at_xpath('errorText').content.chomp('.') - elsif(element = doc.at_xpath('//transactionResponse/messages/message')) + elsif (element = doc.at_xpath('//transactionResponse/messages/message')) response[:response_reason_code] = element.at_xpath('code').content[/0*(\d+)$/, 1] response[:response_reason_text] = element.at_xpath('description').content.chomp('.') - elsif(element = doc.at_xpath('//messages/message')) + elsif (element = doc.at_xpath('//messages/message')) response[:response_reason_code] = element.at_xpath('code').content[/0*(\d+)$/, 1] response[:response_reason_text] = element.at_xpath('text').content.chomp('.') else @@ -835,73 +923,96 @@ def parse_normal(action, body) response[:response_reason_text] = '' end - response[:avs_result_code] = if(element = doc.at_xpath('//avsResultCode')) - (empty?(element.content) ? nil : element.content) - end + response[:avs_result_code] = + if (element = doc.at_xpath('//avsResultCode')) + empty?(element.content) ? nil : element.content + end - response[:transaction_id] = if(element = doc.at_xpath('//transId')) - (empty?(element.content) ? nil : element.content) - end + response[:transaction_id] = + if element = doc.at_xpath('//transId') + empty?(element.content) ? nil : element.content + end - response[:card_code] = if(element = doc.at_xpath('//cvvResultCode')) - (empty?(element.content) ? nil : element.content) - end + response[:card_code] = + if element = doc.at_xpath('//cvvResultCode') + empty?(element.content) ? nil : element.content + end - response[:authorization_code] = if(element = doc.at_xpath('//authCode')) - (empty?(element.content) ? nil : element.content) - end + response[:authorization_code] = + if element = doc.at_xpath('//authCode') + empty?(element.content) ? nil : element.content + end - response[:cardholder_authentication_code] = if(element = doc.at_xpath('//cavvResultCode')) - (empty?(element.content) ? nil : element.content) - end + response[:cardholder_authentication_code] = + if element = doc.at_xpath('//cavvResultCode') + empty?(element.content) ? nil : element.content + end - response[:account_number] = if(element = doc.at_xpath('//accountNumber')) - (empty?(element.content) ? nil : element.content[-4..-1]) - end + response[:account_number] = + if element = doc.at_xpath('//accountNumber') + empty?(element.content) ? nil : element.content[-4..-1] + end - response[:test_request] = if(element = doc.at_xpath('//testRequest')) - (empty?(element.content) ? nil : element.content) - end + response[:test_request] = + if element = doc.at_xpath('//testRequest') + empty?(element.content) ? nil : element.content + end + + response[:full_response_code] = + if element = doc.at_xpath('//messages/message/code') + empty?(element.content) ? nil : element.content + end + + response[:network_trans_id] = + if element = doc.at_xpath('//networkTransId') + empty?(element.content) ? nil : element.content + end response end - def parse_cim(body) + def parse_cim(body, options) response = {} doc = Nokogiri::XML(body).remove_namespaces! - if (element = doc.at_xpath('//messages/message')) + if element = doc.at_xpath('//messages/message') response[:message_code] = element.at_xpath('code').content[/0*(\d+)$/, 1] response[:message_text] = element.at_xpath('text').content.chomp('.') end - response[:result_code] = if(element = doc.at_xpath('//messages/resultCode')) - (empty?(element.content) ? nil : element.content) - end + response[:result_code] = + if element = doc.at_xpath('//messages/resultCode') + empty?(element.content) ? nil : element.content + end - response[:test_request] = if(element = doc.at_xpath('//testRequest')) - (empty?(element.content) ? nil : element.content) - end + response[:test_request] = + if element = doc.at_xpath('//testRequest') + empty?(element.content) ? nil : element.content + end - response[:customer_profile_id] = if(element = doc.at_xpath('//customerProfileId')) - (empty?(element.content) ? nil : element.content) - end + response[:customer_profile_id] = + if element = doc.at_xpath('//customerProfileId') + empty?(element.content) ? nil : element.content + end - response[:customer_payment_profile_id] = if(element = doc.at_xpath('//customerPaymentProfileIdList/numericString')) - (empty?(element.content) ? nil : element.content) - end + response[:customer_payment_profile_id] = + if element = doc.at_xpath('//customerPaymentProfileIdList/numericString') + empty?(element.content) ? nil : element.content + end - response[:customer_payment_profile_id] = if(element = doc.at_xpath('//customerPaymentProfileIdList/numericString') || - doc.at_xpath('//customerPaymentProfileId')) - (empty?(element.content) ? nil : element.content) - end + response[:customer_payment_profile_id] = + if element = doc.at_xpath('//customerPaymentProfileIdList/numericString') || + doc.at_xpath('//customerPaymentProfileId') + empty?(element.content) ? nil : element.content + end - response[:direct_response] = if(element = doc.at_xpath('//directResponse')) - (empty?(element.content) ? nil : element.content) - end + response[:direct_response] = + if element = doc.at_xpath('//directResponse') + empty?(element.content) ? nil : element.content + end - response.merge!(parse_direct_response_elements(response)) + response.merge!(parse_direct_response_elements(response, options)) response end @@ -918,7 +1029,7 @@ def message_from(action, response, avs_result, cvv_result) if response[:response_code] == DECLINED if CARD_CODE_ERRORS.include?(cvv_result.code) return cvv_result.message - elsif(AVS_REASON_CODES.include?(response[:response_reason_code]) && AVS_ERRORS.include?(avs_result.code)) + elsif AVS_REASON_CODES.include?(response[:response_reason_code]) && AVS_ERRORS.include?(avs_result.code) return avs_result.message end end @@ -943,7 +1054,7 @@ def cim?(action) end def transaction_id_from(authorization) - transaction_id, _, _ = split_authorization(authorization) + transaction_id, = split_authorization(authorization) transaction_id end @@ -961,14 +1072,14 @@ def map_error_code(response_code, response_reason_code) def auth_was_for_cim?(authorization) _, _, action = split_authorization(authorization) - action && is_cim_action?(action) + action && cim_action?(action) end - def parse_direct_response_elements(response) - params = response[:direct_response] + def parse_direct_response_elements(response, options) + params = response[:direct_response]&.tr('"', '') return {} unless params - parts = params.split(',') + parts = params.split(options[:delimiter] || ',') { response_code: parts[0].to_i, response_subcode: parts[1], @@ -1014,10 +1125,9 @@ def parse_direct_response_elements(response) card_type: parts[51] || '', split_tender_id: parts[52] || '', requested_amount: parts[53] || '', - balance_on_card: parts[54] || '', + balance_on_card: parts[54] || '' } end - 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 8781f60ea66..b0224c91906 100644 --- a/lib/active_merchant/billing/gateways/authorize_net_arb.rb +++ b/lib/active_merchant/billing/gateways/authorize_net_arb.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # For more information on the Authorize.Net Gateway please visit their {Integration Center}[http://developer.authorize.net/] # # The login and password are not the username and password you use to @@ -31,18 +31,18 @@ class AuthorizeNetArbGateway < Gateway self.default_currency = 'USD' - self.supported_countries = ['US', 'CA', 'GB'] - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb] + self.supported_countries = %w[US CA GB] + self.supported_cardtypes = %i[visa master american_express discover diners_club jcb] self.homepage_url = 'http://www.authorize.net/' self.display_name = 'Authorize.Net' AUTHORIZE_NET_ARB_NAMESPACE = 'AnetApi/xml/v1/schema/AnetApiSchema.xsd' RECURRING_ACTIONS = { - :create => 'ARBCreateSubscription', - :update => 'ARBUpdateSubscription', - :cancel => 'ARBCancelSubscription', - :status => 'ARBGetSubscriptionStatus' + create: 'ARBCreateSubscription', + update: 'ARBUpdateSubscription', + cancel: 'ARBCancelSubscription', + status: 'ARBGetSubscriptionStatus' } # Creates a new AuthorizeNetArbGateway @@ -82,9 +82,9 @@ def initialize(options = {}) # +:interval => { :unit => :months, :length => 3 }+ (REQUIRED) # * :duration -- A hash containing keys for the :start_date the subscription begins (also the date the # initial billing occurs) and the total number of billing :occurrences or payments for the subscription. (REQUIRED) - def recurring(money, creditcard, options={}) + def recurring(money, creditcard, options = {}) requires!(options, :interval, :duration, :billing_address) - requires!(options[:interval], :length, [:unit, :days, :months]) + requires!(options[:interval], :length, %i[unit days months]) requires!(options[:duration], :start_date, :occurrences) requires!(options[:billing_address], :first_name, :last_name) @@ -110,7 +110,7 @@ def recurring(money, creditcard, options={}) # # * :subscription_id -- A string containing the :subscription_id of the recurring payment already in place # for a given credit card. (REQUIRED) - def update_recurring(options={}) + def update_recurring(options = {}) requires!(options, :subscription_id) request = build_recurring_request(:update, options) recurring_commit(:update, request) @@ -126,7 +126,7 @@ def update_recurring(options={}) # * subscription_id -- A string containing the +subscription_id+ of the recurring payment already in place # for a given credit card. (REQUIRED) def cancel_recurring(subscription_id) - request = build_recurring_request(:cancel, :subscription_id => subscription_id) + request = build_recurring_request(:cancel, subscription_id:) recurring_commit(:cancel, request) end @@ -139,7 +139,7 @@ def cancel_recurring(subscription_id) # * subscription_id -- A string containing the +subscription_id+ of the recurring payment already in place # for a given credit card. (REQUIRED) def status_recurring(subscription_id) - request = build_recurring_request(:status, :subscription_id => subscription_id) + request = build_recurring_request(:status, subscription_id:) recurring_commit(:status, request) end @@ -147,13 +147,11 @@ def status_recurring(subscription_id) # Builds recurring billing request def build_recurring_request(action, options = {}) - unless RECURRING_ACTIONS.include?(action) - raise StandardError, "Invalid Automated Recurring Billing Action: #{action}" - end + raise StandardError, "Invalid Automated Recurring Billing Action: #{action}" unless RECURRING_ACTIONS.include?(action) - xml = Builder::XmlMarkup.new(:indent => 2) - xml.instruct!(:xml, :version => '1.0', :encoding => 'utf-8') - xml.tag!("#{RECURRING_ACTIONS[action]}Request", :xmlns => AUTHORIZE_NET_ARB_NAMESPACE) do + xml = Builder::XmlMarkup.new(indent: 2) + xml.instruct!(:xml, version: '1.0', encoding: 'utf-8') + xml.tag!("#{RECURRING_ACTIONS[action]}Request", xmlns: AUTHORIZE_NET_ARB_NAMESPACE) do add_merchant_authentication(xml) # Merchant-assigned reference ID for the request xml.tag!('refId', options[:ref_id]) if options[:ref_id] @@ -210,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 @@ -232,6 +230,7 @@ def add_subscription(xml, options) def add_interval(xml, options) interval = options[:interval] return unless interval + xml.tag!('interval') do # The measurement of time, in association with the Interval Unit, # that is used to define the frequency of the billing occurrences @@ -246,6 +245,7 @@ def add_interval(xml, options) def add_duration(xml, options) duration = options[:duration] return unless duration + # The date the subscription begins # (also the date the initial billing occurs) xml.tag!('startDate', duration[:start_date]) if duration[:start_date] @@ -255,13 +255,14 @@ def add_duration(xml, options) def add_payment_schedule(xml, options) return unless options[:interval] || options[:duration] + xml.tag!('paymentSchedule') do # 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 @@ -269,6 +270,7 @@ def add_payment_schedule(xml, options) # Adds customer's credit card or bank account payment information def add_payment(xml, options) return unless options[:credit_card] || options[:bank_account] + xml.tag!('payment') do # Contains the customer’s credit card information add_credit_card(xml, options) @@ -283,6 +285,7 @@ def add_payment(xml, options) def add_credit_card(xml, options) credit_card = options[:credit_card] return unless credit_card + xml.tag!('creditCard') do # The credit card number used for payment of the subscription xml.tag!('cardNumber', credit_card.number) @@ -297,6 +300,7 @@ def add_credit_card(xml, options) def add_bank_account(xml, options) bank_account = options[:bank_account] return unless bank_account + xml.tag!('bankAccount') do # The type of bank account used for payment of the subscription xml.tag!('accountType', bank_account[:account_type]) @@ -319,6 +323,7 @@ def add_bank_account(xml, options) def add_order(xml, options) order = options[:order] return unless order + xml.tag!('order') do # Merchant-assigned invoice number for the subscription (optional) xml.tag!('invoiceNumber', order[:invoice_number]) @@ -331,6 +336,7 @@ def add_order(xml, options) def add_customer(xml, options) customer = options[:customer] return unless customer + xml.tag!('customer') do xml.tag!('type', customer[:type]) if customer[:type] xml.tag!('id', customer[:id]) if customer[:id] @@ -346,6 +352,7 @@ def add_customer(xml, options) def add_drivers_license(xml, options) return unless customer = options[:customer] return unless drivers_license = customer[:drivers_license] + xml.tag!('driversLicense') do # The customer's driver's license number xml.tag!('number', drivers_license[:number]) @@ -359,6 +366,7 @@ def add_drivers_license(xml, options) # Adds address information def add_address(xml, container_name, address) return if address.blank? + xml.tag!(container_name) do xml.tag!('firstName', address[:first_name]) xml.tag!('lastName', address[:last_name]) @@ -385,9 +393,12 @@ def recurring_commit(action, request) test_mode = test? || message =~ /Test Mode/ success = response[:result_code] == 'Ok' - Response.new(success, message, response, - :test => test_mode, - :authorization => response[:subscription_id] + Response.new( + success, + message, + response, + test: test_mode, + authorization: response[:subscription_id] ) end @@ -407,7 +418,7 @@ def recurring_parse(action, xml) def recurring_parse_element(response, node) if node.has_elements? - node.elements.each{|e| recurring_parse_element(response, e) } + node.elements.each { |e| recurring_parse_element(response, e) } else response[node.name.underscore.to_sym] = node.text end diff --git a/lib/active_merchant/billing/gateways/authorize_net_cim.rb b/lib/active_merchant/billing/gateways/authorize_net_cim.rb index bfde6385998..90d4e19eecd 100644 --- a/lib/active_merchant/billing/gateways/authorize_net_cim.rb +++ b/lib/active_merchant/billing/gateways/authorize_net_cim.rb @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- -module ActiveMerchant #:nodoc: - module Billing #:nodoc: + +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # ==== Customer Information Manager (CIM) # # The Authorize.Net Customer Information Manager (CIM) is an optional additional service that allows you to store sensitive payment information on @@ -33,55 +34,55 @@ class AuthorizeNetCimGateway < Gateway AUTHORIZE_NET_CIM_NAMESPACE = 'AnetApi/xml/v1/schema/AnetApiSchema.xsd' CIM_ACTIONS = { - :create_customer_profile => 'createCustomerProfile', - :create_customer_payment_profile => 'createCustomerPaymentProfile', - :create_customer_shipping_address => 'createCustomerShippingAddress', - :get_customer_profile => 'getCustomerProfile', - :get_customer_profile_ids => 'getCustomerProfileIds', - :get_customer_payment_profile => 'getCustomerPaymentProfile', - :get_customer_shipping_address => 'getCustomerShippingAddress', - :delete_customer_profile => 'deleteCustomerProfile', - :delete_customer_payment_profile => 'deleteCustomerPaymentProfile', - :delete_customer_shipping_address => 'deleteCustomerShippingAddress', - :update_customer_profile => 'updateCustomerProfile', - :update_customer_payment_profile => 'updateCustomerPaymentProfile', - :update_customer_shipping_address => 'updateCustomerShippingAddress', - :create_customer_profile_transaction => 'createCustomerProfileTransaction', - :validate_customer_payment_profile => 'validateCustomerPaymentProfile' + create_customer_profile: 'createCustomerProfile', + create_customer_payment_profile: 'createCustomerPaymentProfile', + create_customer_shipping_address: 'createCustomerShippingAddress', + get_customer_profile: 'getCustomerProfile', + get_customer_profile_ids: 'getCustomerProfileIds', + get_customer_payment_profile: 'getCustomerPaymentProfile', + get_customer_shipping_address: 'getCustomerShippingAddress', + delete_customer_profile: 'deleteCustomerProfile', + delete_customer_payment_profile: 'deleteCustomerPaymentProfile', + delete_customer_shipping_address: 'deleteCustomerShippingAddress', + update_customer_profile: 'updateCustomerProfile', + update_customer_payment_profile: 'updateCustomerPaymentProfile', + update_customer_shipping_address: 'updateCustomerShippingAddress', + create_customer_profile_transaction: 'createCustomerProfileTransaction', + validate_customer_payment_profile: 'validateCustomerPaymentProfile' } CIM_TRANSACTION_TYPES = { - :auth_capture => 'profileTransAuthCapture', - :auth_only => 'profileTransAuthOnly', - :capture_only => 'profileTransCaptureOnly', - :prior_auth_capture => 'profileTransPriorAuthCapture', - :refund => 'profileTransRefund', - :void => 'profileTransVoid' + auth_capture: 'profileTransAuthCapture', + auth_only: 'profileTransAuthOnly', + capture_only: 'profileTransCaptureOnly', + prior_auth_capture: 'profileTransPriorAuthCapture', + refund: 'profileTransRefund', + void: 'profileTransVoid' } CIM_VALIDATION_MODES = { - :none => 'none', - :test => 'testMode', - :live => 'liveMode', - :old => 'oldLiveMode' + none: 'none', + test: 'testMode', + live: 'liveMode', + old: 'oldLiveMode' } BANK_ACCOUNT_TYPES = { - :checking => 'checking', - :savings => 'savings', - :business_checking => 'businessChecking' + checking: 'checking', + savings: 'savings', + business_checking: 'businessChecking' } ECHECK_TYPES = { - :ccd => 'CCD', - :ppd => 'PPD', - :web => 'WEB' + ccd: 'CCD', + ppd: 'PPD', + web: 'WEB' } self.homepage_url = 'http://www.authorize.net/' self.display_name = 'Authorize.Net CIM' self.supported_countries = ['US'] - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] # Creates a new AuthorizeNetCimGateway # @@ -379,19 +380,19 @@ def create_customer_profile_transaction(options) requires!(options, :transaction) requires!(options[:transaction], :type) case options[:transaction][:type] - when :void - requires!(options[:transaction], :trans_id) - when :refund - requires!(options[:transaction], :trans_id) && - ( - (options[:transaction][:customer_profile_id] && options[:transaction][:customer_payment_profile_id]) || - options[:transaction][:credit_card_number_masked] || - (options[:transaction][:bank_routing_number_masked] && options[:transaction][:bank_account_number_masked]) - ) - when :prior_auth_capture - requires!(options[:transaction], :amount, :trans_id) - else - requires!(options[:transaction], :amount, :customer_profile_id, :customer_payment_profile_id) + when :void + requires!(options[:transaction], :trans_id) + when :refund + requires!(options[:transaction], :trans_id) && + ( + (options[:transaction][:customer_profile_id] && options[:transaction][:customer_payment_profile_id]) || + options[:transaction][:credit_card_number_masked] || + (options[:transaction][:bank_routing_number_masked] && options[:transaction][:bank_account_number_masked]) + ) + when :prior_auth_capture + requires!(options[:transaction], :amount, :trans_id) + else + requires!(options[:transaction], :amount, :customer_profile_id, :customer_payment_profile_id) end request = build_request(:create_customer_profile_transaction, options) commit(:create_customer_profile_transaction, request) @@ -485,13 +486,11 @@ def expdate(credit_card) end def build_request(action, options = {}) - unless CIM_ACTIONS.include?(action) - raise StandardError, "Invalid Customer Information Manager Action: #{action}" - end + raise StandardError, "Invalid Customer Information Manager Action: #{action}" unless CIM_ACTIONS.include?(action) - xml = Builder::XmlMarkup.new(:indent => 2) - xml.instruct!(:xml, :version => '1.0', :encoding => 'utf-8') - xml.tag!("#{CIM_ACTIONS[action]}Request", :xmlns => AUTHORIZE_NET_CIM_NAMESPACE) do + xml = Builder::XmlMarkup.new(indent: 2) + xml.instruct!(:xml, version: '1.0', encoding: 'utf-8') + xml.tag!("#{CIM_ACTIONS[action]}Request", xmlns: AUTHORIZE_NET_CIM_NAMESPACE) do add_merchant_authentication(xml) # Merchant-assigned reference ID for the request xml.tag!('refId', options[:ref_id]) if options[:ref_id] @@ -564,6 +563,8 @@ def build_delete_customer_shipping_address_request(xml, options) def build_get_customer_profile_request(xml, options) xml.tag!('customerProfileId', options[:customer_profile_id]) + xml.tag!('unmaskExpirationDate', options[:unmask_expiration_date]) if options[:unmask_expiration_date] + xml.tag!('includeIssuerInfo', options[:include_issuer_info]) if options[:include_issuer_info] xml.target! end @@ -575,6 +576,7 @@ def build_get_customer_payment_profile_request(xml, options) xml.tag!('customerProfileId', options[:customer_profile_id]) xml.tag!('customerPaymentProfileId', options[:customer_payment_profile_id]) xml.tag!('unmaskExpirationDate', options[:unmask_expiration_date]) if options[:unmask_expiration_date] + xml.tag!('includeIssuerInfo', options[:include_issuer_info]) if options[:include_issuer_info] xml.target! end @@ -614,7 +616,7 @@ def build_update_customer_shipping_address_request(xml, options) def build_create_customer_profile_transaction_request(xml, options) options[:extra_options] ||= {} - options[:extra_options].merge!('x_delim_char' => @options[:delimiter]) if @options[:delimiter] + options[:extra_options]['x_delim_char'] = @options[:delimiter] if @options[:delimiter] add_transaction(xml, options[:transaction]) xml.tag!('extraOptions') do @@ -657,51 +659,44 @@ def add_profile(xml, profile, update = false) end def add_transaction(xml, transaction) - unless CIM_TRANSACTION_TYPES.include?(transaction[:type]) - raise StandardError, "Invalid Customer Information Manager Transaction Type: #{transaction[:type]}" - end + raise StandardError, "Invalid Customer Information Manager Transaction Type: #{transaction[:type]}" unless CIM_TRANSACTION_TYPES.include?(transaction[:type]) xml.tag!('transaction') do xml.tag!(CIM_TRANSACTION_TYPES[transaction[:type]]) do # The amount to be billed to the customer case transaction[:type] - when :void - tag_unless_blank(xml,'customerProfileId', transaction[:customer_profile_id]) - tag_unless_blank(xml,'customerPaymentProfileId', transaction[:customer_payment_profile_id]) - tag_unless_blank(xml,'customerShippingAddressId', transaction[:customer_shipping_address_id]) - xml.tag!('transId', transaction[:trans_id]) - when :refund - #TODO - add lineItems field - xml.tag!('amount', transaction[:amount]) - tag_unless_blank(xml, 'customerProfileId', transaction[:customer_profile_id]) - tag_unless_blank(xml, 'customerPaymentProfileId', transaction[:customer_payment_profile_id]) - tag_unless_blank(xml, 'customerShippingAddressId', transaction[:customer_shipping_address_id]) - tag_unless_blank(xml, 'creditCardNumberMasked', transaction[:credit_card_number_masked]) - tag_unless_blank(xml, 'bankRoutingNumberMasked', transaction[:bank_routing_number_masked]) - tag_unless_blank(xml, 'bankAccountNumberMasked', transaction[:bank_account_number_masked]) - add_order(xml, transaction[:order]) if transaction[:order].present? - xml.tag!('transId', transaction[:trans_id]) - add_tax(xml, transaction[:tax]) if transaction[:tax] - add_duty(xml, transaction[:duty]) if transaction[:duty] - add_shipping(xml, transaction[:shipping]) if transaction[:shipping] - when :prior_auth_capture - xml.tag!('amount', transaction[:amount]) - add_order(xml, transaction[:order]) if transaction[:order].present? - xml.tag!('transId', transaction[:trans_id]) - else - xml.tag!('amount', transaction[:amount]) - xml.tag!('customerProfileId', transaction[:customer_profile_id]) - xml.tag!('customerPaymentProfileId', transaction[:customer_payment_profile_id]) - xml.tag!('approvalCode', transaction[:approval_code]) if transaction[:type] == :capture_only - add_order(xml, transaction[:order]) if transaction[:order].present? + when :void + tag_unless_blank(xml, 'customerProfileId', transaction[:customer_profile_id]) + tag_unless_blank(xml, 'customerPaymentProfileId', transaction[:customer_payment_profile_id]) + tag_unless_blank(xml, 'customerShippingAddressId', transaction[:customer_shipping_address_id]) + xml.tag!('transId', transaction[:trans_id]) + when :refund + xml.tag!('amount', transaction[:amount]) + tag_unless_blank(xml, 'customerProfileId', transaction[:customer_profile_id]) + tag_unless_blank(xml, 'customerPaymentProfileId', transaction[:customer_payment_profile_id]) + tag_unless_blank(xml, 'customerShippingAddressId', transaction[:customer_shipping_address_id]) + tag_unless_blank(xml, 'creditCardNumberMasked', transaction[:credit_card_number_masked]) + tag_unless_blank(xml, 'bankRoutingNumberMasked', transaction[:bank_routing_number_masked]) + tag_unless_blank(xml, 'bankAccountNumberMasked', transaction[:bank_account_number_masked]) + add_order(xml, transaction[:order]) if transaction[:order].present? + xml.tag!('transId', transaction[:trans_id]) + add_tax(xml, transaction[:tax]) if transaction[:tax] + add_duty(xml, transaction[:duty]) if transaction[:duty] + add_shipping(xml, transaction[:shipping]) if transaction[:shipping] + when :prior_auth_capture + xml.tag!('amount', transaction[:amount]) + add_order(xml, transaction[:order]) if transaction[:order].present? + xml.tag!('transId', transaction[:trans_id]) + else + xml.tag!('amount', transaction[:amount]) + xml.tag!('customerProfileId', transaction[:customer_profile_id]) + xml.tag!('customerPaymentProfileId', transaction[:customer_payment_profile_id]) + xml.tag!('approvalCode', transaction[:approval_code]) if transaction[:type] == :capture_only + add_order(xml, transaction[:order]) if transaction[:order].present? end - if [:auth_capture, :auth_only, :capture_only].include?(transaction[:type]) - xml.tag!('recurringBilling', transaction[:recurring_billing]) if transaction.has_key?(:recurring_billing) - end - unless [:void,:refund,:prior_auth_capture].include?(transaction[:type]) - tag_unless_blank(xml, 'cardCode', transaction[:card_code]) - 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 end @@ -798,6 +793,7 @@ def add_address(xml, address) # when the payment method is credit card. def add_credit_card(xml, credit_card) return unless credit_card + xml.tag!('creditCard') do # The credit card number used for payment of the subscription xml.tag!('cardNumber', full_or_masked_card_number(credit_card.number)) @@ -858,7 +854,7 @@ def commit(action, request) response_params = parse(action, xml) - message_element= response_params['messages']['message'] + message_element = response_params['messages']['message'] first_error = message_element.is_a?(Array) ? message_element.first : message_element message = first_error['text'] test_mode = @options[:test_requests] || message =~ /Test Mode/ @@ -879,12 +875,12 @@ def tag_unless_blank(xml, tag_name, data) end def format_extra_options(options) - options.map{ |k, v| "#{k}=#{v}" }.join('&') unless options.nil? + options&.map { |k, v| "#{k}=#{v}" }&.join('&') end def parse_direct_response(params) delimiter = @options[:delimiter] || ',' - direct_response = {'raw' => params} + direct_response = { 'raw' => params } direct_response_fields = params.split(delimiter) direct_response.merge( { @@ -934,7 +930,7 @@ def parse_direct_response(params) 'card_type' => direct_response_fields[51] || '', 'split_tender_id' => direct_response_fields[52] || '', 'requested_amount' => direct_response_fields[53] || '', - 'balance_on_card' => direct_response_fields[54] || '', + 'balance_on_card' => direct_response_fields[54] || '' } ) end @@ -943,9 +939,7 @@ def parse(action, xml) xml = REXML::Document.new(xml) root = REXML::XPath.first(xml, "//#{CIM_ACTIONS[action]}Response") || REXML::XPath.first(xml, '//ErrorResponse') - if root - response = parse_element(root) - end + response = parse_element(root) if root response end @@ -953,7 +947,7 @@ def parse(action, xml) def parse_element(node) if node.has_elements? response = {} - node.elements.each{ |e| + node.elements.each { |e| key = e.name.underscore value = parse_element(e) if response.has_key?(key) diff --git a/lib/active_merchant/billing/gateways/axcessms.rb b/lib/active_merchant/billing/gateways/axcessms.rb index d45dde62713..574d92d469d 100644 --- a/lib/active_merchant/billing/gateways/axcessms.rb +++ b/lib/active_merchant/billing/gateways/axcessms.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class AxcessmsGateway < Gateway self.test_url = 'https://test.ctpe.io/payment/ctpe' self.live_url = 'https://ctpe.io/payment/ctpe' @@ -8,7 +8,7 @@ class AxcessmsGateway < Gateway GI GR HR HU IE IL IM IS IT LI LT LU LV MC MT MX NL NO PL PT RO RU SE SI SK TR US VA) - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :jcb, :maestro, :solo] + self.supported_cardtypes = %i[visa master american_express discover jcb maestro] self.homepage_url = 'http://www.axcessms.com/' self.display_name = 'Axcess MS' @@ -23,33 +23,33 @@ class AxcessmsGateway < Gateway PAYMENT_CODE_REFUND = 'CC.RF' PAYMENT_CODE_REBILL = 'CC.RB' - def initialize(options={}) + def initialize(options = {}) requires!(options, :sender, :login, :password, :channel) super end - def purchase(money, payment, options={}) + def purchase(money, payment, options = {}) payment_code = payment.respond_to?(:number) ? PAYMENT_CODE_DEBIT : PAYMENT_CODE_REBILL commit(payment_code, money, payment, options) end - def authorize(money, authorization, options={}) + def authorize(money, authorization, options = {}) commit(PAYMENT_CODE_PREAUTHORIZATION, money, authorization, options) end - def capture(money, authorization, options={}) + def capture(money, authorization, options = {}) commit(PAYMENT_CODE_CAPTURE, money, authorization, options) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) commit(PAYMENT_CODE_REFUND, money, authorization, options) end - def void(authorization, options={}) + def void(authorization, options = {}) commit(PAYMENT_CODE_REVERSAL, nil, authorization, options) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -71,9 +71,12 @@ def commit(paymentcode, money, payment, options) message = "#{response[:reason]} - #{response[:return]}" authorization = response[:unique_id] - Response.new(success, message, response, - :authorization => authorization, - :test => (response[:mode] != 'LIVE') + Response.new( + success, + message, + response, + authorization:, + test: (response[:mode] != 'LIVE') ) end @@ -93,19 +96,17 @@ def parse(body) end def parse_element(response, node) - if node.has_attributes? - node.attributes.each{|name, value| response["#{node.name}_#{name}".underscore.to_sym] = value } - end + node.attributes.each { |name, value| response["#{node.name}_#{name}".underscore.to_sym] = value } if node.has_attributes? if node.has_elements? - node.elements.each{|element| parse_element(response, element) } + node.elements.each { |element| parse_element(response, element) } else response[node.name.underscore.to_sym] = node.text end end def build_request(payment_code, money, payment, options) - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 xml.instruct! xml.tag! 'Request', 'version' => API_VERSION do xml.tag! 'Header' do @@ -168,6 +169,7 @@ def add_payment(xml, payment) def add_address(xml, address) raise ArgumentError.new('Address is required') unless address + xml.tag! 'Address' do xml.tag! 'Street', "#{address[:address1]} #{address[:address2]}".strip xml.tag! 'City', address[:city] diff --git a/lib/active_merchant/billing/gateways/balanced.rb b/lib/active_merchant/billing/gateways/balanced.rb index 50a3dfcd2c0..b33a49ef321 100644 --- a/lib/active_merchant/billing/gateways/balanced.rb +++ b/lib/active_merchant/billing/gateways/balanced.rb @@ -1,7 +1,7 @@ require 'json' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # For more information on Balanced visit https://www.balancedpayments.com # or visit #balanced on irc.freenode.net # @@ -22,7 +22,7 @@ class BalancedGateway < Gateway self.live_url = 'https://api.balancedpayments.com' self.supported_countries = ['US'] - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'https://www.balancedpayments.com/' self.display_name = 'Balanced' self.money_format = :cents @@ -44,13 +44,14 @@ def purchase(money, payment_method, options = {}) add_common_params(post, options) MultiResponse.run do |r| - identifier = if(payment_method.respond_to?(:number)) - r.process{store(payment_method, options)} - r.authorization - else - payment_method - end - r.process{commit('debits', "cards/#{card_identifier_from(identifier)}/debits", post)} + identifier = + if payment_method.respond_to?(:number) + r.process { store(payment_method, options) } + r.authorization + else + payment_method + end + r.process { commit('debits', "cards/#{card_identifier_from(identifier)}/debits", post) } end end @@ -61,13 +62,14 @@ def authorize(money, payment_method, options = {}) add_common_params(post, options) MultiResponse.run do |r| - identifier = if(payment_method.respond_to?(:number)) - r.process{store(payment_method, options)} - r.authorization - else - payment_method - end - r.process{commit('card_holds', "cards/#{card_identifier_from(identifier)}/card_holds", post)} + identifier = + if payment_method.respond_to?(:number) + r.process { store(payment_method, options) } + r.authorization + else + payment_method + end + r.process { commit('card_holds', "cards/#{card_identifier_from(identifier)}/card_holds", post) } end end @@ -97,7 +99,7 @@ def refund(money, identifier, options = {}) commit('refunds', "debits/#{reference_identifier_from(identifier)}/refunds", post) end - def store(credit_card, options={}) + def store(credit_card, options = {}) post = {} post[:number] = credit_card.number @@ -117,8 +119,8 @@ def reference_identifier_from(identifier) case identifier when %r{\|} uri = identifier. - split('|'). - detect{|part| part.size > 0} + split('|'). + detect { |part| part.size > 0 } uri.split('/')[2] when %r{\/} identifier.split('/')[5] @@ -137,7 +139,7 @@ def add_amount(post, money) def add_address(post, options) address = (options[:billing_address] || options[:address]) - if(address && address[:zip].present?) + if address && address[:zip].present? post[:address] = {} post[:address][:line1] = address[:address1] if address[:address1] post[:address][:line2] = address[:address2] if address[:address2] @@ -154,37 +156,41 @@ def add_common_params(post, options) post[:meta] = options[:meta] end - def commit(entity_name, path, post, method=:post) - raw_response = begin - parse(ssl_request( - method, - live_url + "/#{path}", - post_data(post), - headers - )) - rescue ResponseError => e - raise unless(e.response.code.to_s =~ /4\d\d/) - parse(e.response.body) - end + def commit(entity_name, path, post, method = :post) + raw_response = + begin + parse( + ssl_request( + method, + live_url + "/#{path}", + post_data(post), + headers + ) + ) + rescue ResponseError => e + raise unless e.response.code.to_s =~ /4\d\d/ + + parse(e.response.body) + end Response.new( success_from(entity_name, raw_response), message_from(raw_response), raw_response, authorization: authorization_from(entity_name, raw_response), - test: test?, + test: test? ) end def success_from(entity_name, raw_response) entity = (raw_response[entity_name] || []).first - if(!entity) + if !entity false - elsif((entity_name == 'refunds') && entity.include?('status')) + elsif (entity_name == 'refunds') && entity.include?('status') %w(succeeded pending).include?(entity['status']) - elsif(entity.include?('status')) + elsif entity.include?('status') (entity['status'] == 'succeeded') - elsif(entity_name == 'cards') + elsif entity_name == 'cards' !!entity['id'] else false @@ -192,7 +198,7 @@ def success_from(entity_name, raw_response) end def message_from(raw_response) - if(raw_response['errors']) + if raw_response['errors'] error = raw_response['errors'].first (error['additional'] || error['message'] || error['description']) else @@ -222,6 +228,7 @@ def post_data(params) params.map do |key, value| next if value.blank? + if value.is_a?(Hash) h = {} value.each do |k, v| @@ -236,19 +243,19 @@ def post_data(params) def headers @@ua ||= JSON.dump( - bindings_version: ActiveMerchant::VERSION, - lang: 'ruby', - lang_version: "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})", - lib_version: BalancedGateway::VERSION, - platform: RUBY_PLATFORM, - publisher: 'active_merchant' + bindings_version: ActiveMerchant::VERSION, + lang: 'ruby', + lang_version: "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})", + lib_version: BalancedGateway::VERSION, + platform: RUBY_PLATFORM, + publisher: 'active_merchant' ) { - 'Authorization' => 'Basic ' + Base64.encode64(@options[:login].to_s + ':').strip, - 'User-Agent' => "Balanced/v1.1 ActiveMerchantBindings/#{ActiveMerchant::VERSION}", - 'Accept' => 'application/vnd.api+json;revision=1.1', - 'X-Balanced-User-Agent' => @@ua, + 'Authorization' => 'Basic ' + Base64.encode64(@options[:login].to_s + ':').strip, + 'User-Agent' => "Balanced/v1.1 ActiveMerchantBindings/#{ActiveMerchant::VERSION}", + 'Accept' => 'application/vnd.api+json;revision=1.1', + 'X-Balanced-User-Agent' => @@ua } end end diff --git a/lib/active_merchant/billing/gateways/bambora_apac.rb b/lib/active_merchant/billing/gateways/bambora_apac.rb new file mode 100644 index 00000000000..aa0af48fa04 --- /dev/null +++ b/lib/active_merchant/billing/gateways/bambora_apac.rb @@ -0,0 +1,222 @@ +require 'nokogiri' + +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class BamboraApacGateway < Gateway + self.live_url = 'https://www.bambora.co.nz/interface/api' + self.test_url = 'https://demo.bambora.co.nz/interface/api' + + self.supported_countries = %w[AU NZ] + self.supported_cardtypes = %i[visa master american_express diners_club jcb] + + self.homepage_url = 'http://www.bambora.com/' + self.display_name = 'Bambora Asia-Pacific' + + self.money_format = :cents + + STANDARD_ERROR_CODE_MAPPING = { + '05' => STANDARD_ERROR_CODE[:card_declined], + '06' => STANDARD_ERROR_CODE[:processing_error], + '14' => STANDARD_ERROR_CODE[:invalid_number], + '54' => STANDARD_ERROR_CODE[:expired_card] + } + + def initialize(options = {}) + requires!(options, :username, :password) + super + end + + def purchase(money, payment, options = {}) + commit('SubmitSinglePayment') do |xml| + xml.Transaction do + xml.CustRef options[:order_id] + xml.Amount amount(money) + xml.TrnType '1' + add_payment(xml, payment) + add_credentials(xml, options) + xml.TrnSource options[:ip] + end + end + end + + def authorize(money, payment, options = {}) + commit('SubmitSinglePayment') do |xml| + xml.Transaction do + xml.CustRef options[:order_id] + xml.Amount amount(money) + xml.TrnType '2' + add_payment(xml, payment) + add_credentials(xml, options) + xml.TrnSource options[:ip] + end + end + end + + def capture(money, authorization, options = {}) + commit('SubmitSingleCapture') do |xml| + xml.Capture do + xml.Receipt authorization + xml.Amount amount(money) + add_credentials(xml, options) + end + end + end + + def refund(money, authorization, options = {}) + commit('SubmitSingleRefund') do |xml| + xml.Refund do + xml.Receipt authorization + xml.Amount amount(money) + add_credentials(xml, options) + end + end + end + + def void(authorization, options = {}) + commit('SubmitSingleVoid') do |xml| + xml.Void do + xml.Receipt authorization + xml.Amount amount(options[:amount]) + add_credentials(xml, options) + end + end + end + + def store(payment, options = {}) + commit('TokeniseCreditCard') do |xml| + xml.TokeniseCreditCard do + xml.CardNumber payment.number + xml.ExpM format(payment.month, :two_digits) + xml.ExpY format(payment.year, :four_digits) + xml.TokeniseAlgorithmID options[:tokenise_algorithm_id] || 2 + xml.UserName @options[:username] + xml.Password @options[:password] + end + end + 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') + end + + private + + def add_credentials(xml, options) + xml.AccountNumber options[:account_number] if options[:account_number] + xml.Security do + xml.UserName @options[:username] + xml.Password @options[:password] + end + end + + def add_payment(xml, payment) + if payment.is_a?(String) + add_token(xml, payment) + else + add_credit_card(xml, payment) + end + end + + def add_token(xml, payment) + xml.CreditCard do + xml.TokeniseAlgorithmID options[:tokenise_algorithm_id] || 2 + xml.CardNumber payment + end + end + + def add_credit_card(xml, payment) + xml.CreditCard Registered: 'False' do + xml.CardNumber payment.number + xml.ExpM format(payment.month, :two_digits) + xml.ExpY format(payment.year, :four_digits) + xml.CVN payment.verification_value + xml.CardHolderName payment.name + end + end + + def parse(body) + element = Nokogiri::XML(body).root.first_element_child.first_element_child + + response = {} + doc = Nokogiri::XML(element) + doc.root.elements.each do |e| + response[e.name.underscore.to_sym] = e.inner_text + end + response + end + + def commit(action, &block) + headers = { + 'Content-Type' => 'text/xml; charset=utf-8', + 'SOAPAction' => "http://www.ippayments.com.au/interface/api/#{endpoint(action)}/#{action}" + } + response = parse(ssl_post("#{commit_url}/#{endpoint(action)}.asmx", new_submit_xml(action, &block), headers)) + + Response.new( + success_from(response), + message_from(response), + response, + authorization: authorization_from(response), + error_code: error_code_from(response), + test: test? + ) + end + + def new_submit_xml(action) + xml = Builder::XmlMarkup.new(indent: 2) + xml.instruct! + xml.soap :Envelope, 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema', 'xmlns:soap' => 'http://schemas.xmlsoap.org/soap/envelope/' do + xml.soap :Body do + xml.__send__(action, 'xmlns' => "http://www.ippayments.com.au/interface/api/#{endpoint(action)}") do + if action == 'TokeniseCreditCard' + xml.tokeniseCreditCardXML do + inner_xml = Builder::XmlMarkup.new(indent: 2) + yield(inner_xml) + xml.cdata!(inner_xml.target!) + end + else + xml.trnXML do + inner_xml = Builder::XmlMarkup.new(indent: 2) + yield(inner_xml) + xml.cdata!(inner_xml.target!) + end + end + end + end + end + xml.target! + end + + def endpoint(action) + action == 'TokeniseCreditCard' ? 'sipp' : 'dts' + end + + def commit_url + test? ? test_url : live_url + end + + def success_from(response) + response[:response_code] == '0' || response[:return_value] == '0' + end + + def error_code_from(response) + STANDARD_ERROR_CODE_MAPPING[response[:declined_code]] + end + + def message_from(response) + success_from(response) ? 'Succeeded' : response[:declined_message] + end + + def authorization_from(response) + response[:receipt] || response[:token] + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/bank_frick.rb b/lib/active_merchant/billing/gateways/bank_frick.rb index fdfdfcff642..322970c9fcf 100644 --- a/lib/active_merchant/billing/gateways/bank_frick.rb +++ b/lib/active_merchant/billing/gateways/bank_frick.rb @@ -1,7 +1,7 @@ require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # For more information visit {Bank Frick Acquiring Services}[http://www.bankfrickacquiring.com/merchantsolutions_en.html] # # Written by Piers Chambers (Varyonic.com) @@ -9,9 +9,9 @@ class BankFrickGateway < Gateway self.test_url = 'https://test.ctpe.io/payment/ctpe' self.live_url = 'https://ctpe.io/payment/ctpe' - self.supported_countries = ['LI','US'] + self.supported_countries = %w[LI US] self.default_currency = 'EUR' - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'http://www.bankfrickacquiring.com/' self.display_name = 'Bank Frick' @@ -24,15 +24,15 @@ class BankFrickGateway < Gateway 'authonly' => 'CC.PA', 'capture' => 'CC.CP', 'refund' => 'CC.RF', - 'void' => 'CC.RV', + 'void' => 'CC.RV' } - def initialize(options={}) + def initialize(options = {}) requires!(options, :sender, :channel, :userid, :userpwd) super end - def purchase(money, payment, options={}) + def purchase(money, payment, options = {}) post = {} add_invoice(post, money, options) add_payment(post, payment) @@ -42,7 +42,7 @@ def purchase(money, payment, options={}) commit('sale', post) end - def authorize(money, payment, options={}) + def authorize(money, payment, options = {}) post = {} add_invoice(post, money, options) add_payment(post, payment) @@ -52,7 +52,7 @@ def authorize(money, payment, options={}) commit('authonly', post) end - def capture(money, authorization, options={}) + def capture(money, authorization, options = {}) post = {} post[:authorization] = authorization add_invoice(post, money, options) @@ -60,7 +60,7 @@ def capture(money, authorization, options={}) commit('capture', post) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) post = {} post[:authorization] = authorization add_invoice(post, money, options) @@ -68,14 +68,14 @@ def refund(money, authorization, options={}) commit('refund', post) end - def void(authorization, options={}) + def void(authorization, options = {}) post = {} post[:authorization] = authorization commit('void', post) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -97,7 +97,7 @@ def add_address(post, creditcard, options) post[:zip] = address[:zip].to_s post[:city] = address[:city].to_s post[:country] = address[:country].to_s - post[:state] = address[:state].blank? ? 'n/a' : address[:state] + post[:state] = address[:state].blank? ? 'n/a' : address[:state] end end @@ -119,7 +119,7 @@ def add_payment(post, payment) end def parse(body) - results = {} + results = {} xml = Nokogiri::XML(body) resp = xml.xpath('//Response/Transaction/Identification') resp.children.each do |element| @@ -166,7 +166,7 @@ def post_data(action, parameters = {}) end def build_xml_request(action, data) - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 xml.Request(version: '1.0') do xml.Header do xml.Security(sender: @options[:sender], type: 'MERCHANT') diff --git a/lib/active_merchant/billing/gateways/banwire.rb b/lib/active_merchant/billing/gateways/banwire.rb index 09aeb583fe2..0f5093629c0 100644 --- a/lib/active_merchant/billing/gateways/banwire.rb +++ b/lib/active_merchant/billing/gateways/banwire.rb @@ -1,10 +1,10 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class BanwireGateway < Gateway URL = 'https://banwire.com/api.pago_pro' self.supported_countries = ['MX'] - self.supported_cardtypes = [:visa, :master, :american_express] + self.supported_cardtypes = %i[visa master american_express] self.homepage_url = 'http://www.banwire.com/' self.display_name = 'Banwire' @@ -63,7 +63,7 @@ def add_creditcard(post, creditcard) post[:card_num] = creditcard.number post[:card_name] = creditcard.name post[:card_type] = card_brand(creditcard) - post[:card_exp] = "#{sprintf("%02d", creditcard.month)}/#{"#{creditcard.year}"[-2, 2]}" + post[:card_exp] = "#{sprintf('%02d', creditcard.month)}/#{creditcard.year.to_s[-2, 2]}" post[:card_ccv2] = creditcard.verification_value end @@ -74,7 +74,7 @@ def add_amount(post, money, options) def card_brand(card) brand = super - ({'master' => 'mastercard', 'american_express' => 'amex'}[brand] || brand) + ({ 'master' => 'mastercard', 'american_express' => 'amex' }[brand] || brand) end def parse(body) @@ -89,11 +89,13 @@ def commit(money, parameters) response = json_error(raw_response) end - Response.new(success?(response), - response['message'], - response, - :test => test?, - :authorization => response['code_auth']) + Response.new( + success?(response), + response['message'], + response, + test: test?, + authorization: response['code_auth'] + ) end def success?(response) diff --git a/lib/active_merchant/billing/gateways/barclaycard_smartpay.rb b/lib/active_merchant/billing/gateways/barclaycard_smartpay.rb index 56fe408d503..4597490f0fa 100644 --- a/lib/active_merchant/billing/gateways/barclaycard_smartpay.rb +++ b/lib/active_merchant/billing/gateways/barclaycard_smartpay.rb @@ -1,18 +1,21 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class BarclaycardSmartpayGateway < Gateway self.test_url = 'https://pal-test.barclaycardsmartpay.com/pal/servlet' self.live_url = 'https://pal-live.barclaycardsmartpay.com/pal/servlet' - self.supported_countries = ['AL', 'AD', 'AM', 'AT', 'AZ', 'BY', 'BE', 'BA', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IS', 'IE', 'IT', 'KZ', 'LV', 'LI', 'LT', 'LU', 'MK', 'MT', 'MD', 'MC', 'ME', 'NL', 'NO', 'PL', 'PT', 'RO', 'RU', 'SM', 'RS', 'SK', 'SI', 'ES', 'SE', 'CH', 'TR', 'UA', 'GB', 'VA'] + self.supported_countries = %w[AL AD AM AT AZ BY BE BA BG HR CY CZ DK EE FI FR DE GR HU IS IE IT KZ LV LI LT LU MK MT MD MC ME NL NO PL PT RO RU SM RS SK SI ES SE CH TR UA GB VA] self.default_currency = 'EUR' - self.currencies_with_three_decimal_places = %w(BHD KWD OMR RSD TND) + self.currencies_with_three_decimal_places = %w(BHD KWD OMR RSD TND IQD JOD LYD) self.money_format = :cents - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb, :dankort, :maestro] + self.supported_cardtypes = %i[visa master american_express discover diners_club jcb dankort maestro] + self.currencies_without_fractions = %w(CVE DJF GNF IDR JPY KMF KRW PYG RWF UGX VND VUV XAF XOF XPF) self.homepage_url = 'https://www.barclaycardsmartpay.com/' self.display_name = 'Barclaycard Smartpay' + API_VERSION = 'v40' + def initialize(options = {}) requires!(options, :company, :merchant, :password) super @@ -35,7 +38,9 @@ def authorize(money, creditcard, options = {}) post[:card] = credit_card_hash(creditcard) post[:billingAddress] = billing_address_hash(options) if options[:billing_address] post[:deliveryAddress] = shipping_address_hash(options) if options[:shipping_address] - add_3ds(post, options) if options[:execute_threed] + post[:shopperStatement] = options[:shopper_statement] if options[:shopper_statement] + + add_3ds(post, options) commit('authorise', post) end @@ -66,7 +71,29 @@ def credit(money, creditcard, options = {}) post[:nationality] = options[:nationality] if options[:nationality] post[:shopperName] = options[:shopper_name] if options[:shopper_name] - commit('refundWithData', post) + if options[:third_party_payout] + post[:recurring] = options[:recurring_contract] || { contract: 'PAYOUT' } + MultiResponse.run do |r| + r.process { + commit( + 'storeDetailAndSubmitThirdParty', + post, + @options[:store_payout_account], + @options[:store_payout_password] + ) + } + r.process { + commit( + 'confirmThirdParty', + modification_request(r.authorization, @options), + @options[:review_payout_account], + @options[:review_payout_password] + ) + } + end + else + commit('refundWithData', post) + end end def void(identification, options = {}) @@ -84,7 +111,7 @@ def verify(creditcard, options = {}) def store(creditcard, options = {}) post = store_request(options) post[:card] = credit_card_hash(creditcard) - post[:recurring] = {:contract => 'RECURRING'} + post[:recurring] = { contract: 'RECURRING' } commit('store', post) end @@ -105,30 +132,31 @@ def scrub(transcript) # Smartpay may return AVS codes not covered by standard AVSResult codes. # Smartpay's descriptions noted below. AVS_MAPPING = { - '0' => 'R', # Unknown - '1' => 'A', # Address matches, postal code doesn't - '2' => 'N', # Neither postal code nor address match - '3' => 'R', # AVS unavailable - '4' => 'E', # AVS not supported for this card type - '5' => 'U', # No AVS data provided - '6' => 'Z', # Postal code matches, address doesn't match - '7' => 'D', # Both postal code and address match - '8' => 'U', # Address not checked, postal code unknown - '9' => 'B', # Address matches, postal code unknown - '10' => 'N', # Address doesn't match, postal code unknown - '11' => 'U', # Postal code not checked, address unknown - '12' => 'B', # Address matches, postal code not checked - '13' => 'U', # Address doesn't match, postal code not checked - '14' => 'P', # Postal code matches, address unknown - '15' => 'P', # Postal code matches, address not checked - '16' => 'N', # Postal code doesn't match, address unknown - '17' => 'U', # Postal code doesn't match, address not checked - '18' => 'I' # Neither postal code nor address were checked + '0' => 'R', # Unknown + '1' => 'A', # Address matches, postal code doesn't + '2' => 'N', # Neither postal code nor address match + '3' => 'R', # AVS unavailable + '4' => 'E', # AVS not supported for this card type + '5' => 'U', # No AVS data provided + '6' => 'Z', # Postal code matches, address doesn't match + '7' => 'D', # Both postal code and address match + '8' => 'U', # Address not checked, postal code unknown + '9' => 'B', # Address matches, postal code unknown + '10' => 'N', # Address doesn't match, postal code unknown + '11' => 'U', # Postal code not checked, address unknown + '12' => 'B', # Address matches, postal code not checked + '13' => 'U', # Address doesn't match, postal code not checked + '14' => 'P', # Postal code matches, address unknown + '15' => 'P', # Postal code matches, address not checked + '16' => 'N', # Postal code doesn't match, address unknown + '17' => 'U', # Postal code doesn't match, address not checked + '18' => 'I' # Neither postal code nor address were checked } - def commit(action, post) + def commit(action, post, account = 'ws', password = @options[:password]) request = post_data(flatten_hash(post)) - raw_response = ssl_post(build_url(action), request, headers) + request_headers = headers(account, password) + raw_response = ssl_post(build_url(action), request, request_headers) response = parse(raw_response) Response.new( @@ -136,21 +164,20 @@ def commit(action, post) message_from(response), response, test: test?, - avs_result: AVSResult.new(:code => parse_avs_code(response)), + avs_result: AVSResult.new(code: parse_avs_code(response)), authorization: response['recurringDetailReference'] || authorization_from(post, response) ) - rescue ResponseError => e case e.response.code when '401' - return Response.new(false, 'Invalid credentials', {}, :test => test?) + return Response.new(false, 'Invalid credentials', {}, test: test?) when '403' - return Response.new(false, 'Not allowed', {}, :test => test?) - when '422' - return Response.new(false, 'Unprocessable Entity', {}, :test => test?) - when '500' - if e.response.body.split(' ')[0] == 'validation' - return Response.new(false, e.response.body.split(' ', 3)[2], {}, :test => test?) + return Response.new(false, 'Not allowed', {}, test: test?) + when '422', '500' + if e.response.body.split(/\W+/).any? { |word| %w(validation configuration security).include?(word) } + error_message = e.response.body[/#{Regexp.escape('message=')}(.*?)#{Regexp.escape('&')}/m, 1].tr('+', ' ') + error_code = e.response.body[/#{Regexp.escape('errorCode=')}(.*?)#{Regexp.escape('&')}/m, 1] + return Response.new(false, error_code + ': ' + error_message, {}, test: test?) end end raise @@ -160,11 +187,12 @@ def authorization_from(parameters, response) authorization = [parameters[:originalReference], response['pspReference']].compact return nil if authorization.empty? + return authorization.join('#') end def parse_avs_code(response) - AVS_MAPPING[response['avsResult'][0..1].strip] if response['avsResult'] + AVS_MAPPING[response['additionalData']['avsResult'][0..1].strip] if response.dig('additionalData', 'avsResult') end def flatten_hash(hash, prefix = nil) @@ -180,20 +208,26 @@ def flatten_hash(hash, prefix = nil) flat_hash end - def headers + def headers(account, password) { 'Content-Type' => 'application/x-www-form-urlencoded; charset=utf-8', - 'Authorization' => 'Basic ' + Base64.strict_encode64("ws@Company.#{@options[:company]}:#{@options[:password]}").strip + 'Authorization' => 'Basic ' + Base64.strict_encode64("#{account}@Company.#{@options[:company]}:#{password}").strip } end def parse(response) - Hash[ - response.split('&').map do |x| - key, val = x.split('=', 2) - [key.split('.').last, CGI.unescape(val)] + parsed_response = {} + params = CGI.parse(response) + params.each do |key, value| + parsed_key = key.split('.', 2) + if parsed_key.size > 1 + parsed_response[parsed_key[0]] ||= {} + parsed_response[parsed_key[0]][parsed_key[1]] = value[0] + else + parsed_response[parsed_key[0]] = value[0] end - ] + end + parsed_response end def post_data(data) @@ -208,25 +242,28 @@ def message_from(response) return response['resultCode'] if response.has_key?('resultCode') # Payment request return response['response'] if response['response'] # Modification request return response['result'] if response.has_key?('result') # Store/Recurring request + 'Failure' # Negative fallback in case of error end def success_from(response) return true if response['result'] == 'Success' - return true if response['resultCode'] == 'Authorised' - return true if response['resultCode'] == 'Received' - successful_responses = %w([capture-received] [cancel-received] [refund-received]) - successful_responses.include?(response['response']) + + successful_results = %w(Authorised Received [payout-submit-received]) + successful_responses = %w([capture-received] [cancel-received] [refund-received] [payout-confirm-received]) + successful_results.include?(response['resultCode']) || successful_responses.include?(response['response']) end def build_url(action) case action when 'store' - "#{test? ? self.test_url : self.live_url}/Recurring/v12/storeToken" + "#{test? ? self.test_url : self.live_url}/Recurring/#{API_VERSION}/storeToken" when 'finalize3ds' - "#{test? ? self.test_url : self.live_url}/Payment/v12/authorise3d" + "#{test? ? self.test_url : self.live_url}/Payment/#{API_VERSION}/authorise3d" + when 'storeDetailAndSubmitThirdParty', 'confirmThirdParty' + "#{test? ? self.test_url : self.live_url}/Payout/#{API_VERSION}/#{action}" else - "#{test? ? self.test_url : self.live_url}/Payment/v12/#{action}" + "#{test? ? self.test_url : self.live_url}/Payment/#{API_VERSION}/#{action}" end end @@ -262,11 +299,11 @@ def create_address_hash(address, house, street) hash = {} hash[:houseNumberOrName] = house hash[:street] = street - hash[:city] = address[:city] if address[:city] - hash[:stateOrProvince] = address[:state] if address[:state] - hash[:postalCode] = address[:zip] if address[:zip] - hash[:country] = address[:country] if address[:country] - hash + hash[:city] = address[:city] + hash[:stateOrProvince] = address[:state] + hash[:postalCode] = address[:zip] + hash[:country] = address[:country] + hash.keep_if { |_, v| v } end def amount_hash(money, currency) @@ -300,26 +337,60 @@ def psp_reference_from(authorization) def payment_request(money, options) hash = {} - hash[:merchantAccount] = @options[:merchant] - hash[:reference] = options[:order_id] if options[:order_id] - hash[:shopperEmail] = options[:email] if options[:email] - hash[:shopperIP] = options[:ip] if options[:ip] - hash[:shopperReference] = options[:customer] if options[:customer] - hash[:shopperInteraction] = options[:shopper_interaction] if options[:shopper_interaction] + hash[:merchantAccount] = @options[:merchant] + hash[:reference] = options[:order_id] + hash[:shopperEmail] = options[:email] + hash[:shopperIP] = options[:ip] + hash[:shopperReference] = options[:customer] + hash[:shopperInteraction] = options[:shopper_interaction] + hash[:deviceFingerprint] = options[:device_fingerprint] hash.keep_if { |_, v| v } end def store_request(options) hash = {} hash[:merchantAccount] = @options[:merchant] - hash[:shopperEmail] = options[:email] if options[:email] + hash[:shopperEmail] = options[:email] hash[:shopperReference] = options[:customer] if options[:customer] hash.keep_if { |_, v| v } end def add_3ds(post, options) - post[:additionalData] = { executeThreeD: 'true' } - post[:browserInfo] = { userAgent: options[:user_agent], acceptHeader: options[:accept_header] } + if three_ds_2_options = options[:three_ds_2] + device_channel = three_ds_2_options[:channel] + if device_channel == 'app' + post[:threeDS2RequestData] = { deviceChannel: device_channel } + else + add_browser_info(three_ds_2_options[:browser_info], post) + post[:threeDS2RequestData] = { deviceChannel: device_channel, notificationURL: three_ds_2_options[:notification_url] } + end + + if options.has_key?(:execute_threed) + post[:additionalData] ||= {} + post[:additionalData][:executeThreeD] = options[:execute_threed] + post[:additionalData][:scaExemption] = options[:sca_exemption] if options[:sca_exemption] + end + else + return unless options[:execute_threed] || options[:threed_dynamic] + + post[:browserInfo] = { userAgent: options[:user_agent], acceptHeader: options[:accept_header] } + post[:additionalData] = { executeThreeD: 'true' } if options[:execute_threed] + end + end + + def add_browser_info(browser_info, post) + return unless browser_info + + post[:browserInfo] = { + acceptHeader: browser_info[:accept_header], + colorDepth: browser_info[:depth], + javaEnabled: browser_info[:java], + language: browser_info[:language], + screenHeight: browser_info[:height], + screenWidth: browser_info[:width], + timeZoneOffset: browser_info[:timezone], + userAgent: browser_info[:user_agent] + } end end end diff --git a/lib/active_merchant/billing/gateways/barclays_epdq_extra_plus.rb b/lib/active_merchant/billing/gateways/barclays_epdq_extra_plus.rb index 6f020860b2a..2d6d9e985dc 100644 --- a/lib/active_merchant/billing/gateways/barclays_epdq_extra_plus.rb +++ b/lib/active_merchant/billing/gateways/barclays_epdq_extra_plus.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class BarclaysEpdqExtraPlusGateway < OgoneGateway self.test_url = 'https://mdepayments.epdq.co.uk/ncol/test/' self.live_url = 'https://payments.epdq.co.uk/ncol/prod/' @@ -8,7 +8,7 @@ class BarclaysEpdqExtraPlusGateway < OgoneGateway self.homepage_url = 'http://www.barclaycard.co.uk/business/accepting-payments/epdq-ecomm/' self.supported_countries = ['GB'] - self.supported_cardtypes = [:visa, :master, :american_express, :diners_club, :discover, :jcb, :maestro] + self.supported_cardtypes = %i[visa master american_express diners_club discover jcb maestro] self.default_currency = 'GBP' end end diff --git a/lib/active_merchant/billing/gateways/be2bill.rb b/lib/active_merchant/billing/gateways/be2bill.rb index f309272466f..07e8aa7a432 100644 --- a/lib/active_merchant/billing/gateways/be2bill.rb +++ b/lib/active_merchant/billing/gateways/be2bill.rb @@ -1,7 +1,7 @@ require 'digest/sha2' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class Be2billGateway < Gateway self.test_url = 'https://secure-test.be2bill.com/front/service/rest/process.php' self.live_url = 'https://secure-magenta1.be2bill.com/front/service/rest/process.php' @@ -9,7 +9,7 @@ class Be2billGateway < Gateway self.display_name = 'Be2Bill' self.homepage_url = 'http://www.be2bill.com/' self.supported_countries = ['FR'] - self.supported_cardtypes = [:visa, :master, :american_express] + self.supported_cardtypes = %i[visa master american_express] self.default_currency = 'EUR' self.money_format = :cents @@ -92,8 +92,8 @@ def commit(action, money, parameters) successful?(response), message_from(response), response, - :authorization => response['TRANSACTIONID'], - :test => test? + authorization: response['TRANSACTIONID'], + test: test? ) end @@ -111,8 +111,8 @@ def message_from(response) def post_data(action, parameters = {}) { - :method => action, - :params => parameters.merge(HASH: signature(parameters, action)) + method: action, + params: parameters.merge(HASH: signature(parameters, action)) }.to_query end diff --git a/lib/active_merchant/billing/gateways/beanstream.rb b/lib/active_merchant/billing/gateways/beanstream.rb index e86e21d60a5..d1bc0efe741 100644 --- a/lib/active_merchant/billing/gateways/beanstream.rb +++ b/lib/active_merchant/billing/gateways/beanstream.rb @@ -1,7 +1,7 @@ require 'active_merchant/billing/gateways/beanstream/beanstream_core' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # This class implements the Canadian {Beanstream}[http://www.beanstream.com] payment gateway. # It is also named TD Canada Trust Online Mart payment gateway. # To learn more about the specification of Beanstream gateway, please read the OM_Direct_Interface_API.pdf, @@ -76,6 +76,7 @@ def authorize(money, source, options = {}) add_transaction_type(post, :authorization) add_customer_ip(post, options) add_recurring_payment(post, options) + add_three_ds(post, options) commit(post) end @@ -88,20 +89,24 @@ def purchase(money, source, options = {}) add_transaction_type(post, purchase_action(source)) add_customer_ip(post, options) add_recurring_payment(post, options) + add_three_ds(post, options) commit(post) end def void(authorization, options = {}) reference, amount, type = split_auth(authorization) - - post = {} - add_reference(post, reference) - add_original_amount(post, amount) - add_transaction_type(post, void_action(type)) - commit(post) + if type == TRANSACTIONS[:authorization] + capture(0, authorization, options) + else + post = {} + add_reference(post, reference) + add_original_amount(post, amount) + add_transaction_type(post, void_action(type)) + commit(post) + end end - def verify(source, options={}) + def verify(source, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, source, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -153,6 +158,8 @@ def interac # To match the other stored-value gateways, like TrustCommerce, # store and unstore need to be defined + # + # When passing a single-use token the :name option is required def store(payment_method, options = {}) post = {} add_address(post, options) @@ -167,13 +174,13 @@ def store(payment_method, options = {}) commit(post, true) end - #can't actually delete a secure profile with the supplicated API. This function sets the status of the profile to closed (C). - #Closed profiles will have to removed manually. + # can't actually delete a secure profile with the supplicated API. This function sets the status of the profile to closed (C). + # Closed profiles will have to removed manually. def delete(vault_id) - update(vault_id, false, {:status => 'C'}) + update(vault_id, false, { status: 'C' }) end - alias_method :unstore, :delete + alias unstore delete # Update the values (such as CC expiration) stored at # the gateway. The CC number must be supplied in the @@ -186,8 +193,9 @@ def update(vault_id, payment_method, options = {}) else post[:singleUseToken] = payment_method end - options.merge!({:vault_id => vault_id, :operation => secure_profile_action(:modify)}) - add_secure_profile_variables(post,options) + options[:vault_id] = vault_id + options[:operation] = secure_profile_action(:modify) + add_secure_profile_variables(post, options) commit(post, true) end @@ -199,14 +207,32 @@ def scrub(transcript) transcript. gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). gsub(/(&?password=)[^&\s]*(&?)/, '\1[FILTERED]\2'). + gsub(/(&?passcode=)[^&\s]*(&?)/, '\1[FILTERED]\2'). gsub(/(&?trnCardCvd=)\d*(&?)/, '\1[FILTERED]\2'). gsub(/(&?trnCardNumber=)\d*(&?)/, '\1[FILTERED]\2') end private + def build_response(*args) Response.new(*args) end + + def add_three_ds(post, options) + return unless three_d_secure = options[:three_d_secure] + + post[:SecureXID] = (three_d_secure[:ds_transaction_id] || three_d_secure[:xid]) if three_d_secure.slice(:ds_transaction_id, :xid).values.any? + post[:SecureECI] = formatted_three_ds_eci(three_d_secure[:eci]) if three_d_secure[:eci].present? + post[:SecureCAVV] = three_d_secure[:cavv] if three_d_secure[:cavv].present? + end + + def formatted_three_ds_eci(val) + case val + when '05', '02' then 5 + when '06', '01' then 6 + else val.to_i + end + end end end end diff --git a/lib/active_merchant/billing/gateways/beanstream/beanstream_core.rb b/lib/active_merchant/billing/gateways/beanstream/beanstream_core.rb index f6d8ba851d3..2c8133ccbb5 100644 --- a/lib/active_merchant/billing/gateways/beanstream/beanstream_core.rb +++ b/lib/active_merchant/billing/gateways/beanstream/beanstream_core.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: module BeanstreamCore include Empty @@ -9,20 +9,20 @@ module BeanstreamCore SP_SERVICE_VERSION = '1.1' TRANSACTIONS = { - :authorization => 'PA', - :purchase => 'P', - :capture => 'PAC', - :refund => 'R', - :void => 'VP', - :check_purchase => 'D', - :check_refund => 'C', - :void_purchase => 'VP', - :void_refund => 'VR' + authorization: 'PA', + purchase: 'P', + capture: 'PAC', + refund: 'R', + void: 'VP', + check_purchase: 'D', + check_refund: 'C', + void_purchase: 'VP', + void_refund: 'VR' } PROFILE_OPERATIONS = { - :new => 'N', - :modify => 'M' + new: 'N', + modify: 'M' } CVD_CODES = { @@ -41,24 +41,24 @@ module BeanstreamCore } PERIODS = { - :days => 'D', - :weeks => 'W', - :months => 'M', - :years => 'Y' + days: 'D', + weeks: 'W', + months: 'M', + years: 'Y' } PERIODICITIES = { - :daily => [:days, 1], - :weekly => [:weeks, 1], - :biweekly => [:weeks, 2], - :monthly => [:months, 1], - :bimonthly => [:months, 2], - :yearly => [:years, 1] + daily: [:days, 1], + weekly: [:weeks, 1], + biweekly: [:weeks, 2], + monthly: [:months, 1], + bimonthly: [:months, 2], + yearly: [:years, 1] } RECURRING_OPERATION = { - :update => 'M', - :cancel => 'C' + update: 'M', + cancel: 'C' } STATES = { @@ -131,10 +131,10 @@ def self.included(base) base.default_currency = 'CAD' # The countries the gateway supports merchants from as 2 digit ISO country codes - base.supported_countries = ['CA', 'US'] + base.supported_countries = %w[CA US] # The card types supported by the payment gateway - base.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb] + base.supported_cardtypes = %i[visa master american_express discover diners_club jcb] # The homepage URL of the gateway base.homepage_url = 'http://www.beanstream.com/' @@ -155,8 +155,7 @@ def initialize(options = {}) end def capture(money, authorization, options = {}) - reference, _, _ = split_auth(authorization) - + reference, = split_auth(authorization) post = {} add_amount(post, money) add_reference(post, reference) @@ -194,11 +193,11 @@ def add_customer_ip(post, options) end def void_action(original_transaction_type) - (original_transaction_type == TRANSACTIONS[:refund]) ? :void_refund : :void_purchase + original_transaction_type == TRANSACTIONS[:refund] ? :void_refund : :void_purchase end def refund_action(type) - (type == TRANSACTIONS[:check_purchase]) ? :check_refund : :refund + type == TRANSACTIONS[:check_purchase] ? :check_refund : :refund end def secure_profile_action(type) @@ -229,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] @@ -257,9 +256,10 @@ def state_for(address) end def prepare_address_for_non_american_countries(options) - [ options[:billing_address], options[:shipping_address] ].compact.each do |address| + [options[:billing_address], options[:shipping_address]].compact.each do |address| next if empty?(address[:country]) - unless ['US', 'CA'].include?(address[:country]) + + unless %w[US CA].include?(address[:country]) address[:state] = '--' address[:zip] = '000000' unless address[:zip] end @@ -267,7 +267,7 @@ def prepare_address_for_non_american_countries(options) end def add_recurring_payment(post, options) - post[:recurringPayment] = true if options[:recurring].to_s == 'true' + post[:recurringPayment] = 1 if options[:recurring].to_s == 'true' end def add_invoice(post, options) @@ -288,9 +288,9 @@ def add_credit_card(post, credit_card) post[:trnExpYear] = format(credit_card.year, :two_digits) post[:trnCardCvd] = credit_card.verification_value if credit_card.is_a?(NetworkTokenizationCreditCard) - post[:"3DSecureXID"] = credit_card.transaction_id - post[:"3DSecureECI"] = credit_card.eci - post[:"3DSecureCAVV"] = credit_card.payment_cryptogram + post[:'3DSecureXID'] = credit_card.transaction_id + post[:'3DSecureECI'] = credit_card.eci + post[:'3DSecureCAVV'] = credit_card.payment_cryptogram end end end @@ -313,10 +313,12 @@ def add_secure_profile_variables(post, options = {}) post[:serviceVersion] = SP_SERVICE_VERSION post[:responseFormat] = 'QS' post[:cardValidation] = (options[:cardValidation].to_i == 1) || '0' - post[:operationType] = options[:operationType] || options[:operation] || secure_profile_action(:new) post[:customerCode] = options[:billing_id] || options[:vault_id] || false post[:status] = options[:status] + + billing_address = options[:billing_address] || options[:address] + post[:trnCardOwner] = billing_address ? billing_address[:name] : nil end def add_recurring_amount(post, money) @@ -365,6 +367,7 @@ def interval(options) if interval.respond_to? :parts parts = interval.parts raise ArgumentError.new("Cannot recur with mixed interval (#{interval}). Use only one of: days, weeks, months or years") if parts.length > 1 + parts.first elsif interval.kind_of? Hash requires!(interval, :unit) @@ -377,11 +380,9 @@ def interval(options) def parse(body) results = {} - if !body.nil? - body.split(/&/).each do |pair| - key, val = pair.split(/\=/) - results[key.to_sym] = val.nil? ? nil : CGI.unescape(val) - end + body&.split(/&/)&.each do |pair| + key, val = pair.split(/\=/) + results[key.to_sym] = val.nil? ? nil : CGI.unescape(val) end # Clean up the message text if there is any @@ -402,21 +403,24 @@ def recurring_parse(data) end def commit(params, use_profile_api = false) - post(post_data(params,use_profile_api),use_profile_api) + post(post_data(params, use_profile_api), use_profile_api) end def recurring_commit(params) recurring_post(post_data(params, false)) end - def post(data, use_profile_api=nil) + 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, - :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] } + 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] } ) end @@ -442,7 +446,7 @@ def recurring_success?(response) end def add_source(post, source) - if source.is_a?(String) or source.is_a?(Integer) + if source.is_a?(String) || source.is_a?(Integer) post[:customerCode] = source else card_brand(source) == 'check' ? add_check(post, source) : add_credit_card(post, source) @@ -462,13 +466,13 @@ def post_data(params, use_profile_api) params[:username] = @options[:user] if @options[:user] params[:password] = @options[:password] if @options[:password] params[:merchant_id] = @options[:login] + params[:passcode] = @options[:api_key] end params[:vbvEnabled] = '0' params[:scEnabled] = '0' - params.reject{|k, v| v.blank?}.collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&') + params.reject { |_k, v| v.blank? }.collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&') end - end end end diff --git a/lib/active_merchant/billing/gateways/beanstream_interac.rb b/lib/active_merchant/billing/gateways/beanstream_interac.rb index 836f686607d..1a16112cb54 100644 --- a/lib/active_merchant/billing/gateways/beanstream_interac.rb +++ b/lib/active_merchant/billing/gateways/beanstream_interac.rb @@ -1,7 +1,7 @@ require 'active_merchant/billing/gateways/beanstream/beanstream_core' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class BeanstreamInteracResponse < Response def redirect params['pageContents'] @@ -18,7 +18,7 @@ class BeanstreamInteracGateway < Gateway # post back is for until the response of the confirmation is # received, which contains the order number. def self.confirm(transaction) - gateway = new(:login => '') + gateway = new(login: '') gateway.confirm(transaction) end @@ -55,4 +55,3 @@ def build_response(*args) end end end - diff --git a/lib/active_merchant/billing/gateways/blue_pay.rb b/lib/active_merchant/billing/gateways/blue_pay.rb index 5e708624538..9e308d4f766 100644 --- a/lib/active_merchant/billing/gateways/blue_pay.rb +++ b/lib/active_merchant/billing/gateways/blue_pay.rb @@ -1,7 +1,7 @@ require 'digest/md5' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class BluePayGateway < Gateway class_attribute :rebilling_url, :ignore_http_status @@ -10,26 +10,26 @@ class BluePayGateway < Gateway self.ignore_http_status = true - CARD_CODE_ERRORS = %w( N S ) - AVS_ERRORS = %w( A E N R W Z ) + CARD_CODE_ERRORS = %w(N S) + AVS_ERRORS = %w(A E N R W Z) AVS_REASON_CODES = %w(27 45) FIELD_MAP = { 'TRANS_ID' => :transaction_id, 'STATUS' => :response_code, 'AVS' => :avs_result_code, - 'CVV2'=> :card_code, + 'CVV2' => :card_code, 'AUTH_CODE' => :authorization, 'MESSAGE' => :message, 'REBID' => :rebid, 'TRANS_TYPE' => :trans_type, 'PAYMENT_ACCOUNT_MASK' => :acct_mask, - 'CARD_TYPE' => :card_type, + 'CARD_TYPE' => :card_type } REBILL_FIELD_MAP = { 'REBILL_ID' => :rebill_id, - 'ACCOUNT_ID'=> :account_id, + 'ACCOUNT_ID' => :account_id, 'USER_ID' => :user_id, 'TEMPLATE_ID' => :template_id, 'STATUS' => :status, @@ -41,10 +41,11 @@ class BluePayGateway < Gateway 'REB_AMOUNT' => :rebill_amount, 'NEXT_AMOUNT' => :next_amount, 'USUAL_DATE' => :undoc_usual_date, # Not found in the bp20rebadmin API doc. + 'CUST_TOKEN' => :cust_token } - self.supported_countries = ['US', 'CA'] - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb] + self.supported_countries = %w[US CA] + self.supported_cardtypes = %i[visa master american_express discover diners_club jcb] self.homepage_url = 'http://www.bluepay.com/' self.display_name = 'BluePay' self.money_format = :dollars @@ -83,8 +84,9 @@ def authorize(money, payment_object, options = {}) add_customer_data(post, options) add_rebill(post, options) if options[:rebill] add_duplicate_override(post, options) - post[:TRANS_TYPE] = 'AUTH' - commit('AUTH_ONLY', money, post) + add_stored_credential(post, options) + post[:TRANS_TYPE] = 'AUTH' + commit('AUTH_ONLY', money, post, options) end # Perform a purchase, which is essentially an authorization and capture in a single operation. @@ -106,8 +108,9 @@ def purchase(money, payment_object, options = {}) add_customer_data(post, options) add_rebill(post, options) if options[:rebill] add_duplicate_override(post, options) - post[:TRANS_TYPE] = 'SALE' - commit('AUTH_CAPTURE', money, post) + add_stored_credential(post, options) + post[:TRANS_TYPE] = 'SALE' + commit('AUTH_CAPTURE', money, post, options) end # Captures the funds from an authorize transaction. @@ -123,7 +126,7 @@ def capture(money, identification, options = {}) add_customer_data(post, options) post[:MASTER_ID] = identification post[:TRANS_TYPE] = 'CAPTURE' - commit('PRIOR_AUTH_CAPTURE', money, post) + commit('PRIOR_AUTH_CAPTURE', money, post, options) end # Void a previous transaction @@ -136,7 +139,7 @@ def void(identification, options = {}) post = {} post[:MASTER_ID] = identification post[:TRANS_TYPE] = 'VOID' - commit('VOID', nil, post) + commit('VOID', nil, post, options) end # Performs a credit. @@ -154,7 +157,7 @@ def void(identification, options = {}) # If the payment_object is either a CreditCard or Check object, then the transaction type will be an unmatched credit placing funds in the specified account. This is referred to a CREDIT transaction in BluePay. # * options -- A hash of parameters. def refund(money, identification, options = {}) - if(identification && !identification.kind_of?(String)) + if identification && !identification.kind_of?(String) ActiveMerchant.deprecated 'refund should only be used to refund a referenced transaction' return credit(money, identification, options) end @@ -163,17 +166,18 @@ def refund(money, identification, options = {}) post[:PAYMENT_ACCOUNT] = '' post[:MASTER_ID] = identification post[:TRANS_TYPE] = 'REFUND' + post[:DOC_TYPE] = options[:doc_type] if options[:doc_type] post[:NAME1] = options[:first_name] || '' post[:NAME2] = options[:last_name] if options[:last_name] post[:ZIP] = options[:zip] if options[:zip] add_invoice(post, options) add_address(post, options) add_customer_data(post, options) - commit('CREDIT', money, post) + commit('CREDIT', money, post, options) end def credit(money, payment_object, options = {}) - if(payment_object && payment_object.kind_of?(String)) + if payment_object&.kind_of?(String) ActiveMerchant.deprecated 'credit should only be used to credit a payment method' return refund(money, payment_object, options) end @@ -182,6 +186,7 @@ def credit(money, payment_object, options = {}) post[:PAYMENT_ACCOUNT] = '' add_payment_method(post, payment_object) post[:TRANS_TYPE] = 'CREDIT' + post[:DOC_TYPE] = options[:doc_type] if options[:doc_type] post[:NAME1] = options[:first_name] || '' post[:NAME2] = options[:last_name] if options[:last_name] @@ -189,7 +194,7 @@ def credit(money, payment_object, options = {}) add_invoice(post, options) add_address(post, options) add_customer_data(post, options) - commit('CREDIT', money, post) + commit('CREDIT', money, post, options) end # Create a new recurring payment. @@ -313,10 +318,11 @@ def scrub(transcript) private - def commit(action, money, fields) - fields[:AMOUNT] = amount(money) unless(fields[:TRANS_TYPE] == 'VOID' || action == 'rebill') + def commit(action, money, fields, options = {}) + fields[:AMOUNT] = amount(money) unless fields[:TRANS_TYPE] == 'VOID' || action == 'rebill' fields[:MODE] = (test? ? 'TEST' : 'LIVE') fields[:ACCOUNT_ID] = @options[:login] + fields[:CUSTOMER_IP] = options[:ip] if options[:ip] if action == 'rebill' url = rebilling_url @@ -328,9 +334,9 @@ def commit(action, money, fields) parse(ssl_post(url, post_data(action, fields))) end - def parse_recurring(response_fields, opts={}) # expected status? + def parse_recurring(response_fields, opts = {}) # expected status? parsed = {} - response_fields.each do |k,v| + response_fields.each do |k, v| mapped_key = REBILL_FIELD_MAP.include?(k) ? REBILL_FIELD_MAP[k] : k parsed[mapped_key] = v end @@ -338,21 +344,23 @@ def parse_recurring(response_fields, opts={}) # expected status? success = parsed[:status] != 'error' message = parsed[:status] - Response.new(success, message, parsed, - :test => test?, - :authorization => parsed[:rebill_id]) + Response.new( + success, + message, + parsed, + test: test?, + 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 - if response_fields.include? 'REBILL_ID' - return parse_recurring(response_fields) - end + return parse_recurring(response_fields) if response_fields.include? 'REBILL_ID' parsed = {} - response_fields.each do |k,v| + response_fields.each do |k, v| mapped_key = FIELD_MAP.include?(k) ? FIELD_MAP[k] : k parsed[mapped_key] = v end @@ -360,30 +368,33 @@ def parse(body) # normalize message message = message_from(parsed) success = parsed[:response_code] == '1' - 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] + 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] ) end def message_from(parsed) message = parsed[:message] - if(parsed[:response_code].to_i == 2) + if parsed[:response_code].to_i == 2 if CARD_CODE_ERRORS.include?(parsed[:card_code]) message = CVVResult.messages[parsed[:card_code]] elsif AVS_ERRORS.include?(parsed[:avs_result_code]) - message = AVSResult.messages[ parsed[:avs_result_code] ] + message = AVSResult.messages[parsed[:avs_result_code]] else message = message.chomp('.') end elsif message == 'Missing ACCOUNT_ID' message = 'The merchant login ID or password is invalid' - elsif message =~ /Approved/ + elsif /Approved/.match?(message) message = 'This transaction has been approved' - elsif message =~ /Expired/ - message = 'The credit card has expired' + elsif /Expired/.match?(message) + message = 'The credit card has expired' end message end @@ -460,6 +471,33 @@ def add_rebill(post, options) post[:REB_CYCLES] = options[:rebill_cycles] end + def add_stored_credential(post, options) + post[:cof] = initiator(options) + post[:cofscheduled] = scheduled(options) + end + + def initiator(options) + return unless initiator = options.dig(:stored_credential, :initiator) + + case initiator + when 'merchant' + 'M' + when 'cardholder' + 'C' + end + end + + def scheduled(options) + return unless reason_type = options.dig(:stored_credential, :reason_type) + + case reason_type + when 'recurring', 'installment' + 'Y' + when 'unscheduled' + 'N' + end + end + def post_data(action, parameters = {}) post = {} post[:version] = '1' @@ -510,9 +548,8 @@ def calc_rebill_tps(post) end def handle_response(response) - if ignore_http_status || (200...300).include?(response.code.to_i) - return response.body - end + return response.body if ignore_http_status || (200...300).cover?(response.code.to_i) + raise ResponseError.new(response) end end diff --git a/lib/active_merchant/billing/gateways/blue_snap.rb b/lib/active_merchant/billing/gateways/blue_snap.rb index e736fe14152..8490f0643d9 100644 --- a/lib/active_merchant/billing/gateways/blue_snap.rb +++ b/lib/active_merchant/billing/gateways/blue_snap.rb @@ -5,10 +5,12 @@ module Billing class BlueSnapGateway < Gateway self.test_url = 'https://sandbox.bluesnap.com/services/2' self.live_url = 'https://ws.bluesnap.com/services/2' - self.supported_countries = %w(US CA GB AT BE BG HR CY CZ DK EE FI FR DE GR HU IE IT LV LT LU MT NL PL PT RO SK SI ES SE) + self.supported_countries = %w(US CA GB AT BE BG HR CY CZ DK EE FI FR DE GR HU IE IT LV LT LU MT NL PL PT RO SK SI ES SE AR BO BR BZ CL CO CR DO EC GF GP GT HN HT MF MQ MX NI PA PE PR PY SV UY VE) self.default_currency = 'USD' - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :jcb, :diners_club, :maestro] + self.supported_cardtypes = %i[visa master american_express discover jcb diners_club maestro naranja cabal] + self.currencies_without_fractions = %w(BYR CLP ILS JPY KRW VND XOF) + self.currencies_with_three_decimal_places = %w(BHD JOD KWD OMR TND) self.homepage_url = 'https://home.bluesnap.com/' self.display_name = 'BlueSnap' @@ -56,67 +58,101 @@ class BlueSnapGateway < Gateway 'line1: N, zip: M, name: N' => 'W', 'line1: N, zip: N, name: U' => 'N', 'line1: N, zip: N, name: M' => 'K', - 'line1: N, zip: N, name: N' => 'N', + 'line1: N, zip: N, name: N' => 'N' } - def initialize(options={}) + BANK_ACCOUNT_TYPE_MAPPING = { + 'personal_checking' => 'CONSUMER_CHECKING', + 'personal_savings' => 'CONSUMER_SAVINGS', + 'business_checking' => 'CORPORATE_CHECKING', + 'business_savings' => 'CORPORATE_SAVINGS' + } + + SHOPPER_INITIATOR = %w(CUSTOMER CARDHOLDER) + + STATE_CODE_COUNTRIES = %w(US CA) + + def initialize(options = {}) requires!(options, :api_username, :api_password) super end - def purchase(money, payment_method, options={}) - commit(:purchase) do |doc| - add_auth_purchase(doc, money, payment_method, options) + def purchase(money, payment_method, options = {}) + payment_method_details = PaymentMethodDetails.new(payment_method) + + commit(:purchase, options, :post, payment_method_details) do |doc| + if payment_method_details.alt_transaction? + add_alt_transaction_purchase(doc, money, payment_method_details, options) + else + add_auth_purchase(doc, money, payment_method, options) + end end end - def authorize(money, payment_method, options={}) - commit(:authorize) do |doc| + def authorize(money, payment_method, options = {}) + commit(:authorize, options) do |doc| add_auth_purchase(doc, money, payment_method, options) end end - def capture(money, authorization, options={}) - commit(:capture, :put) do |doc| + def capture(money, authorization, options = {}) + commit(:capture, options, :put) do |doc| add_authorization(doc, authorization) add_order(doc, options) + add_amount(doc, money, options) if options[:include_capture_amount] == true end end - def refund(money, authorization, options={}) - commit(:refund, :put) do |doc| - add_authorization(doc, authorization) - add_amount(doc, money, options) - add_order(doc, options) + def refund(money, authorization, options = {}) + options[:endpoint] = options[:merchant_transaction_id] ? "/refund/merchant/#{options[:merchant_transaction_id]}" : "/refund/#{authorization}" + commit(:refund, options, :post) do |doc| + add_amount(doc, money, options) if money + %i[reason cancel_subscription tax_amount].each { |field| send_when_present(doc, field, options) } + add_metadata(doc, options) end end - def void(authorization, options={}) - commit(:void, :put) do |doc| + def void(authorization, options = {}) + commit(:void, options, :put) do |doc| add_authorization(doc, authorization) add_order(doc, options) end end - def verify(payment_method, options={}) + def verify(payment_method, options = {}) authorize(0, payment_method, options) end - def store(credit_card, options = {}) - commit(:store) do |doc| - add_personal_info(doc, credit_card, options) + def store(payment_method, options = {}) + payment_method_details = PaymentMethodDetails.new(payment_method) + + commit(:store, options, :post, payment_method_details) do |doc| + add_personal_info(doc, payment_method, options) + add_echeck_company(doc, payment_method) if payment_method_details.check? doc.send('payment-sources') do - doc.send('credit-card-info') do - add_credit_card(doc, credit_card) - end + payment_method_details.check? ? store_echeck(doc, payment_method) : store_credit_card(doc, payment_method) end add_order(doc, options) end end + def store_credit_card(doc, payment_method) + doc.send('credit-card-info') do + add_credit_card(doc, payment_method) + end + end + + def store_echeck(doc, payment_method) + doc.send('ecp-info') do + doc.send('ecp') do + add_echeck(doc, payment_method) + end + end + end + def verify_credentials begin - ssl_get("#{url}/nonexistent", headers) + ssl_get(url.to_s, headers(options)) rescue ResponseError => e return false if e.response.code.to_i == 401 end @@ -132,7 +168,9 @@ 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((<(?:public-)?account-number>).+()), '\1[FILTERED]\2'). + gsub(%r((<(?:public-)?routing-number>).+()), '\1[FILTERED]\2') end private @@ -140,10 +178,10 @@ def scrub(transcript) def add_auth_purchase(doc, money, payment_method, options) doc.send('recurring-transaction', options[:recurring] ? 'RECURRING' : 'ECOMMERCE') add_order(doc, options) + doc.send('store-card', options[:store_card] || false) add_amount(doc, money, options) - doc.send('transaction-fraud-info') do - doc.send('shopper-ip-address', options[:ip]) if options[:ip] - end + add_fraud_info(doc, payment_method, options) + add_stored_credentials(doc, options) if payment_method.is_a?(String) doc.send('vaulted-shopper-id', payment_method) @@ -155,15 +193,31 @@ def add_auth_purchase(doc, money, payment_method, options) end end + def add_stored_credentials(doc, options) + return unless stored_credential = options[:stored_credential] + + initiator = stored_credential[:initiator]&.upcase + initiator = 'SHOPPER' if SHOPPER_INITIATOR.include?(initiator) + doc.send('transaction-initiator', initiator) if stored_credential[:initiator] + if stored_credential[:network_transaction_id] + doc.send('network-transaction-info') do + doc.send('original-network-transaction-id', stored_credential[:network_transaction_id]) + end + end + end + def add_amount(doc, money, options) - doc.amount(amount(money)) - doc.currency(options[:currency] || currency(money)) + currency = options[:currency] || currency(money) + doc.amount(localized_amount(money, currency)) + doc.currency(currency) end - def add_personal_info(doc, credit_card, options) - doc.send('first-name', credit_card.first_name) - doc.send('last-name', credit_card.last_name) + def add_personal_info(doc, payment_method, options) + doc.send('first-name', payment_method.first_name) + doc.send('last-name', payment_method.last_name) + doc.send('personal-identification-number', options[:personal_identification_number]) if options[:personal_identification_number] doc.email(options[:email]) if options[:email] + doc.phone(options[:phone_number]) if options[:phone_number] add_address(doc, options) end @@ -176,12 +230,29 @@ def add_credit_card(doc, card) end end - def add_description(doc, description) + def add_metadata(doc, options) + transaction_meta_data = options[:transaction_meta_data] || [] + return if transaction_meta_data.empty? && !options[:description] + doc.send('transaction-meta-data') do - doc.send('meta-data') do - doc.send('meta-key', 'description') - doc.send('meta-value', truncate(description, 500)) - doc.send('meta-description', 'Description') + # ensure backwards compatibility for calls expecting :description + # to become meta-data fields. + if options[:description] + doc.send('meta-data') do + doc.send('meta-key', 'description') + doc.send('meta-value', truncate(options[:description], 500)) + doc.send('meta-description', 'Description') + end + end + + # https://developers.bluesnap.com/v8976-XML/docs/meta-data + transaction_meta_data.each do |entry| + doc.send('meta-data') do + doc.send('meta-key', truncate(entry[:meta_key], 40)) + doc.send('meta-value', truncate(entry[:meta_value], 500)) + doc.send('meta-description', truncate(entry[:meta_description], 40)) + doc.send('is-visible', truncate(entry[:meta_is_visible], 5)) + end end end end @@ -189,7 +260,10 @@ def add_description(doc, description) 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] - add_description(doc, options[:description]) if options[:description] + 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) end def add_address(doc, options) @@ -197,28 +271,161 @@ def add_address(doc, options) return unless address doc.country(address[:country]) if address[:country] - doc.state(address[:state]) if address[:state] - doc.address(address[:address]) if address[:address] + doc.state(address[:state]) if address[:state] && STATE_CODE_COUNTRIES.include?(address[:country]) + doc.address(address[:address1]) if address[:address1] + doc.address2(address[:address2]) if address[:address2] doc.city(address[:city]) if address[:city] doc.zip(address[:zip]) if address[:zip] end + def add_3ds(doc, three_d_secure_options) + eci = three_d_secure_options[:eci] + cavv = three_d_secure_options[:cavv] + xid = three_d_secure_options[:xid] + ds_transaction_id = three_d_secure_options[:ds_transaction_id] + version = three_d_secure_options[:version] + + doc.send('three-d-secure') do + doc.eci(eci) if eci + doc.cavv(cavv) if cavv + doc.xid(xid) if xid + doc.send('three-d-secure-version', version) if version + doc.send('ds-transaction-id', ds_transaction_id) if ds_transaction_id + end + end + + def add_level_3_data(doc, options) + return unless options[:customer_reference_number] + + doc.send('level-3-data') do + send_when_present(doc, :customer_reference_number, options) + send_when_present(doc, :sales_tax_amount, options) + send_when_present(doc, :freight_amount, options) + send_when_present(doc, :duty_amount, options) + send_when_present(doc, :destination_zip_code, options) + send_when_present(doc, :destination_country_code, options) + send_when_present(doc, :ship_from_zip_code, options) + send_when_present(doc, :discount_amount, options) + send_when_present(doc, :tax_amount, options) + send_when_present(doc, :tax_rate, options) + add_level_3_data_items(doc, options[:level_3_data_items]) if options[:level_3_data_items] + end + end + + def send_when_present(doc, options_key, options, xml_element_name = nil) + return unless options[options_key] + + xml_element_name ||= options_key.to_s + + doc.send(xml_element_name.dasherize, options[options_key]) + end + + def add_level_3_data_items(doc, items) + items.each do |item| + doc.send('level-3-data-item') do + item.each do |key, value| + key = key.to_s.dasherize + doc.send(key, value) + end + end + end + end + def add_authorization(doc, authorization) doc.send('transaction-id', authorization) end + def add_fraud_info(doc, payment_method, options) + doc.send('transaction-fraud-info') do + doc.send('shopper-ip-address', options[:ip]) if options[:ip] + if fraud_info = options[:transaction_fraud_info] + doc.send('fraud-session-id', fraud_info[:fraud_session_id]) if fraud_info[:fraud_session_id] + end + unless payment_method.is_a? String + doc.send('shipping-contact-info') do + add_shipping_contact_info(doc, payment_method, options) + end + end + end + end + + def add_shipping_contact_info(doc, payment_method, options) + if address = options[:shipping_address] + # https://developers.bluesnap.com/v8976-XML/docs/shipping-contact-info + doc.send('first-name', payment_method.first_name) + doc.send('last-name', payment_method.last_name) + + doc.country(address[:country]) if address[:country] + doc.state(address[:state]) if address[:state] && STATE_CODE_COUNTRIES.include?(address[:country]) + doc.address1(address[:address1]) if address[:address1] + doc.address2(address[:address2]) if address[:address2] + doc.city(address[:city]) if address[:city] + doc.zip(address[:zip]) if address[:zip] + end + end + + 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 + doc.send('vaulted-shopper-id', vaulted_shopper_id) if vaulted_shopper_id + + add_echeck_transaction(doc, payment_method_details.payment_method, options, vaulted_shopper_id.present?) if payment_method_details.check? + + add_fraud_info(doc, payment_method_details.payment_method, options) + add_stored_credentials(doc, options) + add_metadata(doc, options) + end + + def add_echeck_transaction(doc, check, options, vaulted_shopper) + unless vaulted_shopper + doc.send('payer-info') do + add_personal_info(doc, check, options) + add_echeck_company(doc, check) + end + end + + doc.send('ecp-transaction') do + add_echeck(doc, check) unless vaulted_shopper + end + + doc.send('authorized-by-shopper', options[:authorized_by_shopper]) + end + + def add_echeck_company(doc, check) + doc.send('company-name', truncate(check.name, 50)) if check.account_holder_type = 'business' + end + + def add_echeck(doc, check) + doc.send('account-number', check.account_number) + doc.send('routing-number', check.routing_number) + doc.send('account-type', BANK_ACCOUNT_TYPE_MAPPING["#{check.account_holder_type}_#{check.account_type}"]) + end + def parse(response) return bad_authentication_response if response.code.to_i == 401 + return generic_error_response(response.body) if [403, 405, 429].include?(response.code.to_i) parsed = {} doc = Nokogiri::XML(response.body) doc.root.xpath('*').each do |node| - if (node.elements.empty?) - parsed[node.name.downcase] = node.text + name = node.name.downcase + if node.elements.empty? + parsed[name] = node.text + elsif name == 'transaction-meta-data' + metadata = [] + node.elements.each { |m| + metadata.push parse_metadata_entry(m) + } + + parsed['transaction-meta-data'] = metadata else - node.elements.each do |childnode| + node.elements.each { |childnode| parse_element(parsed, childnode) - end + } end end @@ -226,43 +433,54 @@ def parse(response) parsed end + def parse_metadata_entry(node) + entry = {} + + node.elements.each { |e| + entry = entry.merge({ + e.name => e.text + }) + } + + entry + 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 - def api_request(action, request, verb) - begin - ssl_request(verb, url(action), request, headers) - rescue ResponseError => e - e.response - end + def api_request(action, request, verb, payment_method_details, options) + ssl_request(verb, url(action, options, payment_method_details), request, headers(options)) + rescue ResponseError => e + e.response end - def commit(action, verb = :post) - request = build_xml_request(action) { |doc| yield(doc) } - response = api_request(action, request, verb) + 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) succeeded = success_from(action, response) Response.new( succeeded, - message_from(succeeded, parsed), + message_from(succeeded, response), parsed, - authorization: authorization_from(action, parsed), + authorization: authorization_from(action, parsed, payment_method_details), avs_result: avs_result(parsed), cvv_result: cvv_result(parsed), error_code: error_code_from(parsed), - test: test?, + test: test? ) end - def url(action = nil) + def url(action = nil, options = {}, payment_method_details = PaymentMethodDetails.new()) base = test? ? test_url : live_url - resource = (action == :store) ? 'vaulted-shoppers' : 'transactions' + resource = action == :store ? 'vaulted-shoppers' : payment_method_details.resource_url + resource += options[:endpoint] if action == :refund "#{base}/#{resource}" end @@ -279,21 +497,51 @@ def avs_lookup_key(p) end def success_from(action, response) - (200...300).include?(response.code.to_i) + (200...300).cover?(response.code.to_i) end - def message_from(succeeded, parsed_response) + def message_from(succeeded, response) return 'Success' if succeeded - parsed_response['description'] + + parsed = parse(response) + if parsed.dig('error-name') == 'FRAUD_DETECTED' + fraud_codes_from(response) + else + parsed['description'] + end end - def authorization_from(action, parsed_response) - (action == :store) ? vaulted_shopper_id(parsed_response) : parsed_response['transaction-id'] + def fraud_codes_from(response) + event_summary = {} + doc = Nokogiri::XML(response.body) + fraud_events = doc.xpath('//xmlns:fraud-events', 'xmlns' => 'http://ws.plimus.com') + fraud_events.children.each do |child| + if child.children.children.any? + event_summary[child.name] = event_summary[child.name] || [] + event = {} + child.children.each do |chi| + event[chi.name] = chi.text + end + event_summary[child.name] << event + else + event_summary[child.name] = child.text + end + end + event_summary.to_json + end + + def authorization_from(action, parsed_response, payment_method_details) + return vaulted_shopper_id(parsed_response, payment_method_details) if action == :store + + parsed_response['refund-transaction-id'] || parsed_response['transaction-id'] end - def vaulted_shopper_id(parsed_response) + def vaulted_shopper_id(parsed_response, payment_method_details) return nil unless parsed_response['content-location-header'] - parsed_response['content-location-header'].split('/').last + + vaulted_shopper_id = parsed_response['content-location-header'].split('/').last + vaulted_shopper_id += "|#{payment_method_details.payment_method_type}" if payment_method_details.alt_transaction? + vaulted_shopper_id end def error_code_from(parsed_response) @@ -306,21 +554,29 @@ def root_attributes } end - def root_element(action) - (action == :store) ? 'vaulted-shopper' : 'card-transaction' + def root_element(action, payment_method_details) + return 'refund' if action == :refund + return 'vaulted-shopper' if action == :store + + payment_method_details.root_element end - def headers - { + def headers(options) + idempotency_key = options[:idempotency_key] if options[:idempotency_key] + + headers = { 'Content-Type' => 'application/xml', - 'Authorization' => ('Basic ' + Base64.strict_encode64("#{@options[:api_username]}:#{@options[:api_password]}").strip), + 'Authorization' => ('Basic ' + Base64.strict_encode64("#{@options[:api_username]}:#{@options[:api_password]}").strip) } + + headers['Idempotency-Key'] = idempotency_key if idempotency_key + headers end - def build_xml_request(action) + def build_xml_request(action, payment_method_details) builder = Nokogiri::XML::Builder.new - builder.__send__(root_element(action), root_attributes) do |doc| - doc.send('card-transaction-type', TRANSACTIONS[action]) if TRANSACTIONS[action] + builder.__send__(root_element(action, payment_method_details), root_attributes) do |doc| + doc.send('card-transaction-type', TRANSACTIONS[action]) if TRANSACTIONS[action] && !payment_method_details.alt_transaction? && action != :refund yield(doc) end builder.doc.root.to_xml @@ -338,6 +594,51 @@ def handle_response(response) def bad_authentication_response { 'description' => 'Unable to authenticate. Please check your credentials.' } end + + def generic_error_response(body) + { 'description' => body } + end + end + + class PaymentMethodDetails + attr_reader :payment_method, :vaulted_shopper_id, :payment_method_type + + def initialize(payment_method = nil) + @payment_method = payment_method + @payment_method_type = nil + parse(payment_method) + end + + def check? + @payment_method.is_a?(Check) || @payment_method_type == 'check' + end + + def alt_transaction? + check? + end + + def root_element + alt_transaction? ? 'alt-transaction' : 'card-transaction' + end + + def resource_url + alt_transaction? ? 'alt-transactions' : 'transactions' + end + + private + + def parse(payment_method) + return unless payment_method + + if payment_method.is_a?(String) + @vaulted_shopper_id, payment_method_type = payment_method.split('|') + @payment_method_type = payment_method_type if payment_method_type.present? + elsif payment_method.is_a?(Check) + @payment_method_type = payment_method.type + else + @payment_method_type = 'credit_card' + end + end end end end diff --git a/lib/active_merchant/billing/gateways/bogus.rb b/lib/active_merchant/billing/gateways/bogus.rb index b5d51182368..0960d69ed55 100644 --- a/lib/active_merchant/billing/gateways/bogus.rb +++ b/lib/active_merchant/billing/gateways/bogus.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # Bogus Gateway class BogusGateway < Gateway AUTHORIZATION = '53433' @@ -47,9 +47,9 @@ def credit(money, paysource, options = {}) money = amount(money) case normalize(paysource) when /1$/ - Response.new(true, SUCCESS_MESSAGE, {:paid_amount => money}, :test => true ) + Response.new(true, SUCCESS_MESSAGE, { paid_amount: money }, test: true) when /2$/ - Response.new(false, FAILURE_MESSAGE, {:paid_amount => money, :error => FAILURE_MESSAGE }, :test => true, :error_code => STANDARD_ERROR_CODE[:processing_error]) + Response.new(false, FAILURE_MESSAGE, { paid_amount: money, error: FAILURE_MESSAGE }, test: true, error_code: STANDARD_ERROR_CODE[:processing_error]) else raise Error, error_message(paysource) end @@ -61,9 +61,9 @@ def refund(money, reference, options = {}) when /1$/ raise Error, REFUND_ERROR_MESSAGE when /2$/ - Response.new(false, FAILURE_MESSAGE, {:paid_amount => money, :error => FAILURE_MESSAGE }, :test => true, :error_code => STANDARD_ERROR_CODE[:processing_error]) + Response.new(false, FAILURE_MESSAGE, { paid_amount: money, error: FAILURE_MESSAGE }, test: true, error_code: STANDARD_ERROR_CODE[:processing_error]) else - Response.new(true, SUCCESS_MESSAGE, {:paid_amount => money}, :test => true) + Response.new(true, SUCCESS_MESSAGE, { paid_amount: money }, test: true) end end @@ -73,9 +73,9 @@ def capture(money, reference, options = {}) when /1$/ raise Error, CAPTURE_ERROR_MESSAGE when /2$/ - Response.new(false, FAILURE_MESSAGE, {:paid_amount => money, :error => FAILURE_MESSAGE }, :test => true, :error_code => STANDARD_ERROR_CODE[:processing_error]) + Response.new(false, FAILURE_MESSAGE, { paid_amount: money, error: FAILURE_MESSAGE }, test: true, error_code: STANDARD_ERROR_CODE[:processing_error]) else - Response.new(true, SUCCESS_MESSAGE, {:paid_amount => money}, :test => true) + Response.new(true, SUCCESS_MESSAGE, { paid_amount: money }, test: true) end end @@ -84,18 +84,22 @@ def void(reference, options = {}) when /1$/ raise Error, VOID_ERROR_MESSAGE when /2$/ - Response.new(false, FAILURE_MESSAGE, {:authorization => reference, :error => FAILURE_MESSAGE }, :test => true, :error_code => STANDARD_ERROR_CODE[:processing_error]) + Response.new(false, FAILURE_MESSAGE, { authorization: reference, error: FAILURE_MESSAGE }, test: true, error_code: STANDARD_ERROR_CODE[:processing_error]) else - Response.new(true, SUCCESS_MESSAGE, {:authorization => reference}, :test => true) + Response.new(true, SUCCESS_MESSAGE, { authorization: reference }, test: true) end end + def verify(credit_card, options = {}) + authorize(0, credit_card, options) + end + def store(paysource, options = {}) case normalize(paysource) when /1$/ - Response.new(true, SUCCESS_MESSAGE, {:billingid => '1'}, :test => true, :authorization => AUTHORIZATION) + Response.new(true, SUCCESS_MESSAGE, { billingid: '1' }, test: true, authorization: AUTHORIZATION) when /2$/ - Response.new(false, FAILURE_MESSAGE, {:billingid => nil, :error => FAILURE_MESSAGE }, :test => true, :error_code => STANDARD_ERROR_CODE[:processing_error]) + Response.new(false, FAILURE_MESSAGE, { billingid: nil, error: FAILURE_MESSAGE }, test: true, error_code: STANDARD_ERROR_CODE[:processing_error]) else raise Error, error_message(paysource) end @@ -104,9 +108,9 @@ def store(paysource, options = {}) def unstore(reference, options = {}) case reference when /1$/ - Response.new(true, SUCCESS_MESSAGE, {}, :test => true) + Response.new(true, SUCCESS_MESSAGE, {}, test: true) when /2$/ - Response.new(false, FAILURE_MESSAGE, {:error => FAILURE_MESSAGE },:test => true, :error_code => STANDARD_ERROR_CODE[:processing_error]) + Response.new(false, FAILURE_MESSAGE, { error: FAILURE_MESSAGE }, test: true, error_code: STANDARD_ERROR_CODE[:processing_error]) else raise Error, UNSTORE_ERROR_MESSAGE end @@ -118,9 +122,9 @@ def authorize_emv(money, paysource, options = {}) money = amount(money) case money when /00$/ - Response.new(true, SUCCESS_MESSAGE, {:authorized_amount => money}, :test => true, :authorization => AUTHORIZATION, :emv_authorization => AUTHORIZATION_EMV_SUCCESS) + Response.new(true, SUCCESS_MESSAGE, { authorized_amount: money }, test: true, authorization: AUTHORIZATION, emv_authorization: AUTHORIZATION_EMV_SUCCESS) when /05$/ - Response.new(false, FAILURE_MESSAGE, {:authorized_amount => money, :error => FAILURE_MESSAGE }, :test => true, :error_code => STANDARD_ERROR_CODE[:processing_error], :emv_authorization => AUTHORIZATION_EMV_DECLINE) + Response.new(false, FAILURE_MESSAGE, { authorized_amount: money, error: FAILURE_MESSAGE }, test: true, error_code: STANDARD_ERROR_CODE[:processing_error], emv_authorization: AUTHORIZATION_EMV_DECLINE) else raise Error, error_message(paysource) end @@ -130,9 +134,9 @@ def authorize_swipe(money, paysource, options = {}) money = amount(money) case normalize(paysource) when /1$/, AUTHORIZATION - Response.new(true, SUCCESS_MESSAGE, {:authorized_amount => money}, :test => true, :authorization => AUTHORIZATION ) + Response.new(true, SUCCESS_MESSAGE, { authorized_amount: money }, test: true, authorization: AUTHORIZATION) when /2$/ - Response.new(false, FAILURE_MESSAGE, {:authorized_amount => money, :error => FAILURE_MESSAGE }, :test => true, :error_code => STANDARD_ERROR_CODE[:processing_error]) + Response.new(false, FAILURE_MESSAGE, { authorized_amount: money, error: FAILURE_MESSAGE }, test: true, error_code: STANDARD_ERROR_CODE[:processing_error]) else raise Error, error_message(paysource) end @@ -142,9 +146,9 @@ def purchase_emv(money, paysource, options = {}) money = amount(money) case money when /00$/ - Response.new(true, SUCCESS_MESSAGE, {:paid_amount => money}, :test => true, :authorization => AUTHORIZATION, :emv_authorization => AUTHORIZATION_EMV_SUCCESS) + Response.new(true, SUCCESS_MESSAGE, { paid_amount: money }, test: true, authorization: AUTHORIZATION, emv_authorization: AUTHORIZATION_EMV_SUCCESS) when /05$/ - Response.new(false, FAILURE_MESSAGE, {:paid_amount => money, :error => FAILURE_MESSAGE }, :test => true, :error_code => STANDARD_ERROR_CODE[:processing_error], :emv_authorization => AUTHORIZATION_EMV_DECLINE) + Response.new(false, FAILURE_MESSAGE, { paid_amount: money, error: FAILURE_MESSAGE }, test: true, error_code: STANDARD_ERROR_CODE[:processing_error], emv_authorization: AUTHORIZATION_EMV_DECLINE) else raise Error, error_message(paysource) end @@ -154,9 +158,9 @@ def purchase_swipe(money, paysource, options = {}) money = amount(money) case normalize(paysource) when /1$/, AUTHORIZATION - Response.new(true, SUCCESS_MESSAGE, {:paid_amount => money}, :test => true, :authorization => AUTHORIZATION) + Response.new(true, SUCCESS_MESSAGE, { paid_amount: money }, test: true, authorization: AUTHORIZATION) when /2$/ - Response.new(false, FAILURE_MESSAGE, {:paid_amount => money, :error => FAILURE_MESSAGE }, :test => true, :error_code => STANDARD_ERROR_CODE[:processing_error]) + Response.new(false, FAILURE_MESSAGE, { paid_amount: money, error: FAILURE_MESSAGE }, test: true, error_code: STANDARD_ERROR_CODE[:processing_error]) else raise Error, error_message(paysource) end diff --git a/lib/active_merchant/billing/gateways/borgun.rb b/lib/active_merchant/billing/gateways/borgun.rb index c6bf5612262..f8dc552e453 100644 --- a/lib/active_merchant/billing/gateways/borgun.rb +++ b/lib/active_merchant/billing/gateways/borgun.rb @@ -1,7 +1,7 @@ require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class BorgunGateway < Gateway self.display_name = 'Borgun' self.homepage_url = 'http://www.borgun.com' @@ -9,35 +9,51 @@ class BorgunGateway < Gateway self.test_url = 'https://gatewaytest.borgun.is/ws/Heimir.pub.ws:Authorization' self.live_url = 'https://gateway01.borgun.is/ws/Heimir.pub.ws:Authorization' - self.supported_countries = ['IS', 'GB', 'HU', 'CZ', 'DE', 'DK', 'SE' ] + self.supported_countries = %w[IS GB HU CZ DE DK SE] self.default_currency = 'ISK' self.money_format = :cents - self.supported_cardtypes = [:visa, :master, :american_express, :diners_club, :discover, :jcb] + self.supported_cardtypes = %i[visa master american_express diners_club discover jcb] self.homepage_url = 'https://www.borgun.is/' - def initialize(options={}) + def initialize(options = {}) requires!(options, :processor, :merchant_id, :username, :password) super end - def purchase(money, payment, options={}) + def purchase(money, payment, options = {}) post = {} - post[:TransType] = '1' + action = '' + if options[:apply_3d_secure] == '1' + add_3ds_preauth_fields(post, options) + action = '3ds_preauth' + else + post[:TransType] = '1' + add_3ds_fields(post, options) + action = 'sale' + end add_invoice(post, money, options) add_payment_method(post, payment) - commit('sale', post) + commit(action, post, options) end - def authorize(money, payment, options={}) + def authorize(money, payment, options = {}) post = {} - post[:TransType] = '5' + action = '' + if options[:apply_3d_secure] == '1' + add_3ds_preauth_fields(post, options) + action = '3ds_preauth' + else + post[:TransType] = '5' + add_3ds_fields(post, options) + action = 'authonly' + end add_invoice(post, money, options) add_payment_method(post, payment) - commit('authonly', post) + commit(action, post, options) end - def capture(money, authorization, options={}) + def capture(money, authorization, options = {}) post = {} post[:TransType] = '1' add_invoice(post, money, options) @@ -45,7 +61,7 @@ def capture(money, authorization, options={}) commit('capture', post) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) post = {} post[:TransType] = '3' add_invoice(post, money, options) @@ -53,7 +69,7 @@ def refund(money, authorization, options={}) commit('refund', post) end - def void(authorization, options={}) + def void(authorization, options = {}) post = {} # TransType, TrAmount, and currency must match original values from auth or purchase. _, _, _, _, _, transtype, tramount, currency = split_authorization(authorization) @@ -76,14 +92,32 @@ def scrub(transcript) private - CURRENCY_CODES = Hash.new{|h,k| raise ArgumentError.new("Unsupported currency for HDFC: #{k}")} + CURRENCY_CODES = Hash.new { |_h, k| raise ArgumentError.new("Unsupported currency for HDFC: #{k}") } CURRENCY_CODES['ISK'] = '352' CURRENCY_CODES['EUR'] = '978' CURRENCY_CODES['USD'] = '840' + CURRENCY_CODES['GBP'] = '826' + + def add_3ds_fields(post, options) + post[:ThreeDSMessageId] = options[:three_ds_message_id] if options[:three_ds_message_id] + post[:ThreeDS_PARes] = options[:three_ds_pares] if options[:three_ds_pares] + post[:ThreeDS_CRes] = options[:three_ds_cres] if options[:three_ds_cres] + end + + def add_3ds_preauth_fields(post, options) + post[:SaleDescription] = options[:sale_description] || '' + post[:MerchantReturnURL] = options[:redirect_url] if options[:redirect_url] + end def add_invoice(post, money, options) post[:TrAmount] = amount(money) post[:TrCurrency] = CURRENCY_CODES[options[:currency] || currency(money)] + # The ISK currency must have a currency exponent of 2 on the 3DS request but not on the auth request + if post[:TrCurrency] == '352' && options[:apply_3d_secure] != '1' + post[:TrCurrencyExponent] = 0 + else + post[:TrCurrencyExponent] = 2 + end post[:TerminalID] = options[:terminal_id] || '1' end @@ -96,23 +130,23 @@ def add_payment_method(post, payment_method) end def add_reference(post, authorization) - dateandtime, _batch, transaction, rrn, authcode, _, _, _ = split_authorization(authorization) + dateandtime, _batch, transaction, rrn, authcode, = split_authorization(authorization) post[:DateAndTime] = dateandtime post[:Transaction] = transaction post[:RRN] = rrn post[:AuthCode] = authcode end - def parse(xml) + def parse(xml, options = nil) response = {} doc = Nokogiri::XML(CGI.unescapeHTML(xml)) - body = doc.xpath('//getAuthorizationReply') + body = options[:apply_3d_secure] == '1' ? doc.xpath('//get3DSAuthenticationReply') : doc.xpath('//getAuthorizationReply') body = doc.xpath('//cancelAuthorizationReply') if body.length == 0 body.children.each do |node| if node.text? next - elsif (node.elements.size == 0) + elsif node.elements.size == 0 response[node.name.downcase.to_sym] = node.text else node.elements.each do |childnode| @@ -121,44 +155,42 @@ def parse(xml) end end end - response end - def commit(action, post) + def commit(action, post, options = {}) post[:Version] = '1000' post[:Processor] = @options[:processor] post[:MerchantID] = @options[:merchant_id] - url = (test? ? test_url : live_url) - request = build_request(action, post) + request = build_request(action, post, options) raw = ssl_post(url(action), request, headers) - pairs = parse(raw) + pairs = parse(raw, options) success = success_from(pairs) Response.new( success, message_from(success, pairs), pairs, - authorization: authorization_from(pairs), + authorization: authorization_from(pairs, options), test: test? ) end def success_from(response) - (response[:actioncode] == '000') + (response[:actioncode] == '000') || (response[:status_resultcode] == '0') end def message_from(succeeded, response) if succeeded 'Succeeded' else - response[:message] || "Error with ActionCode=#{response[:actioncode]}" + response[:message] || response[:status_errormessage] || "Error with ActionCode=#{response[:actioncode]}" end end - def authorization_from(response) - [ + def authorization_from(response, options) + authorization = [ response[:dateandtime], response[:batch], response[:transaction], @@ -168,6 +200,8 @@ def authorization_from(response) response[:tramount], response[:trcurrency] ].join('|') + + authorization == '|||||||' ? nil : authorization end def split_authorization(authorization) @@ -177,36 +211,55 @@ def split_authorization(authorization) def headers { - 'Authorization' => 'Basic ' + Base64.strict_encode64(@options[:username].to_s + ':' + @options[:password].to_s), + 'Authorization' => 'Basic ' + Base64.strict_encode64(@options[:username].to_s + ':' + @options[:password].to_s) } end - def build_request(action, post) - mode = (action == 'void') ? 'cancel' : 'get' - xml = Builder::XmlMarkup.new :indent => 18 - xml.instruct!(:xml, :version => '1.0', :encoding => 'utf-8') - xml.tag!("#{mode}Authorization") do + def build_request(action, post, options = {}) + mode = action == 'void' ? 'cancel' : 'get' + transaction_type = action == '3ds_preauth' ? '3DSAuthentication' : 'Authorization' + xml = Builder::XmlMarkup.new indent: 18 + xml.instruct!(:xml, version: '1.0', encoding: 'utf-8') + xml.tag!("#{mode}#{transaction_type}") do post.each do |field, value| xml.tag!(field, value) end + build_airline_xml(xml, options[:passenger_itinerary_data]) if options[:passenger_itinerary_data] end inner = CGI.escapeHTML(xml.target!) - envelope(mode).sub(/{{ :body }}/,inner) + envelope(mode, action).sub(/{{ :body }}/, inner) + end + + def build_airline_xml(xml, airline_data) + xml.tag!('PassengerItineraryData') do + xml.tag!('A1') do + airline_data.each do |field, value| + xml.tag!(field, value) + end + end + end end - def envelope(mode) - <<-EOS + def envelope(mode, action) + if action == '3ds_preauth' + transaction_action = "#{mode}3DSAuthentication" + request_action = "#{mode}Auth3DSReqXml" + else + transaction_action = "#{mode}AuthorizationInput" + request_action = "#{mode}AuthReqXml" + end + <<-XML - - <#{mode}AuthReqXml> + + <#{request_action}> {{ :body }} - - + + - EOS + XML end def url(action) @@ -214,7 +267,7 @@ def url(action) end def six_random_digits - (0...6).map { (48 + rand(10)).chr }.join + (0...6).map { rand(48..57).chr }.join end end end diff --git a/lib/active_merchant/billing/gateways/bpoint.rb b/lib/active_merchant/billing/gateways/bpoint.rb index dc48ffa47a1..545e996063e 100644 --- a/lib/active_merchant/billing/gateways/bpoint.rb +++ b/lib/active_merchant/billing/gateways/bpoint.rb @@ -1,23 +1,23 @@ require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class BpointGateway < Gateway self.live_url = 'https://www.bpoint.com.au/evolve/service_1_4_4.asmx' self.supported_countries = ['AU'] self.default_currency = 'AUD' - self.supported_cardtypes = [:visa, :master, :american_express, :diners_club] + self.supported_cardtypes = %i[visa master american_express diners_club] self.homepage_url = 'https://www.bpoint.com.au/bpoint' self.display_name = 'BPoint' - def initialize(options={}) + def initialize(options = {}) requires!(options, :username, :password, :merchant_number) super end - def store(credit_card, options={}) + def store(credit_card, options = {}) options[:crn1] ||= 'DEFAULT' request_body = soap_request do |xml| add_token(xml, credit_card, options) @@ -25,7 +25,7 @@ def store(credit_card, options={}) commit(request_body) end - def purchase(amount, credit_card, options={}) + def purchase(amount, credit_card, options = {}) request_body = soap_request do |xml| process_payment(xml) do |payment_xml| add_purchase(payment_xml, amount, credit_card, options) @@ -34,7 +34,7 @@ def purchase(amount, credit_card, options={}) commit(request_body) end - def authorize(amount, credit_card, options={}) + def authorize(amount, credit_card, options = {}) request_body = soap_request do |xml| process_payment(xml) do |payment_xml| add_authorize(payment_xml, amount, credit_card, options) @@ -43,7 +43,7 @@ def authorize(amount, credit_card, options={}) commit(request_body) end - def capture(amount, authorization, options={}) + def capture(amount, authorization, options = {}) request_body = soap_request do |xml| process_payment(xml) do |payment_xml| add_capture(payment_xml, amount, authorization, options) @@ -52,7 +52,7 @@ def capture(amount, authorization, options={}) commit(request_body) end - def refund(amount, authorization, options={}) + def refund(amount, authorization, options = {}) request_body = soap_request do |xml| process_payment(xml) do |payment_xml| add_refund(payment_xml, amount, authorization, options) @@ -61,19 +61,19 @@ def refund(amount, authorization, options={}) commit(request_body) end - def void(amount, authorization, options={}) + def void(authorization, options = {}) request_body = soap_request do |xml| process_payment(xml) do |payment_xml| - add_void(payment_xml, amount, authorization, options) + add_void(payment_xml, authorization, options) end end commit(request_body) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, credit_card, options) } - r.process(:ignore_result) { void(100, r.authorization, options) } + r.process(:ignore_result) { void(r.authorization, options.merge(amount: 100)) } end end @@ -91,7 +91,7 @@ def scrub(transcript) private def soap_request - Nokogiri::XML::Builder.new(:encoding => 'utf-8') do |xml| + Nokogiri::XML::Builder.new(encoding: 'utf-8') do |xml| xml.send('soap12:Envelope', soap_envelope_attributes) { xml.send('soap12:Body') { yield(xml) if block_given? @@ -154,7 +154,9 @@ def add_refund(xml, amount, transaction_number, options) transaction_number_xml(xml, transaction_number) end - def add_void(xml, amount, transaction_number, options) + def add_void(xml, transaction_number, options) + # The amount parameter is required for void requests on BPoint. + amount = options[:amount] payment_xml(xml, 'REVERSAL', amount, options) transaction_number_xml(xml, transaction_number) end @@ -163,10 +165,10 @@ def payment_xml(xml, payment_type, amount, options) xml.send('PaymentType', payment_type) xml.send('TxnType', 'WEB_SHOP') xml.send('BillerCode', options.fetch(:biller_code, '')) - xml.send('MerchantReference', '') - xml.send('CRN1', '') - xml.send('CRN2', '') - xml.send('CRN3', '') + xml.send('MerchantReference', options[:order_id]) if options[:order_id] + xml.send('CRN1', options[:crn1]) if options[:crn1] + xml.send('CRN2', options[:crn2]) if options[:crn2] + xml.send('CRN3', options[:crn3]) if options[:crn3] xml.send('Amount', amount) end @@ -240,7 +242,6 @@ def options end class ProcessPaymentResponse < BPointResponse - private def authorization_key @@ -257,7 +258,6 @@ def message end class AddTokenResponse < BPointResponse - private def authorization_key diff --git a/lib/active_merchant/billing/gateways/braintree.rb b/lib/active_merchant/billing/gateways/braintree.rb index bfe682c31db..172ca0c41ff 100644 --- a/lib/active_merchant/billing/gateways/braintree.rb +++ b/lib/active_merchant/billing/gateways/braintree.rb @@ -1,13 +1,13 @@ require 'active_merchant/billing/gateways/braintree/braintree_common' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class BraintreeGateway < Gateway include BraintreeCommon self.abstract_class = true - def self.new(options={}) + def self.new(options = {}) if options.has_key?(:login) BraintreeOrangeGateway.new(options) else diff --git a/lib/active_merchant/billing/gateways/braintree/braintree_common.rb b/lib/active_merchant/billing/gateways/braintree/braintree_common.rb index 7343584f7aa..1d9f1df0890 100644 --- a/lib/active_merchant/billing/gateways/braintree/braintree_common.rb +++ b/lib/active_merchant/billing/gateways/braintree/braintree_common.rb @@ -1,7 +1,7 @@ module BraintreeCommon def self.included(base) base.supported_countries = %w(US CA AD AT BE BG HR CY CZ DK EE FI FR GI DE GR GG HU IS IM IE IT JE LV LI LT LU MT MC NL NO PL PT RO SM SK SI ES SE CH TR GB SG HK MY AU NZ) - base.supported_cardtypes = [:visa, :master, :american_express, :discover, :jcb, :diners_club, :maestro] + base.supported_cardtypes = %i[visa master american_express discover jcb diners_club maestro] base.homepage_url = 'http://www.braintreepaymentsolutions.com' base.display_name = 'Braintree' base.default_currency = 'USD' @@ -14,9 +14,17 @@ def supports_scrubbing def scrub(transcript) return '' if transcript.blank? + transcript. gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). gsub(%r((&?ccnumber=)\d*(&?)), '\1[FILTERED]\2'). - gsub(%r((&?cvv=)\d*(&?)), '\1[FILTERED]\2') + gsub(%r((&?cvv=)\d*(&?)), '\1[FILTERED]\2'). + gsub(%r(()\d+()), '\1[FILTERED]\2'). + gsub(%r(()[^<]+()), '\1[FILTERED]\2'). + gsub(%r(()[^<]+()), '\1[FILTERED]\2'). + gsub(%r(()[^<]{100,}()), '\1[FILTERED]\2'). + gsub(%r(()[^<]+()), '\1[FILTERED]\2'). + gsub(%r(()[^<]+()), '\1[FILTERED]\2'). + gsub(%r(()[^<]+()), '\1[FILTERED]\2') end end diff --git a/lib/active_merchant/billing/gateways/braintree/token_nonce.rb b/lib/active_merchant/billing/gateways/braintree/token_nonce.rb new file mode 100644 index 00000000000..83d67bbfdd0 --- /dev/null +++ b/lib/active_merchant/billing/gateways/braintree/token_nonce.rb @@ -0,0 +1,158 @@ +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class TokenNonce # :nodoc: + include PostsData + # This class emulates the behavior of the front-end js library to + # create token nonce for a bank account base on the docs: + # https://developer.paypal.com/braintree/docs/guides/ach/client-side + + attr_reader :braintree_gateway, :options + + def initialize(gateway, options = {}) + @braintree_gateway = gateway + @options = options + end + + def url + sandbox = @braintree_gateway.config.environment == :sandbox + "https://payments#{'.sandbox' if sandbox}.braintree-api.com/graphql" + end + + def create_token_nonce_for_payment_method(payment_method, options = {}) + headers = { + 'Accept' => 'application/json', + 'Authorization' => "Bearer #{client_token(options)['authorizationFingerprint']}", + 'Content-Type' => 'application/json', + 'Braintree-Version' => '2018-05-10' + } + resp = ssl_post(url, build_nonce_request(payment_method), headers) + json_response = JSON.parse(resp) + + message = json_response['errors'].map { |err| err['message'] }.join("\n") if json_response['errors'].present? + token = token_from(payment_method, json_response) + + return token, message + end + + def client_token(options = {}) + base64_token = @braintree_gateway.client_token.generate({ merchant_account_id: options[:merchant_account_id] || @options[:merchant_account_id] }.compact) + JSON.parse(Base64.decode64(base64_token)) + end + + private + + def graphql_bank_query + <<-GRAPHQL + mutation TokenizeUsBankAccount($input: TokenizeUsBankAccountInput!) { + tokenizeUsBankAccount(input: $input) { + paymentMethod { + id + details { + ... on UsBankAccountDetails { + last4 + } + } + } + } + } + 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? + + address = options[:billing_address] + + { + streetAddress: address[:address1], + extendedAddress: address[:address2], + city: address[:city], + state: address[:state], + zipCode: address[:zip] + }.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: + } + }.to_json + end + + def build_nonce_bank_request(payment_method) + input = { + usBankAccount: { + achMandate: options[:ach_mandate], + routingNumber: payment_method.routing_number, + accountNumber: payment_method.account_number, + accountType: payment_method.account_type.upcase, + billingAddress: billing_address_from_options + } + } + + if payment_method.account_holder_type == 'personal' + input[:usBankAccount][:individualOwner] = { + firstName: payment_method.first_name, + lastName: payment_method.last_name + } + else + input[:usBankAccount][:businessOwner] = { + businessName: payment_method.name + } + end + + 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 +end diff --git a/lib/active_merchant/billing/gateways/braintree_blue.rb b/lib/active_merchant/billing/gateways/braintree_blue.rb index 96bdbdae5b8..dc60b596e35 100644 --- a/lib/active_merchant/billing/gateways/braintree_blue.rb +++ b/lib/active_merchant/billing/gateways/braintree_blue.rb @@ -1,4 +1,6 @@ require 'active_merchant/billing/gateways/braintree/braintree_common' +require 'active_merchant/billing/gateways/braintree/token_nonce' +require 'active_support/core_ext/array/extract_options' begin require 'braintree' @@ -6,12 +8,10 @@ raise 'Could not load the braintree gem. Use `gem install braintree` to install it.' end -unless Braintree::Version::Major == 2 && Braintree::Version::Minor >= 78 - raise "Need braintree gem >= 2.78.0. Run `gem install braintree --version '~>2.78'` to get the correct version." -end +raise 'Need braintree gem >= 2.0.0.' unless Braintree::Version::Major >= 2 && Braintree::Version::Minor >= 0 -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # For more information on the Braintree Gateway please visit their # {Developer Portal}[https://www.braintreepayments.com/developers] # @@ -39,13 +39,19 @@ module Billing #:nodoc: # class BraintreeBlueGateway < Gateway include BraintreeCommon + include Empty self.display_name = 'Braintree (Blue Platform)' + version Braintree::Configuration::API_VERSION + version Braintree::Version::String, :gem + ERROR_CODES = { cannot_refund_if_unsettled: 91506 } + DIRECT_BANK_ERROR = 'Direct bank account transactions are not supported. Bank accounts must be successfully stored before use.'.freeze + def initialize(options = {}) requires!(options, :merchant_id, :public_key, :private_key) @merchant_account_id = options[:merchant_account_id] @@ -53,7 +59,7 @@ def initialize(options = {}) super if wiredump_device.present? - logger = ((Logger === wiredump_device) ? wiredump_device : Logger.new(wiredump_device)) + logger = (Logger === wiredump_device ? wiredump_device : Logger.new(wiredump_device)) logger.level = Logger::DEBUG else logger = Braintree::Configuration.logger.clone @@ -61,33 +67,53 @@ def initialize(options = {}) end @configuration = Braintree::Configuration.new( - :merchant_id => options[:merchant_id], - :public_key => options[:public_key], - :private_key => options[:private_key], - :environment => (options[:environment] || (test? ? :sandbox : :production)).to_sym, - :custom_user_agent => "ActiveMerchant #{ActiveMerchant::VERSION}", - :logger => options[:logger] || logger, + merchant_id: options[:merchant_id], + public_key: options[:public_key], + private_key: options[:private_key], + environment: (options[:environment] || (test? ? :sandbox : :production)).to_sym, + custom_user_agent: "ActiveMerchant #{ActiveMerchant::VERSION}", + logger: options[:logger] || logger ) - @braintree_gateway = Braintree::Gateway.new( @configuration ) + @braintree_gateway = Braintree::Gateway.new(@configuration) + end + + 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(post) }) + end end def authorize(money, credit_card_or_vault_id, options = {}) + return Response.new(false, DIRECT_BANK_ERROR) if credit_card_or_vault_id.is_a? Check + create_transaction(:sale, money, credit_card_or_vault_id, options) end def capture(money, authorization, options = {}) - commit do - result = @braintree_gateway.transaction.submit_for_settlement(authorization, localized_amount(money, options[:currency] || default_currency).to_s) - response_from_result(result) + if options[:partial_capture] == true + commit do + result = @braintree_gateway.transaction.submit_for_partial_settlement(authorization, localized_amount(money, options[:currency] || default_currency).to_s) + response_from_result(result) + end + else + commit do + result = @braintree_gateway.transaction.submit_for_settlement(authorization, localized_amount(money, options[:currency] || default_currency).to_s) + response_from_result(result) + end end end def purchase(money, credit_card_or_vault_id, options = {}) - authorize(money, credit_card_or_vault_id, options.merge(:submit_for_settlement => true)) + authorize(money, credit_card_or_vault_id, options.merge(submit_for_settlement: true)) 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 @@ -99,10 +125,13 @@ def refund(*args) commit do response = response_from_result(@braintree_gateway.transaction.refund(transaction_id, money)) - return response if response.success? - return response unless options[:force_full_refund_if_unsettled] - void(transaction_id) if response.message =~ /#{ERROR_CODES[:cannot_refund_if_unsettled]}/ + if !response.success? && options[:force_full_refund_if_unsettled] && + response.message =~ /#{ERROR_CODES[:cannot_refund_if_unsettled]}/ + void(transaction_id) + else + response + end end end @@ -112,66 +141,95 @@ def void(authorization, options = {}) end end - def verify(credit_card, options = {}) - MultiResponse.run(:use_first_response) do |r| - r.process { authorize(100, credit_card, options) } - r.process(:ignore_result) { void(r.authorization, options) } - end - end + def verify(creditcard, options = {}) + if options[:allow_card_verification] == true + options.delete(:allow_card_verification) + exp_month = creditcard.month.to_s + exp_year = creditcard.year.to_s + expiration = "#{exp_month}/#{exp_year}" + zip = options[:billing_address].try(:[], :zip) + address1 = options[:billing_address].try(:[], :address1) + payload = { + credit_card: { + number: creditcard.number, + expiration_date: expiration, + cvv: creditcard.verification_value + } + } + if zip || address1 + payload[:credit_card][:billing_address] = {} + payload[:credit_card][:billing_address][:postal_code] = zip if zip + payload[:credit_card][:billing_address][:street_address] = address1 if address1 + end - def store(creditcard, options = {}) - if options[:customer].present? - MultiResponse.new.tap do |r| - customer_exists_response = nil - r.process{customer_exists_response = check_customer_exists(options[:customer])} - r.process do - if customer_exists_response.params['exists'] - add_credit_card_to_customer(creditcard, options) - else - add_customer_with_credit_card(creditcard, options) - end - end + if merchant_account_id = (options[:merchant_account_id] || @merchant_account_id) + payload[:options] = { 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)) + response.cvv_result['message'] = '' + response.cvv_result['code'] = response.params['cvv_result'] if response.params['cvv_result'] + response.avs_result['code'] = response.params['avs_result'][:code] if response.params.dig('avs_result', :code) + response end + else - add_customer_with_credit_card(creditcard, options) + MultiResponse.run(:use_first_response) do |r| + r.process { authorize(100, creditcard, options) } + r.process(:ignore_result) { void(r.authorization, options) } + end + end + end + + def store(payment_method, options = {}) + return Response.new(false, bank_account_errors(payment_method, options)) if payment_method.is_a?(Check) && bank_account_errors(payment_method, options).present? + + MultiResponse.run do |r| + r.process { check_customer_exists(options[:customer]) } + process_by = payment_method.is_a?(Check) ? :store_bank_account : :store_credit_card + send process_by, payment_method, options, r end end def update(vault_id, creditcard, options = {}) braintree_credit_card = nil commit do - braintree_credit_card = @braintree_gateway.customer.find(vault_id).credit_cards.detect { |cc| cc.default? } + braintree_credit_card = @braintree_gateway.customer.find(vault_id).credit_cards.detect(&:default?) return Response.new(false, 'Braintree::NotFoundError') if braintree_credit_card.nil? - options.merge!(:update_existing_token => braintree_credit_card.token) + options[:update_existing_token] = braintree_credit_card.token credit_card_params = merge_credit_card_options({ - :credit_card => { - :cardholder_name => creditcard.name, - :number => creditcard.number, - :cvv => creditcard.verification_value, - :expiration_month => creditcard.month.to_s.rjust(2, '0'), - :expiration_year => creditcard.year.to_s + credit_card: { + cardholder_name: creditcard.name, + number: creditcard.number, + cvv: creditcard.verification_value, + expiration_month: creditcard.month.to_s.rjust(2, '0'), + expiration_year: creditcard.year.to_s } }, options)[:credit_card] - 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 + result = @braintree_gateway.customer.update( + vault_id, + first_name: creditcard.first_name, + last_name: creditcard.last_name, + email: scrub_email(options[:email]), + 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?) + 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?) ) end end def unstore(customer_vault_id, options = {}) commit do - if(!customer_vault_id && options[:credit_card_token]) + if !customer_vault_id && options[:credit_card_token] @braintree_gateway.credit_card.delete(options[:credit_card_token]) else @braintree_gateway.customer.delete(customer_vault_id) @@ -179,7 +237,7 @@ def unstore(customer_vault_id, options = {}) Response.new(true, 'OK') end end - alias_method :delete, :unstore + alias delete unstore def supports_network_tokenization? true @@ -200,13 +258,13 @@ def verify_credentials private def check_customer_exists(customer_vault_id) + return Response.new true, 'Customer not found', { exists: false } if customer_vault_id.blank? + commit do - begin - @braintree_gateway.customer.find(customer_vault_id) - ActiveMerchant::Billing::Response.new(true, 'Customer found', {exists: true}, authorization: customer_vault_id) - rescue Braintree::NotFoundError - ActiveMerchant::Billing::Response.new(true, 'Customer not found', {exists: false}) - end + @braintree_gateway.customer.find(customer_vault_id) + ActiveMerchant::Billing::Response.new(true, 'Customer found', { exists: true }, authorization: customer_vault_id) + rescue Braintree::NotFoundError + ActiveMerchant::Billing::Response.new(true, 'Customer not found', { exists: false }) end end @@ -216,32 +274,34 @@ def add_customer_with_credit_card(creditcard, options) credit_card_params = { payment_method_nonce: options[:payment_method_nonce] } else credit_card_params = { - :credit_card => { - :cardholder_name => creditcard.name, - :number => creditcard.number, - :cvv => creditcard.verification_value, - :expiration_month => creditcard.month.to_s.rjust(2, '0'), - :expiration_year => creditcard.year.to_s, - :token => options[:credit_card_token] + credit_card: { + cardholder_name: creditcard.name, + number: creditcard.number, + cvv: creditcard.verification_value, + expiration_month: creditcard.month.to_s.rjust(2, '0'), + expiration_year: creditcard.year.to_s, + token: options[:credit_card_token] } } end parameters = { - :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]), - :id => options[:customer], + first_name: creditcard.first_name, + last_name: creditcard.last_name, + email: scrub_email(options[:email]), + 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?) + 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 @@ -256,8 +316,12 @@ def add_credit_card_to_customer(credit_card, options) cvv: credit_card.verification_value, expiration_month: credit_card.month.to_s.rjust(2, '0'), expiration_year: credit_card.year.to_s, + device_data: options[:device_data] } - parameters[:billing_address] = map_address(options[:billing_address]) if options[:billing_address] + if options[:billing_address] + address = map_address(options[:billing_address]) + parameters[:billing_address] = address unless address.all? { |_k, v| empty?(v) } + end result = @braintree_gateway.credit_card.create(parameters) ActiveMerchant::Billing::Response.new( @@ -274,64 +338,66 @@ def add_credit_card_to_customer(credit_card, options) def scrub_email(email) return nil unless email.present? - return nil if ( + return nil if email !~ /^.+@[^\.]+(\.[^\.]+)+[a-z]$/i || email =~ /\.(con|met)$/i - ) + email end def scrub_zip(zip) return nil unless zip.present? - return nil if( + return nil if zip.gsub(/[^a-z0-9]/i, '').length > 9 || zip =~ /[^a-z0-9\- ]/i - ) + zip end def merge_credit_card_options(parameters, options) valid_options = {} options.each do |key, value| - valid_options[key] = value if [:update_existing_token, :verify_card, :verification_merchant_account_id].include?(key) + valid_options[key] = value if %i[update_existing_token verify_card verification_merchant_account_id].include?(key) end - if valid_options.include?(:verify_card) && @merchant_account_id - valid_options[:verification_merchant_account_id] ||= @merchant_account_id - end + valid_options[:verification_merchant_account_id] ||= @merchant_account_id if valid_options.include?(:verify_card) && @merchant_account_id parameters[:credit_card] ||= {} - parameters[:credit_card].merge!(:options => valid_options) - parameters[:credit_card][:billing_address] = map_address(options[:billing_address]) if options[:billing_address] + parameters[:credit_card][:options] = valid_options + if options[:billing_address] + address = map_address(options[:billing_address]) + parameters[:credit_card][:billing_address] = address unless address.all? { |_k, v| empty?(v) } + end parameters end + def phone_from(options) + options[:phone] || options.dig(:billing_address, :phone) || options.dig(:billing_address, :phone_number) + end + def map_address(address) - return {} if address.nil? mapped = { - :street_address => address[:address1], - :extended_address => address[:address2], - :company => address[:company], - :locality => address[:city], - :region => address[:state], - :postal_code => scrub_zip(address[:zip]), + street_address: address[:address1], + extended_address: address[:address2], + company: address[:company], + locality: address[:city], + region: address[:state], + postal_code: scrub_zip(address[:zip]) } - if (address[:country] || address[:country_code_alpha2]) - mapped[:country_code_alpha2] = (address[:country] || address[:country_code_alpha2]) - elsif address[:country_name] - mapped[:country_name] = address[:country_name] - elsif address[:country_code_alpha3] - mapped[:country_code_alpha3] = address[:country_code_alpha3] - elsif address[:country_code_numeric] - mapped[:country_code_numeric] = address[:country_code_numeric] - end + + mapped[:country_code_alpha2] = (address[:country] || address[:country_code_alpha2]) if address[:country] || address[:country_code_alpha2] + mapped[:country_name] = address[:country_name] if address[:country_name] + mapped[:country_code_alpha3] = address[:country_code_alpha3] if address[:country_code_alpha3] + mapped[:country_code_alpha3] ||= Country.find(address[:country]).code(:alpha3).value unless address[:country].blank? + mapped[:country_code_numeric] = address[:country_code_numeric] if address[:country_code_numeric] + mapped end 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) @@ -347,10 +413,15 @@ def message_from_result(result) end def response_from_result(result) - Response.new(result.success?, message_from_result(result), - { braintree_transaction: transaction_hash(result) }, - { authorization: (result.transaction.id if result.transaction) } - ) + response_hash = { braintree_transaction: transaction_hash(result) } + + Response.new( + result.success?, + message_from_result(result), + response_hash, + authorization: result.transaction&.id, + test: test? + ) end def response_params(result) @@ -362,15 +433,22 @@ def response_params(result) def response_options(result) options = {} - if result.transaction + if result.credit_card_verification + options[:authorization] = result.credit_card_verification.id + options[:avs_result] = { code: avs_code_from(result.credit_card_verification) } + options[:cvv_result] = result.credit_card_verification.cvv_response_code + elsif result.transaction options[:authorization] = result.transaction.id options[:avs_result] = { code: avs_code_from(result.transaction) } options[:cvv_result] = result.transaction.cvv_response_code end + options[:test] = test? options 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 @@ -405,7 +483,9 @@ def avs_mapping 'street: A, zip: N' => 'C', 'street: A, zip: U' => 'I', 'street: A, zip: I' => 'I', - 'street: A, zip: A' => 'I' + 'street: A, zip: A' => 'I', + + 'street: B, zip: B' => 'B' } end @@ -429,16 +509,80 @@ def response_code_from_result(result) end end + 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 result = @braintree_gateway.transaction.send(transaction_type, transaction_params) + make_default_payment_method_token(result, options) response = Response.new(result.success?, message_from_transaction_result(result), response_params(result), response_options(result)) response.cvv_result['message'] = '' response end end + def make_default_payment_method_token(result, options) + return if options[:prevent_default_payment_method] + return unless options.dig(:paypal, :paypal_flow_type) == 'checkout_with_vault' && result.success? + + @braintree_gateway.customer.update( + result.transaction.customer_details.id, + default_payment_method_token: result.transaction.paypal_details.implicitly_vaulted_payment_method_token + ) + end + def extract_refund_args(args) options = args.extract_options! @@ -452,7 +596,7 @@ def extract_refund_args(args) end end - def customer_hash(customer, include_credit_cards=false) + def customer_hash(customer, include_credit_cards = false) hash = { 'email' => customer.email, 'phone' => customer.phone, @@ -479,7 +623,16 @@ def customer_hash(customer, include_credit_cards=false) def transaction_hash(result) unless result.success? - return { 'processor_response_code' => response_code_from_result(result) } + return { 'processor_response_code' => response_code_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 @@ -495,10 +648,18 @@ 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, - 'phone' => transaction.customer_details.phone, + 'phone' => transaction.customer_details.phone } billing_details = { @@ -508,7 +669,7 @@ def transaction_hash(result) 'locality' => transaction.billing_details.locality, 'region' => transaction.billing_details.region, 'postal_code' => transaction.billing_details.postal_code, - 'country_name' => transaction.billing_details.country_name, + 'country_name' => transaction.billing_details.country_name } shipping_details = { @@ -518,131 +679,450 @@ def transaction_hash(result) 'locality' => transaction.shipping_details.locality, 'region' => transaction.shipping_details.region, 'postal_code' => transaction.shipping_details.postal_code, - 'country_name' => transaction.shipping_details.country_name, + '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, + 'paypal_payment_token' => transaction.paypal_details.implicitly_vaulted_payment_method_token || transaction.paypal_details.token } + if transaction.risk_data + risk_data = { + 'id' => transaction.risk_data.id, + 'decision' => transaction.risk_data.decision, + 'device_data_captured' => transaction.risk_data.device_data_captured, + 'fraud_service_provider' => transaction.risk_data.fraud_service_provider + } + else + 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, - 'processor_response_code' => response_code_from_result(result) + '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 def create_transaction_parameters(money, credit_card_or_vault_id, options) parameters = { - :amount => localized_amount(money, options[:currency] || default_currency).to_s, - :order_id => options[:order_id], - :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]) + amount: localized_amount(money, options[:currency] || default_currency).to_s, + order_id: options[:order_id], + customer: { + id: options[:store] == true ? '' : options[:store], + email: scrub_email(options[:email]), + phone: phone_from(options) }, - :options => { - :store_in_vault => options[:store] ? true : false, - :submit_for_settlement => options[:submit_for_settlement], - :hold_in_escrow => options[:hold_in_escrow], + options: { + store_in_vault: options[:store] ? true : false, + submit_for_settlement: options[:submit_for_settlement], + hold_in_escrow: options[:hold_in_escrow] } } - if options[:skip_advanced_fraud_checking] - parameters[:options].merge!({ :skip_advanced_fraud_checking => options[:skip_advanced_fraud_checking] }) - end - parameters[:custom_fields] = options[:custom_fields] parameters[:device_data] = options[:device_data] if options[:device_data] parameters[:service_fee_amount] = options[:service_fee_amount] if options[:service_fee_amount] - if merchant_account_id = (options[:merchant_account_id] || @merchant_account_id) - parameters[:merchant_account_id] = merchant_account_id + + add_account_type(parameters, options) if options[:account_type] + add_skip_options(parameters, options) + add_merchant_account_id(parameters, options) + add_profile_id(parameters, options) + + add_payment_method(parameters, credit_card_or_vault_id, options) + add_stored_credential_data(parameters, credit_card_or_vault_id, options) + add_addresses(parameters, 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) + add_transaction_source(parameters, options) + + add_level_2_data(parameters, options) + add_level_3_data(parameters, 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] end - if options[:recurring] - parameters[:recurring] = true + parameters + end + + def add_account_type(parameters, options) + parameters[:options][:credit_card] = {} + parameters[:options][:credit_card][:account_type] = options[:account_type] + end + + def add_skip_options(parameters, options) + parameters[:options][:skip_advanced_fraud_checking] = options[:skip_advanced_fraud_checking] if options[:skip_advanced_fraud_checking] + parameters[:options][:skip_avs] = options[:skip_avs] if options[:skip_avs] + parameters[:options][:skip_cvv] = options[:skip_cvv] if options[:skip_cvv] + end + + def add_merchant_account_id(parameters, options) + return unless merchant_account_id = (options[:merchant_account_id] || @merchant_account_id) + + parameters[:merchant_account_id] = merchant_account_id + end + + def add_profile_id(parameters, options) + return unless profile_id = options[:venmo_profile_id] + + parameters[:options][:venmo] = {} + parameters[:options][:venmo][:profile_id] = profile_id + end + + def add_transaction_source(parameters, options) + parameters[:transaction_source] = options[:transaction_source] if options[:transaction_source] + parameters[:transaction_source] = 'recurring' if options[:recurring] + end + + def add_addresses(parameters, options) + parameters[:billing] = map_address(options[:billing_address]) if options[:billing_address] + parameters[:shipping] = map_address(options[:shipping_address]) if options[:shipping_address] + end + + def add_channel(parameters, options) + channel = options[:override_application_id] || @options[:channel] || application_id + parameters[:channel] = channel if channel + end + + def add_descriptor(parameters, options) + return unless options[:descriptor_name] || options[:descriptor_phone] || options[:descriptor_url] + + parameters[:descriptor] = { + name: options[:descriptor_name], + phone: options[:descriptor_phone], + url: options[:descriptor_url] + } + end + + def add_risk_data(parameters, options) + return unless options[:risk_data] + + parameters[:risk_data] = { + customer_browser: options[:risk_data][:customer_browser], + customer_ip: options[:risk_data][:customer_ip] + } + 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] + parameters[:purchase_order_number] = options[:purchase_order_number] if options[:purchase_order_number] + end + + def add_level_3_data(parameters, options) + parameters[:shipping_amount] = options[:shipping_amount] if options[:shipping_amount] + parameters[:discount_amount] = options[:discount_amount] if options[:discount_amount] + parameters[:ships_from_postal_code] = options[:ships_from_postal_code] if options[:ships_from_postal_code] + + parameters[:line_items] = options[:line_items] if options[:line_items] + end + + def add_travel_data(parameters, options) + parameters[:industry] = { + industry_type: Braintree::Transaction::IndustryType::TravelAndCruise, + data: {} + } + + parameters[:industry][:data][:travel_package] = options[:travel_data][:travel_package] if options[:travel_data][:travel_package] + parameters[:industry][:data][:departure_date] = options[:travel_data][:departure_date] if options[:travel_data][:departure_date] + parameters[:industry][:data][:lodging_check_in_date] = options[:travel_data][:lodging_check_in_date] if options[:travel_data][:lodging_check_in_date] + parameters[:industry][:data][:lodging_check_out_date] = options[:travel_data][:lodging_check_out_date] if options[:travel_data][:lodging_check_out_date] + parameters[:industry][:data][:lodging_name] = options[:travel_data][:lodging_name] if options[:travel_data][:lodging_name] + end + + def add_lodging_data(parameters, options) + parameters[:industry] = { + industry_type: Braintree::Transaction::IndustryType::Lodging, + data: {} + } + + parameters[:industry][:data][:folio_number] = options[:lodging_data][:folio_number] if options[:lodging_data][:folio_number] + parameters[:industry][:data][:check_in_date] = options[:lodging_data][:check_in_date] if options[:lodging_data][:check_in_date] + parameters[:industry][:data][:check_out_date] = options[:lodging_data][:check_out_date] if options[:lodging_data][:check_out_date] + parameters[:industry][:data][:room_rate] = options[:lodging_data][:room_rate] if options[:lodging_data][:room_rate] + end + + def add_3ds_info(parameters, three_d_secure_opts) + return if empty?(three_d_secure_opts) + + pass_thru = {} + + pass_thru[:three_d_secure_version] = three_d_secure_opts[:version] if three_d_secure_opts[:version] + pass_thru[:eci_flag] = three_d_secure_opts[:eci] if three_d_secure_opts[:eci] + pass_thru[:cavv_algorithm] = three_d_secure_opts[:cavv_algorithm] if three_d_secure_opts[:cavv_algorithm] + pass_thru[:cavv] = three_d_secure_opts[:cavv] if three_d_secure_opts[:cavv] + pass_thru[:directory_response] = three_d_secure_opts[:directory_response_status] if three_d_secure_opts[:directory_response_status] + pass_thru[:authentication_response] = three_d_secure_opts[:authentication_response_status] if three_d_secure_opts[:authentication_response_status] + + parameters[:three_d_secure_pass_thru] = pass_thru.merge(xid_or_ds_trans_id(three_d_secure_opts)) + end + + def xid_or_ds_trans_id(three_d_secure_opts) + if three_d_secure_opts[:version].to_f >= 2 + { ds_transaction_id: three_d_secure_opts[:ds_transaction_id] } + else + { xid: three_d_secure_opts[:xid] } end + end - 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 + 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]) + + add_external_vault(parameters, options) + stored_credentials(parameters, stored_credential) + end + + def stored_credentials(parameters, stored_credential) + case stored_credential[:reason_type] + when 'recurring', 'installment' + if stored_credential[:initial_transaction] + parameters[:transaction_source] = "#{stored_credential[:reason_type]}_first" else - parameters[:customer_id] = credit_card_or_vault_id + 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 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) + 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 + 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 - parameters[:android_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, options) + 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 - parameters[:billing] = map_address(options[:billing_address]) if options[:billing_address] - parameters[:shipping] = map_address(options[:shipping_address]) if options[:shipping_address] + end - channel = @options[:channel] || application_id - parameters[:channel] = channel if channel + 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 - if options[:descriptor_name] || options[:descriptor_phone] || options[:descriptor_url] - parameters[:descriptor] = { - name: options[:descriptor_name], - phone: options[:descriptor_phone], - url: options[:descriptor_url] + 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, options) + if options.dig(:stored_credential, :initiator) == 'merchant' + 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: 'cryptogram' + } + else + 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 + 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 - if options[:three_d_secure] - parameters[:three_d_secure_pass_thru] = { - cavv: options[:three_d_secure][:cavv], - eci_flag: options[:three_d_secure][:eci], - xid: options[:three_d_secure][:xid], + 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 + elsif options[:billing_address].blank? + 'billing_address is required parameter to store and verify Bank accounts.' + elsif options[:ach_mandate].blank? + 'ach_mandate is a required parameter to process bank acccount transactions see (https://developer.paypal.com/braintree/docs/guides/ach/client-side#show-required-authorization-language)' end + end - parameters + def add_bank_account_to_customer(payment_method, options) + bank_account_nonce, error_message = TokenNonce.new(@braintree_gateway, options).create_token_nonce_for_payment_method(payment_method, options) + return Response.new(false, error_message) unless bank_account_nonce.present? + + result = @braintree_gateway.payment_method.create( + customer_id: options[:customer], + payment_method_nonce: bank_account_nonce, + options: { + us_bank_account_verification_method: 'network_check' + } + ) + + success = bank_account_verified?(result) + resp_body = if result.respond_to?(:payment_method) + { + customer_vault_id: options[:customer], + bank_account_token: result.payment_method&.token, + verified: success + } + end + Response.new( + success, + message_from_bank_account_result(success, result), + resp_body || {}, + authorization: (result.payment_method&.token if result.respond_to?(:payment_method)) + ) + end + + def bank_account_verified?(result) + return false unless result.respond_to?(:payment_method) + + result.success? && result.payment_method&.verified + end + + def message_from_bank_account_result(success, response) + return message_from_result(response) if !response.respond_to?(:payment_method) || success + return unless response.payment_method.verifications.present? + + verification = response.payment_method.verifications.first + "verification_status: [#{verification.status}], processor_response: [#{verification.processor_response_code}-#{verification.processor_response_text}]" + end + + def store_bank_account(payment_method, options, multi_response) + multi_response.process { create_customer_from_bank_account payment_method, options } unless multi_response.params['exists'] + multi_response.process { add_bank_account_to_customer payment_method, options } + end + + def store_credit_card(payment_method, options, multi_response) + process_by = multi_response.params['exists'] ? :add_credit_card_to_customer : :add_customer_with_credit_card + multi_response.process { send process_by, payment_method, options } + end + + def create_customer_from_bank_account(payment_method, options) + parameters = { + id: options[:customer], + first_name: payment_method.first_name, + last_name: payment_method.last_name, + email: scrub_email(options[:email]), + phone: phone_from(options), + device_data: options[:device_data] + }.compact + + result = @braintree_gateway.customer.create(parameters) + customer_id = result.customer.id if result.success? + options[:customer] = customer_id + + Response.new( + result.success?, + message_from_result(result), + { customer_vault_id: customer_id, exists: true } + ) end end end diff --git a/lib/active_merchant/billing/gateways/braintree_orange.rb b/lib/active_merchant/billing/gateways/braintree_orange.rb index f56502eb7a0..422b18890e5 100644 --- a/lib/active_merchant/billing/gateways/braintree_orange.rb +++ b/lib/active_merchant/billing/gateways/braintree_orange.rb @@ -1,8 +1,8 @@ -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: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class BraintreeOrangeGateway < SmartPs include BraintreeCommon diff --git a/lib/active_merchant/billing/gateways/bridge_pay.rb b/lib/active_merchant/billing/gateways/bridge_pay.rb index 091d8ac1a65..1444f13f1f4 100644 --- a/lib/active_merchant/billing/gateways/bridge_pay.rb +++ b/lib/active_merchant/billing/gateways/bridge_pay.rb @@ -1,7 +1,7 @@ require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class BridgePayGateway < Gateway self.display_name = 'BridgePay' self.homepage_url = 'http://www.bridgepaynetwork.com/' @@ -9,17 +9,16 @@ class BridgePayGateway < Gateway self.test_url = 'https://gatewaystage.itstgate.com/SmartPayments/transact3.asmx' self.live_url = 'https://gateway.itstgate.com/SmartPayments/transact3.asmx' - self.supported_countries = ['CA', 'US'] + self.supported_countries = %w[CA US] self.default_currency = 'USD' - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb] + self.supported_cardtypes = %i[visa master american_express discover diners_club jcb] - - def initialize(options={}) + def initialize(options = {}) requires!(options, :user_name, :password) super end - def purchase(amount, payment_method, options={}) + def purchase(amount, payment_method, options = {}) post = initialize_required_fields('Sale') # Allow the same amount in multiple transactions. @@ -31,7 +30,7 @@ def purchase(amount, payment_method, options={}) commit(post) end - def authorize(amount, payment_method, options={}) + def authorize(amount, payment_method, options = {}) post = initialize_required_fields('Auth') add_invoice(post, amount, options) @@ -41,7 +40,7 @@ def authorize(amount, payment_method, options={}) commit(post) end - def capture(amount, authorization, options={}) + def capture(amount, authorization, options = {}) post = initialize_required_fields('Force') add_invoice(post, amount, options) @@ -51,7 +50,7 @@ def capture(amount, authorization, options={}) commit(post) end - def refund(amount, authorization, options={}) + def refund(amount, authorization, options = {}) post = initialize_required_fields('Return') add_invoice(post, amount, options) @@ -60,7 +59,7 @@ def refund(amount, authorization, options={}) commit(post) end - def void(authorization, options={}) + def void(authorization, options = {}) post = initialize_required_fields('Void') add_reference(post, authorization) @@ -75,10 +74,10 @@ def verify(creditcard, options = {}) end end - def store(creditcard, options={}) + def store(creditcard, options = {}) post = initialize_required_fields('') post[:transaction] = 'Create' - post[:CardNumber] = creditcard.number + post[:CardNumber] = creditcard.number post[:CustomerPaymentInfoKey] = '' post[:token] = '' add_payment_method(post, creditcard) @@ -148,7 +147,7 @@ def initialize_required_fields(transaction_type) end def add_customer_data(post, options) - if(billing_address = (options[:billing_address] || options[:address])) + if (billing_address = (options[:billing_address] || options[:address])) post[:Street] = billing_address[:address1] post[:Zip] = billing_address[:zip] end @@ -167,8 +166,8 @@ def parse(xml) response = {} doc = Nokogiri::XML(xml) - doc.root.xpath('*').each do |node| - if (node.elements.size == 0) + doc.root&.xpath('*')&.each do |node| + if node.elements.size == 0 response[node.name.downcase.to_sym] = node.text else node.elements.each do |childnode| @@ -176,7 +175,7 @@ def parse(xml) response[name.to_sym] = childnode.text end end - end unless doc.root.nil? + end response end @@ -236,9 +235,9 @@ def add_reference(post, authorization) def post_data(post) { - :UserName => @options[:user_name], - :Password => @options[:password] - }.merge(post).collect{|k,v| "#{k}=#{CGI.escape(v.to_s)}"}.join('&') + UserName: @options[:user_name], + Password: @options[:password] + }.merge(post).collect { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&') end end end diff --git a/lib/active_merchant/billing/gateways/cams.rb b/lib/active_merchant/billing/gateways/cams.rb index c053f77ecbc..17b5981787d 100644 --- a/lib/active_merchant/billing/gateways/cams.rb +++ b/lib/active_merchant/billing/gateways/cams.rb @@ -1,11 +1,11 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class CamsGateway < Gateway self.live_url = 'https://secure.centralams.com/gw/api/transact.php' self.supported_countries = ['US'] self.default_currency = 'USD' - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'https://www.centralams.com/' self.display_name = 'CAMS: Central Account Management System' @@ -67,12 +67,12 @@ class CamsGateway < Gateway '894' => STANDARD_ERROR_CODE[:processing_error] } - def initialize(options={}) + def initialize(options = {}) requires!(options, :username, :password) super end - def purchase(money, payment, options={}) + def purchase(money, payment, options = {}) post = {} add_invoice(post, money, options) @@ -86,7 +86,7 @@ def purchase(money, payment, options={}) commit('sale', post) end - def authorize(money, payment, options={}) + def authorize(money, payment, options = {}) post = {} add_invoice(post, money, options) add_payment(post, payment) @@ -95,7 +95,7 @@ def authorize(money, payment, options={}) commit('auth', post) end - def capture(money, authorization, options={}) + def capture(money, authorization, options = {}) post = {} add_reference(post, authorization) add_invoice(post, money, options) @@ -103,20 +103,20 @@ def capture(money, authorization, options={}) commit('capture', post) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) post = {} add_reference(post, authorization) add_invoice(post, money, options) commit('refund', post) end - def void(authorization, options={}) + def void(authorization, options = {}) post = {} add_reference(post, authorization) commit('void', post) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) post = {} add_invoice(post, 0, options) add_payment(post, credit_card) @@ -138,20 +138,20 @@ def scrub(transcript) private - def add_address(post, creditcard, options={}) + def add_address(post, creditcard, options = {}) post[:firstname] = creditcard.first_name - post[:lastname ] = creditcard.last_name + post[:lastname] = creditcard.last_name return unless options[:billing_address] address = options[:billing_address] - post[:address1 ] = address[:address1] - post[:address2 ] = address[:address2] - post[:city ] = address[:city] - post[:state ] = address[:state] - post[:zip ] = address[:zip] - post[:country ] = address[:country] - post[:phone ] = address[:phone] + post[:address1] = address[:address1] + post[:address2] = address[:address2] + post[:city] = address[:city] + post[:state] = address[:state] + post[:zip] = address[:zip] + post[:country] = address[:country] + post[:phone] = address[:phone] end def add_reference(post, authorization) @@ -167,15 +167,15 @@ def add_invoice(post, money, options) def add_payment(post, payment) post[:ccnumber] = payment.number - post[:ccexp ] = "#{payment.month.to_s.rjust(2,"0")}#{payment.year.to_s[-2..-1]}" - post[:cvv ] = payment.verification_value + post[:ccexp] = "#{payment.month.to_s.rjust(2, '0')}#{payment.year.to_s[-2..-1]}" + post[:cvv] = payment.verification_value end def parse(body) kvs = body.split('&') kvs.inject({}) { |h, kv| - k,v = kv.split('=') + k, v = kv.split('=') h[k] = v h } @@ -219,7 +219,7 @@ def post_data(parameters = {}) parameters[:password] = @options[:password] parameters[:username] = @options[:username] - parameters.collect{|k,v| "#{k}=#{v}" }.join('&') + parameters.collect { |k, v| "#{k}=#{v}" }.join('&') end def error_code_from(response) diff --git a/lib/active_merchant/billing/gateways/card_connect.rb b/lib/active_merchant/billing/gateways/card_connect.rb index 40444cade8c..4b12257d1d1 100644 --- a/lib/active_merchant/billing/gateways/card_connect.rb +++ b/lib/active_merchant/billing/gateways/card_connect.rb @@ -1,12 +1,12 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class CardConnectGateway < Gateway - self.test_url = 'https://fts.cardconnect.com:6443/cardconnect/rest/' - self.live_url = 'https://fts.cardconnect.com:8443/cardconnect/rest/' + self.test_url = 'https://fts-uat.cardconnect.com/cardconnect/rest/' + self.live_url = 'https://fts.cardconnect.com/cardconnect/rest/' self.supported_countries = ['US'] self.default_currency = 'USD' - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'https://cardconnect.com/' self.display_name = 'Card Connect' @@ -61,6 +61,8 @@ class CardConnectGateway < Gateway '60' => STANDARD_ERROR_CODE[:pickup_card] } + SCHEDULED_PAYMENT_TYPES = %w(recurring installment) + def initialize(options = {}) requires!(options, :merchant_id, :username, :password) require_valid_domain!(options, :domain) @@ -68,8 +70,8 @@ def initialize(options = {}) end def require_valid_domain!(options, param) - if options.key?(param) - raise ArgumentError.new('not a valid cardconnect domain') unless /\Dcardconnect.com:\d{1,}\D/ =~ options[param] + if options[param] + raise ArgumentError.new('not a valid cardconnect domain') unless /https:\/\/\D*cardconnect.com/ =~ options[param] end end @@ -87,7 +89,9 @@ def purchase(money, payment, options = {}) add_currency(post, money, options) add_address(post, options) add_customer_data(post, options) - add_3DS(post, options) + add_three_ds_mpi_data(post, options) + add_additional_data(post, options) + add_stored_credential(post, options) post[:capture] = 'Y' commit('auth', post) end @@ -101,7 +105,9 @@ def authorize(money, payment, options = {}) add_payment(post, payment) add_address(post, options) add_customer_data(post, options) - add_3DS(post, options) + add_three_ds_mpi_data(post, options) + add_additional_data(post, options) + add_stored_credential(post, options) commit('auth', post) end @@ -140,9 +146,12 @@ def store(payment, options = {}) def unstore(authorization, options = {}) account_id, profile_id = authorization.split('|') - commit('profile', {}, - verb: :delete, - path: "/#{profile_id}/#{account_id}/#{@options[:merchant_id]}") + commit( + 'profile', + {}, + verb: :delete, + path: "/#{profile_id}/#{account_id}/#{@options[:merchant_id]}" + ) end def supports_scrubbing? @@ -150,12 +159,12 @@ def supports_scrubbing? end def scrub(transcript) - transcript - .gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]') - .gsub(%r(("cvv2\\":\\")\d*), '\1[FILTERED]') - .gsub(%r(("merchid\\":\\")\d*), '\1[FILTERED]') - .gsub(%r((&?"account\\":\\")\d*), '\1[FILTERED]') - .gsub(%r((&?"token\\":\\")\d*), '\1[FILTERED]') + transcript. + gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). + gsub(%r(("cvv2\\":\\")\d*), '\1[FILTERED]'). + gsub(%r(("merchid\\":\\")\d*), '\1[FILTERED]'). + gsub(%r((&?"account\\":\\")\d*), '\1[FILTERED]'). + gsub(%r((&?"token\\":\\")\d*), '\1[FILTERED]') end private @@ -167,7 +176,7 @@ def add_customer_data(post, options) def add_address(post, options) if address = options[:billing_address] || options[:address] post[:address] = address[:address1] if address[:address1] - post[:address].concat(" #{address[:address2]}") if address[:address2] + post[:address2] = address[:address2] if address[:address2] post[:city] = address[:city] if address[:city] post[:region] = address[:state] if address[:state] post[:country] = address[:country] if address[:country] @@ -186,7 +195,11 @@ def add_currency(post, money, options) def add_invoice(post, options) post[:orderid] = options[:order_id] - post[:ecomind] = (options[:recurring] ? 'R' : 'E') + post[:ecomind] = if options[:ecomind] + options[:ecomind].capitalize + else + (options[:recurring] ? 'R' : 'E') + end end def add_payment(post, payment) @@ -231,16 +244,28 @@ def add_additional_data(post, options) post[:items] = options[:items].map do |item| updated = {} item.each_pair do |k, v| - updated.merge!(k.to_s.gsub(/_/, '') => v) + updated.merge!(k.to_s.delete('_') => v) end + updated end end + post[:userfields] = options[:user_fields] if options[:user_fields] + end + + def add_three_ds_mpi_data(post, options) + return unless three_d_secure = options[:three_d_secure] + + post[:secureflag] = three_d_secure[:eci] + post[:securevalue] = three_d_secure[:cavv] + post[:securedstid] = three_d_secure[:ds_transaction_id] end - def add_3DS(post, options) - post[:secureflag] = options[:secure_flag] if options[:secure_flag] - post[:securevalue] = options[:secure_value] if options[:secure_value] - post[:securexid] = options[:secure_xid] if options[:secure_xid] + def add_stored_credential(post, options) + return unless stored_credential = options[:stored_credential] + + post[:cof] = stored_credential[:initiator] == 'merchant' ? 'M' : 'C' + post[:cofscheduled] = SCHEDULED_PAYMENT_TYPES.include?(stored_credential[:reason_type]) ? 'Y' : 'N' + post[:cofpermission] = stored_credential[:initial_transaction] ? 'Y' : 'N' end def headers @@ -267,6 +292,7 @@ def url(action, path) end def commit(action, parameters, verb: :put, path: '') + parameters[:frontendid] = application_id parameters[:merchid] = @options[:merchant_id] url = url(action, path) response = parse(ssl_request(verb, url, post_data(parameters), headers)) @@ -281,6 +307,10 @@ def commit(action, parameters, verb: :put, path: '') test: test?, error_code: error_code_from(response) ) + rescue ResponseError => e + return Response.new(false, 'Unable to authenticate. Please check your credentials.', {}, test: test?) if e.response.code == '401' + + raise end def success_from(response) diff --git a/lib/active_merchant/billing/gateways/card_save.rb b/lib/active_merchant/billing/gateways/card_save.rb index 7bd9ee8e4d2..34b8754b9db 100644 --- a/lib/active_merchant/billing/gateways/card_save.rb +++ b/lib/active_merchant/billing/gateways/card_save.rb @@ -1,23 +1,21 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class CardSaveGateway < IridiumGateway - #CardSave lets you handle failovers on payments by providing 3 gateways in case one happens to be down - #URLS = ['https://gw1.cardsaveonlinepayments.com:4430/','https://gw2.cardsaveonlinepayments.com:4430/','https://gw3.cardsaveonlinepayments.com:4430/'] - + # CardSave lets you handle failovers on payments by providing 3 gateways in case one happens to be down + # URLS = ['https://gw1.cardsaveonlinepayments.com:4430/','https://gw2.cardsaveonlinepayments.com:4430/','https://gw3.cardsaveonlinepayments.com:4430/'] + self.money_format = :cents self.default_currency = 'GBP' - self.supported_cardtypes = [ :visa, :switch, :maestro, :master, :solo, :american_express, :jcb ] - self.supported_countries = [ 'GB' ] + self.supported_cardtypes = %i[visa maestro master american_express jcb] + self.supported_countries = ['GB'] self.homepage_url = 'http://www.cardsave.net/' self.display_name = 'CardSave' - - def initialize(options={}) + + def initialize(options = {}) super @test_url = 'https://gw1.cardsaveonlinepayments.com:4430/' @live_url = 'https://gw1.cardsaveonlinepayments.com:4430/' end - end end end - diff --git a/lib/active_merchant/billing/gateways/card_stream.rb b/lib/active_merchant/billing/gateways/card_stream.rb index dd929a56d2e..6378ffd3d22 100644 --- a/lib/active_merchant/billing/gateways/card_stream.rb +++ b/lib/active_merchant/billing/gateways/card_stream.rb @@ -1,14 +1,14 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class CardStreamGateway < Gateway - THREEDSECURE_REQUIRED_DEPRECATION_MESSAGE = 'Specifying the :threeDSRequired initialization option is deprecated. Please use the `:threeds_required => true` *transaction* option instead.' self.test_url = self.live_url = 'https://gateway.cardstream.com/direct/' self.money_format = :cents self.default_currency = 'GBP' - self.supported_countries = ['GB', 'US', 'CH', 'SE', 'SG', 'NO', 'JP', 'IS', 'HK', 'NL', 'CZ', 'CA', 'AU'] - self.supported_cardtypes = [:visa, :master, :american_express, :diners_club, :discover, :jcb, :maestro, :solo, :switch] + self.currencies_without_fractions = %w(CVE ISK JPY UGX) + self.supported_countries = %w[GB US CH SE SG NO JP IS HK NL CZ CA AU] + self.supported_cardtypes = %i[visa master american_express diners_club discover jcb maestro] self.homepage_url = 'http://www.cardstream.com/' self.display_name = 'CardStream' @@ -141,7 +141,7 @@ class CardStreamGateway < Gateway def initialize(options = {}) requires!(options, :login, :shared_secret) @threeds_required = false - if (options[:threeDSRequired]) + if options[:threeDSRequired] ActiveMerchant.deprecated(THREEDSECURE_REQUIRED_DEPRECATION_MESSAGE) @threeds_required = options[:threeDSRequired] end @@ -150,30 +150,20 @@ def initialize(options = {}) def authorize(money, credit_card_or_reference, options = {}) post = {} - add_pair(post, :captureDelay, -1) - add_amount(post, money, options) - add_invoice(post, credit_card_or_reference, money, options) - add_credit_card_or_reference(post, credit_card_or_reference) - add_customer_data(post, options) - add_remote_address(post, options) + add_auth_purchase(post, -1, money, credit_card_or_reference, options) commit('SALE', post) end def purchase(money, credit_card_or_reference, options = {}) post = {} - add_pair(post, :captureDelay, 0) - add_amount(post, money, options) - add_invoice(post, credit_card_or_reference, money, options) - add_credit_card_or_reference(post, credit_card_or_reference) - add_customer_data(post, options) - add_remote_address(post, options) + add_auth_purchase(post, 0, money, credit_card_or_reference, options) commit('SALE', post) end def capture(money, authorization, options = {}) post = {} add_pair(post, :xref, authorization) - add_pair(post, :amount, amount(money), :required => true) + add_pair(post, :amount, localized_amount(money, options[:currency] || currency(money)), required: true) add_remote_address(post, options) commit('CAPTURE', post) @@ -184,6 +174,7 @@ def refund(money, authorization, options = {}) add_pair(post, :xref, authorization) add_amount(post, money, options) add_remote_address(post, options) + add_country_code(post, options) response = commit('REFUND_SALE', post) return response if response.success? @@ -203,7 +194,7 @@ def void(authorization, options = {}) commit('CANCEL', post) end - def verify(creditcard, options={}) + def verify(creditcard, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, creditcard, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -223,9 +214,21 @@ def scrub(transcript) private + def add_auth_purchase(post, pair_value, money, credit_card_or_reference, options) + add_pair(post, :captureDelay, pair_value) + add_amount(post, money, options) + add_invoice(post, credit_card_or_reference, money, options) + add_credit_card_or_reference(post, credit_card_or_reference) + add_customer_data(post, options) + add_remote_address(post, options) + add_country_code(post, options) + add_threeds_fields(post, options) + end + def add_amount(post, money, options) - add_pair(post, :amount, amount(money), :required => true) - add_pair(post, :currencyCode, currency_code(options[:currency] || currency(money))) + currency = options[:currency] || currency(money) + add_pair(post, :amount, localized_amount(money, currency), required: true) + add_pair(post, :currencyCode, currency_code(currency)) end def add_customer_data(post, options) @@ -241,16 +244,14 @@ def add_customer_data(post, options) end def add_invoice(post, credit_card_or_reference, money, options) - add_pair(post, :transactionUnique, options[:order_id], :required => true) - add_pair(post, :orderRef, options[:description] || options[:order_id], :required => true) + add_pair(post, :transactionUnique, options[:order_id], required: true) + 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 ['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, amount(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') @@ -266,32 +267,41 @@ def add_credit_card_or_reference(post, credit_card_or_reference) end def add_reference(post, reference) - add_pair(post, :xref, reference, :required => true) + add_pair(post, :xref, reference, required: true) end def add_credit_card(post, credit_card) - add_pair(post, :customerName, credit_card.name, :required => true) - add_pair(post, :cardNumber, credit_card.number, :required => true) + add_pair(post, :customerName, credit_card.name, required: true) + add_pair(post, :cardNumber, credit_card.number, required: true) + add_pair(post, :cardExpiryMonth, format(credit_card.month, :two_digits), required: true) + add_pair(post, :cardExpiryYear, format(credit_card.year, :two_digits), required: true) + add_pair(post, :cardCVV, credit_card.verification_value) + end - add_pair(post, :cardExpiryMonth, format(credit_card.month, :two_digits), :required => true) - add_pair(post, :cardExpiryYear, format(credit_card.year, :two_digits), :required => true) + def add_threeds_required(post, options) + add_pair(post, :threeDSRequired, options[:threeds_required] || @threeds_required ? 'Y' : 'N') + end - if requires_start_date_or_issue_number?(credit_card) - add_pair(post, :cardStartMonth, format(credit_card.start_month, :two_digits)) - add_pair(post, :cardStartYear, format(credit_card.start_year, :two_digits)) + def add_threeds_fields(post, options) + return unless three_d_secure = options[:three_d_secure] - add_pair(post, :cardIssueNumber, credit_card.issue_number) + add_pair(post, :threeDSEnrolled, formatted_enrollment(three_d_secure[:enrolled])) + if three_d_secure[:enrolled] == 'true' + add_pair(post, :threeDSAuthenticated, three_d_secure[:authentication_response_status]) + if three_d_secure[:authentication_response_status] == 'Y' + post[:threeDSECI] = three_d_secure[:eci] + post[:threeDSCAVV] = three_d_secure[:cavv] + post[:threeDSXID] = three_d_secure[:xid] || three_d_secure[:ds_transaction_id] + end end - - add_pair(post, :cardCVV, credit_card.verification_value) end - def add_threeds_required(post, options) - add_pair(post, :threeDSRequired, (options[:threeds_required] || @threeds_required) ? 'Y' : 'N') + def add_remote_address(post, options = {}) + add_pair(post, :remoteAddress, options[:ip] || '1.1.1.1') end - def add_remote_address(post, options={}) - add_pair(post, :remoteAddress, options[:ip] || '1.1.1.1') + def add_country_code(post, options) + post[:countryCode] = options[:country_code] || self.supported_countries[0] end def normalize_line_endings(str) @@ -317,23 +327,23 @@ def parse(body) end def commit(action, parameters) - parameters.update(:countryCode => self.supported_countries[0]) unless ['CAPTURE', 'CANCEL'].include?(action) parameters.update( - :merchantID => @options[:login], - :action => action + merchantID: @options[:login], + action: ) # adds a signature to the post hash/array add_hmac(parameters) response = parse(ssl_post(self.live_url, post_data(action, parameters))) - Response.new(response[:responseCode] == '0', - response[:responseCode] == '0' ? 'APPROVED' : response[:responseMessage], - response, - :test => test?, - :authorization => response[:xref], - :cvv_result => CVV_CODE[response[:avscv2ResponseCode].to_s[0, 1]], - :avs_result => avs_from(response) + Response.new( + response[:responseCode] == '0', + response[:responseCode] == '0' ? 'APPROVED' : response[:responseMessage], + response, + test: test?, + authorization: response[:xref], + cvv_result: CVV_CODE[response[:avscv2ResponseCode].to_s[0, 1]], + avs_result: avs_from(response) ) end @@ -342,23 +352,22 @@ def avs_from(response) street_match = AVS_STREET_MATCH[response[:avscv2ResponseCode].to_s[2, 1]] code = if postal_match == 'Y' && street_match == 'Y' - 'M' - elsif postal_match == 'Y' - 'P' - elsif street_match == 'Y' - 'A' - else - 'I' - end + 'M' + elsif postal_match == 'Y' + 'P' + elsif street_match == 'Y' + 'A' + else + 'I' + end AVSResult.new({ - :code => code, - :postal_match => postal_match, - :street_match => street_match + code:, + postal_match:, + street_match: }) end - def currency_code(currency) CURRENCY_CODES[currency] end @@ -371,6 +380,13 @@ def add_pair(post, key, value, options = {}) post[key] = value if !value.blank? || options[:required] end + def formatted_enrollment(val) + case val + when 'Y', 'N', 'U' then val + when true, 'true' then 'Y' + when false, 'false' then 'N' + end + end end end end diff --git a/lib/active_merchant/billing/gateways/cardknox.rb b/lib/active_merchant/billing/gateways/cardknox.rb index afafc938d9e..bce2f2b5e3e 100644 --- a/lib/active_merchant/billing/gateways/cardknox.rb +++ b/lib/active_merchant/billing/gateways/cardknox.rb @@ -1,11 +1,11 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class CardknoxGateway < Gateway self.live_url = 'https://x1.cardknox.com/gateway' - self.supported_countries = ['US','CA','GB'] + self.supported_countries = %w[US CA GB] self.default_currency = 'USD' - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb] + self.supported_cardtypes = %i[visa master american_express discover diners_club jcb] self.homepage_url = 'https://www.cardknox.com/' self.display_name = 'Cardknox' @@ -27,7 +27,7 @@ class CardknoxGateway < Gateway } } - def initialize(options={}) + def initialize(options = {}) requires!(options, :api_key) super end @@ -37,7 +37,7 @@ def initialize(options={}) # - check # - cardknox token, which is returned in the the authorization string "ref_num;token;command" - def purchase(amount, source, options={}) + def purchase(amount, source, options = {}) post = {} add_amount(post, amount, options) add_invoice(post, options) @@ -48,7 +48,7 @@ def purchase(amount, source, options={}) commit(:purchase, source_type(source), post) end - def authorize(amount, source, options={}) + def authorize(amount, source, options = {}) post = {} add_amount(post, amount) add_invoice(post, options) @@ -66,7 +66,7 @@ def capture(amount, authorization, options = {}) commit(:capture, source_type(authorization), post) end - def refund(amount, authorization, options={}) + def refund(amount, authorization, options = {}) post = {} add_reference(post, authorization) add_amount(post, amount) @@ -79,10 +79,10 @@ def void(authorization, options = {}) commit(:void, source_type(authorization), post) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| - r.process { authorize(100, credit_card, options) } - r.process(:ignore_result) { void(r.authorization, options) } + r.process { authorize(100, credit_card, options) } + r.process(:ignore_result) { void(r.authorization, options) } end end @@ -116,7 +116,7 @@ def split_authorization(authorization) end def add_reference(post, reference) - reference, _, _ = split_authorization(reference) + reference, = split_authorization(reference) post[:Refnum] = reference end @@ -186,7 +186,7 @@ def add_address_for_type(type, post, source, address) post[address_key(prefix, 'FirstName')] = address[:first_name] post[address_key(prefix, 'LastName')] = address[:last_name] end - post[address_key(prefix, 'MiddleName')] = address[:middle_name] + post[address_key(prefix, 'MiddleName')] = address[:middle_name] post[address_key(prefix, 'Company')] = address[:company] post[address_key(prefix, 'Street')] = address[:address1] @@ -247,7 +247,7 @@ def add_check(post, check) end def add_cardknox_token(post, authorization) - _, token, _ = split_authorization(authorization) + _, token, = split_authorization(authorization) post[:Token] = token end @@ -255,7 +255,7 @@ def add_cardknox_token(post, authorization) def parse(body) fields = {} for line in body.split('&') - key, value = *line.scan( %r{^(\w+)\=(.*)$} ).flatten + key, value = *line.scan(%r{^(\w+)\=(.*)$}).flatten fields[key] = CGI.unescape(value.to_s) end @@ -276,14 +276,13 @@ def parse(body) amount: fields['xAuthAmount'], masked_card_num: fields['xMaskedCardNumber'], masked_account_number: fields['MaskedAccountNumber'] - }.delete_if{|k, v| v.nil?} + }.delete_if { |_k, v| v.nil? } end - def commit(action, source_type, parameters) response = parse(ssl_post(live_url, post_data(COMMANDS[source_type][action], parameters))) - Response.new( + Response.new( (response[:status] == 'Approved'), message_from(response), response, @@ -312,8 +311,8 @@ def post_data(command, parameters = {}) Key: @options[:api_key], Version: '4.5.4', SoftwareName: 'Active Merchant', - SoftwareVersion: "#{ActiveMerchant::VERSION}", - Command: command, + SoftwareVersion: ActiveMerchant::VERSION.to_s, + Command: command } seed = SecureRandom.hex(32).upcase @@ -321,7 +320,7 @@ def post_data(command, parameters = {}) initial_parameters[:Hash] = "s/#{seed}/#{hash}/n" unless @options[:pin].blank? parameters = initial_parameters.merge(parameters) - parameters.reject{|k, v| v.blank?}.collect{ |key, value| "x#{key}=#{CGI.escape(value.to_s)}" }.join('&') + parameters.reject { |_k, v| v.blank? }.collect { |key, value| "x#{key}=#{CGI.escape(value.to_s)}" }.join('&') end end end diff --git a/lib/active_merchant/billing/gateways/cardprocess.rb b/lib/active_merchant/billing/gateways/cardprocess.rb index 020a39ec17f..1bd88304fa3 100644 --- a/lib/active_merchant/billing/gateways/cardprocess.rb +++ b/lib/active_merchant/billing/gateways/cardprocess.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class CardprocessGateway < Gateway self.test_url = 'https://test.vr-pay-ecommerce.de/v1/payments' self.live_url = 'https://vr-pay-ecommerce.de/v1/payments' @@ -8,7 +8,7 @@ class CardprocessGateway < Gateway MT HU NL AT PL PT RO SI SK FI SE GB IS LI NO CH ME MK AL RS TR BA ] self.default_currency = 'EUR' - self.supported_cardtypes = [:visa, :master, :american_express, :diners_club, :jcb] + self.supported_cardtypes = %i[visa master american_express diners_club jcb] self.homepage_url = 'https://vr-pay-ecommerce.docs.oppwa.com/' self.display_name = 'CardProcess VR-Pay' @@ -26,7 +26,7 @@ class CardprocessGateway < Gateway # * :user_id -- The CardProcess user ID # * :password -- The CardProcess password # * :entity_id -- The CardProcess channel or entity ID for any transactions - def initialize(options={}) + def initialize(options = {}) requires!(options, :user_id, :password, :entity_id) super # This variable exists purely to allow remote tests to force error codes; @@ -99,10 +99,10 @@ def supports_scrubbing? end def scrub(transcript) - transcript - .gsub(%r{(authentication\.[^=]+=)[^&]+}, '\1[FILTERED]') - .gsub(%r{(card\.number=)\d+}, '\1[FILTERED]') - .gsub(%r{(cvv=)\d{3,4}}, '\1[FILTERED]\2') + transcript. + gsub(%r{(authentication\.[^=]+=)[^&]+}, '\1[FILTERED]'). + gsub(%r{(card\.number=)\d+}, '\1[FILTERED]'). + gsub(%r{(cvv=)\d{3,4}}, '\1[FILTERED]\2') end private @@ -123,6 +123,7 @@ def add_address(post, _card, options) def add_invoice(post, money, options) return if money.nil? + post[:amount] = amount(money) post[:currency] = (options[:currency] || currency(money)) post[:merchantInvoiceId] = options[:merchant_invoice_id] if options[:merchant_invoice_id] @@ -132,6 +133,7 @@ def add_invoice(post, money, options) def add_payment(post, payment) return if payment.is_a?(String) + post[:paymentBrand] = payment.brand.upcase if payment.brand post[:card] ||= {} post[:card][:number] = payment.number @@ -189,7 +191,7 @@ def post_data(action, parameters = {}) post[:authentication][:password] = @options[:password] post[:authentication][:entityId] = @options[:entity_id] post[:paymentType] = action - dot_flatten_hash(post).map {|key, value| "#{key}=#{CGI.escape(value.to_s)}"}.join('&') + dot_flatten_hash(post).map { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&') end def error_code_from(response) diff --git a/lib/active_merchant/billing/gateways/cashnet.rb b/lib/active_merchant/billing/gateways/cashnet.rb index f3ae7dc87f5..64fe12d7c2d 100644 --- a/lib/active_merchant/billing/gateways/cashnet.rb +++ b/lib/active_merchant/billing/gateways/cashnet.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class CashnetGateway < Gateway include Empty @@ -7,11 +7,11 @@ class CashnetGateway < Gateway self.test_url = 'https://train.cashnet.com/' self.supported_countries = ['US'] - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb] - self.homepage_url = 'http://www.higherone.com/' + self.supported_cardtypes = %i[visa master american_express discover diners_club jcb] + self.homepage_url = 'https://transactcampus.com' self.display_name = 'Cashnet' self.money_format = :dollars - self.max_retries = 0 + self.max_retries = 1 # Creates a new CashnetGateway # @@ -41,7 +41,7 @@ def initialize(options = {}) def purchase(money, payment_object, options = {}) post = {} add_creditcard(post, payment_object) - add_invoice(post, options) + add_invoice(post, money, options) add_address(post, options) add_customer_data(post, options) commit('SALE', money, post) @@ -49,8 +49,8 @@ def purchase(money, payment_object, options = {}) def refund(money, identification, options = {}) post = {} - post[:origtx] = identification - add_invoice(post, options) + post[:origtx] = identification + add_invoice(post, money, options) add_customer_data(post, options) commit('REFUND', money, post) end @@ -60,23 +60,22 @@ def supports_scrubbing? end def scrub(transcript) - transcript - .gsub(%r{(password=)[^&]+}, '\1[FILTERED]') - .gsub(%r{(cardno=)[^&]+}, '\1[FILTERED]') - .gsub(%r{(cid=)[^&]+}, '\1[FILTERED]') + transcript. + gsub(%r{(password=)[^&]+}, '\1[FILTERED]'). + gsub(%r{(cardno=)[^&]+}, '\1[FILTERED]'). + gsub(%r{(cid=)[^&]+}, '\1[FILTERED]') end private def commit(action, money, fields) - fields[:amount] = amount(money) url = (test? ? test_url : live_url) + CGI.escape(@options[:merchant_gateway_name]) raw_response = ssl_post(url, post_data(action, fields)) parsed_response = parse(raw_response) return unparsable_response(raw_response) unless parsed_response - success = (parsed_response[:result] == '0') + success = success?(parsed_response) Response.new( success, CASHNET_CODES[parsed_response[:result]], @@ -86,8 +85,13 @@ def commit(action, money, fields) ) end + def success?(response) + response[:result] == '0' + end + def post_data(action, parameters = {}) post = {} + post[:command] = action post[:merchant] = @options[:merchant] post[:operator] = @options[:operator] @@ -106,9 +110,19 @@ def add_creditcard(post, creditcard) post[:lname] = creditcard.last_name end - def add_invoice(post, options) - post[:order_number] = options[:order_id] if options[:order_id].present? - post[:itemcode] = (options[:item_code] || @options[:default_item_code]) + def add_invoice(post, money, options) + post[:order_number] = options[:order_id] if options[:order_id].present? + + if options[:item_codes].present? + codes_and_amounts = options[:item_codes].transform_keys { |key| key.to_s.delete('_') } + codes_and_amounts.each do |key, value| + post[key] = value if key.start_with?('itemcode') + post[key] = amount(value.to_i) if key.start_with?('amount') + end + else + post[:itemcode] = (options[:item_code] || @options[:default_item_code]) + post[:amount] = amount(money.to_i) + end end def add_address(post, options) @@ -121,8 +135,8 @@ def add_address(post, options) end def add_customer_data(post, options) - post[:email_g] = options[:email] - post[:custcode] = options[:custcode] unless empty?(options[:custcode]) + post[:email_g] = options[:email] + post[:custcode] = options[:custcode] unless empty?(options[:custcode]) end def expdate(creditcard) @@ -136,15 +150,16 @@ 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) - if (200...300).include?(response.code.to_i) + if (200...300).cover?(response.code.to_i) return response.body elsif response.code.to_i == 302 return ssl_get(URI.parse(response['location'])) end + raise ResponseError.new(response) end @@ -190,6 +205,7 @@ def unparsable_response(raw_response) '215' => 'Old PIN does not validate ', '221' => 'Invalid credit card processor type specified in location or payment code', '222' => 'Credit card processor error', + '230' => 'Host Error (USE VOID OR REVERSAL TO REFUND UNSETTLED TRANSACTIONS)', '280' => 'SmartPay transaction not posted', '301' => 'Original transaction not found for this customer', '302' => 'Amount to refund exceeds original payment amount or is missing', diff --git a/lib/active_merchant/billing/gateways/cc5.rb b/lib/active_merchant/billing/gateways/cc5.rb index 869296dcfb9..0fe0d488363 100644 --- a/lib/active_merchant/billing/gateways/cc5.rb +++ b/lib/active_merchant/billing/gateways/cc5.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # CC5 API is used by many banks in Turkey. Extend this base class to provide # concrete implementations. class CC5Gateway < Gateway @@ -48,9 +48,9 @@ def credit(money, creditcard, options = {}) protected def build_sale_request(type, money, creditcard, options = {}) - requires!(options, :order_id) + requires!(options, :order_id) - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 xml.tag! 'CC5Request' do add_login_tags(xml) @@ -62,7 +62,7 @@ def build_sale_request(type, money, creditcard, options = {}) add_amount_tags(money, options, xml) xml.tag! 'Email', options[:email] if options[:email] - if(address = (options[:billing_address] || options[:address])) + if (address = (options[:billing_address] || options[:address])) xml.tag! 'BillTo' do add_address(xml, address) end @@ -70,14 +70,13 @@ def build_sale_request(type, money, creditcard, options = {}) add_address(xml, address) end end - end xml.target! end def build_capture_request(money, authorization, options = {}) - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 xml.tag! 'CC5Request' do add_login_tags(xml) @@ -88,7 +87,7 @@ def build_capture_request(money, authorization, options = {}) end def build_void_request(authorization, options = {}) - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 xml.tag! 'CC5Request' do add_login_tags(xml) @@ -98,7 +97,7 @@ def build_void_request(authorization, options = {}) end def build_authorization_credit_request(money, authorization, options = {}) - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 xml.tag! 'CC5Request' do add_login_tags(xml) @@ -109,7 +108,7 @@ def build_authorization_credit_request(money, authorization, options = {}) end def build_creditcard_credit_request(money, creditcard, options = {}) - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 xml.tag! 'CC5Request' do add_login_tags(xml) @@ -158,8 +157,8 @@ def commit(request) success, (success ? 'Approved' : "Declined (Reason: #{response[:proc_return_code]} - #{response[:err_msg]})"), response, - :test => test?, - :authorization => response[:order_id] + test: test?, + authorization: response[:order_id] ) end @@ -175,7 +174,7 @@ def parse(body) def parse_element(response, node) if node.has_elements? - node.elements.each{|element| parse_element(response, element) } + node.elements.each { |element| parse_element(response, element) } else response[node.name.underscore.to_sym] = node.text end @@ -189,7 +188,7 @@ def normalize(text) return unless text if ActiveSupport::Inflector.method(:transliterate).arity == -2 - ActiveSupport::Inflector.transliterate(text,'') + ActiveSupport::Inflector.transliterate(text, '') else text.gsub(/[^\x00-\x7F]+/, '') end diff --git a/lib/active_merchant/billing/gateways/cecabank.rb b/lib/active_merchant/billing/gateways/cecabank.rb index 8bb644aa847..18a9824f4af 100644 --- a/lib/active_merchant/billing/gateways/cecabank.rb +++ b/lib/active_merchant/billing/gateways/cecabank.rb @@ -1,238 +1,15 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: - class CecabankGateway < Gateway - self.test_url = 'http://tpv.ceca.es:8000' - self.live_url = 'https://pgw.ceca.es' - - self.supported_countries = ['ES'] - self.supported_cardtypes = [: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 = 'SHA1' - 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 = 'tpvanularparcialmente' #use partial refund's URL to avoid time frame limitations and decision logic on client side - CECA_ACTION_PURCHASE = 'tpv' - 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 - - 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' - if(root.elements['OPERACION/numeroAutorizacion']) - response[:authorization] = root.elements['OPERACION/numeroAutorizacion'].text - end - else - response[:authorization] = root.attributes['numeroOperacion'] - end - end - - return response +require 'active_merchant/billing/gateways/cecabank/cecabank_xml' +require 'active_merchant/billing/gateways/cecabank/cecabank_json' - 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) + "/cgi-bin/#{action}" - xml = ssl_post(url, post_data(parameters)) - response = parse(xml) - Response.new( - response[:success], - response[:message], - response, - :test => test?, - :authorization => build_authorization(response) - ) - 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 +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class CecabankGateway < Gateway + self.abstract_class = true - 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::SHA1.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..61014f131dc --- /dev/null +++ b/lib/active_merchant/billing/gateways/cecabank/cecabank_json.rb @@ -0,0 +1,328 @@ +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 + + WALLET_PAYMENT_METHODS = { + apple_pay: 'A', + google_pay: 'G' + } + + 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_payment_method(post, creditcard, options) + 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_payment_method(post, payment_method, options) + params = post[:parametros] ||= {} + three_d_secure = options.fetch(:three_d_secure, {}) + + pm = { + pan: payment_method.number, + caducidad: strftime_yyyymm(payment_method) + } + + if payment_method.is_a?(NetworkTokenizationCreditCard) && WALLET_PAYMENT_METHODS[payment_method.source.to_sym] + pm[:wallet] = { + + # the authentication value should come nil (for recurring cases) or should I remove it? + authentication_value: (payment_method.payment_cryptogram unless options.dig(:stored_credential, :network_transaction_id)), + xid: three_d_secure[:xid] || three_d_secure[:ds_transaction_id] || options[:xid], + walletType: WALLET_PAYMENT_METHODS[payment_method.source.to_sym], + eci: payment_method.eci || (three_d_secure[:eci] if three_d_secure) || '07' + }.compact.to_json + else + pm[CreditCard.brand?(payment_method.number) == 'american_express' ? :csc : :cvv2] = payment_method.verification_value + end + @options[:encryption_key] ? params[:encryptedData] = pm : params.merge!(pm) + 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/cenpos.rb b/lib/active_merchant/billing/gateways/cenpos.rb index f6030e64f5c..034d2cf714a 100644 --- a/lib/active_merchant/billing/gateways/cenpos.rb +++ b/lib/active_merchant/billing/gateways/cenpos.rb @@ -1,24 +1,25 @@ require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class CenposGateway < Gateway self.display_name = 'CenPOS' self.homepage_url = 'https://www.cenpos.com/' self.live_url = 'https://ww3.cenpos.net/6/transact.asmx' + self.test_url = 'https://abistaging.cenpos.net/6/transact.asmx' - self.supported_countries = %w(AD AI AG AR AU AT BS BB BE BZ BM BR BN BG CA HR CY CZ DK DM EE FI FR DE GR GD GY HK HU IS IN IL IT JP LV LI LT LU MY MT MX MC MS NL PA PL PT KN LC MF VC SM SG SK SI ZA ES SR SE CH TR GB US UY) + self.supported_countries = %w(AD AI AG AR AU AT BS BB BE BZ BM BR BN BG CA HR CY CZ DK DM EE FI FR DE GR GD GY HK HU IS IL IT JP LV LI LT LU MY MT MX MC MS NL PA PL PT KN LC MF VC SM SG SK SI ZA ES SR SE CH TR GB US UY) self.default_currency = 'USD' self.money_format = :dollars - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] - def initialize(options={}) + def initialize(options = {}) requires!(options, :merchant_id, :password, :user_id) super end - def purchase(amount, payment_method, options={}) + def purchase(amount, payment_method, options = {}) post = {} add_invoice(post, amount, options) add_payment_method(post, payment_method) @@ -27,7 +28,7 @@ def purchase(amount, payment_method, options={}) commit('Sale', post) end - def authorize(amount, payment_method, options={}) + def authorize(amount, payment_method, options = {}) post = {} add_invoice(post, amount, options) add_payment_method(post, payment_method) @@ -36,7 +37,7 @@ def authorize(amount, payment_method, options={}) commit('Auth', post) end - def capture(amount, authorization, options={}) + def capture(amount, authorization, options = {}) post = {} add_invoice(post, amount, options) add_reference(post, authorization) @@ -45,7 +46,7 @@ def capture(amount, authorization, options={}) commit('SpecialForce', post) end - def void(authorization, options={}) + def void(authorization, options = {}) post = {} add_void_required_elements(post) add_reference(post, authorization) @@ -56,7 +57,7 @@ def void(authorization, options={}) commit('Void', post) end - def refund(amount, authorization, options={}) + def refund(amount, authorization, options = {}) post = {} add_invoice(post, amount, options) add_reference(post, authorization) @@ -65,7 +66,7 @@ def refund(amount, authorization, options={}) commit('SpecialReturn', post) end - def credit(amount, payment_method, options={}) + def credit(amount, payment_method, options = {}) post = {} add_invoice(post, amount, options) add_payment_method(post, payment_method) @@ -73,7 +74,7 @@ def credit(amount, payment_method, options={}) commit('Credit', post) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -112,7 +113,7 @@ def add_payment_method(post, payment_method) end def add_customer_data(post, options) - if(billing_address = (options[:billing_address] || options[:address])) + if (billing_address = (options[:billing_address] || options[:address])) post[:CustomerEmailAddress] = billing_address[:email] post[:CustomerPhone] = billing_address[:phone] post[:CustomerBillingAddress] = billing_address[:address1] @@ -145,6 +146,10 @@ def add_remembered_amount(post, authorization) post[:Amount] = split_authorization(authorization).last end + def url + test? ? test_url : live_url + end + def commit(action, post) post[:MerchantId] = @options[:merchant_id] post[:Password] = @options[:password] @@ -153,10 +158,10 @@ def commit(action, post) data = build_request(post) begin - xml = ssl_post(self.live_url, data, headers) + xml = ssl_post(url, data, headers) raw = parse(xml) rescue ActiveMerchant::ResponseError => e - if(e.response.code == '500' && e.response.body.start_with?(' 'identity', - 'Content-Type' => 'text/xml;charset=UTF-8', - 'SOAPAction' => 'http://tempuri.org/Transactional/ProcessCreditCard' + 'Content-Type' => 'text/xml;charset=UTF-8', + 'SOAPAction' => 'http://tempuri.org/Transactional/ProcessCreditCard' } end def build_request(post) - xml = Builder::XmlMarkup.new :indent => 8 + xml = Builder::XmlMarkup.new indent: 8 xml.tag!('acr:MerchantId', post.delete(:MerchantId)) xml.tag!('acr:Password', post.delete(:Password)) xml.tag!('acr:UserId', post.delete(:UserId)) @@ -198,18 +203,18 @@ def build_request(post) end def envelope(body) - <<-EOS - - - - - - #{body} - - - - - EOS + <<~XML + + + + + + #{body} + + + + + XML end def parse(xml) @@ -219,7 +224,7 @@ def parse(xml) doc.remove_namespaces! body = doc.xpath('//ProcessCreditCardResult') body.children.each do |node| - if (node.elements.size == 0) + if node.elements.size == 0 response[node.name.underscore.to_sym] = node.text else node.elements.each do |childnode| @@ -250,11 +255,11 @@ def message_from(succeeded, response) '257' => STANDARD_ERROR_CODE[:invalid_cvc], '333' => STANDARD_ERROR_CODE[:expired_card], '1' => STANDARD_ERROR_CODE[:card_declined], - '99' => STANDARD_ERROR_CODE[:processing_error], + '99' => STANDARD_ERROR_CODE[:processing_error] } def authorization_from(request, response) - [ response[:reference_number], request[:CardLastFourDigits], request[:Amount] ].join('|') + [response[:reference_number], request[:CardLastFourDigits], request[:Amount]].join('|') end def split_authorization(authorization) @@ -277,6 +282,7 @@ def avs_result_from_xml(xml) def cvv_result_code(xml) cvv = validation_result_element(xml, 'CVV') return nil unless cvv + validation_result_matches?(*validation_result_element_text(cvv.parent)) ? 'M' : 'N' end @@ -313,7 +319,7 @@ def validation_result_element(xml, name) def validation_result_element_text(element) result_text = element.elements.detect { |elem| elem.name == 'Result' - }.children.detect { |elem| elem.text }.text + }.children.detect(&:text).text result_text.split(';').collect(&:strip) end diff --git a/lib/active_merchant/billing/gateways/checkout.rb b/lib/active_merchant/billing/gateways/checkout.rb index 51cf3919292..f2eba33f1b2 100644 --- a/lib/active_merchant/billing/gateways/checkout.rb +++ b/lib/active_merchant/billing/gateways/checkout.rb @@ -1,14 +1,14 @@ require 'rubygems' require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class CheckoutGateway < Gateway self.default_currency = 'USD' self.money_format = :cents - self.supported_countries = ['AD', 'AT', 'BE', 'BG', 'CH', 'CY', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FO', 'FI', 'FR', 'GB', 'GI', 'GL', 'GR', 'HR', 'HU', 'IE', 'IS', 'IL', 'IT', 'LI', 'LT', 'LU', 'LV', 'MC', 'MT', 'NL', 'NO', 'PL', 'PT', 'RO', 'SE', 'SI', 'SM', 'SK', 'SJ', 'TR', 'VA'] - self.supported_cardtypes = [:visa, :master, :american_express, :diners_club] + self.supported_countries = %w[AD AT BE BG CH CY CZ DE DK EE ES FO FI FR GB GI GL GR HR HU IE IS IL IT LI LT LU LV MC MT NL NO PL PT RO SE SI SM SK SJ TR VA] + self.supported_cardtypes = %i[visa master american_express diners_club] self.homepage_url = 'https://www.checkout.com/' self.display_name = 'Checkout.com' @@ -70,7 +70,7 @@ def void(authorization, options = {}) _, _, orig_action, amount, currency = split_authorization(authorization) commit("void_#{orig_action}") do |xml| add_credentials(xml, options) - add_invoice(xml, amount.to_i, options.merge(currency: currency)) + add_invoice(xml, amount.to_i, options.merge(currency:)) add_reference(xml, authorization) end end @@ -83,7 +83,7 @@ def refund(amount, authorization, options = {}) end end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -107,9 +107,7 @@ def add_payment_method(xml, payment_method) xml.bill_cc_ payment_method.number xml.bill_expmonth_ format(payment_method.month, :two_digits) xml.bill_expyear_ format(payment_method.year, :four_digits) - if payment_method.verification_value? - xml.bill_cvv2_ payment_method.verification_value - end + xml.bill_cvv2_ payment_method.verification_value if payment_method.verification_value? end def add_billing_info(xml, options) @@ -125,13 +123,13 @@ def add_billing_info(xml, options) def add_shipping_info(xml, options) if options[:shipping_address] - xml.ship_address_ options[:shipping_address][:address1] - xml.ship_address2_ options[:shipping_address][:address2] - xml.ship_city_ options[:shipping_address][:city] - xml.ship_state_ options[:shipping_address][:state] - xml.ship_postal_ options[:shipping_address][:zip] - xml.ship_country_ options[:shipping_address][:country] - xml.ship_phone_ options[:shipping_address][:phone] + xml.ship_address_ options[:shipping_address][:address1] + xml.ship_address2_ options[:shipping_address][:address2] + xml.ship_city_ options[:shipping_address][:city] + xml.ship_state_ options[:shipping_address][:state] + xml.ship_postal_ options[:shipping_address][:zip] + xml.ship_country_ options[:shipping_address][:country] + xml.ship_phone_ options[:shipping_address][:phone] end end @@ -144,7 +142,7 @@ def add_user_defined_fields(xml, options) end def add_other_fields(xml, options) - xml.bill_email_ options[:email] + xml.bill_email_ options[:email] xml.bill_customerip_ options[:ip] xml.merchantcustomerid_ options[:customer] xml.descriptor_name options[:descriptor_name] @@ -152,7 +150,7 @@ def add_other_fields(xml, options) end def add_reference(xml, authorization) - transid, trackid, _, _, _ = split_authorization(authorization) + transid, trackid, = split_authorization(authorization) xml.transid transid add_track_id(xml, trackid) end @@ -161,8 +159,8 @@ def add_track_id(xml, trackid) xml.trackid(trackid) if trackid end - def commit(action, amount=nil, options={}, &builder) - response = parse_xml(ssl_post(live_url, build_xml(action, &builder))) + def commit(action, amount = nil, options = {}, &) + response = parse_xml(ssl_post(live_url, build_xml(action, &))) Response.new( (response[:responsecode] == '0'), (response[:result] || response[:error_text] || 'Unknown Response'), @@ -187,7 +185,7 @@ def parse_xml(xml) Nokogiri::XML(CGI.unescapeHTML(xml)).xpath('//response').children.each do |node| if node.text? next - elsif (node.elements.size == 0) + elsif node.elements.size == 0 response[node.name.downcase.to_sym] = node.text else node.elements.each do |childnode| @@ -200,7 +198,7 @@ def parse_xml(xml) response end - def authorization_from(response, action, amount, options) + def authorization_from(response, action, amount, options) currency = options[:currency] || currency(amount) [response[:tranid], response[:trackid], action, amount, currency].join('|') end diff --git a/lib/active_merchant/billing/gateways/checkout_v2.rb b/lib/active_merchant/billing/gateways/checkout_v2.rb index 7d1f688f971..717b033ce79 100644 --- a/lib/active_merchant/billing/gateways/checkout_v2.rb +++ b/lib/active_merchant/billing/gateways/checkout_v2.rb @@ -1,70 +1,94 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class CheckoutV2Gateway < Gateway - self.display_name = 'Checkout.com V2 Gateway' + self.display_name = 'Checkout.com Unified Payments' self.homepage_url = 'https://www.checkout.com/' - self.live_url = 'https://api2.checkout.com/v2' - self.test_url = 'https://sandbox.checkout.com/api2/v2' + self.live_url = 'https://api.checkout.com' + self.test_url = 'https://api.sandbox.checkout.com' - self.supported_countries = ['AD', 'AE', 'AT', 'BE', 'BG', 'CH', 'CY', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FO', 'FI', 'FR', 'GB', 'GI', 'GL', 'GR', 'HR', 'HU', 'IE', 'IS', 'IL', 'IT', 'LI', 'LT', 'LU', 'LV', 'MC', 'MT', 'NL', 'NO', 'PL', 'PT', 'RO', 'SE', 'SI', 'SM', 'SK', 'SJ', 'TR', 'VA'] + self.supported_countries = %w[AD AE AR AT AU BE BG BH BR CH CL CN CO CY CZ DE DK EE EG ES FI FR GB GR HK HR HU IE IS IT JO JP KW LI LT LU LV MC MT MX MY NL NO NZ OM PE PL PT QA RO SA SE SG SI SK SM TR US] self.default_currency = 'USD' self.money_format = :cents - self.supported_cardtypes = [:visa, :master, :american_express, :diners_club] + self.supported_cardtypes = %i[visa master american_express diners_club maestro discover jcb mada bp_plus patagonia_365 tarjeta_sol] + self.currencies_without_fractions = %w(BIF DJF GNF ISK KMF XAF CLF XPF JPY PYG RWF KRW VUV VND XOF) + self.currencies_with_three_decimal_places = %w(BHD LYD JOD KWD OMR TND) + + LIVE_ACCESS_TOKEN_URL = 'https://access.checkout.com/connect/token' + TEST_ACCESS_TOKEN_URL = 'https://access.sandbox.checkout.com/connect/token' + + def initialize(options = {}) + options.has_key?(:secret_key) ? requires!(options, :secret_key) : requires!(options, :client_id, :client_secret) - def initialize(options={}) - requires!(options, :secret_key) super end - def purchase(amount, payment_method, options={}) - multi = MultiResponse.run do |r| - r.process { authorize(amount, payment_method, options) } - r.process { capture(amount, r.authorization, options) } - end + def purchase(amount, payment_method, options = {}) + post = {} + build_auth_or_purchase(post, amount, payment_method, options) + + commit(:purchase, post, options) + end - merged_params = multi.responses.map { |r| r.params }.reduce({}, :merge) - succeeded = success_from(merged_params) + def authorize(amount, payment_method, options = {}) + post = {} + post[:capture] = false + build_auth_or_purchase(post, amount, payment_method, options) - response(:purchase, succeeded, merged_params) + options[:incremental_authorization] ? commit(:incremental_authorize, post, options, options[:incremental_authorization]) : commit(:authorize, post, options) end - def authorize(amount, payment_method, options={}) + def capture(amount, authorization, options = {}) post = {} - post[:autoCapture] = 'n' + post[:capture_type] = options[:capture_type] || 'Final' add_invoice(post, amount, options) - add_payment_method(post, payment_method) add_customer_data(post, options) - add_transaction_data(post, options) + add_shipping_address(post, options) + add_metadata(post, options) + add_level_two_three_data(post, options) - commit(:authorize, post) + commit(:capture, post, options, authorization) end - def capture(amount, authorization, options={}) + def credit(amount, payment, options = {}) post = {} + add_processing_channel(post, options) add_invoice(post, amount, options) - add_customer_data(post, options) - - commit(:capture, post, authorization) + 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) + add_metadata(post, options) + + commit(:credit, post, options) end - def void(authorization, options={}) + def void(authorization, _options = {}) post = {} - commit(:void, post, authorization) + add_metadata(post, options) + + commit(:void, post, options, authorization) end - def refund(amount, authorization, options={}) + def refund(amount, authorization, options = {}) post = {} add_invoice(post, amount, 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={}) - MultiResponse.run(:use_first_response) do |r| - r.process { authorize(100, credit_card, options) } - r.process(:ignore_result) { void(r.authorization, options) } - end + def verify(credit_card, options = {}) + authorize(0, credit_card, options) + end + + def inquire(authorization, options = {}) + verify_payment(authorization, {}) + end + + def verify_payment(authorization, options = {}) + commit(:verify_payment, nil, options, authorization, :get) end def supports_scrubbing? @@ -73,96 +97,603 @@ def supports_scrubbing? def scrub(transcript) transcript. - gsub(%r((Authorization: )[^\\]*)i, '\1[FILTERED]'). - gsub(%r(("number\\":\\")\d+), '\1[FILTERED]'). - gsub(%r(("cvv\\":\\")\d+), '\1[FILTERED]') + gsub(/(Authorization: )[^\\]*/i, '\1[FILTERED]'). + gsub(/("number\\":\\")\d+/, '\1[FILTERED]'). + gsub(/("cvv\\":\\")\d+/, '\1[FILTERED]'). + gsub(/("cryptogram\\":\\")\w+/, '\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 + def build_auth_or_purchase(post, amount, payment_method, options) + add_invoice(post, amount, options) + add_authorization_type(post, options) + add_payment_method(post, payment_method, options) + add_customer_data(post, options) + add_extra_customer_data(post, payment_method, options) + add_shipping_address(post, options) + add_stored_credential_options(post, options) + add_transaction_data(post, options) + add_3ds(post, options) + add_metadata(post, options, 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) + add_risk_data(post, options) + add_level_two_three_data(post, options) + truncate_amex_reference_id(post, options, payment_method) + end + def add_invoice(post, money, options) - post[:value] = localized_amount(money, options[:currency]) - post[:trackId] = options[:order_id] + post[:amount] = localized_amount(money, options[:currency]) + post[:reference] = options[:order_id] post[:currency] = options[:currency] || currency(money) - post[:descriptor] = {} - post[:descriptor][:name] = options[:descriptor_name] if options[:descriptor_name] - post[:descriptor][:city] = options[:descriptor_city] if options[:descriptor_city] + if options[:descriptor_name] || options[:descriptor_city] + post[:billing_descriptor] = {} + post[:billing_descriptor][:name] = options[:descriptor_name] if options[:descriptor_name] + post[:billing_descriptor][:city] = options[:descriptor_city] if options[:descriptor_city] + end + post[:metadata] = {} + post[:metadata][:udf5] = application_id || 'ActiveMerchant' end - def add_payment_method(post, payment_method) - post[:card] = {} - post[:card][:name] = payment_method.name - post[:card][:number] = payment_method.number - post[:card][:cvv] = payment_method.verification_value - post[:card][:expiryYear] = format(payment_method.year, :four_digits) - post[:card][:expiryMonth] = format(payment_method.month, :two_digits) + def truncate_amex_reference_id(post, options, payment_method) + post[:reference] = truncate(options[:order_id], 30) if payment_method.respond_to?(:brand) && payment_method.brand == 'american_express' + 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_risk_data(post, options) + return unless options[:risk].is_a?(Hash) + + risk = options[:risk] + post[:risk] = {} unless risk.empty? + + if risk[:enabled].to_s == 'true' + post[:risk][:enabled] = true + post[:risk][:device_session_id] = risk[:device_session_id] if risk[:device_session_id] + elsif risk[:enabled].to_s == 'false' + post[:risk][:enabled] = false + end + 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 + + def add_metadata(post, options, payment_method = nil) + post[:metadata] = {} unless post[:metadata] + post[:metadata].merge!(options[:metadata]) if options[:metadata] + post[:metadata][:udf1] = 'mada' if payment_method.try(:brand) == 'mada' + end + + def add_payment_method(post, payment_method, options, key = :source) + # the key = :destination when this method is called in def credit + post[key] = {} + case payment_method + when NetworkTokenizationCreditCard + token_type = token_type_from(payment_method) + cryptogram = payment_method.payment_cryptogram + eci = payment_method.eci || options[:eci] + eci ||= '05' if token_type == 'vts' + + post[key][:type] = 'network_token' + post[key][:token] = payment_method.number + post[key][:token_type] = token_type + post[key][:cryptogram] = cryptogram if cryptogram + post[key][:eci] = eci if eci + when ->(pm) { pm.try(:credit_card?) } + post[key][:type] = 'card' + post[key][:name] = payment_method.name + post[key][:number] = payment_method.number + post[key][:cvv] = payment_method.verification_value unless options[:funds_transfer_type] + post[key][:stored] = 'true' if options[:card_on_file] == true + + # because of the way the key = is implemented in the method signature, some of the destination + # data will be added here, some in the destination specific method below. + # at first i was going to move this, but since this data is coming from the payment method + # i think it makes sense to leave it + if options[:account_holder_type] + post[key][:account_holder] = {} + post[key][:account_holder][:type] = options[:account_holder_type] + + if options[:account_holder_type] == 'corporate' || options[:account_holder_type] == 'government' + post[key][:account_holder][:company_name] = payment_method.name if payment_method.respond_to?(:name) + else + post[key][:account_holder][:first_name] = payment_method.first_name if payment_method.first_name + post[key][:account_holder][:last_name] = payment_method.last_name if payment_method.last_name + end + else + post[key][:first_name] = payment_method.first_name if payment_method.first_name + post[key][:last_name] = payment_method.last_name if payment_method.last_name + end + end + if payment_method.is_a?(String) + if /tok/.match?(payment_method) + post[:type] = 'token' + post[:token] = payment_method + elsif /src/.match?(payment_method) + post[key][:type] = 'id' + post[key][:id] = payment_method + else + add_source(post, options) + end + elsif payment_method.try(:year) + post[key][:expiry_year] = format(payment_method.year, :four_digits) + post[key][:expiry_month] = format(payment_method.month, :two_digits) + end + end + + def add_source(post, options) + post[:source] = {} + post[:source][:type] = options[:source_type] if options[:source_type] + post[:source][:id] = options[:source_id] if options[:source_id] end def add_customer_data(post, options) - post[:email] = options[:email] || 'unspecified@example.com' - post[:customerIp] = options[:ip] if options[:ip] + post[:customer] = {} + post[:customer][:email] = options[:email] || nil + post[:payment_ip] = options[:ip] if options[:ip] address = options[:billing_address] - if(address && post[:card]) - post[:card][:billingDetails] = {} - post[:card][:billingDetails][:addressLine1] = address[:address1] - post[:card][:billingDetails][:addressLine2] = address[:address2] - post[:card][:billingDetails][:city] = address[:city] - post[:card][:billingDetails][:state] = address[:state] - post[:card][:billingDetails][:country] = address[:country] - post[:card][:billingDetails][:postcode] = address[:zip] - post[:card][:billingDetails][:phone] = { number: address[:phone] } unless address[:phone].blank? + if address && post[:source] + post[:source][:billing_address] = {} + post[:source][:billing_address][:address_line1] = address[:address1] unless address[:address1].blank? + post[:source][:billing_address][:address_line2] = address[:address2] unless address[:address2].blank? + post[:source][:billing_address][:city] = address[:city] unless address[:city].blank? + post[:source][:billing_address][:state] = address[:state] unless address[:state].blank? + post[:source][:billing_address][:country] = address[:country] unless address[:country].blank? + post[:source][:billing_address][:zip] = address[:zip] unless address[:zip].blank? + 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 + post[:payment_type] = 'MOTO' if options[:transaction_indicator] == 3 || options.dig(:metadata, :manual_entry) + post[:previous_payment_id] = options[:previous_charge_id] if options[:previous_charge_id] + end + + def merchant_initiated_override(post, options) + post[:payment_type] ||= 'Regular' + post[:merchant_initiated] = true + post[:source][:stored] = true + post[:previous_payment_id] = options[:merchant_initiated_transaction_id] + end + + def add_stored_credentials_using_normalized_fields(post, options) + if options[:stored_credential][:initiator] == 'cardholder' + post[:merchant_initiated] = false + else + post[:source][:stored] = true + post[:previous_payment_id] = options[:stored_credential][:network_transaction_id] if options[:stored_credential][:network_transaction_id] + post[:merchant_initiated] = true + end + end + + def add_stored_credential_options(post, options = {}) + return unless options[:stored_credential] + + post[:payment_type] = options[:stored_credential][:reason_type]&.capitalize + + if options[:merchant_initiated_transaction_id] + merchant_initiated_override(post, options) + else + add_stored_credentials_using_normalized_fields(post, options) + end + end + + def add_3ds(post, options) + if options[:three_d_secure] || options[:execute_threed] + post[:'3ds'] = {} + post[:'3ds'][:enabled] = true + post[:success_url] = options[:callback_url] if options[:callback_url] + post[:failure_url] = options[:callback_url] if options[:callback_url] + post[:'3ds'][:attempt_n3d] = options[:attempt_n3d] if options[:attempt_n3d] + post[:'3ds'][:challenge_indicator] = options[:challenge_indicator] if options[:challenge_indicator] + post[:'3ds'][:exemption] = options[:exemption] if options[:exemption] + end + + if options[:three_d_secure] + post[:'3ds'][:eci] = options[:three_d_secure][:eci] if options[:three_d_secure][:eci] + post[:'3ds'][:cryptogram] = options[:three_d_secure][:cavv] if options[:three_d_secure][:cavv] + post[:'3ds'][:version] = options[:three_d_secure][:version] if options[:three_d_secure][:version] + post[:'3ds'][:xid] = options[:three_d_secure][:ds_transaction_id] || options[:three_d_secure][:xid] + post[:'3ds'][:status] = options[:three_d_secure][:authentication_response_status] + end + end + + 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] = {} + post[:marketplace][:sub_entity_id] = options[:marketplace][:sub_entity_id] if options[:marketplace][:sub_entity_id] + end + end + + def add_level_two_three_data(post, options) + post[:processing] ||= {} + post[:customer] ||= {} + + # American Express only supports Level 2 data. + # Only is required add items info in lvl2 data for amex + add_items(post, options) + add_level_two_data(post, options) + add_level_three_data(post, options) + add_shipping_data(post, options) + end + + def add_items(post, options) + items = build_items(options[:line_items] || []) + post[:items] = items unless items.empty? + end + + def add_level_two_data(post, options) + post[:customer][:tax_number] = options[:tax_number] # field no require for amex + + post[:processing].merge!( + { + order_id: options[:invoice_id], + tax_amount: options[:tax_amount] + }.compact + ) + end + + def add_level_three_data(post, options) + post[:processing].merge!( + { + discount_amount: options[:discount_amount], + shipping_amount: options[:shipping_amount], + duty_amount: options[:duty_amount] + }.compact + ) + end + + def add_shipping_data(post, options) + post[:shipping] ||= {} + post[:shipping][:from_address_zip] = options[:from_address_zip] + end + + def build_items(line_items = []) + line_items.map do |item| + { + # for lvl 2 amex and lvl 3 visa/master + name: item[:name], + quantity: item[:quantity], + unit_price: item[:unit_price], + # for lvl3 visa/master + reference: item[:reference], + tax_amount: item[:tax_amount], + discount_amount: item[:discount_amount], + total_amount: item[:total_amount], + commodity_code: item[:commodity_code], + unit_of_measure: item[:unit_of_measure] + }.compact end end - def add_transaction_data(post, options={}) - post[:cardOnFile] = true if options[:card_on_file] == true - post[:transactionIndicator] = options[:transaction_indicator] || 1 - post[:previousChargeId] = options[:previous_charge_id] if options[:previous_charge_id] + def access_token_header + { + 'Authorization' => "Basic #{Base64.encode64("#{@options[:client_id]}:#{@options[:client_secret]}").delete("\n")}", + 'Content-Type' => 'application/x-www-form-urlencoded' + } end - def commit(action, post, authorization = nil) + 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 + 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 perform_request(action, post, options, authorization = nil, method = :post) begin - raw_response = 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 - raise unless(e.response.code.to_s =~ /4\d\d/) - response = parse(e.response.body) + @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) end - succeeded = success_from(response) + 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:, + 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), + pending: pending_result(response, action) ) end - def headers - { - 'Authorization' => @options[:secret_key], - 'Content-Type' => 'application/json;charset=UTF-8' + def pending_result(response, action) + return unless action == :credit + + response['status'] == 'Pending' + end + + 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 action == :authorize - "#{base_url}/charges/card" + 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" + when :unstore, :store + "#{base_url}/instruments/#{authorization}" + when :capture + "#{base_url}/payments/#{authorization}/captures" + when :refund + "#{base_url}/payments/#{authorization}/refunds" + when :void + "#{base_url}/payments/#{authorization}/voids" + when :incremental_authorize + "#{base_url}/payments/#{authorization}/authorizations" + when :tokens + "#{base_url}/tokens" + when :verify_payment + "#{base_url}/payments/#{authorization}" else - "#{base_url}/charges/#{authorization}/#{action}" + "#{base_url}/payments/#{authorization}/#{action}" end end @@ -171,33 +702,45 @@ def base_url end def avs_result(response) - response['card'] && response['card']['avsCheck'] ? AVSResult.new(code: response['card']['avsCheck']) : nil + response.respond_to?(:dig) && response.dig('source', 'avs_check') ? AVSResult.new(code: response['source']['avs_check']) : nil end def cvv_result(response) - response['card'] && response['card']['cvvCheck'] ? CVVResult.new(response['card']['cvvCheck']) : nil + response.respond_to?(:dig) && response.dig('source', 'cvv_check') ? CVVResult.new(response['source']['cvv_check']) : nil end - def parse(body) + def parse(body, error: nil) JSON.parse(body) - rescue JSON::ParserError - { - 'message' => 'Invalid JSON response received from CheckoutV2Gateway. Please contact CheckoutV2Gateway if you continue to receive this message.', - 'raw_response' => scrub(body) - } + rescue JSON::ParserError + response = { + 'error_type' => error&.code, + 'message' => 'Invalid JSON response received from Checkout.com Unified Payments Gateway. Please contact Checkout.com if you continue to receive this message.', + 'raw_response' => scrub(body) + } + response['error_codes'] = [error&.message] if error&.message + response end - def success_from(response) - (response['responseCode'] == '10000' && !response['responseMessage'].start_with?('40')) || response['responseCode'] == '10100' + 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['errors'] - response['message'] + ': ' + response['errors'].first + elsif response['error_type'] + return response['error_type'] unless response['error_codes'] + + "#{response['error_type']}: #{response['error_codes'].first}" else - response['responseMessage'] || response['message'] || 'Unable to read error message' + response_summary = response['response_summary'] || response.dig('actions', 0, 'response_summary') + response_summary || response['response_code'] || response['status'] || response['message'] || 'Unable to read error message' end end @@ -218,14 +761,38 @@ 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['errorCode'] && response['errorMessageCodes'] - "#{response["errorCode"]}: #{response["errorMessageCodes"].join(", ")}" - elsif response['errorCode'] - response['errorCode'] + + if response['error_type'] && response['error_codes'] + "#{response['error_type']}: #{response['error_codes'].join(', ')}" + elsif response['error_type'] + response['error_type'] + else + response_code = response['response_code'] || response.dig('actions', 0, 'response_code') + + STANDARD_ERROR_CODE_MAPPING[response_code] + end + end + + def token_type_from(payment_method) + case payment_method.source + when :network_token + payment_method.brand == 'visa' ? 'vts' : 'mdes' + when :google_pay, :android_pay + 'googlepay' + when :apple_pay + '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 - STANDARD_ERROR_CODE_MAPPING[response['responseCode']] + raise ResponseError.new(response) end end end diff --git a/lib/active_merchant/billing/gateways/citrus_pay.rb b/lib/active_merchant/billing/gateways/citrus_pay.rb index f8661e23e1d..b7f30ac2d89 100644 --- a/lib/active_merchant/billing/gateways/citrus_pay.rb +++ b/lib/active_merchant/billing/gateways/citrus_pay.rb @@ -15,9 +15,7 @@ class CitrusPayGateway < Gateway self.homepage_url = 'http://www.citruspay.com/' self.supported_countries = %w(AR AU BR FR DE HK MX NZ SG GB US) self.default_currency = 'USD' - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb, :maestro, :laser] - + self.supported_cardtypes = %i[visa master american_express discover diners_club jcb maestro] end end end - diff --git a/lib/active_merchant/billing/gateways/clearhaus.rb b/lib/active_merchant/billing/gateways/clearhaus.rb index 43b68fb56c4..bfcaef1c1de 100644 --- a/lib/active_merchant/billing/gateways/clearhaus.rb +++ b/lib/active_merchant/billing/gateways/clearhaus.rb @@ -1,15 +1,15 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class ClearhausGateway < Gateway self.test_url = 'https://gateway.test.clearhaus.com' self.live_url = 'https://gateway.clearhaus.com' - self.supported_countries = ['DK', 'NO', 'SE', 'FI', 'DE', 'CH', 'NL', 'AD', 'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'FO', 'GL', 'EE', 'FR', 'GR', - 'HU', 'IS', 'IE', 'IT', 'LV', 'LI', 'LT', 'LU', 'MT', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'GB'] + self.supported_countries = %w[DK NO SE FI DE CH NL AD AT BE BG HR CY CZ FO GL EE FR GR + HU IS IE IT LV LI LT LU MT PL PT RO SK SI ES GB] self.default_currency = 'EUR' - self.currencies_without_fractions = %w(BIF BYR DJF GNF JPY KMF KRW PYG RWF VND VUV XAF XOF XPF) - self.supported_cardtypes = [:visa, :master] + self.currencies_without_fractions = %w(BIF CLP DJF GNF JPY KMF KRW PYG RWF UGX VND VUV XAF XOF XPF) + self.supported_cardtypes = %i[visa master] self.homepage_url = 'https://www.clearhaus.com' self.display_name = 'Clearhaus' @@ -36,46 +36,47 @@ class ClearhausGateway < Gateway 50000 => 'Clearhaus error' } - def initialize(options={}) + def initialize(options = {}) requires!(options, :api_key) options[:private_key] = options[:private_key].strip if options[:private_key] super end - def purchase(amount, payment, options={}) + def purchase(amount, payment, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(amount, payment, options) } r.process { capture(amount, r.authorization, options) } end end - def authorize(amount, payment, options={}) + def authorize(amount, payment, options = {}) post = {} add_invoice(post, amount, options) - action = if payment.respond_to?(:number) - add_payment(post, payment) - '/authorizations' - elsif payment.kind_of?(String) - "/cards/#{payment}/authorizations" - else - raise ArgumentError.new("Unknown payment type #{payment.inspect}") - end + action = + if payment.respond_to?(:number) + add_payment(post, payment) + '/authorizations' + elsif payment.kind_of?(String) + "/cards/#{payment}/authorizations" + else + raise ArgumentError.new("Unknown payment type #{payment.inspect}") + end post[:recurring] = options[:recurring] if options[:recurring] - post[:threed_secure] = {pares: options[:pares]} if options[:pares] + post[:card][:pares] = options[:pares] if options[:pares] commit(action, post) end - def capture(amount, authorization, options={}) + def capture(amount, authorization, options = {}) post = {} add_invoice(post, amount, options) commit("/authorizations/#{authorization}/captures", post) end - def refund(amount, authorization, options={}) + def refund(amount, authorization, options = {}) post = {} add_amount(post, amount, options) @@ -86,14 +87,14 @@ def void(authorization, options = {}) commit("/authorizations/#{authorization}/voids", options) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| - r.process { authorize(100, credit_card, options) } + r.process { authorize(0, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } end end - def store(credit_card, options={}) + def store(credit_card, options = {}) post = {} add_payment(post, credit_card) @@ -108,7 +109,7 @@ def scrub(transcript) transcript. gsub(%r((Authorization: Basic )[\w=]+), '\1[FILTERED]'). gsub(%r((&?card(?:\[|%5B)csc(?:\]|%5D)=)[^&]*)i, '\1[FILTERED]'). - gsub(%r((&?card(?:\[|%5B)number(?:\]|%5D)=)[^&]*)i, '\1[FILTERED]') + gsub(%r((&?card(?:\[|%5B)pan(?:\]|%5D)=)[^&]*)i, '\1[FILTERED]') end private @@ -126,21 +127,19 @@ def add_amount(post, amount, options) def add_payment(post, payment) card = {} - card[:number] = payment.number - card[:expire_month] = '%02d'% payment.month + card[:pan] = payment.number + card[:expire_month] = '%02d' % payment.month card[:expire_year] = payment.year - if payment.verification_value? - card[:csc] = payment.verification_value - end + card[:csc] = payment.verification_value if payment.verification_value? post[:card] = card if card.any? end def headers(api_key) { - 'Authorization' => 'Basic ' + Base64.strict_encode64("#{api_key}:"), - 'User-Agent' => "Clearhaus ActiveMerchantBindings/#{ActiveMerchant::VERSION}" + 'Authorization' => 'Basic ' + Base64.strict_encode64("#{api_key}:"), + 'User-Agent' => "Clearhaus ActiveMerchantBindings/#{ActiveMerchant::VERSION}" } end @@ -161,12 +160,14 @@ def commit(action, parameters) end end - response = begin - parse(ssl_post(url, body, headers)) - rescue ResponseError => e - raise unless(e.response.code.to_s =~ /400/) - parse(e.response.body) - end + response = + begin + parse(ssl_post(url, body, headers)) + rescue ResponseError => e + raise unless e.response.code.to_s =~ /400/ + + parse(e.response.body) + end Response.new( success_from(response), @@ -205,15 +206,13 @@ def id_of_auth_for_capture(action) def generate_signature(body) key = OpenSSL::PKey::RSA.new(@options[:private_key]) - hex = key.sign(OpenSSL::Digest.new('sha256'), body).unpack('H*').first + hex = key.sign(OpenSSL::Digest.new('sha256'), body).unpack1('H*') "#{@options[:signing_key]} RS256-hex #{hex}" end def error_code_from(response) - unless success_from(response) - response['status']['code'] - end + response['status']['code'] unless success_from(response) end end end diff --git a/lib/active_merchant/billing/gateways/commerce_hub.rb b/lib/active_merchant/billing/gateways/commerce_hub.rb new file mode 100644 index 00000000000..92ef3709961 --- /dev/null +++ b/lib/active_merchant/billing/gateways/commerce_hub.rb @@ -0,0 +1,434 @@ +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class CommerceHubGateway < Gateway + self.test_url = 'https://connect-cert.fiservapps.com/ch' + self.live_url = 'https://connect.fiservapis.com/ch' + + self.supported_countries = ['US'] + self.default_currency = 'USD' + self.supported_cardtypes = %i[visa master american_express discover] + + self.homepage_url = 'https://developer.fiserv.com/product/CommerceHub' + self.display_name = 'CommerceHub' + + STANDARD_ERROR_CODE_MAPPING = {} + + SCHEDULED_REASON_TYPES = %w(recurring installment) + ENDPOINTS = { + 'sale' => '/payments/v1/charges', + 'void' => '/payments/v1/cancels', + 'refund' => '/payments/v1/refunds', + 'vault' => '/payments-vas/v1/tokens', + 'verify' => '/payments-vas/v1/accounts/verification' + } + + def initialize(options = {}) + requires!(options, :api_key, :api_secret, :merchant_id, :terminal_id) + super + end + + def purchase(money, payment, options = {}) + post = {} + options[:capture_flag] = true + options[:create_token] = false + + add_transaction_details(post, options, 'sale') + build_purchase_and_auth_request(post, money, payment, options) + + commit('sale', post, options) + end + + def authorize(money, payment, options = {}) + post = {} + options[:capture_flag] = false + options[:create_token] = false + + add_transaction_details(post, options, 'sale') + build_purchase_and_auth_request(post, money, payment, options) + + commit('sale', post, options) + end + + def capture(money, authorization, options = {}) + post = {} + options[:capture_flag] = true + add_invoice(post, money, options) + add_transaction_details(post, options, 'capture') + add_reference_transaction_details(post, authorization, options, :capture) + add_dynamic_descriptors(post, options) + + commit('sale', post, options) + end + + def refund(money, authorization, options = {}) + post = {} + add_invoice(post, money, options) if money + add_transaction_details(post, options) + add_reference_transaction_details(post, authorization, options, :refund) + + commit('refund', post, options) + end + + def 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 + + def void(authorization, options = {}) + post = {} + add_transaction_details(post, options) + add_reference_transaction_details(post, authorization, options, :void) + + commit('void', post, options) + end + + def store(credit_card, options = {}) + post = {} + add_payment(post, credit_card, options) + add_billing_address(post, credit_card, options) + add_transaction_details(post, options) + add_transaction_interaction(post, options) + + commit('vault', post, options) + end + + def verify(credit_card, options = {}) + post = {} + add_payment(post, credit_card, options) + add_billing_address(post, credit_card, options) + + commit('verify', post, options) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r((Authorization: )[a-zA-Z0-9+./=]+), '\1[FILTERED]'). + gsub(%r((Api-Key: )\w+), '\1[FILTERED]'). + gsub(%r(("apiKey\\?":\\?")\w+), '\1[FILTERED]'). + gsub(%r(("cardData\\?":\\?")\d+), '\1[FILTERED]'). + gsub(%r(("securityCode\\?":\\?")\d+), '\1[FILTERED]'). + gsub(%r(("cavv\\?":\\?")\w+), '\1[FILTERED]') + end + + private + + def add_three_d_secure(post, 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], + serverTransactionId: 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[:origin] || 'ECOM' + post[:transactionInteraction][:eciIndicator] = options[:eci_indicator] || 'CHANNEL_ENCRYPTED' + post[:transactionInteraction][:posConditionCode] = options[:pos_condition_code] || 'CARD_NOT_PRESENT_ECOM' + post[:transactionInteraction][:posEntryMode] = (options[:pos_entry_mode] || 'MANUAL') unless options[:encryption_data].present? + post[:transactionInteraction][:additionalPosInformation] = {} + post[:transactionInteraction][:additionalPosInformation][:dataEntrySource] = options[:data_entry_source] || 'UNSPECIFIED' + end + + def add_transaction_details(post, options, action = nil) + details = { + captureFlag: options[:capture_flag], + createToken: options[:create_token], + physicalGoodsIndicator: [true, 'true'].include?(options[:physical_goods_indicator]) + } + + if action == 'sale' + details[:merchantOrderId] = options[:order_id] + details[:merchantTransactionId] = rand.to_s[2..13] + end + + details[:merchantInvoiceNumber] = options[:order_id] if action != 'capture' + + post[:transactionDetails] = details.compact + end + + def add_billing_address(post, payment, options) + return unless billing = options[:billing_address] + + billing_address = {} + name_from_address(billing_address, billing) || name_from_payment(billing_address, payment) + address = {} + address[:street] = billing[:address1] if billing[:address1] + address[:houseNumberOrName] = billing[:address2] if billing[:address2] + address[:recipientNameOrAddress] = billing[:name] if billing[:name] + address[:city] = billing[:city] if billing[:city] + address[:stateOrProvince] = billing[:state] if billing[:state] + address[:postalCode] = billing[:zip] if billing[:zip] + address[:country] = billing[:country] if billing[:country] + + billing_address[:address] = address unless address.empty? + if billing[:phone_number] + billing_address[:phone] = {} + billing_address[:phone][:phoneNumber] = billing[:phone_number] + end + post[:billingAddress] = billing_address + end + + def name_from_payment(billing_address, payment) + return unless payment.respond_to?(:first_name) && payment.respond_to?(:last_name) + + billing_address[:firstName] = payment.first_name if payment.first_name + billing_address[:lastName] = payment.last_name if payment.last_name + end + + def name_from_address(billing_address, billing) + return unless address = billing + + first_name, last_name = split_names(address[:name]) if address[:name] + + billing_address[:firstName] = first_name if first_name + billing_address[:lastName] = last_name if last_name + end + + def add_shipping_address(post, options) + return unless shipping = options[:shipping_address] + + shipping_address = {} + address = {} + address[:street] = shipping[:address1] if shipping[:address1] + address[:houseNumberOrName] = shipping[:address2] if shipping[:address2] + address[:recipientNameOrAddress] = shipping[:name] if shipping[:name] + address[:city] = shipping[:city] if shipping[:city] + address[:stateOrProvince] = shipping[:state] if shipping[:state] + address[:postalCode] = shipping[:zip] if shipping[:zip] + address[:country] = shipping[:country] if shipping[:country] + + shipping_address[:address] = address unless address.empty? + if shipping[:phone_number] + shipping_address[:phone] = {} + shipping_address[:phone][:phoneNumber] = shipping[:phone_number] + end + post[:shippingAddress] = shipping_address + end + + def build_purchase_and_auth_request(post, money, payment, options) + add_three_d_secure(post, 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) + reference_details = {} + _merchant_reference, transaction_id = authorization.include?('|') ? authorization.split('|') : [nil, authorization] + + reference_details[:referenceTransactionId] = transaction_id + reference_details[:referenceTransactionType] = (options[:reference_transaction_type] || 'CHARGES') unless action == :capture + post[:referenceTransactionDetails] = reference_details.compact + end + + def add_invoice(post, money, options) + post[:amount] = { + total: amount(money).to_f, + currency: options[:currency] || self.default_currency + } + end + + def add_stored_credentials(post, options) + return unless stored_credential = options[:stored_credential] + + post[:storedCredentials] = {} + post[:storedCredentials][:sequence] = stored_credential[:initial_transaction] ? 'FIRST' : 'SUBSEQUENT' + post[:storedCredentials][:initiator] = stored_credential[:initiator] == 'merchant' ? 'MERCHANT' : 'CARD_HOLDER' + post[:storedCredentials][:scheduled] = SCHEDULED_REASON_TYPES.include?(stored_credential[:reason_type]) + post[:storedCredentials][:schemeReferenceTransactionId] = options[:scheme_reference_transaction_id] || stored_credential[:network_transaction_id] + end + + def add_credit_card(source, payment, options) + source[:sourceType] = 'PaymentCard' + source[:card] = {} + source[:card][:cardData] = payment.number + source[:card][:expirationMonth] = format(payment.month, :two_digits) if payment.month + source[:card][:expirationYear] = format(payment.year, :four_digits) if payment.year + if payment.verification_value + source[:card][:securityCode] = payment.verification_value + source[:card][:securityCodeIndicator] = 'PROVIDED' + end + end + + def add_payment_token(source, payment, options) + source[:sourceType] = 'PaymentToken' + source[:tokenData] = payment + source[:tokenSource] = options[:token_source] if options[:token_source] + if options[:card_expiration_month] || options[:card_expiration_year] + source[:card] = {} + source[:card][:expirationMonth] = options[:card_expiration_month] if options[:card_expiration_month] + source[:card][:expirationYear] = options[:card_expiration_year] if options[:card_expiration_year] + end + end + + def add_decrypted_wallet(source, payment, options) + source[:sourceType] = 'DecryptedWallet' + source[:card] = {} + source[:card][:cardData] = payment.number + source[:card][:expirationMonth] = format(payment.month, :two_digits) + source[:card][:expirationYear] = format(payment.year, :four_digits) + source[:cavv] = payment.payment_cryptogram + source[:walletType] = payment.source.to_s.upcase + end + + def add_network_token(source, payment, options) + source[:sourceType] = 'PaymentToken' + source[:tokenData] = payment.number + source[:tokenSource] = 'NETWORK_TOKEN' + source[:cryptogram] = payment.payment_cryptogram + source[:card] = { + expirationMonth: format(payment.month, :two_digits), + expirationYear: format(payment.year, :four_digits) + } + end + + def add_payment(post, payment, options = {}) + source = {} + + if payment.is_a?(String) + add_payment_token(source, payment, options) + elsif payment.mobile_wallet? + add_decrypted_wallet(source, payment, options) + elsif payment.network_token? + add_network_token(source, payment, options) + elsif options[:encryption_data].present? + source[:sourceType] = 'PaymentCard' + source[:encryptionData] = options[:encryption_data] + elsif payment.is_a?(CreditCard) + add_credit_card(source, payment, options) + end + post[:source] = source + end + + def parse(body) + JSON.parse(body) + end + + def headers(request, options) + time = DateTime.now.strftime('%Q').to_s + client_request_id = options[:client_request_id] || rand.to_s[2..8] + raw_signature = @options[:api_key] + client_request_id.to_s + time + request + hmac = OpenSSL::HMAC.digest('sha256', @options[:api_secret], raw_signature) + signature = Base64.strict_encode64(hmac.to_s).to_s + custom_headers = options.fetch(:headers_identifiers, {}) + { + 'Client-Request-Id' => client_request_id, + 'Api-Key' => @options[:api_key], + 'Timestamp' => time, + 'Accept-Language' => 'application/json', + 'Auth-Token-Type' => 'HMAC', + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + 'Authorization' => signature + }.merge!(custom_headers) + end + + def add_merchant_details(post) + post[:merchantDetails] = {} + post[:merchantDetails][:terminalId] = @options[:terminal_id] + post[:merchantDetails][:merchantId] = @options[:merchant_id] + end + + def commit(action, parameters, options) + url = (test? ? test_url : live_url) + ENDPOINTS[action] + add_merchant_details(parameters) + response = parse(ssl_post(url, parameters.to_json, headers(parameters.to_json, options))) + + Response.new( + success_from(response, action), + message_from(response, action), + response, + authorization: authorization_from(action, response, options), + test: test?, + error_code: error_code_from(response, action), + avs_result: AVSResult.new(code: get_avs_cvv(response, 'avs')), + cvv_result: CVVResult.new(get_avs_cvv(response, 'cvv')) + ) + end + + def get_avs_cvv(response, type = 'avs') + response.dig( + 'paymentReceipt', + 'processorResponseDetails', + 'bankAssociationDetails', + 'avsSecurityCodeResponse', + 'association', + type == 'avs' ? 'avsCode' : 'securityCodeResponse' + ) + end + + def handle_response(response) + case response.code.to_i + when 200...300, 400, 401, 429 + response.body + else + raise ResponseError.new(response) + end + end + + def success_from(response, action = nil) + return message_from(response, action) == 'VERIFIED' if action == 'verify' + + (response.dig('paymentReceipt', 'processorResponseDetails', 'responseCode') || response.dig('paymentTokens', 0, 'tokenResponseCode')) == '000' + end + + def message_from(response, action = nil) + return response.dig('error', 0, 'message') if response['error'].present? + return response.dig('gatewayResponse', 'transactionState') if action == 'verify' + + response.dig('paymentReceipt', 'processorResponseDetails', 'responseMessage') || response.dig('gatewayResponse', 'transactionType') + end + + def authorization_from(action, response, options) + case action + when 'vault' + response.dig('paymentTokens', 0, 'tokenData') + when 'sale' + [options[:order_id] || '', response.dig('gatewayResponse', 'transactionProcessingDetails', 'transactionId')].join('|') + else + response.dig('gatewayResponse', 'transactionProcessingDetails', 'transactionId') + end + end + + def error_code_from(response, action) + response.dig('error', 0, 'code') unless success_from(response, action) + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/commercegate.rb b/lib/active_merchant/billing/gateways/commercegate.rb index 77aede1e9de..acd898f0a57 100644 --- a/lib/active_merchant/billing/gateways/commercegate.rb +++ b/lib/active_merchant/billing/gateways/commercegate.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class CommercegateGateway < Gateway self.test_url = self.live_url = 'https://secure.commercegate.com/gateway/nvp' @@ -11,7 +11,7 @@ class CommercegateGateway < Gateway self.money_format = :dollars self.default_currency = 'EUR' - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'http://www.commercegate.com/' self.display_name = 'CommerceGate' @@ -74,9 +74,8 @@ def add_auth_purchase_options(post, money, options) post[:customerIP] = options[:ip] || '127.0.0.1' post[:amount] = amount(money) post[:email] = options[:email] || 'unknown@example.com' - post[:currencyCode]= options[:currency] || currency(money) - post[:merchAcct] = options[:merchant] - + post[:currencyCode] = options[:currency] || currency(money) + post[:merchAcct] = options[:merchant] end def add_creditcard(params, creditcard) @@ -103,7 +102,7 @@ def commit(action, parameters) response, authorization: response['transID'], test: test?, - avs_result: {code: response['avsCode']}, + avs_result: { code: response['avsCode'] }, cvv_result: response['cvvCode'] ) end @@ -112,7 +111,7 @@ def parse(body) results = {} body.split(/\&/).each do |pair| - key,val = pair.split(%r{=}) + key, val = pair.split(%r{=}) results[key] = CGI.unescape(val) end @@ -127,9 +126,9 @@ def message_from(response) if response['returnText'].present? response['returnText'] else - 'Invalid response received from the CommerceGate API. ' + - 'Please contact CommerceGate support if you continue to receive this message. ' + - "(The raw response returned by the API was #{response.inspect})" + 'Invalid response received from the CommerceGate API. ' \ + 'Please contact CommerceGate support if you continue to receive this message. ' \ + "(The raw response returned by the API was #{response.inspect})" end end diff --git a/lib/active_merchant/billing/gateways/conekta.rb b/lib/active_merchant/billing/gateways/conekta.rb index 6b3dccd915e..e0c8080e7f1 100644 --- a/lib/active_merchant/billing/gateways/conekta.rb +++ b/lib/active_merchant/billing/gateways/conekta.rb @@ -1,10 +1,10 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class ConektaGateway < Gateway self.live_url = 'https://api.conekta.io/' self.supported_countries = ['MX'] - self.supported_cardtypes = [:visa, :master, :american_express] + self.supported_cardtypes = %i[visa master american_express carnet] self.homepage_url = 'https://conekta.io/' self.display_name = 'Conekta Gateway' self.money_format = :cents @@ -105,7 +105,7 @@ def add_shipment(post, options) end def add_shipment_address(post, options) - if(address = options[:shipping_address]) + if (address = options[:shipping_address]) post[:address] = {} post[:address][:street1] = address[:address1] if address[:address1] post[:address][:street2] = address[:address2] if address[:address2] @@ -124,7 +124,7 @@ def add_line_items(post, options) end def add_billing_address(post, options) - if(address = (options[:billing_address] || options[:address])) + if (address = (options[:billing_address] || options[:address])) post[:billing_address] = {} post[:billing_address][:street1] = address[:address1] if address[:address1] post[:billing_address][:street2] = address[:address2] if address[:address2] @@ -142,7 +142,7 @@ def add_billing_address(post, options) end def add_address(post, options) - if(address = (options[:billing_address] || options[:address])) + if (address = (options[:billing_address] || options[:address])) post[:address] = {} post[:address][:street1] = address[:address1] if address[:address1] post[:address][:street2] = address[:address2] if address[:address2] @@ -162,14 +162,15 @@ def add_payment_source(post, payment_source, options) post[:card][:name] = payment_source.name post[:card][:cvc] = payment_source.verification_value post[:card][:number] = payment_source.number - post[:card][:exp_month] = "#{sprintf("%02d", payment_source.month)}" - post[:card][:exp_year] = "#{"#{payment_source.year}"[-2, 2]}" + post[:card][:exp_month] = sprintf('%02d', payment_source.month) + post[:card][:exp_year] = payment_source.year.to_s[-2, 2] add_address(post[:card], options) end end def parse(body) return {} unless body + JSON.parse(body) end @@ -179,7 +180,7 @@ def headers(options) 'Accept-Language' => 'es', 'Authorization' => 'Basic ' + Base64.encode64("#{@options[:key]}:"), 'RaiseHtmlError' => 'false', - 'Conekta-Client-User-Agent' => {'agent'=>"Conekta ActiveMerchantBindings/#{ActiveMerchant::VERSION}"}.to_json, + 'Conekta-Client-User-Agent' => { 'agent' => "Conekta ActiveMerchantBindings/#{ActiveMerchant::VERSION}" }.to_json, 'X-Conekta-Client-User-Agent' => conekta_client_user_agent(options), 'X-Conekta-Client-User-Metadata' => options[:meta].to_json } @@ -187,7 +188,8 @@ def headers(options) def conekta_client_user_agent(options) return user_agent unless options[:application] - JSON.dump(JSON.parse(user_agent).merge!({application: options[:application]})) + + JSON.dump(JSON.parse(user_agent).merge!({ application: options[:application] })) end def commit(method, url, parameters, options = {}) @@ -211,11 +213,9 @@ def commit(method, url, parameters, options = {}) end def response_error(raw_response) - begin - parse(raw_response) - rescue JSON::ParserError - json_error(raw_response) - end + parse(raw_response) + rescue JSON::ParserError + json_error(raw_response) end def json_error(raw_response) diff --git a/lib/active_merchant/billing/gateways/creditcall.rb b/lib/active_merchant/billing/gateways/creditcall.rb index f55c4960812..5479f9bfc6d 100644 --- a/lib/active_merchant/billing/gateways/creditcall.rb +++ b/lib/active_merchant/billing/gateways/creditcall.rb @@ -1,7 +1,7 @@ require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class CreditcallGateway < Gateway include Empty @@ -10,50 +10,50 @@ class CreditcallGateway < Gateway self.supported_countries = ['US'] self.default_currency = 'USD' - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'https://www.creditcall.com' self.display_name = 'Creditcall' - + CVV_CODE = { 'matched' => 'M', 'notmatched' => 'N', 'notchecked' => 'P', 'partialmatch' => 'N' } - + AVS_CODE = { 'matched;matched' => 'D', - 'matched;notchecked' =>'B', + 'matched;notchecked' => 'B', 'matched;notmatched' => 'A', 'matched;partialmatch' => 'A', 'notchecked;matched' => 'P', - 'notchecked;notchecked' =>'I', + 'notchecked;notchecked' => 'I', 'notchecked;notmatched' => 'I', 'notchecked;partialmatch' => 'I', 'notmatched;matched' => 'W', - 'notmatched;notchecked' =>'C', + 'notmatched;notchecked' => 'C', 'notmatched;notmatched' => 'C', 'notmatched;partialmatch' => 'C', 'partialmatched;matched' => 'W', - 'partialmatched;notchecked' =>'C', + 'partialmatched;notchecked' => 'C', 'partialmatched;notmatched' => 'C', 'partialmatched;partialmatch' => 'C' } - def initialize(options={}) + def initialize(options = {}) requires!(options, :terminal_id, :transaction_key) super end - def purchase(money, payment_method, options={}) + def purchase(money, payment_method, options = {}) multi_response = MultiResponse.run do |r| r.process { authorize(money, payment_method, options) } r.process { capture(money, r.authorization, options) } end - merged_params = multi_response.responses.map { |r| r.params }.reduce({}, :merge) - + merged_params = multi_response.responses.map(&:params).reduce({}, :merge) + Response.new( multi_response.primary_response.success?, multi_response.primary_response.message, @@ -66,7 +66,7 @@ def purchase(money, payment_method, options={}) ) end - def authorize(money, payment_method, options={}) + def authorize(money, payment_method, options = {}) request = build_xml_request do |xml| add_transaction_details(xml, money, nil, 'Auth', options) add_terminal_details(xml, options) @@ -76,7 +76,7 @@ def authorize(money, payment_method, options={}) commit(request) end - def capture(money, authorization, options={}) + def capture(money, authorization, options = {}) request = build_xml_request do |xml| add_transaction_details(xml, money, authorization, 'Conf', options) add_terminal_details(xml, options) @@ -85,7 +85,7 @@ def capture(money, authorization, options={}) commit(request) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) request = build_xml_request do |xml| add_transaction_details(xml, money, authorization, 'Refund', options) add_terminal_details(xml, options) @@ -94,7 +94,7 @@ def refund(money, authorization, options={}) commit(request) end - def void(authorization, options={}) + def void(authorization, options = {}) request = build_xml_request do |xml| add_transaction_details(xml, nil, authorization, 'Void', options) add_terminal_details(xml, options) @@ -103,7 +103,7 @@ def void(authorization, options={}) commit(request) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -144,24 +144,24 @@ def build_xml_request builder.to_xml end - def add_transaction_details(xml, amount, authorization, type, options={}) + def add_transaction_details(xml, amount, authorization, type, options = {}) xml.TransactionDetails do xml.MessageType type - xml.Amount(unit: 'Minor'){ xml.text(amount) } if amount + xml.Amount(unit: 'Minor') { xml.text(amount) } if amount xml.CardEaseReference authorization if authorization xml.VoidReason '01' if type == 'Void' end end - def add_terminal_details(xml, options={}) + def add_terminal_details(xml, options = {}) xml.TerminalDetails do xml.TerminalID @options[:terminal_id] xml.TransactionKey @options[:transaction_key] - xml.Software(version: 'SoftwareVersion'){ xml.text('SoftwareName') } + xml.Software(version: 'SoftwareVersion') { xml.text('SoftwareName') } end end - def add_card_details(xml, payment_method, options={}) + def add_card_details(xml, payment_method, options = {}) xml.CardDetails do xml.Manual(type: manual_type(options)) do xml.PAN payment_method.number @@ -175,10 +175,11 @@ def add_card_details(xml, payment_method, options={}) def add_additional_verification(xml, options) return unless (options[:verify_zip].to_s == 'true') || (options[:verify_address].to_s == 'true') + if address = options[:billing_address] xml.AdditionalVerification do - xml.Zip address[:zip] if options[:verify_zip].to_s == 'true' - xml.Address address[:address1] if options[:verify_address].to_s == 'true' + xml.Zip address[:zip] if options[:verify_zip].to_s == 'true' + xml.Address address[:address1] if options[:verify_address].to_s == 'true' end end end @@ -214,7 +215,6 @@ def parse(body) end end - response end diff --git a/lib/active_merchant/billing/gateways/credorax.rb b/lib/active_merchant/billing/gateways/credorax.rb index 96f45bc5c81..7a13b877e3d 100644 --- a/lib/active_merchant/billing/gateways/credorax.rb +++ b/lib/active_merchant/billing/gateways/credorax.rb @@ -1,11 +1,15 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class CredoraxGateway < Gateway class_attribute :test_url, :live_na_url, :live_eu_url self.display_name = 'Credorax Gateway' - self.homepage_url = 'https://www.credorax.com/' + self.homepage_url = 'https://www.finaro.com/' + # NOTE: the IP address you run the remote tests from will need to be + # whitelisted by Credorax; contact support@credorax.com as necessary to + # request your IP address be added to the whitelist for your test + # account. self.test_url = 'https://intconsole.credorax.com/intenv/service/gateway' # The live URL is assigned on a per merchant basis once certification has passed @@ -15,13 +19,20 @@ class CredoraxGateway < Gateway # ActiveMerchant::Billing::CredoraxGateway.live_url = "https://assigned-subdomain.credorax.net/crax_gate/service/gateway" self.live_url = 'https://assigned-subdomain.credorax.net/crax_gate/service/gateway' - self.supported_countries = %w(DE GB FR IT ES PL NL BE GR CZ PT SE HU RS AT CH BG DK FI SK NO IE HR BA AL LT MK SI LV EE ME LU MT IS AD MC LI SM) + self.supported_countries = %w(AD AT BE BG HR CY CZ DK EE FR DE GI GR GG HU IS IE IM IT JE LV LI LT LU MT MC NO PL PT RO SM SK ES SE CH GB) + self.default_currency = 'EUR' - self.currencies_without_fractions = %w(CLP JPY KRW PYG VND) - self.currencies_with_three_decimal_places = %w(BHD JOD KWD OMR RSD TND) + self.currencies_without_fractions = %w(BIF CLP DJF GNF ISK JPY KMF KRW PYG RWF VND VUV XAF XOF XPF) + self.currencies_with_three_decimal_places = %w(BHD IQD JOD KWD LYD OMR TND) self.money_format = :cents - self.supported_cardtypes = [:visa, :master, :maestro] + self.supported_cardtypes = %i[visa master maestro american_express jcb discover diners_club] + + NETWORK_TOKENIZATION_CARD_SOURCE = { + 'apple_pay' => 'applepay', + 'google_pay' => 'googlepay', + 'network_token' => 'vts_mdes_token' + } RESPONSE_MESSAGES = { '00' => 'Approved or completed successfully', @@ -58,26 +69,27 @@ class CredoraxGateway < Gateway '31' => 'Issuer signed-off', '32' => 'Completed partially', '33' => 'Pick-up, expired card', - '34' => 'Suspect Fraud', + '34' => 'Implausible card data', '35' => 'Pick-up, card acceptor contact acquirer', '36' => 'Pick up, card restricted', '37' => 'Pick up, call acquirer security', '38' => 'Pick up, Allowable PIN tries exceeded', - '39' => 'Transaction Not Allowed', + '39' => 'No credit account', '40' => 'Requested function not supported', '41' => 'Lost Card, Pickup', '42' => 'No universal account', '43' => 'Pick up, stolen card', '44' => 'No investment account', + '46' => 'Closed account', '50' => 'Do not renew', - '51' => 'Not sufficient funds', + '51' => 'Insufficient funds', '52' => 'No checking Account', '53' => 'No savings account', '54' => 'Expired card', - '55' => 'Pin incorrect', + '55' => 'Incorrect PIN', '56' => 'No card record', '57' => 'Transaction not allowed for cardholder', - '58' => 'Transaction not allowed for merchant', + '58' => 'Transaction not permitted to terminal', '59' => 'Suspected Fraud', '60' => 'Card acceptor contact acquirer', '61' => 'Exceeds withdrawal amount limit', @@ -88,22 +100,22 @@ class CredoraxGateway < Gateway '66' => 'Call acquirers security department', '67' => 'Card to be picked up at ATM', '68' => 'Response received too late.', - '70' => 'Invalid transaction; contact card issuer', + '70' => 'PIN data required', '71' => 'Decline PIN not changed', '75' => 'Pin tries exceeded', '76' => 'Wrong PIN, number of PIN tries exceeded', '77' => 'Wrong Reference No.', - '78' => 'Record Not Found', - '79' => 'Already reversed', + '78' => 'Blocked, first used/ Record not found', + '79' => 'Declined due to lifecycle event', '80' => 'Network error', - '81' => 'Foreign network error / PIN cryptographic error', - '82' => 'Time out at issuer system', + '81' => 'PIN cryptographic error', + '82' => 'Bad CVV/ Declined due to policy event', '83' => 'Transaction failed', '84' => 'Pre-authorization timed out', '85' => 'No reason to decline', '86' => 'Cannot verify pin', '87' => 'Purchase amount only, no cashback allowed', - '88' => 'MAC sync Error', + '88' => 'Cryptographic failure', '89' => 'Authentication failure', '91' => 'Issuer not available', '92' => 'Unable to route at acquirer Module', @@ -111,85 +123,124 @@ class CredoraxGateway < Gateway '94' => 'Duplicate Transmission', '95' => 'Reconcile error / Auth Not found', '96' => 'System malfunction', + '97' => 'Transaction has been declined by the processor', + 'N3' => 'Cash service not available', + 'N4' => 'Cash request exceeds issuer or approved limit', + 'N7' => 'CVV2 failure', 'R0' => 'Stop Payment Order', 'R1' => 'Revocation of Authorisation Order', - 'R3' => 'Revocation of all Authorisations Order' + 'R3' => 'Revocation of all Authorisation Orders', + '1A' => 'Strong Customer Authentication required' } - def initialize(options={}) + def initialize(options = {}) requires!(options, :merchant_id, :cipher_key) super end - def purchase(amount, payment_method, 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) + add_3ds_2_optional_fields(post, options) add_echo(post, options) - add_transaction_type(post, options) + add_submerchant_id(post, options) + add_stored_credential(post, options) + add_processor(post, options) + + if options[:aft] + add_recipient(post, options) + add_sender(post, options) + end commit(:purchase, post) end - def authorize(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) + add_3ds_2_optional_fields(post, options) add_echo(post, options) - add_transaction_type(post, options) + add_submerchant_id(post, options) + add_stored_credential(post, options) + add_account_name_inquiry(post, options) + add_processor(post, options) + add_authorization_details(post, options) + + if options[:aft] + add_recipient(post, options) + add_sender(post, options) + end commit(:authorize, post) end - def capture(amount, authorization, options={}) + def capture(amount, authorization, options = {}) post = {} add_invoice(post, amount, options) add_reference(post, authorization) add_customer_data(post, options) add_echo(post, options) + add_submerchant_id(post, options) + add_processor(post, options) commit(:capture, post) end - def void(authorization, options={}) + def void(authorization, options = {}) post = {} add_customer_data(post, options) reference_action = add_reference(post, authorization) add_echo(post, options) + add_submerchant_id(post, options) post[:a1] = generate_unique_id + add_processor(post, options) commit(:void, post, reference_action) end - def refund(amount, authorization, options={}) + def refund(amount, authorization, options = {}) post = {} add_invoice(post, amount, options) add_reference(post, authorization) add_customer_data(post, options) add_echo(post, options) + add_submerchant_id(post, options) + add_processor(post, options) + add_email(post, options) + add_recipient(post, options) - commit(:refund, post) + if options[:referral_cft] + add_customer_name(post, options) + commit(:referral_cft, post) + else + commit(:refund, post) + end end - def credit(amount, payment_method, options={}) + def credit(amount, payment_method, options = {}) post = {} add_invoice(post, amount, options) - add_payment_method(post, payment_method) + add_payment_method(post, payment_method, options) add_customer_data(post, options) add_email(post, options) add_echo(post, options) + add_submerchant_id(post, options) add_transaction_type(post, options) - + add_processor(post, options) + add_customer_name(post, options) + commit(:credit, post) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -206,6 +257,23 @@ def scrub(transcript) gsub(%r((b5=)\d+), '\1[FILTERED]') end + def add_3ds_2_optional_fields(post, options) + three_ds = options[:three_ds_2] || {} + + if three_ds.has_key?(:optional) + three_ds[:optional].each do |key, value| + normalized_value = normalize(value) + next if normalized_value.nil? + + next if key == :'3ds_homephonecountry' && !(options[:billing_address] && options[:billing_address][:phone]) + + post[key] = normalized_value unless post[key] + end + end + + post + end + private def add_invoice(post, money, options) @@ -224,8 +292,9 @@ def add_invoice(post, money, options) 'maestro' => '9' } - def add_payment_method(post, payment_method) - post[:c1] = payment_method.name + def add_payment_method(post, payment_method, options) + post[:c1] = payment_method&.name || '' unless options[:account_name_inquiry].to_s == 'true' + 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 @@ -233,15 +302,40 @@ 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 + return unless (stored_credential = options[:stored_credential]) && options.dig(:transaction_type).nil? + + if stored_credential[:initiator] == 'merchant' + case stored_credential[:reason_type] + when 'recurring' + 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 add_customer_data(post, options) post[:d1] = options[:ip] || '127.0.0.1' if (billing_address = options[:billing_address]) - post[:c5] = billing_address[:address1] - post[:c7] = billing_address[:city] - post[:c10] = billing_address[:zip] - post[:c8] = billing_address[:state] - post[:c9] = billing_address[:country] - post[:c2] = billing_address[:phone] + post[:c5] = billing_address[:address1] if billing_address[:address1] + post[:c7] = billing_address[:city] if billing_address[:city] + post[:c10] = billing_address[:zip] if billing_address[:zip] + post[:c8] = billing_address[:state] if billing_address[:state] + post[:c9] = billing_address[:country] if billing_address[:country] + post[:c2] = billing_address[:phone] if billing_address[:phone] end end @@ -257,9 +351,115 @@ def add_email(post, options) post[:c3] = options[:email] || 'unspecified@example.com' end + def add_sender(post, options) + return unless options[:sender_ref_number] || options[:sender_fund_source] || options[:sender_country_code] || options[:sender_street_address] || options[:sender_city] || options[:sender_state] || options[:sender_first_name] || options[:sender_last_name] + + sender_country_code = options[:sender_country_code]&.length == 3 ? options[:sender_country_code] : Country.find(options[:sender_country_code]).code(:alpha3).value if options[:sender_country_code] + post[:s15] = sender_country_code + post[:s17] = options[:sender_ref_number] if options[:sender_ref_number] + post[:s18] = options[:sender_fund_source] if options[:sender_fund_source] + post[:s10] = options[:sender_first_name] if options[:sender_first_name] + post[:s11] = options[:sender_last_name] if options[:sender_last_name] + post[:s12] = options[:sender_street_address] if options[:sender_street_address] + post[:s13] = options[:sender_city] if options[:sender_city] + post[:s14] = options[:sender_state] if options[:sender_state] + end + + def add_recipient(post, options) + return unless options[:recipient_street_address] || options[:recipient_city] || options[:recipient_province_code] || options[:recipient_country_code] || options[:recipient_first_name] || options[:recipient_last_name] || options[:recipient_postal_code] + + recipient_country_code = options[:recipient_country_code]&.length == 3 ? options[:recipient_country_code] : Country.find(options[:recipient_country_code]).code(:alpha3).value if options[:recipient_country_code] + post[:j6] = options[:recipient_street_address] if options[:recipient_street_address] + post[:j7] = options[:recipient_city] if options[:recipient_city] + post[:j8] = options[:recipient_province_code] if options[:recipient_province_code] + post[:j12] = options[:recipient_postal_code] if options[:recipient_postal_code] + post[:j9] = recipient_country_code + + if options[:aft] + post[:j5] = options[:recipient_first_name] if options[:recipient_first_name] + post[:j13] = options[:recipient_last_name] if options[:recipient_last_name] + end + end + + def add_customer_name(post, options) + post[:j5] = options[:first_name] if options[:first_name] + post[:j13] = options[:last_name] if options[:last_name] + end + + def add_account_name_inquiry(post, options) + return unless options[:account_name_inquiry].to_s == 'true' + + post[:c22] = options[:first_name] if options[:first_name] + post[:c23] = options[:last_name] if options[:last_name] + post[:a9] = '5' + end + def add_3d_secure(post, options) - return unless options[:eci] && options[:xid] - post[:i8] = "#{options[:eci]}:#{(options[:cavv] || "none")}:#{options[:xid]}" + if (options[:eci] && options[:xid]) || (options[:three_d_secure] && options[:three_d_secure][:version]&.start_with?('1')) + add_3d_secure_1_data(post, options) + elsif options[:execute_threed] && options[:three_ds_2] + three_ds_2_options = options[:three_ds_2] + browser_info = three_ds_2_options[:browser_info] + post[:'3ds_initiate'] = options[:three_ds_initiate] || '01' + post[:f23] = options[:f23] if options[:f23] + post[:'3ds_purchasedate'] = Time.now.utc.strftime('%Y%m%d%I%M%S') + options.dig(:stored_credential, :initiator) == 'merchant' ? post[:'3ds_channel'] = '03' : post[:'3ds_channel'] = '02' + post[:'3ds_reqchallengeind'] = options[:three_ds_reqchallengeind] if options[:three_ds_reqchallengeind] + post[:'3ds_redirect_url'] = three_ds_2_options[:notification_url] + post[:'3ds_challengewindowsize'] = options[:three_ds_challenge_window_size] || '03' + post[:d5] = browser_info[:user_agent] + post[:'3ds_transtype'] = options[:three_ds_transtype] || '01' + post[:'3ds_browsertz'] = browser_info[:timezone] + post[:'3ds_browserscreenwidth'] = browser_info[:width] + post[:'3ds_browserscreenheight'] = browser_info[:height] + post[:'3ds_browsercolordepth'] = browser_info[:depth].to_s == '30' ? '32' : browser_info[:depth] + post[:d6] = browser_info[:language] + post[:'3ds_browserjavaenabled'] = browser_info[:java] + post[:'3ds_browseracceptheader'] = browser_info[:accept_header] + add_complete_shipping_address(post, options[:shipping_address]) if options[:shipping_address] + elsif options[:three_d_secure] + add_normalized_3d_secure_2_data(post, options) + end + end + + def add_3d_secure_1_data(post, options) + if three_d_secure_options = options[:three_d_secure] + post[:i8] = build_i8( + three_d_secure_options[:eci], + three_d_secure_options[:cavv], + three_d_secure_options[:xid] + ) + post[:'3ds_version'] = three_d_secure_options[:version]&.start_with?('1') ? '1.0' : three_d_secure_options[:version] + else + post[:i8] = build_i8(options[:eci], options[:cavv], options[:xid]) + post[:'3ds_version'] = options[:three_ds_version].nil? || options[:three_ds_version]&.start_with?('1') ? '1.0' : options[:three_ds_version] + end + end + + def add_complete_shipping_address(post, shipping_address) + return if shipping_address.values.any?(&:blank?) + + post[:'3ds_shipaddrstate'] = shipping_address[:state] + post[:'3ds_shipaddrpostcode'] = shipping_address[:zip] + post[:'3ds_shipaddrline2'] = shipping_address[:address2] + post[:'3ds_shipaddrline1'] = shipping_address[:address1] + post[:'3ds_shipaddrcountry'] = shipping_address[:country] + post[:'3ds_shipaddrcity'] = shipping_address[:city] + end + + def add_normalized_3d_secure_2_data(post, options) + three_d_secure_options = options[:three_d_secure] + + post[:i8] = build_i8( + three_d_secure_options[:eci], + three_d_secure_options[:cavv] + ) + post[:'3ds_version'] = three_d_secure_options[:version] == '2' ? '2.0' : three_d_secure_options[:version] + post[:'3ds_dstrxid'] = three_d_secure_options[:ds_transaction_id] + end + + def build_i8(eci, cavv = nil, xid = nil) + "#{eci}:#{cavv || 'none'}:#{xid || 'none'}" end def add_echo(post, options) @@ -268,8 +468,23 @@ def add_echo(post, options) post[:d2] = options[:echo] unless options[:echo].blank? end + def add_submerchant_id(post, options) + post[:h3] = options[:submerchant_id] if options[:submerchant_id] + end + def add_transaction_type(post, options) post[:a9] = options[:transaction_type] if options[:transaction_type] + post[:a2] = '3' if options.dig(:metadata, :manual_entry) + end + + def add_processor(post, options) + post[:r1] = options[:processor] if options[:processor] + post[:r2] = options[:processor_merchant_id] if options[:processor_merchant_id] + end + + def add_authorization_details(post, options) + post[:a10] = options[:authorization_type] if options[:authorization_type] + post[:a11] = options[:multiple_capture_count] if options[:multiple_capture_count] end ACTIONS = { @@ -278,10 +493,12 @@ def add_transaction_type(post, options) capture: '3', authorize_void: '4', refund: '5', - credit: '6', + credit: '35', purchase_void: '7', refund_void: '8', - capture_void: '9' + capture_void: '9', + threeds_completion: '92', + referral_cft: '34' } def commit(action, params, reference_action = nil) @@ -292,7 +509,7 @@ def commit(action, params, reference_action = nil) success_from(response), message_from(response), response, - authorization: "#{response["Z1"]};#{response["Z4"]};#{response["A1"]};#{action}", + authorization: "#{response['Z1']};#{response['Z4']};#{response['A1']};#{action}", avs_result: AVSResult.new(code: response['Z9']), cvv_result: CVVResult.new(response['Z14']), test: test? @@ -301,25 +518,25 @@ def commit(action, params, reference_action = nil) def sign_request(params) params = params.sort - params.each { |param| param[1].gsub!(/[<>()\\]/, ' ') } - values = params.map { |param| param[1].strip } + values = params.map do |param| + value = param[1].gsub(/[<>()\\]/, ' ') + value.strip + end Digest::MD5.hexdigest(values.join + @options[:cipher_key]) end def post_data(action, params, reference_action) - params.keys.each { |key| params[key] = params[key].to_s} + params.keys.each { |key| params[key] = params[key].to_s } params[:M] = @options[:merchant_id] params[:O] = request_action(action, reference_action) params[:K] = sign_request(params) - params.map {|k, v| "#{k}=#{CGI.escape(v.to_s)}"}.join('&') + params.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&') end def request_action(action, reference_action) - if reference_action - ACTIONS["#{reference_action}_#{action}".to_sym] - else - ACTIONS[action] - end + return ACTIONS["#{reference_action}_#{action}".to_sym] if reference_action + + ACTIONS[action] end def url @@ -327,7 +544,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/ct_payment.rb b/lib/active_merchant/billing/gateways/ct_payment.rb new file mode 100644 index 00000000000..f40b055259a --- /dev/null +++ b/lib/active_merchant/billing/gateways/ct_payment.rb @@ -0,0 +1,269 @@ +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class CtPaymentGateway < Gateway + self.test_url = 'https://test.ctpaiement.ca/v1/' + self.live_url = 'https://www.ctpaiement.com/v1/' + + self.supported_countries = %w[US CA] + self.default_currency = 'CAD' + self.supported_cardtypes = %i[visa master american_express discover diners_club] + + self.homepage_url = 'http://www.ct-payment.com/' + self.display_name = 'CT Payment' + + STANDARD_ERROR_CODE_MAPPING = { + '14' => STANDARD_ERROR_CODE[:invalid_number], + '05' => STANDARD_ERROR_CODE[:card_declined], + 'M6' => STANDARD_ERROR_CODE[:card_declined], + '9068' => STANDARD_ERROR_CODE[:incorrect_number], + '9067' => STANDARD_ERROR_CODE[:incorrect_number] + } + CARD_BRAND = { + 'american_express' => 'A', + 'master' => 'M', + 'diners_club' => 'I', + 'visa' => 'V', + 'discover' => 'O' + } + + def initialize(options = {}) + requires!(options, :api_key, :company_number, :merchant_number) + super + end + + def purchase(money, payment, options = {}) + requires!(options, :order_id) + post = {} + add_terminal_number(post, options) + add_money(post, money) + add_operator_id(post, options) + add_invoice(post, money, options) + add_payment(post, payment) + add_address(post, payment, options) + add_customer_data(post, options) + + payment.is_a?(String) ? commit('purchaseWithToken', post) : commit('purchase', post) + end + + def authorize(money, payment, options = {}) + requires!(options, :order_id) + post = {} + add_money(post, money) + add_terminal_number(post, options) + add_operator_id(post, options) + add_invoice(post, money, options) + add_payment(post, payment) + add_address(post, payment, options) + add_customer_data(post, options) + + payment.is_a?(String) ? commit('preAuthorizationWithToken', post) : commit('preAuthorization', post) + end + + def capture(money, authorization, options = {}) + requires!(options, :order_id) + post = {} + add_invoice(post, money, options) + add_money(post, money) + add_customer_data(post, options) + transaction_number, authorization_number, invoice_number = split_authorization(authorization) + post[:OriginalTransactionNumber] = transaction_number + post[:OriginalAuthorizationNumber] = authorization_number + post[:OriginalInvoiceNumber] = invoice_number + + commit('completion', post) + end + + def refund(money, authorization, options = {}) + requires!(options, :order_id) + post = {} + add_invoice(post, money, options) + add_money(post, money) + add_customer_data(post, options) + transaction_number, _, invoice_number = split_authorization(authorization) + post[:OriginalTransactionNumber] = transaction_number + post[:OriginalInvoiceNumber] = invoice_number + + commit('refundWithoutCard', post) + end + + def credit(money, payment, options = {}) + requires!(options, :order_id) + post = {} + add_terminal_number(post, options) + add_money(post, money) + add_operator_id(post, options) + add_invoice(post, money, options) + add_payment(post, payment) + add_address(post, payment, options) + add_customer_data(post, options) + + payment.is_a?(String) ? commit('refundWithToken', post) : commit('refund', post) + end + + def void(authorization, options = {}) + post = {} + post[:InputType] = 'I' + post[:LanguageCode] = 'E' + transaction_number, _, invoice_number = split_authorization(authorization) + post[:OriginalTransactionNumber] = transaction_number + post[:OriginalInvoiceNumber] = invoice_number + add_operator_id(post, options) + add_customer_data(post, options) + + commit('void', post) + end + + def verify(credit_card, options = {}) + requires!(options, :order_id) + post = {} + add_terminal_number(post, options) + add_operator_id(post, options) + add_invoice(post, 0, options) + add_payment(post, credit_card) + add_address(post, credit_card, options) + add_customer_data(post, options) + + commit('verifyAccount', post) + end + + def store(credit_card, options = {}) + requires!(options, :email) + post = { + LanguageCode: 'E', + Name: credit_card.name.rjust(50, ' '), + Email: options[:email].rjust(240, ' ') + } + add_operator_id(post, options) + add_payment(post, credit_card) + add_customer_data(post, options) + + commit('recur/AddUser', post) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r((&?auth-api-key=)[^&]*)i, '\1[FILTERED]'). + gsub(%r((&?payload=)[a-zA-Z%0-9=]+)i, '\1[FILTERED]'). + gsub(%r((&?token:)[^&]*)i, '\1[FILTERED]'). + gsub(%r((&?cardNumber:)[^&]*)i, '\1[FILTERED]') + end + + private + + def add_terminal_number(post, options) + post[:MerchantTerminalNumber] = options[:merchant_terminal_number] || (' ' * 5) + end + + def add_money(post, money) + post[:Amount] = money.to_s.rjust(11, '0') + end + + def add_operator_id(post, options) + post[:OperatorID] = options[:operator_id] || ('0' * 8) + end + + def add_customer_data(post, options) + post[:CustomerNumber] = options[:customer_number] || ('0' * 8) + end + + def add_address(post, creditcard, options) + if address = options[:billing_address] || options[:address] + post[:CardHolderAddress] = "#{address[:address1]} #{address[:address2]} #{address[:city]} #{address[:state]}".rjust(20, ' ') + post[:CardHolderPostalCode] = address[:zip].gsub(/\s+/, '').rjust(9, ' ') + end + end + + def add_invoice(post, money, options) + post[:CurrencyCode] = options[:currency] || (currency(money) if money) + post[:InvoiceNumber] = options[:order_id].rjust(12, '0') + post[:InputType] = 'I' + post[:LanguageCode] = 'E' + end + + def add_payment(post, payment) + if payment.is_a?(String) + post[:Token] = split_authorization(payment)[3].strip + else + post[:CardType] = CARD_BRAND[payment.brand] || ' ' + post[:CardNumber] = payment.number.rjust(40, ' ') + post[:ExpirationDate] = expdate(payment) + post[:Cvv2Cvc2Number] = payment.verification_value + end + end + + def parse(body) + JSON.parse(body) + end + + def split_authorization(authorization) + authorization.split(';') + end + + def commit_raw(action, parameters) + url = (test? ? test_url : live_url) + action + response = parse(ssl_post(url, post_data(action, parameters))) + + Response.new( + success_from(response), + message_from(response), + response, + authorization: authorization_from(response), + avs_result: AVSResult.new(code: response['avsStatus']), + cvv_result: CVVResult.new(response['cvv2Cvc2Status']), + test: test?, + error_code: error_code_from(response) + ) + end + + def commit(action, parameters) + if action == 'void' + commit_raw(action, parameters) + else + MultiResponse.run(true) do |r| + r.process { commit_raw(action, parameters) } + r.process { + split_auth = split_authorization(r.authorization) + auth = (action.include?('recur') ? split_auth[4] : split_auth[0]) + action.include?('recur') ? commit_raw('recur/ack', { ID: auth }) : commit_raw('ack', { TransactionNumber: auth }) + } + end + end + end + + def success_from(response) + return true if response['returnCode'] == ' 00' + return true if response['returnCode'] == 'true' + return true if response['recurReturnCode'] == ' 00' + + return false + end + + def message_from(response) + response['errorDescription'] || response['terminalDisp']&.strip + end + + def authorization_from(response) + "#{response['transactionNumber']};#{response['authorizationNumber']};"\ + "#{response['invoiceNumber']};#{response['token']};#{response['id']}" + end + + def post_data(action, parameters = {}) + parameters[:CompanyNumber] = @options[:company_number] + parameters[:MerchantNumber] = @options[:merchant_number] + parameters = parameters.collect do |key, value| + "#{key}=#{value}" unless value.nil? || value.empty? + end.join('&') + payload = Base64.strict_encode64(parameters) + "auth-api-key=#{@options[:api_key]}&payload=#{payload}".strip + end + + def error_code_from(response) + STANDARD_ERROR_CODE_MAPPING[response['returnCode'].strip || response['recurReturnCode'.strip]] unless success_from(response) + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/culqi.rb b/lib/active_merchant/billing/gateways/culqi.rb index d7dde785353..3a6ef526871 100644 --- a/lib/active_merchant/billing/gateways/culqi.rb +++ b/lib/active_merchant/billing/gateways/culqi.rb @@ -1,7 +1,7 @@ require 'digest/md5' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # Important note: # === # Culqi merchant accounts are configured for either purchase or auth/capture @@ -18,18 +18,18 @@ class CulqiGateway < Gateway self.supported_countries = ['PE'] self.default_currency = 'PEN' self.money_format = :dollars - self.supported_cardtypes = [:visa, :master, :diners_club, :american_express] + self.supported_cardtypes = %i[visa master diners_club american_express] - def initialize(options={}) + def initialize(options = {}) requires!(options, :merchant_id, :terminal_id, :secret_key) super end - def purchase(amount, payment_method, options={}) + def purchase(amount, payment_method, options = {}) authorize(amount, payment_method, options) end - def authorize(amount, payment_method, options={}) + def authorize(amount, payment_method, options = {}) if payment_method.is_a?(String) action = :tokenpay else @@ -45,7 +45,7 @@ def authorize(amount, payment_method, options={}) commit(action, post) end - def capture(amount, authorization, options={}) + def capture(amount, authorization, options = {}) action = :capture post = {} add_credentials(post) @@ -56,7 +56,7 @@ def capture(amount, authorization, options={}) commit(action, post) end - def void(authorization, options={}) + def void(authorization, options = {}) action = :void post = {} add_credentials(post) @@ -67,7 +67,7 @@ def void(authorization, options={}) commit(action, post) end - def refund(amount, authorization, options={}) + def refund(amount, authorization, options = {}) action = :refund post = {} add_credentials(post) @@ -78,7 +78,7 @@ def refund(amount, authorization, options={}) commit(action, post) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(1000, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -90,7 +90,7 @@ def verify_credentials response.message.include? 'Transaction not found' end - def store(credit_card, options={}) + def store(credit_card, options = {}) action = :tokenize post = {} post[:partnerid] = options[:partner_id] if options[:partner_id] @@ -103,7 +103,7 @@ def store(credit_card, options={}) commit(action, post) end - def invalidate(authorization, options={}) + def invalidate(authorization, options = {}) action = :invalidate post = {} post[:partnerid] = options[:partner_id] if options[:partner_id] @@ -173,22 +173,23 @@ def add_customer_data(post, options) post[:city] = billing_address[:city] post[:state] = billing_address[:state] post[:countrycode] = billing_address[:country] - post[:zip] = billing_address[:zip] + post[:zip] = billing_address[:zip] post[:telno] = billing_address[:phone] post[:telnocc] = options[:telephone_country_code] || '051' end end def add_checksum(action, post) - checksum_elements = case action - when :capture then [post[:toid], post[:trackingid], post[:captureamount], @options[:secret_key]] - when :void then [post[:toid], post[:description], post[:trackingid], @options[:secret_key]] - when :refund then [post[:toid], post[:trackingid], post[:refundamount], @options[:secret_key]] - when :tokenize then [post[:partnerid], post[:cardnumber], post[:cvv], @options[:secret_key]] - when :invalidate then [post[:partnerid], post[:token], @options[:secret_key]] - else [post[:toid], post[:totype], post[:amount], post[:description], post[:redirecturl], - post[:cardnumber] || post[:token], @options[:secret_key]] - end + checksum_elements = + case action + when :capture then [post[:toid], post[:trackingid], post[:captureamount], @options[:secret_key]] + when :void then [post[:toid], post[:description], post[:trackingid], @options[:secret_key]] + when :refund then [post[:toid], post[:trackingid], post[:refundamount], @options[:secret_key]] + when :tokenize then [post[:partnerid], post[:cardnumber], post[:cvv], @options[:secret_key]] + when :invalidate then [post[:partnerid], post[:token], @options[:secret_key]] + else [post[:toid], post[:totype], post[:amount], post[:description], post[:redirecturl], + post[:cardnumber] || post[:token], @options[:secret_key]] + end post[:checksum] = Digest::MD5.hexdigest(checksum_elements.compact.join('|')) end @@ -204,15 +205,16 @@ def add_reference(post, authorization) refund: 'SingleCallGenericReverse', tokenize: 'SingleCallTokenServlet', invalidate: 'SingleCallInvalidateToken', - tokenpay: 'SingleCallTokenTransaction', + tokenpay: 'SingleCallTokenTransaction' } def commit(action, params) - response = begin - parse(ssl_post(url + ACTIONS[action], post_data(action, params), headers)) - rescue ResponseError => e - parse(e.response.body) - end + response = + begin + parse(ssl_post(url + ACTIONS[action], post_data(action, params), headers)) + rescue ResponseError => e + parse(e.response.body) + end success = success_from(response) @@ -229,13 +231,13 @@ def commit(action, params) def headers { - 'Accept' => 'application/json', - 'Content-Type' => 'application/x-www-form-urlencoded;charset=UTF-8' + 'Accept' => 'application/json', + 'Content-Type' => 'application/x-www-form-urlencoded;charset=UTF-8' } end def post_data(action, params) - params.map {|k, v| "#{k}=#{CGI.escape(v.to_s)}"}.join('&') + params.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&') end def url @@ -243,16 +245,14 @@ def url end def parse(body) - begin - JSON.parse(body) - rescue JSON::ParserError - message = 'Invalid JSON response received from CulqiGateway. Please contact CulqiGateway if you continue to receive this message.' - message += "(The raw response returned by the API was #{body.inspect})" - { - 'status' => 'N', - 'statusdescription' => message - } - end + JSON.parse(body) + rescue JSON::ParserError + message = 'Invalid JSON response received from CulqiGateway. Please contact CulqiGateway if you continue to receive this message.' + message += "(The raw response returned by the API was #{body.inspect})" + { + 'status' => 'N', + 'statusdescription' => message + } 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 7d7beba89d6..1fbdb4f902e 100644 --- a/lib/active_merchant/billing/gateways/cyber_source.rb +++ b/lib/active_merchant/billing/gateways/cyber_source.rb @@ -1,7 +1,7 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # Initial setup instructions can be found in - # http://cybersource.com/support_center/implementation/downloads/soap_api/SOAP_toolkits.pdf + # http://apps.cybersource.com/library/documentation/dev_guides/SOAP_Toolkits/SOAP_toolkits.pdf # # Important Notes # * For checks you can purchase and store. @@ -15,19 +15,36 @@ module Billing #:nodoc: # CyberSource what kind of item you are selling. It is used when # calculating tax/VAT. # * All transactions use dollar values. - # * To process pinless debit cards through the pinless debit card - # network, your Cybersource merchant account must accept pinless - # debit card payments. # * The order of the XML elements does matter, make sure to follow the order in # the documentation exactly. class CyberSourceGateway < Gateway self.test_url = 'https://ics2wstesta.ic3.com/commerce/1.x/transactionProcessor' self.live_url = 'https://ics2wsa.ic3.com/commerce/1.x/transactionProcessor' - XSD_VERSION = '1.121' + # Schema files can be found here: https://ics2ws.ic3.com/commerce/1.x/transactionProcessor/ + TEST_XSD_VERSION = '1.201' + PRODUCTION_XSD_VERSION = '1.201' + ECI_BRAND_MAPPING = { + visa: 'vbv', + master: 'spa', + maestro: 'spa', + american_express: 'aesk', + jcb: 'js', + discover: 'pb', + diners_club: 'pb' + }.freeze + THREEDS_EXEMPTIONS = { + authentication_outage: 'authenticationOutageExemptionIndicator', + corporate_card: 'secureCorporatePaymentIndicator', + delegated_authentication: 'delegatedAuthenticationExemptionIndicator', + low_risk: 'riskAnalysisExemptionIndicator', + low_value: 'lowValueExemptionIndicator', + trusted_merchant: 'trustedMerchantExemptionIndicator' + } + DEFAULT_COLLECTION_INDICATOR = 2 - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb, :switch, :dankort, :maestro] - self.supported_countries = %w(US BR CA CN DK FI FR DE JP MX NO SE GB SG LB) + self.supported_cardtypes = %i[visa master american_express discover diners_club jcb dankort maestro elo patagonia_365 tarjeta_sol] + self.supported_countries = %w(US AE BR CA CN DK FI FR DE IN JP MX NO SE GB SG LB PK) self.default_currency = 'USD' self.currencies_without_fractions = %w(JPY) @@ -36,56 +53,102 @@ class CyberSourceGateway < Gateway self.display_name = 'CyberSource' @@credit_card_codes = { - :visa => '001', - :master => '002', - :american_express => '003', - :discover => '004', - :diners_club => '005', - :jcb => '007', - :switch => '024', - :dankort => '034', - :maestro => '042' + visa: '001', + master: '002', + american_express: '003', + discover: '004', + diners_club: '005', + jcb: '007', + dankort: '034', + maestro: '042', + elo: '054', + carnet: '002' + } + + @@decision_codes = { + accept: 'ACCEPT', + review: 'REVIEW' } @@response_codes = { - :r100 => 'Successful transaction', - :r101 => 'Request is missing one or more required fields' , - :r102 => 'One or more fields contains invalid data', - :r150 => 'General failure', - :r151 => 'The request was received but a server time-out occurred', - :r152 => 'The request was received, but a service timed out', - :r200 => 'The authorization request was approved by the issuing bank but declined by CyberSource because it did not pass the AVS check', - :r201 => 'The issuing bank has questions about the request', - :r202 => 'Expired card', - :r203 => 'General decline of the card', - :r204 => 'Insufficient funds in the account', - :r205 => 'Stolen or lost card', - :r207 => 'Issuing bank unavailable', - :r208 => 'Inactive card or card not authorized for card-not-present transactions', - :r209 => 'American Express Card Identifiction Digits (CID) did not match', - :r210 => 'The card has reached the credit limit', - :r211 => 'Invalid card verification number', - :r221 => "The customer matched an entry on the processor's negative file", - :r230 => 'The authorization request was approved by the issuing bank but declined by CyberSource because it did not pass the card verification check', - :r231 => 'Invalid account number', - :r232 => 'The card type is not accepted by the payment processor', - :r233 => 'General decline by the processor', - :r234 => 'A problem exists with your CyberSource merchant configuration', - :r235 => 'The requested amount exceeds the originally authorized amount', - :r236 => 'Processor failure', - :r237 => 'The authorization has already been reversed', - :r238 => 'The authorization has already been captured', - :r239 => 'The requested transaction amount must match the previous transaction amount', - :r240 => 'The card type sent is invalid or does not correlate with the credit card number', - :r241 => 'The request ID is invalid', - :r242 => 'You requested a capture, but there is no corresponding, unused authorization record.', - :r243 => 'The transaction has already been settled or reversed', - :r244 => 'The bank account number failed the validation check', - :r246 => 'The capture or credit is not voidable because the capture or credit information has already been submitted to your processor', - :r247 => 'You requested a credit for a capture that was previously voided', - :r250 => 'The request was received, but a time-out occurred with the payment processor', - :r254 => 'Your CyberSource account is prohibited from processing stand-alone refunds', - :r255 => 'Your CyberSource account is not configured to process the service in the country you specified' + r100: 'Successful transaction', + r101: 'Request is missing one or more required fields', + r102: 'One or more fields contains invalid data', + r104: 'The merchantReferenceCode sent with this authorization request matches the merchantReferenceCode of another authorization request that you sent in the last 15 minutes.', r110: 'Partial amount was approved', + r150: 'General failure', + r151: 'The request was received but a server time-out occurred', + r152: 'The request was received, but a service timed out', + r200: 'The authorization request was approved by the issuing bank but declined by CyberSource because it did not pass the AVS check', + r201: 'The issuing bank has questions about the request', + r202: 'Expired card', + r203: 'General decline of the card', + r204: 'Insufficient funds in the account', + r205: 'Stolen or lost card', + r207: 'Issuing bank unavailable', + r208: 'Inactive card or card not authorized for card-not-present transactions', + r209: 'American Express Card Identifiction Digits (CID) did not match', + r210: 'The card has reached the credit limit', + r211: 'Invalid card verification number', + r220: 'Generic Decline.', + r221: "The customer matched an entry on the processor's negative file", + r222: 'customer\'s account is frozen', + r230: 'The authorization request was approved by the issuing bank but declined by CyberSource because it did not pass the card verification check', + r231: 'Invalid account number', + r232: 'The card type is not accepted by the payment processor', + r233: 'General decline by the processor', + r234: 'A problem exists with your CyberSource merchant configuration', + r235: 'The requested amount exceeds the originally authorized amount', + r236: 'Processor failure', + r237: 'The authorization has already been reversed', + r238: 'The authorization has already been captured', + r239: 'The requested transaction amount must match the previous transaction amount', + r240: 'The card type sent is invalid or does not correlate with the credit card number', + r241: 'The request ID is invalid', + r242: 'You requested a capture, but there is no corresponding, unused authorization record.', + r243: 'The transaction has already been settled or reversed', + r244: 'The bank account number failed the validation check', + r246: 'The capture or credit is not voidable because the capture or credit information has already been submitted to your processor', + r247: 'You requested a credit for a capture that was previously voided', + r248: 'The boleto request was declined by your processor.', + r250: 'The request was received, but a time-out occurred with the payment processor', + r251: 'The Pinless Debit card\'s use frequency or maximum amount per use has been exceeded.', + r254: 'Your CyberSource account is prohibited from processing stand-alone refunds', + r255: 'Your CyberSource account is not configured to process the service in the country you specified', + r400: 'Soft Decline - Fraud score exceeds threshold.', + r450: 'Apartment number missing or not found.', + r451: 'Insufficient address information.', + r452: 'House/Box number not found on street.', + r453: 'Multiple address matches were found.', + r454: 'P.O. Box identifier not found or out of range.', + r455: 'Route service identifier not found or out of range.', + r456: 'Street name not found in Postal code.', + r457: 'Postal code not found in database.', + r458: 'Unable to verify or correct address.', + r459: 'Multiple addres matches were found (international)', + r460: 'Address match not found (no reason given)', + r461: 'Unsupported character set', + r475: 'The cardholder is enrolled in Payer Authentication. Please authenticate the cardholder before continuing with the transaction.', + r476: 'Encountered a Payer Authentication problem. Payer could not be authenticated.', + r478: 'Strong customer authentication (SCA) is required for this transaction.', + r480: 'The order is marked for review by Decision Manager', + r481: 'The order has been rejected by Decision Manager', + r490: 'Your aggregator or acquirer is not accepting transactions from you at this time.', + r491: 'Your aggregator or acquirer is not accepting this transaction.', + r520: 'Soft Decline - The authorization request was approved by the issuing bank but declined by CyberSource based on your Smart Authorization settings.', + r700: 'The customer matched the Denied Parties List', + r701: 'Export bill_country/ship_country match', + r702: 'Export email_country match', + r703: 'Export hostname_country/ip_country match' + } + + @@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 @@ -112,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, options) + 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 = {}) @@ -122,10 +191,15 @@ def capture(money, authorization, options = {}) commit(build_capture_request(money, authorization, options), :capture, money, options) end - # options[:pinless_debit_card] => true # attempts to process as pinless debit card - 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, options) + 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 = {}) @@ -136,24 +210,36 @@ def refund(money, identification, options = {}) commit(build_refund_request(money, identification, options), :refund, money, options) end + def adjust(money, authorization, options = {}) + commit(build_adjust_request(money, authorization, options), :adjust, money, options) + end + def verify(payment, options = {}) + amount = eligible_for_zero_auth?(payment, options) ? 0 : 100 MultiResponse.run(:use_first_response) do |r| - r.process { authorize(100, payment, options) } - r.process(:ignore_result) { void(r.authorization, options) } + r.process { authorize(amount, payment, options) } + r.process(:ignore_result) { void(r.authorization, options) } unless amount == 0 end end - # Adds credit to a subscription (stand alone credit). - def credit(money, reference, options = {}) - commit(build_credit_request(money, reference, options), :credit, money, options) + # Adds credit to a card or subscription (stand alone credit). + def credit(money, creditcard_or_reference, options = {}) + setup_address_hash(options) + commit(build_credit_request(money, creditcard_or_reference, options), :credit, money, options) end # Stores a customer subscription/profile with type "on-demand". # 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, options) + 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 @@ -202,17 +288,11 @@ def retrieve(reference, options = {}) # This functionality is only supported by this particular gateway may # be changed at any time def calculate_tax(creditcard, options) - requires!(options, :line_items) + requires!(options, :line_items) setup_address_hash(options) commit(build_tax_calculation_request(creditcard, options), :calculate_tax, nil, options) end - # Determines if a card can be used for Pinless Debit Card transactions - def validate_pinless_debit_card(creditcard, options = {}) - requires!(options, :order_id) - commit(build_validate_pinless_debit_request(creditcard,options), :validate_pinless_debit_card, nil, options) - end - def supports_scrubbing? true end @@ -224,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 @@ -238,40 +320,86 @@ def verify_credentials private - # Create all address hash key value pairs so that we still function if we - # were only provided with one or two of them or even none + def valid_payment_method?(payment_method, options) + return true unless payment_method.is_a?(NetworkTokenizationCreditCard) + + brands = %w(visa master american_express) + brands << 'discover' if options[:enable_cybs_discover_apple_pay] + brands.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 def setup_address_hash(options) default_address = { - :address1 => 'Unspecified', - :city => 'Unspecified', - :state => 'NC', - :zip => '00000', - :country => 'US' + address1: 'Unspecified', + city: 'Unspecified', + state: 'NC', + zip: '00000', + country: 'US' } - options[:billing_address] = options[:billing_address] || options[:address] || default_address + + submitted_address = options[:billing_address] || options[:address] || default_address + options[:billing_address] = default_address.merge(submitted_address.symbolize_keys) { |_k, default, submitted| check_billing_field_value(default, submitted) } options[:shipping_address] = options[:shipping_address] || {} end + def check_billing_field_value(default, submitted) + if submitted.nil? + nil + elsif submitted.blank? + default + else + submitted + end + end + def build_auth_request(money, creditcard_or_reference, options) - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 + add_customer_id(xml, options) add_payment_method_or_subscription(xml, money, creditcard_or_reference, 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_payment_network_token(xml) if network_tokenization?(creditcard_or_reference) add_business_rules_data(xml, creditcard_or_reference, options) + add_airline_data(xml, options) + add_merchant_category_code(xml, options) + add_sales_slip_number(xml, options) + 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) + add_partner_solution_id(xml) + add_stored_credential_options(xml, options) + add_merchant_description(xml, options) + xml.target! + end + + def build_adjust_request(money, authorization, options) + _, request_id = authorization.split(';') + + xml = Builder::XmlMarkup.new indent: 2 + add_purchase_data(xml, money, true, options) + add_incremental_auth_service(xml, request_id, options) xml.target! end def build_tax_calculation_request(creditcard, options) - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 add_address(xml, creditcard, options[:billing_address], options, false) add_address(xml, creditcard, options[:shipping_address], options, true) add_line_item_data(xml, options) add_purchase_data(xml, 0, false, options) add_tax_service(xml) add_business_rules_data(xml, creditcard, options) + add_tax_management_indicator(xml, options) xml.target! end @@ -279,40 +407,83 @@ def build_capture_request(money, authorization, options) order_id, request_id, request_token = authorization.split(';') options[:order_id] = order_id - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 add_purchase_data(xml, money, true, options) - add_capture_service(xml, request_id, request_token) + add_other_tax(xml, options) + add_mdd_fields(xml, options) + add_capture_service(xml, request_id, request_token, options) add_business_rules_data(xml, authorization, options) + add_merchant_category_code(xml, options) + add_tax_management_indicator(xml, options) + add_issuer_additional_data(xml, options) + add_merchant_description(xml, options) + add_partner_solution_id(xml) + xml.target! end def build_purchase_request(money, payment_method_or_reference, options) - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 + add_customer_id(xml, options) add_payment_method_or_subscription(xml, money, payment_method_or_reference, 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' + if (!payment_method_or_reference.is_a?(String) && card_brand(payment_method_or_reference) == 'check') || reference_is_a_check?(payment_method_or_reference) add_check_service(xml) + add_airline_data(xml, options) + add_merchant_category_code(xml, options) + add_sales_slip_number(xml, options) + add_tax_management_indicator(xml, options) + add_issuer_additional_data(xml, options) + add_partner_solution_id(xml) + options[:payment_method] = :check else add_purchase_service(xml, payment_method_or_reference, options) add_threeds_services(xml, options) - add_payment_network_token(xml) if network_tokenization?(payment_method_or_reference) - add_business_rules_data(xml, payment_method_or_reference, options) unless options[:pinless_debit_card] + add_business_rules_data(xml, payment_method_or_reference, options) + add_airline_data(xml, options) + add_merchant_category_code(xml, options) + add_sales_slip_number(xml, options) + 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) + add_partner_solution_id(xml) + add_stored_credential_options(xml, options) + options[:payment_method] = :credit_card end + + add_merchant_description(xml, options) + xml.target! end + def reference_is_a_check?(payment_method_or_reference) + payment_method_or_reference.is_a?(String) && payment_method_or_reference.split(';')[7] == 'check' + end + def build_void_request(identification, options) - order_id, request_id, request_token, action, money, currency = identification.split(';') + order_id, request_id, request_token, action, money, currency = identification.split(';') options[:order_id] = order_id - xml = Builder::XmlMarkup.new :indent => 2 - if action == 'capture' + xml = Builder::XmlMarkup.new indent: 2 + case action + when 'capture', 'purchase' + add_mdd_fields(xml, options) add_void_service(xml, request_id, request_token) else - add_purchase_data(xml, money, true, options.merge(:currency => currency || default_currency)) + add_purchase_data(xml, money, true, options.merge(currency: currency || default_currency)) + add_mdd_fields(xml, options) add_auth_reversal_service(xml, request_id, request_token) end + add_merchant_category_code(xml, options) + add_issuer_additional_data(xml, options) + add_partner_solution_id(xml) + xml.target! end @@ -320,55 +491,64 @@ def build_refund_request(money, identification, options) order_id, request_id, request_token = identification.split(';') options[:order_id] = order_id - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 add_purchase_data(xml, money, true, options) - add_credit_service(xml, request_id, request_token) + add_credit_service(xml, request_id:, + request_token:, + use_check_service: reference_is_a_check?(identification)) + add_merchant_category_code(xml, options) + add_partner_solution_id(xml) xml.target! end - def build_credit_request(money, reference, options) - xml = Builder::XmlMarkup.new :indent => 2 + def build_credit_request(money, creditcard_or_reference, options) + xml = Builder::XmlMarkup.new indent: 2 - add_purchase_data(xml, money, true, options) - add_subscription(xml, options, reference) - add_credit_service(xml) + add_payment_method_or_subscription(xml, money, creditcard_or_reference, options) + add_mdd_fields(xml, options) + add_credit_service(xml, use_check_service: creditcard_or_reference.is_a?(Check)) + add_issuer_additional_data(xml, options) + add_merchant_description(xml, options) xml.target! end def build_create_subscription_request(payment_method, options) - default_subscription_params = {:frequency => 'on-demand', :amount => 0, :automatic_renew => false} + default_subscription_params = { frequency: 'on-demand', amount: 0, automatic_renew: false } options[:subscription] = default_subscription_params.update( options[:subscription] || {} ) - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 add_address(xml, payment_method, options[:billing_address], options) add_purchase_data(xml, options[:setup_fee] || 0, true, options) if card_brand(payment_method) == 'check' - add_check(xml, payment_method) + add_check(xml, payment_method, options) add_check_payment_method(xml) + options[:payment_method] = :check else add_creditcard(xml, payment_method) add_creditcard_payment_method(xml) + options[:payment_method] = :credit_card end add_subscription(xml, options) if options[:setup_fee] if card_brand(payment_method) == 'check' - add_check_service(xml, 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) add_business_rules_data(xml, payment_method, options) + add_tax_management_indicator(xml, options) xml.target! end def build_update_subscription_request(reference, creditcard, options) - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 add_address(xml, creditcard, options[:billing_address], options) unless options[:billing_address].blank? add_purchase_data(xml, options[:setup_fee], true, options) unless options[:setup_fee].blank? add_creditcard(xml, creditcard) if creditcard @@ -376,42 +556,34 @@ def build_update_subscription_request(reference, creditcard, options) add_subscription(xml, options, reference) add_subscription_update_service(xml, options) add_business_rules_data(xml, creditcard, options) + add_tax_management_indicator(xml, options) xml.target! end def build_delete_subscription_request(reference, options) - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 add_subscription(xml, options, reference) add_subscription_delete_service(xml, options) xml.target! end def build_retrieve_subscription_request(reference, options) - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 add_subscription(xml, options, reference) add_subscription_retrieve_service(xml, options) xml.target! end - def build_validate_pinless_debit_request(creditcard,options) - xml = Builder::XmlMarkup.new :indent => 2 - add_creditcard(xml, creditcard) - add_validate_pinless_debit_service(xml) - xml.target! - end - 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) - xml.tag!('ignoreCVResult', 'true') if extract_option(prioritized_options, :ignore_cvv) - 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 - def extract_option prioritized_options, option_name + def extract_option(prioritized_options, option_name) options_matching_key = prioritized_options.detect do |options| options.has_key? option_name end @@ -419,38 +591,111 @@ def extract_option prioritized_options, option_name end def add_line_item_data(xml, options) + return unless options[:line_items] + options[:line_items].each_with_index do |value, index| - xml.tag! 'item', {'id' => index} do + xml.tag! 'item', { 'id' => index } do xml.tag! 'unitPrice', localized_amount(value[:declared_value].to_i, options[:currency] || default_currency) xml.tag! 'quantity', value[:quantity] xml.tag! 'productCode', value[:code] || 'shipping_only' xml.tag! 'productName', value[:description] xml.tag! 'productSKU', value[:sku] + xml.tag! 'taxAmount', value[:tax_amount] if value[:tax_amount] + xml.tag! 'nationalTax', value[:national_tax] if value[:national_tax] end end 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 - xml.tag! 'clientEnvironment' , RUBY_PLATFORM + xml.tag! 'clientLibrary', 'Ruby Active Merchant' + xml.tag! 'clientLibraryVersion', VERSION + xml.tag! 'clientEnvironment', RUBY_PLATFORM + + add_merchant_descriptor(xml, options) + end + + def add_merchant_descriptor(xml, options) + return unless options[:merchant_descriptor] || + options[:user_po] || + options[:taxable] || + options[:reference_data_code] || + options[:invoice_number] || + options[:merchant_descriptor_city] || + options[:submerchant_id] || + options[:merchant_descriptor_state] || + options[:merchant_descriptor_country] + + xml.tag! 'invoiceHeader' do + xml.tag! 'merchantDescriptor', options[:merchant_descriptor] if options[:merchant_descriptor] + xml.tag! 'merchantDescriptorCity', options[:merchant_descriptor_city] if options[:merchant_descriptor_city] + xml.tag! 'merchantDescriptorState', options[:merchant_descriptor_state] if options[:merchant_descriptor_state] + xml.tag! 'merchantDescriptorCountry', options[:merchant_descriptor_country] if options[:merchant_descriptor_country] + xml.tag! 'userPO', options[:user_po] if options[:user_po] + xml.tag! 'taxable', options[:taxable] if options[:taxable] + xml.tag! 'submerchantID', options[:submerchant_id] if options[:submerchant_id] + xml.tag! 'referenceDataCode', options[:reference_data_code] if options[:reference_data_code] + xml.tag! 'invoiceNumber', options[:invoice_number] if options[:invoice_number] + end end - def add_purchase_data(xml, money = 0, include_grand_total = false, options={}) + def add_customer_id(xml, options) + return unless options[:customer_id] + + xml.tag! 'customerID', options[:customer_id] + end + + def add_merchant_description(xml, options) + return unless options[:merchant_descriptor_name] || options[:merchant_descriptor_address1] || options[:merchant_descriptor_locality] + + xml.tag! 'merchantInformation' do + xml.tag! 'merchantDescriptor' do + xml.tag! 'name', options[:merchant_descriptor_name] if options[:merchant_descriptor_name] + xml.tag! 'address1', options[:merchant_descriptor_address1] if options[:merchant_descriptor_address1] + xml.tag! 'locality', options[:merchant_descriptor_locality] if options[:merchant_descriptor_locality] + end + end + end + + def add_sales_slip_number(xml, options) + xml.tag! 'salesSlipNumber', options[:sales_slip_number] if options[:sales_slip_number] + end + + def add_airline_data(xml, options) + return unless options[:airline_agent_code] + + xml.tag! 'airlineData' do + xml.tag! 'agentCode', options[:airline_agent_code] + end + end + + def add_tax_management_indicator(xml, options) + return unless options[:tax_management_indicator] + + xml.tag! 'taxManagementIndicator', options[:tax_management_indicator] if options[:tax_management_indicator] + end + + def add_purchase_data(xml, money = 0, include_grand_total = false, options = {}) xml.tag! 'purchaseTotals' do xml.tag! 'currency', options[:currency] || currency(money) - xml.tag!('grandTotalAmount', localized_amount(money.to_i, options[:currency] || default_currency)) if include_grand_total + xml.tag!('discountManagementIndicator', options[:discount_management_indicator]) if options[:discount_management_indicator] + xml.tag!('taxAmount', options[:purchase_tax_amount]) if options[:purchase_tax_amount] + xml.tag!('grandTotalAmount', localized_amount(money.to_i, options[:currency] || default_currency)) if include_grand_total + xml.tag!('originalAmount', options[:original_amount]) if options[:original_amount] + xml.tag!('invoiceAmount', options[:invoice_amount]) if options[:invoice_amount] end end def add_address(xml, payment_method, address, options, shipTo = false) + first_name, last_name = address_names(address[:name], payment_method) + bill_to_merchant_tax_id = options[:merchant_tax_id] unless shipTo + xml.tag! shipTo ? 'shipTo' : 'billTo' do - xml.tag! 'firstName', payment_method.first_name if payment_method - xml.tag! 'lastName', payment_method.last_name if payment_method + xml.tag! 'firstName', first_name if first_name + xml.tag! 'lastName', last_name if last_name xml.tag! 'street1', address[:address1] - xml.tag! 'street2', address[:address2] unless address[:address2].blank? + xml.tag! 'street2', address[:address2] unless address[:address2].blank? xml.tag! 'city', address[:city] xml.tag! 'state', address[:state] xml.tag! 'postalCode', address[:zip] @@ -458,19 +703,44 @@ def add_address(xml, payment_method, address, options, shipTo = false) xml.tag! 'company', address[:company] unless address[:company].blank? xml.tag! 'companyTaxID', address[:companyTaxID] unless address[:company_tax_id].blank? xml.tag! 'phoneNumber', address[:phone] unless address[:phone].blank? - xml.tag! 'email', options[:email] || 'null@cybersource.com' + xml.tag! 'email', options[:email].presence || 'null@cybersource.com' xml.tag! 'ipAddress', options[:ip] unless options[:ip].blank? || shipTo xml.tag! 'driversLicenseNumber', options[:drivers_license_number] unless options[:drivers_license_number].blank? xml.tag! 'driversLicenseState', options[:drivers_license_state] unless options[:drivers_license_state].blank? + xml.tag! 'merchantTaxID', bill_to_merchant_tax_id unless bill_to_merchant_tax_id.blank? + add_browser_info(xml, options, shipTo) end end + def add_browser_info(xml, options, shipTo) + return if shipTo + return unless browser_info = options.dig(:three_ds_2, :browser_info) + + xml.tag! 'httpBrowserColorDepth', browser_info[:depth]&.to_s + xml.tag! 'httpBrowserJavaEnabled', browser_info[:java] + xml.tag! 'httpBrowserJavaScriptEnabled', browser_info[:javascript] + xml.tag! 'httpBrowserLanguage', browser_info[:language]&.to_s + xml.tag! 'httpBrowserScreenHeight', browser_info[:height]&.to_s + xml.tag! 'httpBrowserScreenWidth', browser_info[:width]&.to_s + xml.tag! 'httpBrowserTimeDifference', browser_info[:timezone]&.to_s + 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 add_creditcard(xml, creditcard) xml.tag! 'card' do xml.tag! 'accountNumber', creditcard.number xml.tag! 'expirationMonth', format(creditcard.month, :two_digits) xml.tag! 'expirationYear', format(creditcard.year, :four_digits) - xml.tag!('cvNumber', creditcard.verification_value) unless (@options[:ignore_cvv] || creditcard.verification_value.blank? ) + xml.tag!('cvNumber', creditcard.verification_value) unless @options[:ignore_cvv].to_s == 'true' || creditcard.verification_value.blank? xml.tag! 'cardType', @@credit_card_codes[card_brand(creditcard).to_sym] end end @@ -484,37 +754,152 @@ def add_decision_manager_fields(xml, options) end end + def add_payment_solution(xml, payment_method) + return unless network_tokenization?(payment_method) + + payment_solution = payment_method.network_token? ? NT_PAYMENT_SOLUTION[payment_method.brand] : @@wallet_payment_solution[payment_method.source] + + xml.tag! 'paymentSolution', payment_solution + end + + def add_issuer_additional_data(xml, options) + return unless options[:issuer_additional_data] + + xml.tag! 'issuer' do + xml.tag! 'additionalData', options[:issuer_additional_data] + end + end + + def add_other_tax(xml, options) + 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] + xml.tag! 'localTaxAmount', options[:local_tax_amount] if options[:local_tax_amount] + xml.tag! 'nationalTaxAmount', options[:national_tax_amount] if options[:national_tax_amount] + xml.tag! 'nationalTaxIndicator', options[:national_tax_indicator] if options[:national_tax_indicator] + end + end + def add_mdd_fields(xml, options) - return unless options.keys.any? { |key| key.to_s.start_with?('mdd_field') } + return unless options.keys.any? { |key| key.to_s.start_with?('mdd_field') && options[key] } xml.tag! 'merchantDefinedData' do (1..100).each do |each| key = "mdd_field_#{each}".to_sym - xml.tag!("field#{each}", options[key]) if options[key] + xml.tag!('mddField', options[key], 'id' => each) if options[key] end end end - def add_check(xml, check) + def add_check(xml, check, options) xml.tag! 'check' do xml.tag! 'accountNumber', check.account_number - xml.tag! 'accountType', check.account_type[0] - xml.tag! 'bankTransitNumber', check.routing_number + xml.tag! 'accountType', check.account_type == 'checking' ? 'C' : 'S' + xml.tag! 'bankTransitNumber', format_routing_number(check.routing_number, options) + xml.tag! 'secCode', options[:sec_code] if options[:sec_code] end end def add_tax_service(xml) - xml.tag! 'taxService', {'run' => 'true'} do + xml.tag! 'taxService', { 'run' => 'true' } do xml.tag!('nexus', @options[:nexus]) unless @options[:nexus].blank? xml.tag!('sellerRegistration', @options[:vat_reg_number]) unless @options[:vat_reg_number].blank? end end def add_auth_service(xml, payment_method, options) - if network_tokenization?(payment_method) - add_auth_network_tokenization(xml, payment_method, options) + xml.tag! 'ccAuthService', { 'run' => 'true' } do + if network_tokenization?(payment_method) + add_auth_network_tokenization(xml, payment_method, options) + elsif options[:three_d_secure] + add_normalized_threeds_2_data(xml, payment_method, options) + add_threeds_exemption_data(xml, options) + elsif (indicator = options[:commerce_indicator] || stored_credential_commerce_indicator(options)) + xml.tag!('commerceIndicator', indicator) + end + + unless options[:three_d_secure] + add_reconciliation_and_aggregator_id(xml, options) + add_optional_fields(xml, options) + end + end + end + + def add_reconciliation_and_aggregator_id(xml, options) + xml.tag!('reconciliationID', options[:reconciliation_id]) if options[:reconciliation_id] + xml.tag!('aggregatorID', options[:aggregator_id]) if options[:aggregator_id] + end + + def add_optional_fields(xml, options) + 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 + + def add_threeds_exemption_data(xml, options) + return unless (exemption = THREEDS_EXEMPTIONS[options[:three_ds_exemption_type]&.to_sym]) + + xml.tag!(exemption, '1') + end + + def add_incremental_auth_service(xml, authorization, options) + xml.tag! 'ccIncrementalAuthService', { 'run' => 'true' } do + xml.tag! 'authRequestID', authorization + end + xml.tag! 'subsequentAuthReason', options[:auth_reason] + end + + def add_normalized_threeds_2_data(xml, payment_method, options) + threeds_2_options = options[:three_d_secure] + cc_brand = card_brand(payment_method).to_sym + + return if threeds_2_options[:cavv].blank? && infer_commerce_indicator?(options, cc_brand) + + xid = threeds_2_options[:xid] + + xml.tag!('cavv', threeds_2_options[:cavv]) if threeds_2_options[:cavv] && cc_brand != :master + xml.tag!('cavvAlgorithm', threeds_2_options[:cavv_algorithm]) if threeds_2_options[:cavv_algorithm] + xml.tag!('paSpecificationVersion', threeds_2_options[:version]) if threeds_2_options[:version] + xml.tag!('directoryServerTransactionID', threeds_2_options[:ds_transaction_id]) if threeds_2_options[:ds_transaction_id] + xml.tag!('commerceIndicator', options[:commerce_indicator] || ECI_BRAND_MAPPING[cc_brand]) + xml.tag!('eciRaw', threeds_2_options[:eci]) if threeds_2_options[:eci] + + if xid.present? + xml.tag!('xid', xid) + elsif threeds_2_options[:version]&.start_with?('2') && cc_brand != :master + cavv = threeds_2_options[:cavv] + xml.tag!('xid', cavv) if cavv.present? + end + + add_reconciliation_and_aggregator_id(xml, options) + xml.tag!('veresEnrolled', threeds_2_options[:enrolled]) if threeds_2_options[:enrolled] + xml.tag!('paresStatus', threeds_2_options[:authentication_response_status]) if threeds_2_options[:authentication_response_status] + add_optional_fields(xml, options) + end + + def infer_commerce_indicator?(options, cc_brand) + options[:commerce_indicator].blank? && ECI_BRAND_MAPPING[cc_brand].present? + end + + def add_threeds_2_ucaf_data(xml, payment_method, options) + return unless options[:three_d_secure] && card_brand(payment_method).to_sym == :master + + xml.tag! 'ucaf' do + xml.tag!('authenticationData', options[:three_d_secure][:cavv]) + xml.tag!('collectionIndicator', options[:collection_indicator] || DEFAULT_COLLECTION_INDICATOR) + end + end + + def stored_credential_commerce_indicator(options) + return unless (reason_type = options.dig(:stored_credential, :reason_type)) + + case reason_type + when 'installment' + 'install' + when 'recurring' + 'recurring' else - xml.tag! 'ccAuthService', {'run' => 'true'} + 'internet' end end @@ -522,95 +907,153 @@ def network_tokenization?(payment_method) payment_method.is_a?(NetworkTokenizationCreditCard) end + def subsequent_wallet_auth(payment_method, options) + return unless options[:stored_credential] || options[:stored_credential_overrides] + return unless @@wallet_payment_solution[payment_method.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) + brand = card_brand(payment_method) + return if payment_method.mobile_wallet? && brand == 'discover' && !options[:enable_cybs_discover_apple_pay] + + if payment_method.network_token? + xml.tag!('networkTokenCryptogram', payment_method.payment_cryptogram) + xml.tag!('commerceIndicator', stored_credential_commerce_indicator(options) || 'internet') + elsif send_only_commerce_indicator(payment_method, options) + commerce_indicator = if brand == 'discover' + 'dipb' + elsif subsequent_wallet_auth(payment_method, options) + 'internet' + else + ECI_BRAND_MAPPING[brand.to_sym] + end + + xml.commerceIndicator(commerce_indicator) + else + default_wallet_values(xml, payment_method) + end + end - case card_brand(payment_method).to_sym - when :visa - xml.tag! 'ccAuthService', {'run' => 'true'} do - xml.tag!('cavv', payment_method.payment_cryptogram) - xml.tag!('commerceIndicator', 'vbv') - xml.tag!('xid', payment_method.payment_cryptogram) - end - when :mastercard - xml.tag! 'ucaf' do - xml.tag!('authenticationData', payment_method.payment_cryptogram) - xml.tag!('collectionIndicator', '2') - end - xml.tag! 'ccAuthService', {'run' => 'true'} do - xml.tag!('commerceIndicator', 'spa') - end - when :american_express - cryptogram = Base64.decode64(payment_method.payment_cryptogram) - xml.tag! 'ccAuthService', {'run' => 'true'} do - xml.tag!('cavv', Base64.encode64(cryptogram[0...20])) - xml.tag!('commerceIndicator', 'aesk') - xml.tag!('xid', Base64.encode64(cryptogram[20...40])) - end + def send_only_commerce_indicator(payment_method, options) + brand = card_brand(payment_method) + + return false if brand == 'american_express' + + brand == 'master' || subsequent_wallet_auth(payment_method, options) + end + + def default_wallet_values(xml, payment_method) + brand = card_brand(payment_method).to_sym + commerce_indicator = brand == :discover ? 'dipb' : ECI_BRAND_MAPPING[brand] + cryptogram = brand == :american_express ? Base64.decode64(payment_method.payment_cryptogram) : payment_method.payment_cryptogram + cavv = xid = cryptogram + + if brand == :american_express + cavv = Base64.encode64(cavv[0...20]) + xid = xid.bytes.count > 20 ? Base64.encode64(xid[20...40]) : nil end + + xml.tag! 'cavv', cavv + xml.tag! 'commerceIndicator', commerce_indicator + xml.tag! 'xid', xid if xid 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_wallet_auth(payment_method, 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.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 - def add_capture_service(xml, request_id, request_token) - xml.tag! 'ccCaptureService', {'run' => 'true'} do + def add_capture_service(xml, request_id, request_token, options) + xml.tag! 'ccCaptureService', { 'run' => 'true' } do xml.tag! 'authRequestID', request_id + xml.tag! 'reconciliationID', options[:reconciliation_id] if options[:reconciliation_id] xml.tag! 'authRequestToken', request_token + xml.tag! 'gratuityAmount', options[:gratuity_amount] if options[:gratuity_amount] + 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) - if options[:pinless_debit_card] - xml.tag! 'pinlessDebitService', {'run' => 'true'} - else - add_auth_service(xml, payment_method, options) - xml.tag! 'ccCaptureService', {'run' => 'true'} + 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 def add_void_service(xml, request_id, request_token) - xml.tag! 'voidService', {'run' => 'true'} do + xml.tag! 'voidService', { 'run' => 'true' } do xml.tag! 'voidRequestID', request_id xml.tag! 'voidRequestToken', request_token end end def add_auth_reversal_service(xml, request_id, request_token) - xml.tag! 'ccAuthReversalService', {'run' => 'true'} do + xml.tag! 'ccAuthReversalService', { 'run' => 'true' } do xml.tag! 'authRequestID', request_id xml.tag! 'authRequestToken', request_token end end - def add_credit_service(xml, request_id = nil, request_token = nil) - xml.tag! 'ccCreditService', {'run' => 'true'} do - xml.tag! 'captureRequestID', request_id if request_id - xml.tag! 'captureRequestToken', request_token if request_token + def add_credit_service(xml, options = {}) + service = options[:use_check_service] ? 'ecCreditService' : 'ccCreditService' + request_tag = options[:use_check_service] ? 'debitRequestID' : 'captureRequestID' + options.delete :request_token if options[:use_check_service] + + xml.tag! service, { 'run' => 'true' } do + xml.tag! request_tag, options[:request_id] if options[:request_id] + xml.tag! 'captureRequestToken', options[:request_token] if options[:request_token] end end def add_check_service(xml) - xml.tag! 'ecDebitService', {'run' => 'true'} + xml.tag! 'ecDebitService', { 'run' => 'true' } end def add_subscription_create_service(xml, options) - xml.tag! 'paySubscriptionCreateService', {'run' => 'true'} + xml.tag! 'paySubscriptionCreateService', { 'run' => 'true' } end def add_subscription_update_service(xml, options) - xml.tag! 'paySubscriptionUpdateService', {'run' => 'true'} + xml.tag! 'paySubscriptionUpdateService', { 'run' => 'true' } end def add_subscription_delete_service(xml, options) - xml.tag! 'paySubscriptionDeleteService', {'run' => 'true'} + xml.tag! 'paySubscriptionDeleteService', { 'run' => 'true' } end def add_subscription_retrieve_service(xml, options) - xml.tag! 'paySubscriptionRetrieveService', {'run' => 'true'} + xml.tag! 'paySubscriptionRetrieveService', { 'run' => 'true' } + end + + def add_merchant_category_code(xml, options) + xml.tag! 'merchantCategoryCode', options[:merchant_category_code] if options[:merchant_category_code] end def add_subscription(xml, options, reference = nil) @@ -622,7 +1065,7 @@ def add_subscription(xml, options, reference = nil) xml.tag! 'subscriptionID', subscription_id end - xml.tag! 'status', options[:subscription][:status] if options[:subscription][:status] + xml.tag! 'status', options[:subscription][:status] if options[:subscription][:status] xml.tag! 'amount', localized_amount(options[:subscription][:amount].to_i, options[:currency] || default_currency) if options[:subscription][:amount] xml.tag! 'numberOfPayments', options[:subscription][:occurrences] if options[:subscription][:occurrences] xml.tag! 'automaticRenew', options[:subscription][:automatic_renew] if options[:subscription][:automatic_renew] @@ -650,57 +1093,128 @@ def add_check_payment_method(xml) def add_payment_method_or_subscription(xml, money, payment_method_or_reference, options) if payment_method_or_reference.is_a?(String) add_purchase_data(xml, money, true, options) + add_installments(xml, options) add_subscription(xml, options, payment_method_or_reference) elsif card_brand(payment_method_or_reference) == 'check' add_address(xml, payment_method_or_reference, options[:billing_address], options) add_purchase_data(xml, money, true, options) - add_check(xml, payment_method_or_reference) + add_installments(xml, options) + add_check(xml, payment_method_or_reference, options) else add_address(xml, payment_method_or_reference, options[:billing_address], options) add_address(xml, payment_method_or_reference, options[:shipping_address], options, true) + add_line_item_data(xml, options) add_purchase_data(xml, money, true, options) + add_installments(xml, options) add_creditcard(xml, payment_method_or_reference) end end - def add_validate_pinless_debit_service(xml) - xml.tag!'pinlessDebitValidateService', {'run' => 'true'} + def add_installments(xml, options) + return unless %i[installment_total_count installment_total_amount installment_plan_type first_installment_date installment_annual_interest_rate installment_grace_period_duration].any? { |gsf| options.include?(gsf) } + + xml.tag! 'installment' do + xml.tag!('totalCount', options[:installment_total_count]) if options[:installment_total_count] + xml.tag!('totalAmount', options[:installment_total_amount]) if options[:installment_total_amount] + xml.tag!('planType', options[:installment_plan_type]) if options[:installment_plan_type] + xml.tag!('firstInstallmentDate', options[:first_installment_date]) if options[:first_installment_date] + xml.tag!('annualInterestRate', options[:installment_annual_interest_rate]) if options[:installment_annual_interest_rate] + xml.tag!('gracePeriodDuration', options[:installment_grace_period_duration]) if options[:installment_grace_period_duration] + end end def add_threeds_services(xml, options) - xml.tag! 'payerAuthEnrollService', {'run' => 'true'} if options[:payer_auth_enroll_service] + if options[:payer_auth_enroll_service] + xml.tag! 'payerAuthEnrollService', { 'run' => 'true' } do + browser_info = options.dig(:three_ds_2, :browser_info) + xml.tag! 'httpUserAgent', browser_info[:user_agent] if browser_info&.dig(:user_agent) + xml.tag! 'returnURL', options[:return_url] if options[:return_url] + xml.tag! 'httpUserAccept', browser_info[:accept_header] if browser_info&.dig(:accept_header) + end + end + if options[:payer_auth_validate_service] - xml.tag! 'payerAuthValidateService', {'run' => 'true'} do - xml.tag! 'signedPARes', options[:pares] + xml.tag! 'payerAuthValidateService', { 'run' => 'true' } do + xml.tag! 'authenticationTransactionID', options[:authentication_transaction_id] end end end def lookup_country_code(country_field) country_code = Country.find(country_field) rescue nil - country_code.code(:alpha2) if country_code + country_code&.code(:alpha2) + end + + def add_stored_credential_subsequent_auth(xml, options = {}) + return unless options[:stored_credential] || options[:stored_credential_overrides] + + stored_credential_subsequent_auth = 'true' if options.dig(:stored_credential, :initiator) == 'merchant' + + override_subsequent_auth = options.dig(:stored_credential_overrides, :subsequent_auth) + + xml.subsequentAuth override_subsequent_auth.nil? ? stored_credential_subsequent_auth : override_subsequent_auth + end + + def add_stored_credential_options(xml, options = {}) + return unless options[:stored_credential] || options[:stored_credential_overrides] + + stored_credential_subsequent_auth_first = 'true' if cardholder_or_initiated_transaction?(options) + 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 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) + override_subsequent_auth_stored_cred = options.dig(:stored_credential_overrides, :subsequent_auth_stored_credential) + + xml.subsequentAuthFirst override_subsequent_auth_first.nil? ? stored_credential_subsequent_auth_first : override_subsequent_auth_first + xml.subsequentAuthTransactionID override_subsequent_auth_transaction_id.nil? ? stored_credential_transaction_id : override_subsequent_auth_transaction_id + xml.subsequentAuthStoredCredential override_subsequent_auth_stored_cred.nil? ? stored_credential_subsequent_auth_stored_cred : override_subsequent_auth_stored_cred + end + + def cardholder_or_initiated_transaction?(options) + options.dig(:stored_credential, :initiator) == 'cardholder' || options.dig(:stored_credential, :initial_transaction) + 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] == 'stored_credential' + end + + def add_partner_solution_id(xml) + return unless application_id + + xml.tag!('partnerSolutionID', application_id) end # Where we actually build the full SOAP request using builder def build_request(body, options) - xml = Builder::XmlMarkup.new :indent => 2 - xml.instruct! - xml.tag! 's:Envelope', {'xmlns:s' => 'http://schemas.xmlsoap.org/soap/envelope/'} do - xml.tag! 's:Header' do - xml.tag! 'wsse:Security', {'s:mustUnderstand' => '1', 'xmlns:wsse' => 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'} do - xml.tag! 'wsse:UsernameToken' do - xml.tag! 'wsse:Username', @options[:login] - xml.tag! 'wsse:Password', @options[:password], 'Type' => 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText' - end + xsd_version = test? ? TEST_XSD_VERSION : PRODUCTION_XSD_VERSION + + xml = Builder::XmlMarkup.new indent: 2 + xml.instruct! + xml.tag! 's:Envelope', { 'xmlns:s' => 'http://schemas.xmlsoap.org/soap/envelope/' } do + xml.tag! 's:Header' do + xml.tag! 'wsse:Security', { 's:mustUnderstand' => '1', 'xmlns:wsse' => 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd' } do + xml.tag! 'wsse:UsernameToken' do + xml.tag! 'wsse:Username', @options[:login] + xml.tag! 'wsse:Password', @options[:password], 'Type' => 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText' end end - xml.tag! 's:Body', {'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema'} do - xml.tag! 'requestMessage', {'xmlns' => "urn:schemas-cybersource-com:transaction-data-#{XSD_VERSION}"} do - add_merchant_data(xml, options) - xml << body - end + end + xml.tag! 's:Body', { 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema' } do + xml.tag! 'requestMessage', { 'xmlns' => "urn:schemas-cybersource-com:transaction-data-#{xsd_version}" } do + add_merchant_data(xml, options) + xml << body end end + end xml.target! end @@ -708,24 +1222,41 @@ def build_request(body, options) # Response object def commit(request, action, amount, options) begin - response = parse(ssl_post(test? ? self.test_url : self.live_url, build_request(request, options))) + raw_response = ssl_post(test? ? self.test_url : self.live_url, build_request(request, options)) rescue ResponseError => e - response = parse(e.response.body) + raw_response = e.response.body + end + + begin + response = parse(raw_response) rescue REXML::ParseException => e response = { message: e.to_s } end - success = response[:decision] == 'ACCEPT' - message = response[:message] + success = success?(response) + message = message_from(response) + authorization = success || in_fraud_review?(response) ? authorization_from(response, action, amount, options) : nil + + message = auto_void?(authorization_from(response, action, amount, options), response, message, options) + + Response.new( + success, + message, + response, + test: test?, + authorization:, + fraud_review: in_fraud_review?(response), + avs_result: { code: response[:avsCode] }, + cvv_result: response[:cvCode] + ) + end - authorization = success ? authorization_from(response, action, amount, options) : nil + def auto_void?(authorization, response, message, options = {}) + return message unless response[:reasonCode] == '230' && options[:auto_void_230] - Response.new(success, message, response, - :test => test?, - :authorization => authorization, - :avs_result => { :code => response[:avsCode] }, - :cvv_result => response[:cvCode] - ) + 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 @@ -752,13 +1283,14 @@ def parse(xml) def parse_element(reply, node) if node.has_elements? - node.elements.each{|e| parse_element(reply, e) } + node.elements.each { |e| parse_element(reply, e) } else - if node.parent.name =~ /item/ + if /item/.match?(node.parent.name) parent = node.parent.name 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 @@ -766,12 +1298,39 @@ def parse_element(reply, node) def reason_message(reason_code) return if reason_code.blank? + @@response_codes[:"r#{reason_code}"] end def authorization_from(response, action, amount, options) [options[:order_id], response[:requestID], response[:requestToken], action, amount, - options[:currency], response[:subscriptionID]].join(';') + options[:currency], response[:subscriptionID], options[:payment_method]].join(';') + end + + def in_fraud_review?(response) + response[:decision] == @@decision_codes[:review] + end + + def success?(response) + response[:decision] == @@decision_codes[:accept] + end + + def message_from(response) + if response[:reasonCode] == '101' && response[:missingField] + "#{response[:message]}: #{response[:missingField]}" + elsif response[:reasonCode] == '102' && response[:invalidField] + "#{response[:message]}: #{response[:invalidField]}" + else + response[:message] + end + end + + 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 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..d9f10f792ce --- /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..a34c97bc8b6 --- /dev/null +++ b/lib/active_merchant/billing/gateways/cyber_source_rest.rb @@ -0,0 +1,523 @@ +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 patagonia_365 tarjeta_sol] + + 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', + carnet: '002' + } + + 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(options, 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(options, amount = nil) + { reversalInformation: { amountDetails: { totalAmount: nil } } }.tap do |post| + add_reversal_amount(post, amount.to_i) if amount.present? + add_merchant_category_code(post, options) + 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_merchant_category_code(post, 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_merchant_category_code(post, 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_merchant_category_code(post, 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: + } + 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) + if options.dig(:stored_credential, :initiator) == 'merchant' + post[:paymentInformation][:tokenizedCard] = { + number: payment.number, + expirationMonth: payment.month, + expirationYear: payment.year, + type: CREDIT_CARD_CODES[card_brand(payment).to_sym], + transactionType: payment.source == :network_token ? '3' : '1' + } + else + 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' + } + add_apple_pay_google_pay_cryptogram(post, payment) unless payment.source == :network_token + end + + post[:processingInformation][:commerceIndicator] = 'internet' unless options[:stored_credential] || card_brand(payment) == 'jcb' + + add_payment_solution(post, payment) + end + + def add_payment_solution(post, payment) + if payment.source == :network_token && NT_PAYMENT_SOLUTION[payment.brand] + post[:processingInformation][:paymentSolution] = NT_PAYMENT_SOLUTION[payment.brand] + else + post[:processingInformation][:paymentSolution] = WALLET_PAYMENT_SOLUTION[payment.source] + end + end + + def add_apple_pay_google_pay_cryptogram(post, payment) + if card_brand(payment) == 'master' + post[:consumerAuthenticationInformation] = { + ucafAuthenticationData: payment.payment_cryptogram, + ucafCollectionIndicator: '2' + } + else + post[:consumerAuthenticationInformation] = { cavv: payment.payment_cryptogram } + end + end + + def add_credit_card(post, creditcard) + post[:paymentInformation][:card] = { + number: creditcard.number, + expirationMonth: format(creditcard.month, :two_digits), + expirationYear: format(creditcard.year, :four_digits), + securityCode: creditcard.verification_value, + type: CREDIT_CARD_CODES[card_brand(creditcard).to_sym] + } + end + + def add_address(post, payment_method, address, options, address_type) + return unless address.present? + + first_name, last_name = address_names(address[:name], payment_method) + + post[:orderInformation][address_type] = { + firstName: first_name, + lastName: last_name, + address1: address[:address1], + address2: address[:address2], + locality: address[:city], + administrativeArea: address[:state], + postalCode: address[:zip], + country: lookup_country_code(address[:country])&.value, + email: options[:email].presence || 'null@cybersource.com', + phoneNumber: address[:phone] + # merchantTaxID: ship_to ? options[:merchant_tax_id] : nil, + # company: address[:company], + # companyTaxID: address[:companyTaxID], + # ipAddress: options[:ip], + # driversLicenseNumber: options[:drivers_license_number], + # driversLicenseState: options[:drivers_license_state], + }.compact + end + + def add_merchant_description(post, options) + return unless options[:merchant_descriptor_name] || options[:merchant_descriptor_address1] || options[:merchant_descriptor_locality] + + merchant = post[:merchantInformation][:merchantDescriptor] = {} + merchant[:name] = options[:merchant_descriptor_name] if options[:merchant_descriptor_name] + merchant[:address1] = options[:merchant_descriptor_address1] if options[:merchant_descriptor_address1] + merchant[:locality] = options[:merchant_descriptor_locality] if options[:merchant_descriptor_locality] + end + + def add_merchant_category_code(post, options) + return unless options[:merchant_category_code] + + post[:merchantInformation] ||= {} + post[:merchantInformation][:categoryCode] = options[:merchant_category_code] if options[:merchant_category_code] + 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.dig('errorInformation', 'message') || response['message'] + end + + def authorization_from(response) + id = response['id'] + amount = response.dig('orderInformation', 'amountDetails', 'authorizedAmount')&.delete('.') + + amount.present? ? [id, amount].join('|') : id + end + + def error_code_from(response) + response.dig('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:, + date: gmtdatetime, + 'request-target': "#{http_method} /pts/v2/#{resource}", + 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:, 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 new file mode 100644 index 00000000000..910537b482a --- /dev/null +++ b/lib/active_merchant/billing/gateways/d_local.rb @@ -0,0 +1,365 @@ +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class DLocalGateway < Gateway + self.test_url = 'https://sandbox.dlocal.com' + self.live_url = 'https://api.dlocal.com' + + self.supported_countries = %w[AR BD BO BR CL CM CN CO CR DO EC EG GH GT IN ID JP KE MY MX MA NG PA PY PE PH SN SV TH TR TZ UG UY VN ZA] + self.default_currency = 'USD' + self.supported_cardtypes = %i[visa master american_express discover jcb diners_club maestro naranja cabal elo alia carnet patagonia_365 tarjeta_sol] + + self.homepage_url = 'https://dlocal.com/' + self.display_name = 'dLocal' + + def initialize(options = {}) + requires!(options, :login, :trans_key, :secret_key) + super + end + + def purchase(money, payment, options = {}) + post = {} + add_auth_purchase_params(post, money, payment, 'purchase', options) + add_three_ds(post, options) + + commit('purchase', post, options) + end + + def authorize(money, payment, options = {}) + post = {} + add_auth_purchase_params(post, money, payment, 'authorize', options) + add_three_ds(post, options) + post[:card][:verify] = true if options[:verify].to_s == 'true' + + commit('authorize', post, options) + end + + def capture(money, authorization, options = {}) + post = {} + post[:authorization_id] = authorization + add_invoice(post, money, options) if money + commit('capture', post, options) + end + + def refund(money, authorization, options = {}) + post = {} + add_description(post, options) + post[:payment_id] = authorization + post[:notification_url] = options[:notification_url] + add_invoice(post, money, options) if money + commit('refund', post, options) + end + + def void(authorization, options = {}) + post = {} + post[:authorization_id] = authorization + commit('void', post, options) + end + + def verify(credit_card, options = {}) + authorize(0, credit_card, options.merge(verify: 'true')) + end + + def inquire(authorization, options = {}) + post = {} + post[:payment_id] = authorization + action = authorization ? 'status' : 'orders' + commit(action, post, options) + end + + def supports_scrubbing? + true + end + + def supports_network_tokenization? + true + end + + def scrub(transcript) + transcript. + gsub(%r((X-Trans-Key: )\w+), '\1[FILTERED]'). + gsub(%r((\"number\\\":\\\")\d+), '\1[FILTERED]'). + gsub(%r((\"cvv\\\":\\\")\d+), '\1[FILTERED]') + end + + private + + def add_auth_purchase_params(post, money, card, action, options) + add_invoice(post, money, options) + post[:payment_method_id] = 'CARD' + post[:payment_method_flow] = 'DIRECT' + add_country(post, card, options) + add_payer(post, card, options) + add_card(post, card, action, options) + add_additional_data(post, options) + add_description(post, options) + post[:order_id] = options[:order_id] || generate_unique_id + post[:original_order_id] = options[:original_order_id] if options[:original_order_id] + end + + def add_invoice(post, money, options) + post[:amount] = amount(money) + post[:currency] = (options[:currency] || currency(money)) + end + + def add_description(post, options) + post[:description] = options[:description] if options[:description] + end + + def add_additional_data(post, options) + post[:additional_risk_data] = options[:additional_data] + end + + def add_country(post, card, options) + return unless (address = options[:billing_address] || options[:address]) || options[:country] + + country = options[:country] ? lookup_country_code(options[:country]) : lookup_country_code(address[:country]) + post[:country] = country + end + + def lookup_country_code(country_field) + Country.find(country_field).code(:alpha2).value + rescue InvalidCountryCodeError + nil + end + + def add_payer(post, card, options) + address = options[:billing_address] || options[:address] + phone_number = address[:phone] || address[:phone_number] if address + + post[:payer] = {} + post[:payer][:name] = card.name + post[:payer][:email] = options[:email] if options[:email] + post[:payer][:birth_date] = options[:birth_date] if options[:birth_date] + post[:payer][:phone] = phone_number if phone_number + post[:payer][:document] = options[:document] if options[:document] + post[:payer][:document2] = options[:document2] if options[:document2] + post[:payer][:user_reference] = options[:user_reference] if options[:user_reference] + post[:payer][:event_uuid] = options[:device_id] if options[:device_id] + post[:payer][:ip] = options[:ip] if options[:ip] + post[:payer][:address] = add_address(post, card, options) + end + + def add_address(post, card, options) + return unless address = options[:billing_address] || options[:address] + + address_object = {} + address_object[:state] = address[:state] if address[:state] + address_object[:city] = address[:city] if address[:city] + address_object[:zip_code] = address[:zip] if address[:zip] + address_object[:street] = address[:street] || parse_street(address) if parse_street(address) + address_object[:number] = address[:number] || parse_house_number(address) if parse_house_number(address) + address_object + end + + def parse_street(address) + return unless address[:address1] + + street = address[:address1].split(/\s+/).keep_if { |x| x !~ /\d/ }.join(' ') + street.empty? ? nil : street + end + + def parse_house_number(address) + return unless address[:address1] + + house = address[:address1].split(/\s+/).keep_if { |x| x =~ /\d/ }.join(' ') + house.empty? ? nil : house + end + + def add_card(post, card, action, options = {}) + post[:card] = {} + if card.is_a?(NetworkTokenizationCreditCard) + post[:card][:network_token] = card.number + post[:card][:cryptogram] = card.payment_cryptogram + post[:card][:eci] = card.eci + 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 + post[:card][:descriptor] = options[:dynamic_descriptor] if options[:dynamic_descriptor] + post[:card][:capture] = (action == 'purchase') + post[:card][:installments] = options[:installments] if options[:installments] + post[:card][:installments_id] = options[:installments_id] if options[:installments_id] + post[:card][:force_type] = options[:force_type].to_s.upcase if options[:force_type] + post[:card][:save] = options[:save] if options[:save] + end + + def 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) + JSON.parse(body) + end + + def commit(action, parameters, options = {}) + three_ds_errors = validate_three_ds_params(parameters[:three_dsecure]) if parameters[:three_dsecure].present? + return three_ds_errors if three_ds_errors + + url = url(action, parameters, options) + post = post_data(action, parameters) + begin + raw = if %w(status orders).include?(action) + ssl_get(url, headers(nil, options)) + else + ssl_post(url, post, headers(post, options)) + end + response = parse(raw) + rescue ResponseError => e + raw = e.response.body + response = parse(raw) + end + + Response.new( + success_from(action, response), + 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?, + error_code: error_code_from(action, response) + ) + end + + # A refund may not be immediate, and return a status_code of 100, "Pending". + # Since we aren't handling async notifications of eventual success, + # we count 100 as a success. + def success_from(action, response) + return false unless response['status_code'] + + if action == 'void' + response['status_code'].to_s == '400' && response['status'] == 'CANCELLED' + else + %w[100 200 400 600 700].include? response['status_code'].to_s + end + end + + def message_from(action, response) + response['status_detail'] || response['message'] + end + + 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) + + code = response['status_code'] || response['code'] + code&.to_s + end + + def url(action, parameters, options = {}) + "#{test? ? test_url : live_url}/#{endpoint(action, parameters, options)}/" + end + + def endpoint(action, parameters, options) + case action + when 'purchase' + 'secure_payments' + when 'authorize' + 'secure_payments' + when 'refund' + 'refunds' + when 'capture' + 'payments' + when 'void' + "payments/#{parameters[:authorization_id]}/cancel" + when 'status' + "payments/#{parameters[:payment_id]}/status" + when 'orders' + "orders/#{options[:order_id]}" + end + end + + def headers(post, options = {}) + timestamp = Time.now.utc.iso8601 + headers = { + 'Content-Type' => 'application/json', + 'X-Date' => timestamp, + 'X-Login' => @options[:login], + 'X-Trans-Key' => @options[:trans_key], + 'X-Version' => '2.1', + 'Authorization' => signature(post, timestamp) + } + headers['X-Idempotency-Key'] = options[:idempotency_key] if options[:idempotency_key] + headers['X-Dlocal-Payment-Source'] = application_id if application_id + headers + end + + def signature(post, timestamp) + content = "#{@options[:login]}#{timestamp}#{post}" + digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), @options[:secret_key], content) + "V2-HMAC-SHA256, Signature: #{digest}" + end + + def post_data(action, parameters = {}) + parameters.to_json + end + + def xid_or_ds_trans_id(three_d_secure) + if three_d_secure[:version].to_f >= 2 + { ds_transaction_id: three_d_secure[:ds_transaction_id] } + else + { xid: three_d_secure[:xid] } + end + end + + def add_three_ds(post, options) + return unless three_d_secure = options[:three_d_secure] + + post[:three_dsecure] = { + mpi: true, + three_dsecure_version: three_d_secure[:version], + cavv: three_d_secure[:cavv], + eci: three_d_secure[:eci], + enrollment_response: formatted_enrollment(three_d_secure[:enrolled]), + authentication_response: three_d_secure[:authentication_response_status] + }.merge(xid_or_ds_trans_id(three_d_secure)) + end + + def validate_three_ds_params(three_ds) + errors = {} + supported_version = %w{1.0 2.0 2.1.0 2.2.0}.include?(three_ds[:three_dsecure_version]) + supported_enrollment = ['Y', 'N', 'U', nil].include?(three_ds[:enrollment_response]) + supported_auth_response = ['Y', 'N', 'U', nil].include?(three_ds[:authentication_response]) + + errors[:three_ds_version] = 'ThreeDs version not supported' unless supported_version + errors[:enrollment] = 'Enrollment value not supported' unless supported_enrollment + errors[:auth_response] = 'Authentication response value not supported' unless supported_auth_response + errors.compact! + + errors.present? ? Response.new(false, 'ThreeDs data is invalid', errors) : nil + end + + def formatted_enrollment(val) + case val + when 'Y', 'N', 'U' then val + when true, 'true' then 'Y' + when false, 'false' then 'N' + end + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/data_cash.rb b/lib/active_merchant/billing/gateways/data_cash.rb index 5c94454e2ad..948fe233fc5 100644 --- a/lib/active_merchant/billing/gateways/data_cash.rb +++ b/lib/active_merchant/billing/gateways/data_cash.rb @@ -6,7 +6,7 @@ class DataCashGateway < Gateway self.default_currency = 'GBP' self.supported_countries = ['GB'] - self.supported_cardtypes = [ :visa, :master, :american_express, :discover, :diners_club, :jcb, :maestro, :switch, :solo, :laser ] + self.supported_cardtypes = %i[visa master american_express discover diners_club jcb maestro] self.homepage_url = 'http://www.datacash.com/' self.display_name = 'DataCash' @@ -92,9 +92,9 @@ def scrub(transcript) def build_void_or_capture_request(type, money, authorization, options) parsed_authorization = parse_authorization_string(authorization) - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 xml.instruct! - xml.tag! :Request, :version => '2' do + xml.tag! :Request, version: '2' do add_authentication(xml) xml.tag! :Transaction do @@ -107,7 +107,7 @@ def build_void_or_capture_request(type, money, authorization, options) if money xml.tag! :TxnDetails do xml.tag! :merchantreference, format_reference_number(options[:order_id]) - xml.tag! :amount, amount(money), :currency => options[:currency] || currency(money) + xml.tag! :amount, amount(money), currency: options[:currency] || currency(money) xml.tag! :capturemethod, 'ecomm' end end @@ -117,22 +117,20 @@ def build_void_or_capture_request(type, money, authorization, options) end def build_purchase_or_authorization_request_with_credit_card_request(type, money, credit_card, options) - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 xml.instruct! - xml.tag! :Request, :version => '2' do + xml.tag! :Request, version: '2' do add_authentication(xml) xml.tag! :Transaction do - if options[:set_up_continuous_authority] - xml.tag! :ContAuthTxn, :type => 'setup' - end + xml.tag! :ContAuthTxn, type: 'setup' if options[:set_up_continuous_authority] xml.tag! :CardTxn do xml.tag! :method, type add_credit_card(xml, credit_card, options[:billing_address]) end xml.tag! :TxnDetails do xml.tag! :merchantreference, format_reference_number(options[:order_id]) - xml.tag! :amount, amount(money), :currency => options[:currency] || currency(money) + xml.tag! :amount, amount(money), currency: options[:currency] || currency(money) xml.tag! :capturemethod, 'ecomm' end end @@ -144,19 +142,19 @@ def build_purchase_or_authorization_request_with_continuous_authority_reference_ parsed_authorization = parse_authorization_string(authorization) raise ArgumentError, 'The continuous authority reference is required for continuous authority transactions' if parsed_authorization[:ca_reference].blank? - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 xml.instruct! - xml.tag! :Request, :version => '2' do + xml.tag! :Request, version: '2' do add_authentication(xml) xml.tag! :Transaction do - xml.tag! :ContAuthTxn, :type => 'historic' + xml.tag! :ContAuthTxn, type: 'historic' xml.tag! :HistoricTxn do xml.tag! :reference, parsed_authorization[:ca_reference] xml.tag! :method, type end xml.tag! :TxnDetails do xml.tag! :merchantreference, format_reference_number(options[:order_id]) - xml.tag! :amount, amount(money), :currency => options[:currency] || currency(money) + xml.tag! :amount, amount(money), currency: options[:currency] || currency(money) xml.tag! :capturemethod, 'cont_auth' end end @@ -166,9 +164,9 @@ def build_purchase_or_authorization_request_with_continuous_authority_reference_ def build_transaction_refund_request(money, authorization) parsed_authorization = parse_authorization_string(authorization) - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 xml.instruct! - xml.tag! :Request, :version => '2' do + xml.tag! :Request, version: '2' do add_authentication(xml) xml.tag! :Transaction do xml.tag! :HistoricTxn do @@ -187,9 +185,9 @@ def build_transaction_refund_request(money, authorization) end def build_credit_request(money, credit_card, options) - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 xml.instruct! - xml.tag! :Request, :version => '2' do + xml.tag! :Request, version: '2' do add_authentication(xml) xml.tag! :Transaction do xml.tag! :CardTxn do @@ -214,23 +212,11 @@ def add_authentication(xml) end def add_credit_card(xml, credit_card, address) - xml.tag! :Card do - # DataCash calls the CC number 'pan' xml.tag! :pan, credit_card.number xml.tag! :expirydate, format_date(credit_card.month, credit_card.year) - # optional values - for Solo etc - if [ 'switch', 'solo' ].include?(card_brand(credit_card).to_s) - - xml.tag! :issuenumber, credit_card.issue_number unless credit_card.issue_number.blank? - - if !credit_card.start_month.blank? && !credit_card.start_year.blank? - xml.tag! :startdate, format_date(credit_card.start_month, credit_card.start_year) - end - end - xml.tag! :Cv2Avs do xml.tag! :cv2, credit_card.verification_value if credit_card.verification_value? if address @@ -249,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 @@ -274,18 +260,20 @@ 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, - :test => test?, - :authorization => "#{response[:datacash_reference]};#{response[:authcode]};#{response[:ca_reference]}" + Response.new( + response[:status] == '1', + response[:reason], + response, + test: test?, + authorization: "#{response[:datacash_reference]};#{response[:authcode]};#{response[:ca_reference]}" ) end def format_date(month, year) - "#{format(month,:two_digits)}/#{format(year, :two_digits)}" + "#{format(month, :two_digits)}/#{format(year, :two_digits)}" end def parse(body) - response = {} xml = REXML::Document.new(body) root = REXML::XPath.first(xml, '//Response') @@ -299,7 +287,7 @@ def parse(body) def parse_element(response, node) if node.has_elements? - node.elements.each{|e| parse_element(response, e) } + node.elements.each { |e| parse_element(response, e) } else response[node.name.underscore.to_sym] = node.text end @@ -311,7 +299,7 @@ def format_reference_number(number) def parse_authorization_string(authorization) reference, auth_code, ca_reference = authorization.to_s.split(';') - {:reference => reference, :auth_code => auth_code, :ca_reference => ca_reference} + { reference:, auth_code:, ca_reference: } end end end diff --git a/lib/active_merchant/billing/gateways/datatrans.rb b/lib/active_merchant/billing/gateways/datatrans.rb new file mode 100644 index 00000000000..46b8cc0291c --- /dev/null +++ b/lib/active_merchant/billing/gateways/datatrans.rb @@ -0,0 +1,279 @@ +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class DatatransGateway < Gateway + self.test_url = 'https://api.sandbox.datatrans.com/v1/' + self.live_url = 'https://api.datatrans.com/v1/' + + 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 verify(payment, options = {}) + MultiResponse.run(:use_first_response) do |r| + r.process { authorize(100, payment, options) } + r.process(:ignore_result) { void(r.authorization, options) } + end + 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: }) + 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: }) + end + + def void(authorization, options = {}) + post = {} + transaction_id = authorization.split('|').first + commit('cancel', post, { transaction_id: }) + end + + def store(payment_method, options = {}) + exp_year = format(payment_method.year, :two_digits) + exp_month = format(payment_method.month, :two_digits) + + post = { + requests: [ + { + type: 'CARD', + pan: payment_method.number, + expiryMonth: exp_month, + expiryYear: exp_year + } + ] + } + commit('tokenize', post, { expiry_month: exp_month, expiry_year: exp_year }) + end + + def unstore(authorization, options = {}) + data_alias = authorization.split('|')[2] + commit('delete_alias', {}, { alias_id: data_alias }, :delete) + 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) + case payment_method + when String + token, exp_month, exp_year = payment_method.split('|')[2..4] + card = { + type: 'ALIAS', + alias: token, + expiryMonth: exp_month, + expiryYear: exp_year + } + when NetworkTokenizationCreditCard + card = { + 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, + expiryMonth: format(payment_method.month, :two_digits), + expiryYear: format(payment_method.year, :two_digits) + } + when CreditCard + card = { + number: payment_method.number, + cvv: payment_method.verification_value.to_s, + expiryMonth: format(payment_method.month, :two_digits), + expiryYear: format(payment_method.year, :two_digits) + } + end + post[:card] = card + 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 country_code(country) + Country.find(country).code(:alpha3).value if country + rescue InvalidCountryCodeError + nil + 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_code(billing_address[:country]), + 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 = {}, method = :post) + response = parse(ssl_request(method, url(action, options), post.to_json, headers)) + succeeded = success_from(action, response) + + Response.new( + succeeded, + message_from(succeeded, response), + response, + authorization: authorization_from(response, action, options), + 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}transactions/#{options[:transaction_id]}/#{endpoint}" + when 'tokenize' + "#{test? ? test_url : live_url}aliases/#{endpoint}" + when 'delete_alias' + "#{test? ? test_url : live_url}aliases/#{options[:alias_id]}" + else + "#{test? ? test_url : live_url}transactions/#{endpoint}" + end + end + + def success_from(action, response) + case action + when 'authorize', 'credit' + response.include?('transactionId') && response.include?('acquirerAuthorizationCode') + when 'settle', 'cancel' + response.dig('response_code') == 204 + when 'tokenize' + response.dig('responses', 0, 'alias') && response.dig('overview', 'failed') == 0 + when 'delete_alias' + response.dig('response_code') == 204 + else + false + end + end + + def authorization_from(response, action, options) + token_array = [response.dig('responses', 0, 'alias'), options[:expiry_month], options[:expiry_year]].join('|') if action == 'tokenize' + + auth = [response['transactionId'], response['acquirerAuthorizationCode'], token_array].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 new file mode 100644 index 00000000000..1ceb7402910 --- /dev/null +++ b/lib/active_merchant/billing/gateways/decidir.rb @@ -0,0 +1,402 @@ +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class DecidirGateway < Gateway + self.test_url = 'https://developers.decidir.com/api/v2' + self.live_url = 'https://live.decidir.com/api/v2' + + self.supported_countries = ['AR'] + self.money_format = :cents + self.default_currency = 'ARS' + self.supported_cardtypes = %i[visa master american_express diners_club naranja cabal tuya patagonia_365 tarjeta_sol] + + self.homepage_url = 'http://www.decidir.com' + self.display_name = 'Decidir' + + STANDARD_ERROR_CODE_MAPPING = { + -1 => STANDARD_ERROR_CODE[:processing_error], + 1 => STANDARD_ERROR_CODE[:call_issuer], + 2 => STANDARD_ERROR_CODE[:call_issuer], + 3 => STANDARD_ERROR_CODE[:config_error], + 4 => STANDARD_ERROR_CODE[:pickup_card], + 5 => STANDARD_ERROR_CODE[:card_declined], + 7 => STANDARD_ERROR_CODE[:pickup_card], + 12 => STANDARD_ERROR_CODE[:processing_error], + 14 => STANDARD_ERROR_CODE[:invalid_number], + 28 => STANDARD_ERROR_CODE[:processing_error], + 38 => STANDARD_ERROR_CODE[:incorrect_pin], + 39 => STANDARD_ERROR_CODE[:invalid_number], + 43 => STANDARD_ERROR_CODE[:pickup_card], + 45 => STANDARD_ERROR_CODE[:card_declined], + 46 => STANDARD_ERROR_CODE[:invalid_number], + 47 => STANDARD_ERROR_CODE[:card_declined], + 48 => STANDARD_ERROR_CODE[:card_declined], + 49 => STANDARD_ERROR_CODE[:invalid_expiry_date], + 51 => STANDARD_ERROR_CODE[:card_declined], + 53 => STANDARD_ERROR_CODE[:card_declined], + 54 => STANDARD_ERROR_CODE[:expired_card], + 55 => STANDARD_ERROR_CODE[:incorrect_pin], + 56 => STANDARD_ERROR_CODE[:card_declined], + 57 => STANDARD_ERROR_CODE[:card_declined], + 76 => STANDARD_ERROR_CODE[:call_issuer], + 91 => STANDARD_ERROR_CODE[:call_issuer], + 96 => STANDARD_ERROR_CODE[:processing_error], + 97 => STANDARD_ERROR_CODE[:processing_error] + } + + def initialize(options = {}) + requires!(options, :api_key) + super + @options[:preauth_mode] ||= false + end + + def purchase(money, payment, options = {}) + raise ArgumentError, 'Purchase is not supported on Decidir gateways configured with the preauth_mode option' if @options[:preauth_mode] + + post = {} + add_auth_purchase_params(post, money, payment, options) + commit(:post, 'payments', post) + end + + def authorize(money, payment, options = {}) + raise ArgumentError, 'Authorize is not supported on Decidir gateways unless the preauth_mode option is enabled' unless @options[:preauth_mode] + + post = {} + add_auth_purchase_params(post, money, payment, options) + commit(:post, 'payments', post) + end + + def capture(money, authorization, options = {}) + raise ArgumentError, 'Capture is not supported on Decidir gateways unless the preauth_mode option is enabled' unless @options[:preauth_mode] + + post = {} + add_amount(post, money, options) + commit(:put, "payments/#{authorization}", post) + end + + def refund(money, authorization, options = {}) + post = {} + add_amount(post, money, options) + commit(:post, "payments/#{authorization}/refunds", post) + end + + def void(authorization, options = {}) + post = {} + commit(:post, "payments/#{authorization}/refunds", post) + end + + def inquire(authorization, options = {}) + options[:action] = 'inquire' + commit(:get, "payments/#{authorization}", nil, options) + end + + def verify(credit_card, options = {}) + raise ArgumentError, 'Verify is not supported on Decidir gateways unless the preauth_mode option is enabled' unless @options[:preauth_mode] + + MultiResponse.run(:use_first_response) do |r| + r.process { authorize(100, credit_card, options) } + r.process(:ignore_result) { void(r.authorization, options) } + end + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r((apikey: )\w+)i, '\1[FILTERED]'). + gsub(%r((\"card_number\\\":\\\")\d+), '\1[FILTERED]'). + gsub(%r((\"security_code\\\":\\\")\d+), '\1[FILTERED]'). + gsub(%r((\"emv_issuer_data\\\":\\\")\d+), '\1[FILTERED]'). + gsub(%r((\"cryptogram\\\":\\\"/)\w+), '\1[FILTERED]'). + gsub(%r((\"token_card_data\\\":{.*\\\"token\\\":\\\")\d+), '\1[FILTERED]') + end + + private + + def add_auth_purchase_params(post, money, credit_card, options) + post[:payment_method_id] = add_payment_method_id(credit_card, options) + post[:site_transaction_id] = options[:order_id] + post[:bin] = credit_card.number[0..5] + post[:payment_type] = options[:payment_type] || 'single' + post[:wallet_id] = options[:wallet_id] if options[:wallet_id] + post[:installments] = options[:installments] ? options[:installments].to_i : 1 + post[:description] = options[:description] if options[:description] + post[:email] = options[:email] if options[:email] + post[:establishment_name] = options[:establishment_name] if options[:establishment_name] + post[:fraud_detection] = add_fraud_detection(options[:fraud_detection]) if options[:fraud_detection].present? + post[:site_id] = options[:site_id] if options[:site_id] + + add_invoice(post, money, 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) + if options[:payment_method_id] + options[:payment_method_id].to_i + elsif options[:debit] + if CreditCard.brand?(credit_card.number) == 'visa' + 31 + elsif CreditCard.brand?(credit_card.number) == 'master' + 105 + elsif CreditCard.brand?(credit_card.number) == 'maestro' + 106 + elsif CreditCard.brand?(credit_card.number) == 'cabal' + 108 + end + elsif CreditCard.brand?(credit_card.number) == 'master' + 104 + elsif CreditCard.brand?(credit_card.number) == 'american_express' + 65 + elsif CreditCard.brand?(credit_card.number) == 'diners_club' + 8 + elsif CreditCard.brand?(credit_card.number) == 'cabal' + 63 + elsif CreditCard.brand?(credit_card.number) == 'naranja' + 24 + elsif CreditCard.brand?(credit_card.number) == 'patagonia_365' + 55 + else + 1 + end + end + + def add_invoice(post, money, options) + add_amount(post, money, options) + post[:currency] = (options[:currency] || currency(money)) + end + + def add_amount(post, money, options) + currency = (options[:currency] || currency(money)) + post[:amount] = localized_amount(money, currency).to_i + end + + 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[:card_data][:security_code] = payment_method.verification_value if payment_method.verification_value? + + post[:token_card_data] = { + expiration_month: format(payment_method.month, :two_digits), + expiration_year: format(payment_method.year, :two_digits), + 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? + + # 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 (device_id = options.dig(:fraud_detection, :device_unique_id)) + card_data[:fraud_detection] = { device_unique_identifier: device_id } + end + end + + def add_aggregate_data(post, options) + aggregate_data = {} + data = options[:aggregate_data] + aggregate_data[:indicator] = data[:indicator] if data[:indicator] + aggregate_data[:identification_number] = data[:identification_number] if data[:identification_number] + aggregate_data[:bill_to_pay] = data[:bill_to_pay] if data[:bill_to_pay] + aggregate_data[:bill_to_refund] = data[:bill_to_refund] if data[:bill_to_refund] + aggregate_data[:merchant_name] = data[:merchant_name] if data[:merchant_name] + aggregate_data[:street] = data[:street] if data[:street] + aggregate_data[:number] = data[:number] if data[:number] + aggregate_data[:postal_code] = data[:postal_code] if data[:postal_code] + aggregate_data[:category] = data[:category] if data[:category] + aggregate_data[:channel] = data[:channel] if data[:channel] + aggregate_data[:geographic_code] = data[:geographic_code] if data[:geographic_code] + aggregate_data[:city] = data[:city] if data[:city] + aggregate_data[:merchant_id] = data[:merchant_id] if data[:merchant_id] + aggregate_data[:province] = data[:province] if data[:province] + aggregate_data[:country] = data[:country] if data[:country] + aggregate_data[:merchant_email] = data[:merchant_email] if data[:merchant_email] + aggregate_data[:merchant_phone] = data[:merchant_phone] if data[:merchant_phone] + 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] = [] + + return unless sub_payments = options[:sub_payments] + + sub_payments.each do |sub_payment| + sub_payment_hash = { + site_id: sub_payment[:site_id], + installments: sub_payment[:installments].to_i, + amount: sub_payment[:amount].to_i + } + post[:sub_payments] << sub_payment_hash + end + end + + def add_fraud_detection(options = {}) + {}.tap do |hsh| + hsh[:send_to_cs] = options[:send_to_cs] if valid_fraud_detection_option?(options[:send_to_cs]) # true/false + hsh[:channel] = options[:channel] if valid_fraud_detection_option?(options[:channel]) + hsh[:dispatch_method] = options[:dispatch_method] if valid_fraud_detection_option?(options[:dispatch_method]) + hsh[:csmdds] = options[:csmdds] if valid_fraud_detection_option?(options[:csmdds]) + hsh[:device_unique_id] = options[:device_unique_id] if valid_fraud_detection_option?(options[:device_unique_id]) + hsh[:bill_to] = options[:bill_to] if valid_fraud_detection_option?(options[:bill_to]) + hsh[:purchase_totals] = options[:purchase_totals] if valid_fraud_detection_option?(options[:purchase_totals]) + hsh[:customer_in_site] = options[:customer_in_site] if valid_fraud_detection_option?(options[:customer_in_site]) + hsh[:retail_transaction_data] = options[:retail_transaction_data] if valid_fraud_detection_option?(options[:retail_transaction_data]) + hsh[:ship_to] = options[:ship_to] if valid_fraud_detection_option?(options[:ship_to]) + hsh[:tax_voucher_required] = options[:tax_voucher_required] if valid_fraud_detection_option?(options[:tax_voucher_required]) + hsh[:copy_paste_card_data] = options[:copy_paste_card_data] if valid_fraud_detection_option?(options[:copy_paste_card_data]) + end + end + + # Avoid sending fields with empty or null when not populated. + def valid_fraud_detection_option?(val) + !val.nil? && val != '' + end + + def headers(options = {}) + { + 'apikey' => @options[:api_key], + 'Content-type' => 'application/json', + 'Cache-Control' => 'no-cache' + } + end + + def commit(method, endpoint, parameters, options = {}) + url = "#{test? ? test_url : live_url}/#{endpoint}" + + begin + raw_response = ssl_request(method, url, post_data(parameters), headers(options)) + response = parse(raw_response) + rescue ResponseError => e + raw_response = e.response.body + response = parse(raw_response) + end + + success = success_from(response, options) + Response.new( + success, + message_from(success, response), + response, + authorization: authorization_from(response), + test: test?, + error_code: success ? nil : error_code_from(response) + ) + end + + def post_data(parameters = {}) + parameters&.to_json + end + + def parse(body) + JSON.parse(body) + rescue JSON::ParserError + { + 'message' => "A non-JSON response was received from Decidir where one was expected. The raw response was:\n\n#{body}" + } + end + + def message_from(success, response) + return response['status'] if success + return response['message'] if response['message'] + + message = nil + if error = response.dig('status_details', 'error') + message = "#{error.dig('reason', 'description')} | #{error['type']}" + elsif response['error_type'] + if response['validation_errors'].is_a?(Array) + message = response['validation_errors'].map { |errors| "#{errors['code']}: #{errors['param']}" }.join(', ') + elsif response['validation_errors'].is_a?(Hash) + errors = response['validation_errors'].map { |k, v| "#{k}: #{v}" }.join(', ') + message = "#{response['error_type']} - #{errors}" + end + + message ||= response['error_type'] + end + + message + end + + def success_from(response, options) + status = %w(approved pre_approved) + + if options[:action] == 'inquire' + status.include?(response['status']) || response['status'] == 'rejected' + else + status.include?(response['status']) + end + end + + def authorization_from(response) + response['id'] + end + + def error_code_from(response) + error_code = nil + if error = response.dig('status_details', 'error') + code = error.dig('reason', 'id') + standard_error_code = STANDARD_ERROR_CODE_MAPPING[code] + error_code = "#{code}, #{standard_error_code}" + error_code ||= error['type'] + elsif response['error_type'] + error_code = response['error_type'] if response['validation_errors'] + elsif response.dig('error', 'validation_errors') + error = response.dig('error') + validation_errors = error.dig('validation_errors', 0) + code = validation_errors['code'] if validation_errors && validation_errors['code'] + param = validation_errors['param'] if validation_errors && validation_errors['param'] + error_code = "#{error['error_type']} | #{code} | #{param}" if error['error_type'] + elsif error = response.dig('error') + code = error.dig('reason', 'id') + standard_error_code = STANDARD_ERROR_CODE_MAPPING[code] + error_code = "#{code}, #{standard_error_code}" + end + + error_code || STANDARD_ERROR_CODE[:processing_error] + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/decidir_plus.rb b/lib/active_merchant/billing/gateways/decidir_plus.rb new file mode 100644 index 00000000000..f14fe9c5a4e --- /dev/null +++ b/lib/active_merchant/billing/gateways/decidir_plus.rb @@ -0,0 +1,356 @@ +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class DecidirPlusGateway < Gateway + self.test_url = 'https://developers.decidir.com/api/v2' + self.live_url = 'https://live.decidir.com/api/v2' + + self.supported_countries = ['AR'] + self.default_currency = 'ARS' + self.supported_cardtypes = %i[visa master american_express discover diners_club naranja cabal patagonia_365 tarjeta_sol] + + self.homepage_url = 'http://decidir.com.ar/home' + self.display_name = 'Decidir Plus' + + def initialize(options = {}) + requires!(options, :public_key, :private_key) + super + end + + def purchase(money, payment, options = {}) + post = {} + build_purchase_authorize_request(post, money, payment, options) + + commit(:post, 'payments', post) + end + + def authorize(money, payment, options = {}) + post = {} + build_purchase_authorize_request(post, money, payment, options) + + commit(:post, 'payments', post) + end + + def capture(money, authorization, options = {}) + post = {} + post[:amount] = money + + commit(:put, "payments/#{add_reference(authorization)}", post) + end + + def refund(money, authorization, options = {}) + post = {} + post[:amount] = money + + commit(:post, "payments/#{add_reference(authorization)}/refunds", post) + end + + def void(authorization, options = {}) + commit(:post, "payments/#{add_reference(authorization)}/refunds") + end + + def verify(credit_card, options = {}) + MultiResponse.run(:use_first_response) do |r| + r.process { store(credit_card, options) } + r.process { authorize(100, r.authorization, options) } + r.process(:ignore_result) { void(r.authorization, options) } + end + end + + def store(payment, options = {}) + post = {} + add_payment(post, payment, options) + + commit(:post, 'tokens', post) + end + + def unstore(customer_token) + commit(:delete, "cardtokens/#{customer_token}") + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r((Apikey: )\w+), '\1[FILTERED]'). + gsub(%r(("card_number\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("security_code\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]') + end + + private + + def build_purchase_authorize_request(post, money, payment, options) + add_customer_data(post, options) + add_payment(post, payment, options) + add_purchase_data(post, money, payment, options) + add_fraud_detection(post, options) + end + + def add_reference(authorization) + return unless authorization + + authorization.split('|')[0] + end + + def add_wallet_id(post, options) + return unless options[:wallet_id] + + post[:wallet_id] = options[:wallet_id] + end + + def add_payment(post, payment, options = {}) + if payment.is_a?(String) + token, bin = payment.split('|') + post[:token] = token + post[:bin] = bin + else + post[:card_number] = payment.number + post[:card_expiration_month] = format(payment.month, :two_digits) + post[:card_expiration_year] = format(payment.year, :two_digits) + post[:security_code] = payment.verification_value.to_s + post[:card_holder_name] = payment.name.empty? ? options[:name_override] : payment.name + post[:card_holder_identification] = {} + post[:card_holder_identification][:type] = options[:card_holder_identification_type] if options[:card_holder_identification_type] + post[:card_holder_identification][:number] = options[:card_holder_identification_number] if options[:card_holder_identification_number] + + # additional data used for Visa transactions + post[:card_holder_door_number] = options[:card_holder_door_number].to_i if options[:card_holder_door_number] + post[:card_holder_birthday] = options[:card_holder_birthday] if options[:card_holder_birthday] + end + end + + def add_customer_data(post, options = {}) + return unless customer = options[:customer] + + post[:customer] = {} + post[:customer][:id] = customer[:id] if customer[:id] + post[:customer][:email] = customer[:email] if customer[:email] + end + + def add_purchase_data(post, money, payment, options = {}) + post[:site_transaction_id] = options[:site_transaction_id] || SecureRandom.hex + post[:payment_method_id] = add_payment_method_id(options) + post[:amount] = money + post[:currency] = options[:currency] || self.default_currency + post[:installments] = options[:installments] || 1 + post[:payment_type] = options[:payment_type] || 'single' + post[:establishment_name] = options[:establishment_name] if options[:establishment_name] + + add_aggregate_data(post, options) if options[:aggregate_data] + add_sub_payments(post, options) + add_wallet_id(post, options) + end + + def add_aggregate_data(post, options) + aggregate_data = {} + data = options[:aggregate_data] + aggregate_data[:indicator] = data[:indicator] if data[:indicator] + aggregate_data[:identification_number] = data[:identification_number] if data[:identification_number] + aggregate_data[:bill_to_pay] = data[:bill_to_pay] if data[:bill_to_pay] + aggregate_data[:bill_to_refund] = data[:bill_to_refund] if data[:bill_to_refund] + aggregate_data[:merchant_name] = data[:merchant_name] if data[:merchant_name] + aggregate_data[:street] = data[:street] if data[:street] + aggregate_data[:number] = data[:number] if data[:number] + aggregate_data[:postal_code] = data[:postal_code] if data[:postal_code] + aggregate_data[:category] = data[:category] if data[:category] + aggregate_data[:channel] = data[:channel] if data[:channel] + aggregate_data[:geographic_code] = data[:geographic_code] if data[:geographic_code] + aggregate_data[:city] = data[:city] if data[:city] + aggregate_data[:merchant_id] = data[:merchant_id] if data[:merchant_id] + aggregate_data[:province] = data[:province] if data[:province] + aggregate_data[:country] = data[:country] if data[:country] + aggregate_data[:merchant_email] = data[:merchant_email] if data[:merchant_email] + aggregate_data[:merchant_phone] = data[:merchant_phone] if data[:merchant_phone] + post[:aggregate_data] = aggregate_data + end + + def add_sub_payments(post, options) + # sub_payments field is required for purchase transactions, even if empty + post[:sub_payments] = [] + + return unless sub_payments = options[:sub_payments] + + sub_payments.each do |sub_payment| + sub_payment_hash = { + site_id: sub_payment[:site_id], + installments: sub_payment[:installments].to_i, + amount: sub_payment[:amount].to_i + } + post[:sub_payments] << sub_payment_hash + end + end + + def add_payment_method_id(options) + return options[:payment_method_id].to_i if options[:payment_method_id] + + if options[:debit] + case options[:card_brand] + when 'visa' + 31 + when 'master' + 105 + when 'maestro' + 106 + when 'cabal' + 108 + else + 31 + end + else + case options[:card_brand] + when 'visa' + 1 + when 'master' + 104 + when 'american_express' + 65 + when 'american_express_prisma' + 111 + when 'cabal' + 63 + when 'diners_club' + 8 + when 'patagonia_365' + 55 + else + 1 + end + end + end + + def add_fraud_detection(post, options) + return unless fraud_detection = options[:fraud_detection] + + {}.tap do |hsh| + hsh[:send_to_cs] = fraud_detection[:send_to_cs] == 'true' # true/false + hsh[:channel] = fraud_detection[:channel] if fraud_detection[:channel] + hsh[:dispatch_method] = fraud_detection[:dispatch_method] if fraud_detection[:dispatch_method] + add_csmdds(hsh, fraud_detection) + + post[:fraud_detection] = hsh + end + end + + def add_csmdds(hsh, fraud_detection) + return unless fraud_detection[:csmdds] + + csmdds_arr = [] + fraud_detection[:csmdds].each do |csmdds| + csmdds_hsh = {} + csmdds_hsh[:code] = csmdds[:code].to_i + csmdds_hsh[:description] = csmdds[:description] + csmdds_arr.append(csmdds_hsh) + end + hsh[:csmdds] = csmdds_arr unless csmdds_arr.empty? + end + + def parse(body) + return {} if body.nil? + + JSON.parse(body) + end + + def commit(method, endpoint, parameters = {}, options = {}) + begin + raw_response = ssl_request(method, url(endpoint), post_data(parameters), headers(endpoint)) + response = parse(raw_response) + rescue ResponseError => e + raw_response = e.response.body + response = parse(raw_response) + end + + Response.new( + success_from(response), + message_from(response), + response, + authorization: authorization_from(response), + avs_result: AVSResult.new(code: response['some_avs_response_key']), + cvv_result: CVVResult.new(response['some_cvv_response_key']), + test: test?, + error_code: error_code_from(response) + ) + end + + def headers(endpoint) + { + 'Content-Type' => 'application/json', + 'apikey' => endpoint == 'tokens' ? @options[:public_key] : @options[:private_key] + } + end + + def url(action, options = {}) + base_url = (test? ? test_url : live_url) + + return "#{base_url}/#{action}" + end + + def success_from(response) + response.dig('status') == 'approved' || response.dig('status') == 'active' || response.dig('status') == 'pre_approved' || response.empty? + end + + def message_from(response) + return '' if response.empty? + + rejected?(response) ? message_from_status_details(response) : response.dig('status') || error_message(response) || response.dig('message') + end + + def authorization_from(response) + return nil unless response.dig('id') || response.dig('bin') + + "#{response.dig('id')}|#{response.dig('bin')}" + end + + def post_data(parameters = {}) + parameters.to_json + end + + def error_code_from(response) + return if success_from(response) + + error_code = nil + if error = response.dig('status_details', 'error') + error_code = error.dig('reason', 'id') || error['type'] + elsif response['error_type'] + error_code = response['error_type'] + elsif response.dig('error', 'validation_errors') + error = response.dig('error') + validation_errors = error.dig('validation_errors', 0) + code = validation_errors['code'] if validation_errors && validation_errors['code'] + param = validation_errors['param'] if validation_errors && validation_errors['param'] + error_code = "#{error['error_type']} | #{code} | #{param}" if error['error_type'] + elsif error = response.dig('error') + error_code = error.dig('reason', 'id') + end + + error_code + end + + def error_message(response) + return error_code_from(response) unless validation_errors = response.dig('validation_errors') + + validation_errors = validation_errors[0] + message = "#{validation_errors&.dig('code')}: #{validation_errors&.dig('param')}" + return message unless message == ': ' + + errors = response['validation_errors'].map { |k, v| "#{k}: #{v}" }.join(', ') + "#{response['error_type']} - #{errors}" + end + + def rejected?(response) + return response.dig('status') == 'rejected' + end + + def message_from_status_details(response) + return unless error = response.dig('status_details', 'error') + return message_from_fraud_detection(response) if error.dig('type') == 'cybersource_error' + + "#{error.dig('type')}: #{error.dig('reason', 'description')}" + end + + def message_from_fraud_detection(response) + return error_message(response.dig('fraud_detection', 'status', 'details')) + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/deepstack.rb b/lib/active_merchant/billing/gateways/deepstack.rb new file mode 100644 index 00000000000..6f3b95610aa --- /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/dibs.rb b/lib/active_merchant/billing/gateways/dibs.rb index ef5dfd425c2..565bfccb051 100644 --- a/lib/active_merchant/billing/gateways/dibs.rb +++ b/lib/active_merchant/billing/gateways/dibs.rb @@ -1,43 +1,42 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class DibsGateway < Gateway self.display_name = 'DIBS' self.homepage_url = 'http://www.dibspayment.com/' self.live_url = 'https://api.dibspayment.com/merchant/v1/JSON/Transaction/' - self.supported_countries = ['US', 'FI', 'NO', 'SE', 'GB'] + self.supported_countries = %w[US FI NO SE GB] self.default_currency = 'USD' self.money_format = :cents - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] - def initialize(options={}) + def initialize(options = {}) requires!(options, :merchant_id, :secret_key) super end - def purchase(amount, payment_method, options={}) + def purchase(amount, payment_method, options = {}) MultiResponse.run(false) do |r| r.process { authorize(amount, payment_method, options) } r.process { capture(amount, r.authorization, options) } end end - def authorize(amount, payment_method, options={}) + def authorize(amount, payment_method, options = {}) post = {} add_amount(post, amount) add_invoice(post, amount, options) - if (payment_method.respond_to?(:number)) + if payment_method.respond_to?(:number) add_payment_method(post, payment_method, options) commit(:authorize, post) else add_ticket_id(post, payment_method) commit(:authorize_ticket, post) end - end - def capture(amount, authorization, options={}) + def capture(amount, authorization, options = {}) post = {} add_amount(post, amount) add_reference(post, authorization) @@ -45,14 +44,14 @@ def capture(amount, authorization, options={}) commit(:capture, post) end - def void(authorization, options={}) + def void(authorization, options = {}) post = {} add_reference(post, authorization) commit(:void, post) end - def refund(amount, authorization, options={}) + def refund(amount, authorization, options = {}) post = {} add_amount(post, amount) add_reference(post, authorization) @@ -60,7 +59,7 @@ def refund(amount, authorization, options={}) commit(:refund, post) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -88,7 +87,7 @@ def scrub(transcript) private - CURRENCY_CODES = Hash.new{|h,k| raise ArgumentError.new("Unsupported currency: #{k}")} + CURRENCY_CODES = Hash.new { |_h, k| raise ArgumentError.new("Unsupported currency: #{k}") } CURRENCY_CODES['USD'] = '840' CURRENCY_CODES['DKK'] = '208' CURRENCY_CODES['NOK'] = '578' @@ -110,15 +109,10 @@ def add_payment_method(post, payment_method, options) post[:cvc] = payment_method.verification_value if payment_method.verification_value post[:expYear] = format(payment_method.year, :two_digits) post[:expMonth] = payment_method.month - - post[:startMonth] = payment_method.start_month if payment_method.start_month - post[:startYear] = payment_method.start_year if payment_method.start_year - post[:issueNumber] = payment_method.issue_number if payment_method.issue_number post[:clientIp] = options[:ip] || '127.0.0.1' post[:test] = true if test? end - def add_reference(post, authorization) post[:transactionId] = authorization end @@ -165,7 +159,7 @@ def build_request(post) end def add_hmac(post) - data = post.sort.collect { |key, value| "#{key}=#{value.to_s}" }.join('&') + data = post.sort.collect { |key, value| "#{key}=#{value}" }.join('&') digest = OpenSSL::Digest.new('sha256') key = [@options[:secret_key]].pack('H*') post[:MAC] = OpenSSL::HMAC.hexdigest(digest, key, data) @@ -187,7 +181,7 @@ def message_from(succeeded, response) if succeeded 'Succeeded' else - response['status'] + ': ' + response['declineReason'] || 'Unable to read error message' + (response['status'] + ': ' + response['declineReason']) || 'Unable to read error message' end end diff --git a/lib/active_merchant/billing/gateways/digitzs.rb b/lib/active_merchant/billing/gateways/digitzs.rb index 80510bf8e0a..78524176683 100644 --- a/lib/active_merchant/billing/gateways/digitzs.rb +++ b/lib/active_merchant/billing/gateways/digitzs.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class DigitzsGateway < Gateway include Empty @@ -8,25 +8,25 @@ class DigitzsGateway < Gateway self.supported_countries = ['US'] self.default_currency = 'USD' - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.money_format = :cents self.homepage_url = 'https://digitzs.com' self.display_name = 'Digitzs' - def initialize(options={}) + def initialize(options = {}) requires!(options, :app_key, :api_key) super end - def purchase(money, payment, options={}) + def purchase(money, payment, options = {}) MultiResponse.run do |r| r.process { commit('auth/token', app_token_request(options)) } r.process { commit('payments', purchase_request(money, payment, options), options.merge({ app_token: app_token_from(r) })) } end end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) MultiResponse.run do |r| r.process { commit('auth/token', app_token_request(options)) } r.process { commit('payments', refund_request(money, authorization, options), options.merge({ app_token: app_token_from(r) })) } @@ -36,7 +36,7 @@ def refund(money, authorization, options={}) def store(payment, options = {}) MultiResponse.run do |r| r.process { commit('auth/token', app_token_request(options)) } - options.merge!({ app_token: app_token_from(r) }) + options[:app_token] = app_token_from(r) if options[:customer_id].present? customer_id = check_customer_exists(options) @@ -151,7 +151,7 @@ def refund_request(money, authorization, options) post[:data][:type] = 'payments' post[:data][:attributes][:merchantId] = options[:merchant_id] post[:data][:attributes][:paymentType] = 'cardRefund' - post[:data][:attributes][:originalTransaction] = {id: authorization} + post[:data][:attributes][:originalTransaction] = { id: authorization } add_transaction(post, money, options) post @@ -163,7 +163,7 @@ def create_customer_request(payment, options) post[:data][:attributes] = { merchantId: options[:merchant_id], name: payment.name, - externalId: "#{SecureRandom.hex(16)}" + externalId: SecureRandom.hex(16) } post @@ -175,7 +175,7 @@ def create_token_request(payment, options) post[:data][:attributes] = { tokenType: 'card', customerId: options[:customer_id], - label: 'Credit Card', + label: 'Credit Card' } add_payment(post, payment, options) add_address(post, options) @@ -188,6 +188,7 @@ def check_customer_exists(options = {}) response = parse(ssl_get(url + "/customers/#{options[:customer_id]}", headers(options))) return response.try(:[], 'data').try(:[], 'customerId') if success_from(response) + return nil end @@ -197,7 +198,7 @@ def add_credit_card_to_customer(payment, options = {}) def add_customer_with_credit_card(payment, options = {}) customer_response = commit('customers', create_customer_request(payment, options), options) - options.merge!({customer_id: customer_response.authorization}) + options[:customer_id] = customer_response.authorization commit('tokens', create_token_request(payment, options), options) end @@ -205,7 +206,7 @@ def parse(body) JSON.parse(body) end - def commit(action, parameters, options={}) + def commit(action, parameters, options = {}) url = (test? ? test_url : live_url) response = parse(ssl_post(url + "/#{action}", parameters.to_json, headers(options))) @@ -228,12 +229,13 @@ def success_from(response) def message_from(response) return response['message'] if response['message'] return 'Success' if success_from(response) - response['errors'].map {|error_hash| error_hash['detail'] }.join(', ') + + response['errors'].map { |error_hash| error_hash['detail'] }.join(', ') end def authorization_from(response) if customer_id = response.try(:[], 'data').try(:[], 'attributes').try(:[], 'customerId') - "#{customer_id}|#{response.try(:[], "data").try(:[], "id")}" + "#{customer_id}|#{response.try(:[], 'data').try(:[], 'id')}" else response.try(:[], 'data').try(:[], 'id') end @@ -257,13 +259,13 @@ def headers(options) 'x-api-key' => @options[:api_key] } - headers.merge!({'Authorization' => "Bearer #{options[:app_token]}"}) if options[:app_token] + headers['Authorization'] = "Bearer #{options[:app_token]}" if options[:app_token] headers end def error_code_from(response) unless success_from(response) - response['errors'].nil? ? response['message'] : response['errors'].map {|error_hash| error_hash['code'] }.join(', ') + response['errors'].nil? ? response['message'] : response['errors'].map { |error_hash| error_hash['code'] }.join(', ') end end @@ -276,6 +278,7 @@ def determine_payment_type(payment, options) return 'cardSplit' if options[:payment_type] == 'card_split' return 'tokenSplit' if options[:payment_type] == 'token_split' return 'token' if payment.is_a? String + 'card' end diff --git a/lib/active_merchant/billing/gateways/ebanx.rb b/lib/active_merchant/billing/gateways/ebanx.rb index ef42b12d449..d147f0cf593 100644 --- a/lib/active_merchant/billing/gateways/ebanx.rb +++ b/lib/active_merchant/billing/gateways/ebanx.rb @@ -1,23 +1,17 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class EbanxGateway < Gateway - self.test_url = 'https://sandbox.ebanx.com/ws/' - self.live_url = 'https://api.ebanx.com/ws/' + self.test_url = 'https://sandbox.ebanxpay.com/ws/' + self.live_url = 'https://api.ebanxpay.com/ws/' - self.supported_countries = ['BR', 'MX', 'CO'] + self.supported_countries = %w(BR MX CO CL AR PE BO EC CR DO GT PA PY UY) self.default_currency = 'USD' - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club] + self.supported_cardtypes = %i[visa master american_express discover diners_club elo hipercard] self.homepage_url = 'http://www.ebanx.com/' - self.display_name = 'Ebanx' - - CARD_BRAND = { - visa: 'visa', - master: 'master_card', - american_express: 'amex', - discover: 'discover', - diners_club: 'diners' - } + self.display_name = 'EBANX' + + TAGS = ['Spreedly'] URL_MAP = { purchase: 'direct', @@ -25,7 +19,9 @@ class EbanxGateway < Gateway capture: 'capture', refund: 'refund', void: 'cancel', - store: 'token' + store: 'token', + inquire: 'query', + verify: 'verifycard' } HTTP_METHOD = { @@ -34,51 +30,57 @@ class EbanxGateway < Gateway capture: :get, refund: :post, void: :get, - store: :post + store: :post, + inquire: :get, + verify: :post } - def initialize(options={}) + def initialize(options = {}) requires!(options, :integration_key) super end - def purchase(money, payment, options={}) + def purchase(money, payment, options = {}) post = { payment: {} } add_integration_key(post) add_operation(post) add_invoice(post, money, options) add_customer_data(post, payment, options) - add_card_or_token(post, payment) + add_card_or_token(post, payment, options) add_address(post, options) add_customer_responsible_person(post, payment, options) + add_additional_data(post, options) + add_stored_credentials(post, options) - commit(:purchase, post) + commit(:purchase, post, options) end - def authorize(money, payment, options={}) + def authorize(money, payment, options = {}) post = { payment: {} } add_integration_key(post) add_operation(post) add_invoice(post, money, options) add_customer_data(post, payment, options) - add_card_or_token(post, payment) + add_card_or_token(post, payment, options) add_address(post, options) add_customer_responsible_person(post, payment, options) + add_additional_data(post, options) + add_stored_credentials(post, options) post[:payment][:creditcard][:auto_capture] = false - commit(:authorize, post) + commit(:authorize, post, options) end - def capture(money, authorization, options={}) + def capture(money, authorization, options = {}) post = {} add_integration_key(post) post[:hash] = authorization - post[:amount] = amount(money) + post[:amount] = amount(money) if options[:include_capture_amount].to_s == 'true' - commit(:capture, post) + commit(:capture, post, options) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) post = {} add_integration_key(post) add_operation(post) @@ -86,31 +88,48 @@ def refund(money, authorization, options={}) post[:amount] = amount(money) post[:description] = options[:description] - commit(:refund, post) + commit(:refund, post, options) end - def void(authorization, options={}) + def void(authorization, options = {}) post = {} add_integration_key(post) add_authorization(post, authorization) - commit(:void, post) + commit(:void, post, options) end - def store(credit_card, 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, options) + post[:creditcard] = payment_details(credit_card) - commit(:store, post) + commit(:store, post, options) end - def verify(credit_card, options={}) - MultiResponse.run(:use_first_response) do |r| - r.process { authorize(100, credit_card, options) } - r.process(:ignore_result) { void(r.authorization, options) } - end + def verify(credit_card, options = {}) + post = {} + add_integration_key(post) + add_payment_type(post, options) + customer_country(post, options) + post[:card] = payment_details(credit_card) + post[:device_id] = options[:device_id] if options[:device_id] + + commit(:verify, post, options) + end + + def inquire(authorization, options = {}) + post = {} + add_integration_key(post) + add_authorization(post, authorization) + + commit(:inquire, post, options) + end + + def supports_network_tokenization? + true end def supports_scrubbing? @@ -119,9 +138,11 @@ def supports_scrubbing? def scrub(transcript) transcript. - gsub(/(integration_key\\?":\\?")(\d*)/, '\1[FILTERED]'). + gsub(/(integration_key\\?":\\?")(\w*)/, '\1[FILTERED]'). gsub(/(card_number\\?":\\?")(\d*)/, '\1[FILTERED]'). - gsub(/(card_cvv\\?":\\?")(\d*)/, '\1[FILTERED]') + gsub(/(card_cvv\\?":\\?")(\d*)/, '\1[FILTERED]'). + gsub(/(network_token_pan\\?":\\?")(\d*)/, '\1[FILTERED]'). + gsub(/(network_token_cryptogram\\?":\\?")([\w+=\/]*)/, '\1[FILTERED]') end private @@ -140,14 +161,14 @@ 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 def add_customer_responsible_person(post, payment, options) post[:payment][:person_type] = options[:person_type] if options[:person_type] - if options[:person_type] && options[:person_type].downcase == 'business' + if options[:person_type]&.casecmp('business')&.zero? post[:payment][:responsible] = {} post[:payment][:responsible][:name] = options[:responsible_name] if options[:responsible_name] post[:payment][:responsible][:document] = options[:responsible_document] if options[:responsible_document] @@ -155,6 +176,28 @@ def add_customer_responsible_person(post, payment, options) end end + def add_stored_credentials(post, options) + return unless (stored_creds = options[:stored_credential]) + + post[:cof_info] = { + cof_type: stored_creds[:initial_transaction] ? 'initial' : 'stored', + initiator: stored_creds[:initiator] == 'cardholder' ? 'CIT' : 'MIT', + trans_type: add_trans_type(stored_creds), + mandate_id: stored_creds[:network_transaction_id] + }.compact + end + + def add_trans_type(options) + case options[:reason_type] + when 'recurring' + 'SCHEDULED_RECURRING' + when 'installment' + 'INSTALLMENT' + else + options[:initiator] == 'cardholder' ? 'CUSTOMER_COF' : 'MERCHANT_COF' + end + end + def add_address(post, options) if address = options[:billing_address] || options[:address] post[:payment][:address] = address[:address1].split[1..-1].join(' ') if address[:address1] @@ -170,25 +213,31 @@ def add_address(post, options) def add_invoice(post, money, options) post[:payment][:amount_total] = amount(money) post[:payment][:currency_code] = (options[:currency] || currency(money)) - post[:payment][:merchant_payment_code] = options[:order_id] + post[:payment][:merchant_payment_code] = Digest::MD5.hexdigest(order_id_override(options)) post[:payment][:instalments] = options[:instalments] || 1 + post[:payment][:order_number] = options[:order_id][0..39] if options[:order_id] end - def add_card_or_token(post, payment) - if payment.is_a?(String) - payment, brand = payment.split('|') - end - post[:payment][:payment_type_code] = payment.is_a?(String) ? brand : CARD_BRAND[payment.brand.to_sym] + def add_card_or_token(post, payment, options) + payment = payment.split('|')[0] if payment.is_a?(String) + add_payment_type(post[:payment], options) 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, options) + post[:payment_type_code] = options[:payment_type_code] || 'creditcard' end def payment_details(payment) - if payment.is_a?(String) + case payment + when NetworkTokenizationCreditCard + { + network_token_pan: payment.number, + network_token_expire_date: "#{payment.month}/#{payment.year}", + network_token_cryptogram: payment.payment_cryptogram + } + when String { token: payment } else { @@ -200,19 +249,34 @@ def payment_details(payment) end end + # we will prefer the merchant_payment_code if both fields are provided + def order_id_override(options) + options[:merchant_payment_code] || options[:order_id] + end + + def add_additional_data(post, options) + post[:device_id] = options[:device_id] if options[:device_id] + post[:metadata] = options[:metadata] if options[:metadata] + post[:metadata] = {} if post[:metadata].nil? + post[:metadata][:merchant_payment_code] = order_id_override(options) + post[:payment][:tags] = TAGS + post[:notification_url] = options[:notification_url] if options[:notification_url] + end + def parse(body) JSON.parse(body) end - def commit(action, parameters) + def commit(action, parameters, options = {}) url = url_for((test? ? test_url : live_url), action, parameters) - response = parse(ssl_request(HTTP_METHOD[action], url, post_data(action, parameters), {})) + + response = parse(ssl_request(HTTP_METHOD[action], url, post_data(action, parameters), headers(options))) success = success_from(action, response) Response.new( success, - message_from(response), + message_from(action, response), response, authorization: authorization_from(action, parameters, response), test: test?, @@ -220,28 +284,49 @@ def commit(action, parameters) ) end + def headers(options) + { + 'x-ebanx-client-user-agent' => "ActiveMerchant/#{ActiveMerchant::VERSION}", + 'x-ebanx-api-processing-type' => ('local' if options[:processing_type] == 'local') + }.compact + end + def success_from(action, response) - if [: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 @@ -250,22 +335,26 @@ def authorization_from(action, parameters, response) def post_data(action, parameters = {}) return nil if requires_http_get(action) return convert_to_url_form_encoded(parameters) if action == :refund + "request_body=#{parameters.to_json}" end def url_for(hostname, action, parameters) return "#{hostname}#{URL_MAP[action]}?#{convert_to_url_form_encoded(parameters)}" if requires_http_get(action) + "#{hostname}#{URL_MAP[action]}" end def requires_http_get(action) - return true if [:capture, :void].include?(action) + return true if %i[capture void inquire].include?(action) + false end def convert_to_url_form_encoded(parameters) parameters.map do |key, value| next if value != false && value.blank? + "#{key}=#{value}" end.compact.join('&') end @@ -273,13 +362,14 @@ def convert_to_url_form_encoded(parameters) def error_code_from(response, success) unless success return response['status_code'] if response['status'] == 'ERROR' + response.try(:[], 'payment').try(:[], 'transaction_status').try(:[], 'code') 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 da3d95ae0be..a49768997f2 100644 --- a/lib/active_merchant/billing/gateways/efsnet.rb +++ b/lib/active_merchant/billing/gateways/efsnet.rb @@ -1,11 +1,10 @@ require 'rexml/document' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: - +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class EfsnetGateway < Gateway self.supported_countries = ['US'] - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'http://www.concordefsnet.com/' self.display_name = 'Efsnet' @@ -54,8 +53,8 @@ def refund(money, reference, options = {}) def void(identification, options = {}) requires!(options, :order_id) - original_transaction_id, _ = identification.split(';') - commit(:void_transaction, {:reference_number => format_reference_number(options[:order_id]), :transaction_id => original_transaction_id}) + original_transaction_id, = identification.split(';') + commit(:void_transaction, { reference_number: format_reference_number(options[:order_id]), transaction_id: original_transaction_id }) end def voice_authorize(money, authorization_code, creditcard, options = {}) @@ -82,11 +81,11 @@ def build_refund_or_settle_request(money, identification, options = {}) requires!(options, :order_id) { - :reference_number => format_reference_number(options[:order_id]), - :transaction_amount => amount(money), - :original_transaction_amount => original_transaction_amount, - :original_transaction_id => original_transaction_id, - :client_ip_address => options[:ip] + reference_number: format_reference_number(options[:order_id]), + transaction_amount: amount(money), + original_transaction_amount:, + original_transaction_id:, + client_ip_address: options[:ip] } end @@ -94,64 +93,66 @@ def build_credit_card_request(money, creditcard, options = {}) requires!(options, :order_id) post = { - :reference_number => format_reference_number(options[:order_id]), - :authorization_number => options[:authorization_number], - :transaction_amount => amount(money), - :client_ip_address => options[:ip] + reference_number: format_reference_number(options[:order_id]), + authorization_number: options[:authorization_number], + transaction_amount: amount(money), + client_ip_address: options[:ip] } - add_creditcard(post,creditcard) - add_address(post,options) + add_creditcard(post, creditcard) + add_address(post, options) post end def format_reference_number(number) - number.to_s.slice(0,12) + number.to_s.slice(0, 12) end - def add_address(post,options) + def add_address(post, options) if address = options[:billing_address] || options[:address] if address[:address2] - post[:billing_address] = address[:address1].to_s << ' ' << address[:address2].to_s + post[:billing_address] = address[:address1].to_s << ' ' << address[:address2].to_s else post[:billing_address] = address[:address1].to_s end post[:billing_city] = address[:city].to_s - post[:billing_state] = address[:state].blank? ? 'n/a' : address[:state] + post[:billing_state] = address[:state].blank? ? 'n/a' : address[:state] post[:billing_postal_code] = address[:zip].to_s post[:billing_country] = address[:country].to_s end if address = options[:shipping_address] if address[:address2] - post[:shipping_address] = address[:address1].to_s << ' ' << address[:address2].to_s + post[:shipping_address] = address[:address1].to_s << ' ' << address[:address2].to_s else post[:shipping_address] = address[:address1].to_s end post[:shipping_city] = address[:city].to_s - post[:shipping_state] = address[:state].blank? ? 'n/a' : address[:state] + post[:shipping_state] = address[:state].blank? ? 'n/a' : address[:state] post[:shipping_postal_code] = address[:zip].to_s post[:shipping_country] = address[:country].to_s end end def add_creditcard(post, creditcard) - post[:billing_name] = creditcard.name if creditcard.name - post[:account_number] = creditcard.number + post[:billing_name] = creditcard.name if creditcard.name + post[:account_number] = creditcard.number post[:card_verification_value] = creditcard.verification_value if creditcard.verification_value? - post[:expiration_month] = sprintf('%.2i', creditcard.month) - post[:expiration_year] = sprintf('%.4i', creditcard.year)[-2..-1] + post[:expiration_month] = sprintf('%.2i', creditcard.month) + post[:expiration_year] = sprintf('%.4i', creditcard.year)[-2..-1] end - 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, - :test => test?, - :authorization => authorization_from(response, parameters), - :avs_result => { :code => response[:avs_response_code] }, - :cvv_result => response[:cvv_response_code] + 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] ) end @@ -160,7 +161,7 @@ def success?(response) end def authorization_from(response, params) - [ response[:transaction_id], params[:transaction_amount] ].compact.join(';') + [response[:transaction_id], params[:transaction_amount]].compact.join(';') end def parse(xml) @@ -169,9 +170,7 @@ def parse(xml) xml = REXML::Document.new(xml) xml.elements.each('//Reply//TransactionReply/*') do |node| - response[node.name.underscore.to_sym] = normalize(node.text) - end unless xml.root.nil? response @@ -194,6 +193,7 @@ def post_data(action, parameters = {}) def message_from(message) return 'Unspecified error' if message.blank? + message.gsub(/[^\w]/, ' ').split.join(' ').capitalize end @@ -201,18 +201,18 @@ def actions ACTIONS end - CREDIT_CARD_FIELDS = %w(AuthorizationNumber ClientIpAddress BillingAddress BillingCity BillingState BillingPostalCode BillingCountry BillingName CardVerificationValue ExpirationMonth ExpirationYear ReferenceNumber TransactionAmount AccountNumber ) + CREDIT_CARD_FIELDS = %w(AuthorizationNumber ClientIpAddress BillingAddress BillingCity BillingState BillingPostalCode BillingCountry BillingName CardVerificationValue ExpirationMonth ExpirationYear ReferenceNumber TransactionAmount AccountNumber) ACTIONS = { - :credit_card_authorize => CREDIT_CARD_FIELDS, - :credit_card_charge => CREDIT_CARD_FIELDS, - :credit_card_voice_authorize => CREDIT_CARD_FIELDS, - :credit_card_capture => CREDIT_CARD_FIELDS, - :credit_card_credit => CREDIT_CARD_FIELDS + ['OriginalTransactionAmount'], - :credit_card_refund => %w(ReferenceNumber TransactionAmount OriginalTransactionAmount OriginalTransactionID ClientIpAddress), - :void_transaction => %w(ReferenceNumber TransactionID), - :credit_card_settle => %w(ReferenceNumber TransactionAmount OriginalTransactionAmount OriginalTransactionID ClientIpAddress), - :system_check => %w(SystemCheck), + credit_card_authorize: CREDIT_CARD_FIELDS, + credit_card_charge: CREDIT_CARD_FIELDS, + credit_card_voice_authorize: CREDIT_CARD_FIELDS, + credit_card_capture: CREDIT_CARD_FIELDS, + credit_card_credit: CREDIT_CARD_FIELDS + ['OriginalTransactionAmount'], + credit_card_refund: %w(ReferenceNumber TransactionAmount OriginalTransactionAmount OriginalTransactionID ClientIpAddress), + void_transaction: %w(ReferenceNumber TransactionID), + credit_card_settle: %w(ReferenceNumber TransactionAmount OriginalTransactionAmount OriginalTransactionID ClientIpAddress), + system_check: %w(SystemCheck) } end end diff --git a/lib/active_merchant/billing/gateways/elavon.rb b/lib/active_merchant/billing/gateways/elavon.rb index 9814d760966..b333503c61a 100644 --- a/lib/active_merchant/billing/gateways/elavon.rb +++ b/lib/active_merchant/billing/gateways/elavon.rb @@ -1,31 +1,35 @@ require 'active_merchant/billing/gateways/viaklix' +require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class ElavonGateway < Gateway include Empty class_attribute :test_url, :live_url, :delimiter, :actions - self.test_url = 'https://api.demo.convergepay.com/VirtualMerchantDemo/process.do' - self.live_url = 'https://api.convergepay.com/VirtualMerchant/process.do' + self.test_url = 'https://api.demo.convergepay.com/VirtualMerchantDemo/processxml.do' + self.live_url = 'https://api.convergepay.com/VirtualMerchant/processxml.do' self.display_name = 'Elavon MyVirtualMerchant' self.supported_countries = %w(US CA PR DE IE NO PL LU BE NL MX) - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'http://www.elavon.com/' + self.money_format = :dollars + self.default_currency = 'USD' self.delimiter = "\n" self.actions = { - :purchase => 'CCSALE', - :credit => 'CCCREDIT', - :refund => 'CCRETURN', - :authorize => 'CCAUTHONLY', - :capture => 'CCFORCE', - :capture_complete => 'CCCOMPLETE', - :void => 'CCDELETE', - :store => 'CCGETTOKEN', - :update => 'CCUPDATETOKEN', + purchase: 'CCSALE', + credit: 'CCCREDIT', + refund: 'CCRETURN', + authorize: 'CCAUTHONLY', + capture: 'CCFORCE', + capture_complete: 'CCCOMPLETE', + void: 'CCDELETE', + store: 'CCGETTOKEN', + update: 'CCUPDATETOKEN', + verify: 'CCVERIFY' } def initialize(options = {}) @@ -34,106 +38,147 @@ def initialize(options = {}) end def purchase(money, payment_method, options = {}) - form = {} - add_salestax(form, options) - add_invoice(form, options) - if payment_method.is_a?(String) - add_token(form, payment_method) - else - add_creditcard(form, payment_method) + request = build_xml_request do |xml| + xml.ssl_vendor_id @options[:ssl_vendor_id] || options[:ssl_vendor_id] + xml.ssl_transaction_type self.actions[:purchase] + xml.ssl_amount amount(money) + + add_payment(xml, payment_method, options) + add_invoice(xml, options) + add_salestax(xml, options) + add_currency(xml, money, options) + add_address(xml, options) + add_customer_email(xml, options) + add_test_mode(xml, options) + add_ip(xml, options) + add_auth_purchase_params(xml, payment_method, options) + add_level_3_fields(xml, options) if options[:level_3_data] end - add_address(form, options) - add_customer_data(form, options) - add_test_mode(form, options) - add_ip(form, options) - commit(:purchase, money, form, options) - end - - def authorize(money, creditcard, options = {}) - form = {} - add_salestax(form, options) - add_invoice(form, options) - add_creditcard(form, creditcard) - add_address(form, options) - add_customer_data(form, options) - add_test_mode(form, options) - add_ip(form, options) - commit(:authorize, money, form, options) + commit(request) + end + + 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_payment(xml, payment_method, options) + add_currency(xml, money, options) + add_address(xml, options) + add_customer_email(xml, options) + add_test_mode(xml, options) + add_ip(xml, options) + add_auth_purchase_params(xml, payment_method, options) + add_level_3_fields(xml, options) if options[:level_3_data] + end + commit(request) end def capture(money, authorization, options = {}) - form = {} - if options[:credit_card] - action = :capture - add_salestax(form, options) - add_approval_code(form, authorization) - add_invoice(form, options) - add_creditcard(form, options[:credit_card]) - add_customer_data(form, options) - add_test_mode(form, options) - else - action = :capture_complete - add_txn_id(form, authorization) - add_partial_shipment_flag(form, options) - add_test_mode(form, options) + request = build_xml_request do |xml| + xml.ssl_vendor_id @options[:ssl_vendor_id] || options[:ssl_vendor_id] + + if options[:credit_card] + xml.ssl_transaction_type self.actions[:capture] + xml.ssl_amount amount(money) + add_salestax(xml, options) + add_approval_code(xml, authorization) + add_invoice(xml, options) + add_creditcard(xml, options[:credit_card], options) + add_currency(xml, money, options) + add_address(xml, options) + add_customer_email(xml, options) + add_test_mode(xml, options) + else + xml.ssl_transaction_type self.actions[:capture_complete] + xml.ssl_amount amount(money) + add_currency(xml, money, options) + add_txn_id(xml, authorization) + add_partial_shipment_flag(xml, options) + add_test_mode(xml, options) + end end - commit(action, money, form, options) + commit(request) end def refund(money, identification, options = {}) - form = {} - add_txn_id(form, identification) - add_test_mode(form, options) - commit(:refund, money, form, 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[:refund] + xml.ssl_amount amount(money) + add_txn_id(xml, identification) + add_test_mode(xml, options) + end + commit(request) end def void(identification, options = {}) - form = {} - add_txn_id(form, identification) - add_test_mode(form, options) - commit(:void, nil, form, 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[:void] + + add_txn_id(xml, identification) + add_test_mode(xml, options) + end + commit(request) end def credit(money, creditcard, options = {}) - if creditcard.is_a?(String) - raise ArgumentError, 'Reference credits are not supported. Please supply the original credit card or use the #refund method.' + raise ArgumentError, 'Reference credits are not supported. Please supply the original credit card or use the #refund method.' if creditcard.is_a?(String) + + request = build_xml_request do |xml| + xml.ssl_vendor_id @options[:ssl_vendor_id] || options[:ssl_vendor_id] + xml.ssl_transaction_type self.actions[:credit] + xml.ssl_amount amount(money) + add_invoice(xml, options) + add_creditcard(xml, creditcard, options) + add_currency(xml, money, options) + add_address(xml, options) + add_customer_email(xml, options) + add_test_mode(xml, options) end - - form = {} - add_invoice(form, options) - add_creditcard(form, creditcard) - add_address(form, options) - add_customer_data(form, options) - add_test_mode(form, options) - commit(:credit, money, form, options) + commit(request) end def verify(credit_card, options = {}) - MultiResponse.run(:use_first_response) do |r| - r.process { authorize(100, credit_card, options) } - r.process(:ignore_result) { void(r.authorization, options) } + request = build_xml_request do |xml| + xml.ssl_vendor_id @options[:ssl_vendor_id] || options[:ssl_vendor_id] + xml.ssl_transaction_type self.actions[:verify] + add_creditcard(xml, credit_card, options) + add_address(xml, options) + add_test_mode(xml, options) + add_ip(xml, options) end + commit(request) end def store(creditcard, options = {}) - form = {} - add_creditcard(form, creditcard) - add_address(form, options) - add_customer_data(form, options) - add_test_mode(form, options) - add_verification(form, options) - form[:add_token] = 'Y' - commit(:store, nil, form, 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[:store] + xml.ssl_add_token 'Y' + add_creditcard(xml, creditcard, options) + add_address(xml, options) + add_customer_email(xml, options) + add_test_mode(xml, options) + add_verification(xml, options) + end + commit(request) end def update(token, creditcard, options = {}) - form = {} - add_token(form, token) - add_creditcard(form, creditcard) - add_address(form, options) - add_customer_data(form, options) - add_test_mode(form, options) - commit(:update, nil, form, 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[:update] + xml.ssl_token token + add_creditcard(xml, creditcard, options) + add_address(xml, options) + add_customer_email(xml, options) + add_test_mode(xml, options) + end + commit(request) end def supports_scrubbing? @@ -142,172 +187,321 @@ def supports_scrubbing? def scrub(transcript) transcript. - gsub(%r((&?ssl_pin=)[^&]*)i, '\1[FILTERED]'). - gsub(%r((&?ssl_card_number=)[^&\\n\r\n]*)i, '\1[FILTERED]'). - gsub(%r((&?ssl_cvv2cvc2=)[^&]*)i, '\1[FILTERED]') + gsub(%r(()(.*)()), '\1[FILTERED]\3'). + gsub(%r(()(.*)()), '\1[FILTERED]\3'). + gsub(%r(()(.*)()), '\1[FILTERED]\3') end private - def add_invoice(form,options) - form[:invoice_number] = truncate((options[:order_id] || options[:invoice]), 10) - form[:description] = truncate(options[:description], 255) + def add_payment(xml, payment, options) + if payment.is_a?(String) || options[:ssl_token] + xml.ssl_token options[:ssl_token] || payment + elsif payment.is_a?(NetworkTokenizationCreditCard) + add_network_token(xml, payment) + else + add_creditcard(xml, payment, options) + end end - def add_approval_code(form, authorization) - form[:approval_code] = authorization.split(';').first + 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) end - def add_txn_id(form, authorization) - form[:txn_id] = authorization.split(';').last + def add_approval_code(xml, authorization) + xml.ssl_approval_code authorization.split(';').first end - def authorization_from(response) - [response['approval_code'], response['txn_id']].join(';') + def add_txn_id(xml, authorization) + xml.ssl_txn_id authorization.split(';').last end - def add_creditcard(form, creditcard) - form[:card_number] = creditcard.number - form[:exp_date] = expdate(creditcard) - - if creditcard.verification_value? - add_verification_value(form, creditcard) + def add_network_token(xml, payment_method) + payment = payment_method.payment_data.to_s&.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 - form[:first_name] = truncate(creditcard.first_name, 20) - form[:last_name] = truncate(creditcard.last_name, 30) + def add_creditcard(xml, creditcard, options) + xml.ssl_card_number creditcard.number + xml.ssl_exp_date expdate(creditcard) + + add_verification_value(xml, creditcard, options) + + xml.ssl_first_name url_encode_truncate(creditcard.first_name, 20) + xml.ssl_last_name url_encode_truncate(creditcard.last_name, 30) end - def add_token(form, token) - form[:token] = token + def add_currency(xml, money, options) + currency = options[:currency] || currency(money) + return unless currency && (@options[:multi_currency] || options[:multi_currency]) + + xml.ssl_transaction_currency currency end - def add_verification_value(form, creditcard) - form[:cvv2cvc2] = creditcard.verification_value - form[:cvv2cvc2_indicator] = '1' + def add_verification_value(xml, credit_card, options) + return unless credit_card.verification_value? + + xml.ssl_cvv2cvc2 credit_card.verification_value + xml.ssl_cvv2cvc2_indicator 1 end - def add_customer_data(form, options) - form[:email] = truncate(options[:email], 100) unless empty?(options[:email]) - form[:customer_code] = truncate(options[:customer], 10) unless empty?(options[:customer]) - form[:customer_number] = options[:customer_number] unless empty?(options[:customer_number]) - if options[:custom_fields] - options[:custom_fields].each do |key, value| - form[key.to_s] = value - end - end + def add_customer_email(xml, options) + xml.ssl_email url_encode_truncate(options[:email], 100) unless empty?(options[:email]) end - def add_salestax(form, options) - form[:salestax] = options[:tax] if options[:tax].present? + def add_salestax(xml, options) + return unless options[:tax].present? + + xml.ssl_salestax options[:tax] end - def add_address(form, options) + def add_address(xml, options) billing_address = options[:billing_address] || options[:address] if billing_address - form[:avs_address] = truncate(billing_address[:address1], 30) - form[:address2] = truncate(billing_address[:address2], 30) - form[:avs_zip] = truncate(billing_address[:zip].to_s.gsub(/[^a-zA-Z0-9]/, ''), 9) - form[:city] = truncate(billing_address[:city], 30) - form[:state] = truncate(billing_address[:state], 10) - form[:company] = truncate(billing_address[:company], 50) - form[:phone] = truncate(billing_address[:phone], 20) - form[:country] = truncate(billing_address[:country], 50) + xml.ssl_avs_address url_encode_truncate(billing_address[:address1], 30) + xml.ssl_address2 url_encode_truncate(billing_address[:address2], 30) + xml.ssl_avs_zip url_encode_truncate(billing_address[:zip].to_s.gsub(/[^a-zA-Z0-9]/, ''), 9) + xml.ssl_city url_encode_truncate(billing_address[:city], 30) + xml.ssl_state url_encode_truncate(billing_address[:state], 10) + xml.ssl_company url_encode_truncate(billing_address[:company], 50) + xml.ssl_phone url_encode_truncate(billing_address[:phone], 20) + xml.ssl_country url_encode_truncate(billing_address[:country], 50) end if shipping_address = options[:shipping_address] - first_name, last_name = split_names(shipping_address[:name]) - form[:ship_to_first_name] = truncate(first_name, 20) - form[:ship_to_last_name] = truncate(last_name, 30) - form[:ship_to_address1] = truncate(shipping_address[:address1], 30) - form[:ship_to_address2] = truncate(shipping_address[:address2], 30) - form[:ship_to_city] = truncate(shipping_address[:city], 30) - form[:ship_to_state] = truncate(shipping_address[:state], 10) - form[:ship_to_company] = truncate(shipping_address[:company], 50) - form[:ship_to_country] = truncate(shipping_address[:country], 50) - form[:ship_to_zip] = truncate(shipping_address[:zip], 10) + xml.ssl_ship_to_address1 url_encode_truncate(shipping_address[:address1], 30) + xml.ssl_ship_to_address2 url_encode_truncate(shipping_address[:address2], 30) + xml.ssl_ship_to_city url_encode_truncate(shipping_address[:city], 30) + xml.ssl_ship_to_company url_encode_truncate(shipping_address[:company], 50) + xml.ssl_ship_to_country url_encode_truncate(shipping_address[:country], 50) + xml.ssl_ship_to_first_name url_encode_truncate(shipping_address[:first_name], 20) + xml.ssl_ship_to_last_name url_encode_truncate(shipping_address[:last_name], 30) + xml.ssl_ship_to_phone url_encode_truncate(shipping_address[:phone], 10) + xml.ssl_ship_to_state url_encode_truncate(shipping_address[:state], 2) + xml.ssl_ship_to_zip url_encode_truncate(shipping_address[:zip], 10) end end - def add_verification(form, options) - form[:verify] = 'Y' if options[:verify] + def add_verification(xml, options) + xml.ssl_verify 'Y' if options[:verify] end - def add_test_mode(form, options) - form[:test_mode] = 'TRUE' if options[:test_mode] + def add_test_mode(xml, options) + xml.ssl_test_mode 'TRUE' if options[:test_mode] end - def add_partial_shipment_flag(form, options) - form[:partial_shipment_flag] = 'Y' if options[:partial_shipment_flag] + def add_partial_shipment_flag(xml, options) + xml.ssl_partial_shipment_flag 'Y' if options[:partial_shipment_flag] end - def add_ip(form, options) - form[:cardholder_ip] = options[:ip] if options.has_key?(:ip) + def add_ip(xml, options) + xml.ssl_cardholder_ip options[:ip] if options.has_key?(:ip) end - def message_from(response) - success?(response) ? response['result_message'] : response['errorMessage'] + # add_recurring_token is a field that can be sent in to obtain a token from Elavon for use with their tokenization program + def add_auth_purchase_params(xml, payment_method, options) + xml.ssl_dynamic_dba options[:dba] if options.has_key?(:dba) + xml.ssl_merchant_initiated_unscheduled merchant_initiated_unscheduled(options) if merchant_initiated_unscheduled(options) + xml.ssl_add_token options[:add_recurring_token] if options.has_key?(:add_recurring_token) + xml.ssl_customer_code options[:customer] if options.has_key?(:customer) + xml.ssl_customer_number options[:customer_number] if options.has_key?(:customer_number) + xml.ssl_entry_mode entry_mode(payment_method, options) if entry_mode(payment_method, options) + add_custom_fields(xml, options) if options[:custom_fields] + add_stored_credential(xml, payment_method, options) + add_installment_fields(xml, options) end - def success?(response) - !response.has_key?('errorMessage') + def add_custom_fields(xml, options) + options[:custom_fields]&.each do |key, value| + xml.send(key.to_sym, value) + end end - def commit(action, money, parameters, options) - parameters[:amount] = amount(money) - parameters[:transaction_type] = self.actions[action] + def add_level_3_fields(xml, options) + level_3_data = options[:level_3_data] + xml.ssl_customer_code level_3_data[:customer_code] if level_3_data[:customer_code] + xml.ssl_salestax level_3_data[:salestax] if level_3_data[:salestax] + xml.ssl_salestax_indicator level_3_data[:salestax_indicator] if level_3_data[:salestax_indicator] + xml.ssl_level3_indicator level_3_data[:level3_indicator] if level_3_data[:level3_indicator] + xml.ssl_ship_to_zip level_3_data[:ship_to_zip] if level_3_data[:ship_to_zip] + xml.ssl_ship_to_country level_3_data[:ship_to_country] if level_3_data[:ship_to_country] + xml.ssl_shipping_amount level_3_data[:shipping_amount] if level_3_data[:shipping_amount] + xml.ssl_ship_from_postal_code level_3_data[:ship_from_postal_code] if level_3_data[:ship_from_postal_code] + xml.ssl_discount_amount level_3_data[:discount_amount] if level_3_data[:discount_amount] + xml.ssl_duty_amount level_3_data[:duty_amount] if level_3_data[:duty_amount] + xml.ssl_national_tax_indicator level_3_data[:national_tax_indicator] if level_3_data[:national_tax_indicator] + xml.ssl_national_tax_amount level_3_data[:national_tax_amount] if level_3_data[:national_tax_amount] + xml.ssl_order_date level_3_data[:order_date] if level_3_data[:order_date] + xml.ssl_other_tax level_3_data[:other_tax] if level_3_data[:other_tax] + xml.ssl_summary_commodity_code level_3_data[:summary_commodity_code] if level_3_data[:summary_commodity_code] + xml.ssl_merchant_vat_number level_3_data[:merchant_vat_number] if level_3_data[:merchant_vat_number] + xml.ssl_customer_vat_number level_3_data[:customer_vat_number] if level_3_data[:customer_vat_number] + xml.ssl_freight_tax_amount level_3_data[:freight_tax_amount] if level_3_data[:freight_tax_amount] + xml.ssl_vat_invoice_number level_3_data[:vat_invoice_number] if level_3_data[:vat_invoice_number] + xml.ssl_tracking_number level_3_data[:tracking_number] if level_3_data[:tracking_number] + xml.ssl_shipping_company level_3_data[:shipping_company] if level_3_data[:shipping_company] + xml.ssl_other_fees level_3_data[:other_fees] if level_3_data[:other_fees] + add_line_items(xml, level_3_data) if level_3_data[:line_items] + end + + def add_line_items(xml, level_3_data) + xml.LineItemProducts { + level_3_data[:line_items].each do |line_item| + xml.product { + line_item.each do |key, value| + prefixed_key = "ssl_line_Item_#{key}" + xml.send(prefixed_key, value) + end + } + end + } + end - response = parse( ssl_post(test? ? self.test_url : self.live_url, post_data(parameters, options)) ) + def add_stored_credential(xml, payment_method, options) + return unless options[:stored_credential] + + network_transaction_id = options.dig(:stored_credential, :network_transaction_id) + xml.ssl_recurring_flag recurring_flag(options) if recurring_flag(options) + xml.ssl_par_value options[:par_value] if options[:par_value] + xml.ssl_association_token_data options[:association_token_data] if options[:association_token_data] + + unless payment_method.is_a?(String) || options[:ssl_token].present? + xml.ssl_approval_code options[:approval_code] if options[:approval_code] + if network_transaction_id.to_s.include?('|') + oar_data, ps2000_data = network_transaction_id.split('|') + xml.ssl_oar_data oar_data unless oar_data.blank? + xml.ssl_ps2000_data ps2000_data unless ps2000_data.blank? + elsif network_transaction_id.to_s.length > 22 + xml.ssl_oar_data network_transaction_id + elsif network_transaction_id.present? + xml.ssl_ps2000_data network_transaction_id + end + end + end - Response.new(response['result'] == '0', message_from(response), response, - :test => @options[:test] || test?, - :authorization => authorization_from(response), - :avs_result => { :code => response['avs_response'] }, - :cvv_result => response['cvv2_response'] - ) + def recurring_flag(options) + return unless reason = options.dig(:stored_credential, :reason_type) + return 1 if reason == 'recurring' + return 2 if reason == 'installment' end - def post_data(parameters, options) - result = preamble - result.merge!(parameters) - result.collect { |key, value| post_data_string(key, value, options) }.join('&') + def merchant_initiated_unscheduled(options) + return options[:merchant_initiated_unscheduled] if options[:merchant_initiated_unscheduled] + return 'Y' if options.dig(:stored_credential, :initiator) == 'merchant' && %w(unscheduled recurring).include?(options.dig(:stored_credential, :reason_type)) end - def post_data_string(key, value, options) - if custom_field?(key, options) - "#{key}=#{CGI.escape(value.to_s)}" - else - "ssl_#{key}=#{CGI.escape(value.to_s)}" + def add_installment_fields(xml, options) + return unless options.dig(:stored_credential, :reason_type) == 'installment' + + xml.ssl_payment_number options[:payment_number] + xml.ssl_payment_count options[:installments] + end + + def entry_mode(payment_method, options) + return options[:entry_mode] if options[:entry_mode] + return if payment_method.is_a?(String) || options[:ssl_token] + return 12 if options.dig(:stored_credential, :reason_type) == 'unscheduled' + end + + def build_xml_request + builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml| + xml.txn do + xml.ssl_merchant_id @options[:login] + xml.ssl_user_id @options[:user] + xml.ssl_pin @options[:password] + yield(xml) + end end + + builder.to_xml.gsub("\n", '') end - def custom_field?(field_name, options) - return true if options[:custom_fields] && options[:custom_fields].include?(field_name.to_sym) - field_name == :customer_number + def commit(request) + request = "xmldata=#{request}".delete('&') + store_action = request.match?('CCGETTOKEN') + + response = parse(ssl_post(test? ? self.test_url : self.live_url, request, headers)) + response = hash_html_decode(response) + + Response.new( + response[:result] == '0', + response[:result_message] || response[:errorMessage], + response, + test: @options[:test] || test?, + authorization: authorization_from(response, store_action), + error_code: response[:errorCode], + avs_result: { code: response[:avs_response] }, + cvv_result: response[:cvv2_response], + network_transaction_id: build_network_transaction_id(response) + ) end - def preamble - result = { - 'merchant_id' => @options[:login], - 'pin' => @options[:password], - 'show_form' => 'false', - 'result_format' => 'ASCII' + def build_network_transaction_id(response) + "#{response[:oar_data]}|#{response[:ps2000_data]}" + end + + def headers + { + 'Accept' => 'application/xml', + 'Content-type' => 'application/x-www-form-urlencoded;charset=utf8' } + end + + def parse(body) + xml = Nokogiri::XML(body) + response = Hash.from_xml(xml.to_s)['txn'] - result['user_id'] = @options[:user] unless empty?(@options[:user]) - result + response.deep_transform_keys { |key| key.gsub('ssl_', '').to_sym } end - def parse(msg) - resp = {} - msg.split(self.delimiter).collect{|li| - key, value = li.split('=') - resp[key.to_s.strip.gsub(/^ssl_/, '')] = value.to_s.strip - } - resp + def authorization_from(response, store_action) + return response[:token] if store_action + + [response[:approval_code], response[:txn_id]].join(';') + end + + def url_encode_truncate(value, size) + return nil unless value + + encoded = url_encode(value) + + while encoded.length > size + value.chop! + encoded = url_encode(value) + end + encoded end + def url_encode(value) + if value.is_a?(String) + encoded = CGI.escape(value) + encoded = encoded.tr('+', ' ') # don't encode spaces + encoded.gsub('%26', '%26amp;') # account for Elavon's weird '&' handling + + else + value.to_s + end + end + + def hash_html_decode(hash) + hash.each do |k, v| + if v.is_a?(String) + # decode all string params + v = v.gsub('&amp;', '&') # account for Elavon's weird '&' handling + hash[k] = CGI.unescape_html(v) + elsif v.is_a?(Hash) + hash_html_decode(v) + end + end + hash + end end end end diff --git a/lib/active_merchant/billing/gateways/element.rb b/lib/active_merchant/billing/gateways/element.rb index 163402df436..0896bc32513 100644 --- a/lib/active_merchant/billing/gateways/element.rb +++ b/lib/active_merchant/billing/gateways/element.rb @@ -1,15 +1,15 @@ require 'nokogiri' require 'securerandom' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class ElementGateway < Gateway self.test_url = 'https://certtransaction.elementexpress.com/express.asmx' self.live_url = 'https://transaction.elementexpress.com/express.asmx' self.supported_countries = ['US'] self.default_currency = 'USD' - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb] + self.supported_cardtypes = %i[visa master american_express discover diners_club jcb] self.homepage_url = 'http://www.elementps.com' self.display_name = 'Element' @@ -17,12 +17,17 @@ class ElementGateway < Gateway SERVICE_TEST_URL = 'https://certservices.elementexpress.com/express.asmx' SERVICE_LIVE_URL = 'https://services.elementexpress.com/express.asmx' - def initialize(options={}) + NETWORK_TOKEN_TYPE = { + apple_pay: '2', + google_pay: '1' + } + + def initialize(options = {}) requires!(options, :account_id, :account_token, :application_id, :acceptor_id, :application_name, :application_version) super end - def purchase(money, payment, options={}) + def purchase(money, payment, options = {}) action = payment.is_a?(Check) ? 'CheckSale' : 'CreditCardSale' request = build_soap_request do |xml| @@ -32,13 +37,14 @@ def purchase(money, payment, options={}) add_transaction(xml, money, options) add_terminal(xml, options) add_address(xml, options) + add_lodging(xml, options) end end commit(action, request, money) end - def authorize(money, payment, options={}) + def authorize(money, payment, options = {}) request = build_soap_request do |xml| xml.CreditCardAuthorization(xmlns: 'https://transaction.elementexpress.com') do add_credentials(xml) @@ -46,15 +52,16 @@ def authorize(money, payment, options={}) add_transaction(xml, money, options) add_terminal(xml, options) add_address(xml, options) + add_lodging(xml, options) end end commit('CreditCardAuthorization', request, money) end - def capture(money, authorization, options={}) - trans_id, _ = split_authorization(authorization) - options.merge!({trans_id: trans_id}) + def capture(money, authorization, options = {}) + trans_id, = split_authorization(authorization) + options[:trans_id] = trans_id request = build_soap_request do |xml| xml.CreditCardAuthorizationCompletion(xmlns: 'https://transaction.elementexpress.com') do @@ -67,9 +74,9 @@ def capture(money, authorization, options={}) commit('CreditCardAuthorizationCompletion', request, money) end - def refund(money, authorization, options={}) - trans_id, _ = split_authorization(authorization) - options.merge!({trans_id: trans_id}) + def refund(money, authorization, options = {}) + trans_id, = split_authorization(authorization) + options[:trans_id] = trans_id request = build_soap_request do |xml| xml.CreditCardReturn(xmlns: 'https://transaction.elementexpress.com') do @@ -82,9 +89,22 @@ def refund(money, authorization, options={}) commit('CreditCardReturn', request, money) end - def void(authorization, options={}) + def credit(money, payment, options = {}) + request = build_soap_request do |xml| + xml.CreditCardCredit(xmlns: 'https://transaction.elementexpress.com') do + add_credentials(xml) + add_payment_method(xml, payment) + add_transaction(xml, money, options) + add_terminal(xml, options) + end + end + + commit('CreditCardCredit', request, money) + end + + def void(authorization, options = {}) trans_id, trans_amount = split_authorization(authorization) - options.merge!({trans_id: trans_id, trans_amount: trans_amount, reversal_type: 'Full'}) + options.merge!({ trans_id:, trans_amount:, reversal_type: 'Full' }) request = build_soap_request do |xml| xml.CreditCardReversal(xmlns: 'https://transaction.elementexpress.com') do @@ -110,11 +130,19 @@ def store(payment, options = {}) commit('PaymentAccountCreate', request, nil) end - def verify(credit_card, options={}) - MultiResponse.run(:use_first_response) do |r| - r.process { authorize(100, credit_card, options) } - r.process(:ignore_result) { void(r.authorization, options) } + def verify(credit_card, options = {}) + request = build_soap_request do |xml| + xml.CreditCardAVSOnly(xmlns: 'https://transaction.elementexpress.com') do + add_credentials(xml) + add_payment_method(xml, credit_card) + add_transaction(xml, 0, options) + add_terminal(xml, options) + add_address(xml, options) + end end + + # send request with the transaction amount set to 0 + commit('CreditCardAVSOnly', request, 0) end def supports_scrubbing? @@ -150,6 +178,8 @@ def add_payment_method(xml, payment) add_payment_account_id(xml, payment) elsif payment.is_a?(Check) add_echeck(xml, payment) + elsif payment.is_a?(NetworkTokenizationCreditCard) + add_network_tokenization_card(xml, payment) else add_credit_card(xml, payment) end @@ -178,21 +208,60 @@ def add_transaction(xml, money, options = {}) xml.ReversalType 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 'Default' if money - xml.ReferenceNumber options[:order_id] || SecureRandom.hex(20) + xml.MarketCode market_code(money, options) if options[:market_code] || money + xml.ReferenceNumber options[:order_id].present? ? options[:order_id][0, 50] : SecureRandom.hex(20) + xml.TicketNumber options[:ticket_number] if options[:ticket_number] + xml.MerchantSuppliedTransactionId options[:merchant_supplied_transaction_id] if options[:merchant_supplied_transaction_id] + xml.PaymentType options[:payment_type] if options[:payment_type] + xml.SubmissionType options[:submission_type] if options[:submission_type] + xml.DuplicateCheckDisableFlag options[:duplicate_check_disable_flag].to_s == 'true' ? 'True' : 'False' unless options[:duplicate_check_disable_flag].nil? + xml.DuplicateOverrideFlag options[:duplicate_override_flag].to_s == 'true' ? 'True' : 'False' unless options[:duplicate_override_flag].nil? + xml.MerchantDescriptor options[:merchant_descriptor] if options[:merchant_descriptor] + end + end + + 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 '01' - xml.CardPresentCode 'UseDefault' - xml.CardholderPresentCode 'UseDefault' - xml.CardInputCode 'UseDefault' - xml.CVVPresenceCode 'UseDefault' - xml.TerminalCapabilityCode 'UseDefault' - xml.TerminalEnvironmentCode 'UseDefault' + xml.TerminalID options[:terminal_id] || '01' + xml.TerminalType options[:terminal_type] if options[:terminal_type] + xml.CardPresentCode options[:card_present_code] || '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 @@ -201,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 @@ -214,8 +283,21 @@ def add_echeck(xml, payment) end end + def add_network_tokenization_card(xml, payment) + xml.card do + xml.CardNumber payment.number + xml.ExpirationMonth format(payment.month, :two_digits) + xml.ExpirationYear format(payment.year, :two_digits) + xml.CardholderName "#{payment.first_name} #{payment.last_name}" + xml.Cryptogram payment.payment_cryptogram + xml.ElectronicCommerceIndicator payment.eci if payment.eci.present? + xml.WalletType NETWORK_TOKEN_TYPE.fetch(payment.source, '0') + end + end + def add_address(xml, options) if address = options[:billing_address] || options[:address] + address[:email] ||= options[:email] xml.address do xml.BillingAddress1 address[:address1] if address[:address1] xml.BillingAddress2 address[:address2] if address[:address2] @@ -246,12 +328,10 @@ def parse(xml) doc.remove_namespaces! root = doc.root.xpath('//response/*') - if root.empty? - root = doc.root.xpath('//Response/*') - end + root = doc.root.xpath('//Response/*') if root.empty? root.each do |node| - if (node.elements.empty?) + if node.elements.empty? response[node.name.downcase] = node.text else node_name = node.name.downcase @@ -313,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 @@ -334,9 +413,9 @@ def payment_account_type(payment) def url(action) if action == 'PaymentAccountCreate' - url = (test? ? SERVICE_TEST_URL : SERVICE_LIVE_URL) + test? ? SERVICE_TEST_URL : SERVICE_LIVE_URL else - url = (test? ? test_url : live_url) + test? ? test_url : live_url end end diff --git a/lib/active_merchant/billing/gateways/epay.rb b/lib/active_merchant/billing/gateways/epay.rb index 937589eda90..80bc9db8d65 100644 --- a/lib/active_merchant/billing/gateways/epay.rb +++ b/lib/active_merchant/billing/gateways/epay.rb @@ -1,52 +1,52 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class EpayGateway < Gateway self.live_url = 'https://ssl.ditonlinebetalingssystem.dk/' self.default_currency = 'DKK' self.money_format = :cents - self.supported_cardtypes = [:dankort, :forbrugsforeningen, :visa, :master, - :american_express, :diners_club, :jcb, :maestro] - self.supported_countries = ['DK', 'SE', 'NO'] + self.supported_countries = %w[DK SE NO] + self.supported_cardtypes = %i[dankort forbrugsforeningen visa master + american_express diners_club jcb maestro] self.homepage_url = 'http://epay.dk/' self.display_name = 'ePay' CURRENCY_CODES = { - :ADP => '020', :AED => '784', :AFA => '004', :ALL => '008', :AMD => '051', - :ANG => '532', :AOA => '973', :ARS => '032', :AUD => '036', :AWG => '533', - :AZM => '031', :BAM => '977', :BBD => '052', :BDT => '050', :BGL => '100', - :BGN => '975', :BHD => '048', :BIF => '108', :BMD => '060', :BND => '096', - :BOB => '068', :BOV => '984', :BRL => '986', :BSD => '044', :BTN => '064', - :BWP => '072', :BYR => '974', :BZD => '084', :CAD => '124', :CDF => '976', - :CHF => '756', :CLF => '990', :CLP => '152', :CNY => '156', :COP => '170', - :CRC => '188', :CUP => '192', :CVE => '132', :CYP => '196', :CZK => '203', - :DJF => '262', :DKK => '208', :DOP => '214', :DZD => '012', :ECS => '218', - :ECV => '983', :EEK => '233', :EGP => '818', :ERN => '232', :ETB => '230', - :EUR => '978', :FJD => '242', :FKP => '238', :GBP => '826', :GEL => '981', - :GHC => '288', :GIP => '292', :GMD => '270', :GNF => '324', :GTQ => '320', - :GWP => '624', :GYD => '328', :HKD => '344', :HNL => '340', :HRK => '191', - :HTG => '332', :HUF => '348', :IDR => '360', :ILS => '376', :INR => '356', - :IQD => '368', :IRR => '364', :ISK => '352', :JMD => '388', :JOD => '400', - :JPY => '392', :KES => '404', :KGS => '417', :KHR => '116', :KMF => '174', - :KPW => '408', :KRW => '410', :KWD => '414', :KYD => '136', :KZT => '398', - :LAK => '418', :LBP => '422', :LKR => '144', :LRD => '430', :LSL => '426', - :LTL => '440', :LVL => '428', :LYD => '434', :MAD => '504', :MDL => '498', - :MGF => '450', :MKD => '807', :MMK => '104', :MNT => '496', :MOP => '446', - :MRO => '478', :MTL => '470', :MUR => '480', :MVR => '462', :MWK => '454', - :MXN => '484', :MXV => '979', :MYR => '458', :MZM => '508', :NAD => '516', - :NGN => '566', :NIO => '558', :NOK => '578', :NPR => '524', :NZD => '554', - :OMR => '512', :PAB => '590', :PEN => '604', :PGK => '598', :PHP => '608', - :PKR => '586', :PLN => '985', :PYG => '600', :QAR => '634', :ROL => '642', - :RUB => '643', :RUR => '810', :RWF => '646', :SAR => '682', :SBD => '090', - :SCR => '690', :SDD => '736', :SEK => '752', :SGD => '702', :SHP => '654', - :SIT => '705', :SKK => '703', :SLL => '694', :SOS => '706', :SRG => '740', - :STD => '678', :SVC => '222', :SYP => '760', :SZL => '748', :THB => '764', - :TJS => '972', :TMM => '795', :TND => '788', :TOP => '776', :TPE => '626', - :TRL => '792', :TRY => '949', :TTD => '780', :TWD => '901', :TZS => '834', - :UAH => '980', :UGX => '800', :USD => '840', :UYU => '858', :UZS => '860', - :VEB => '862', :VND => '704', :VUV => '548', :XAF => '950', :XCD => '951', - :XOF => '952', :XPF => '953', :YER => '886', :YUM => '891', :ZAR => '710', - :ZMK => '894', :ZWD => '716' + ADP: '020', AED: '784', AFA: '004', ALL: '008', AMD: '051', + ANG: '532', AOA: '973', ARS: '032', AUD: '036', AWG: '533', + AZM: '031', BAM: '977', BBD: '052', BDT: '050', BGL: '100', + BGN: '975', BHD: '048', BIF: '108', BMD: '060', BND: '096', + BOB: '068', BOV: '984', BRL: '986', BSD: '044', BTN: '064', + BWP: '072', BYR: '974', BZD: '084', CAD: '124', CDF: '976', + CHF: '756', CLF: '990', CLP: '152', CNY: '156', COP: '170', + CRC: '188', CUP: '192', CVE: '132', CYP: '196', CZK: '203', + DJF: '262', DKK: '208', DOP: '214', DZD: '012', ECS: '218', + ECV: '983', EEK: '233', EGP: '818', ERN: '232', ETB: '230', + EUR: '978', FJD: '242', FKP: '238', GBP: '826', GEL: '981', + GHC: '288', GIP: '292', GMD: '270', GNF: '324', GTQ: '320', + GWP: '624', GYD: '328', HKD: '344', HNL: '340', HRK: '191', + HTG: '332', HUF: '348', IDR: '360', ILS: '376', INR: '356', + IQD: '368', IRR: '364', ISK: '352', JMD: '388', JOD: '400', + JPY: '392', KES: '404', KGS: '417', KHR: '116', KMF: '174', + KPW: '408', KRW: '410', KWD: '414', KYD: '136', KZT: '398', + LAK: '418', LBP: '422', LKR: '144', LRD: '430', LSL: '426', + LTL: '440', LVL: '428', LYD: '434', MAD: '504', MDL: '498', + MGF: '450', MKD: '807', MMK: '104', MNT: '496', MOP: '446', + MRO: '478', MTL: '470', MUR: '480', MVR: '462', MWK: '454', + MXN: '484', MXV: '979', MYR: '458', MZM: '508', NAD: '516', + NGN: '566', NIO: '558', NOK: '578', NPR: '524', NZD: '554', + OMR: '512', PAB: '590', PEN: '604', PGK: '598', PHP: '608', + PKR: '586', PLN: '985', PYG: '600', QAR: '634', ROL: '642', + RUB: '643', RUR: '810', RWF: '646', SAR: '682', SBD: '090', + SCR: '690', SDD: '736', SEK: '752', SGD: '702', SHP: '654', + SIT: '705', SKK: '703', SLL: '694', SOS: '706', SRG: '740', + STD: '678', SVC: '222', SYP: '760', SZL: '748', THB: '764', + TJS: '972', TMM: '795', TND: '788', TOP: '776', TPE: '626', + TRL: '792', TRY: '949', TTD: '780', TWD: '901', TZS: '834', + UAH: '980', UGX: '800', USD: '840', UYU: '858', UZS: '860', + VEB: '862', VND: '704', VUV: '548', XAF: '950', XCD: '951', + XOF: '952', XPF: '953', YER: '886', YUM: '891', ZAR: '710', + ZMK: '894', ZWD: '716' } # login: merchant number @@ -63,6 +63,7 @@ def authorize(money, credit_card_or_reference, options = {}) add_invoice(post, options) add_creditcard_or_reference(post, credit_card_or_reference) add_instant_capture(post, false) + add_3ds_auth(post, options) commit(:authorize, post) end @@ -74,6 +75,7 @@ def purchase(money, credit_card_or_reference, options = {}) add_creditcard_or_reference(post, credit_card_or_reference) add_invoice(post, options) add_instant_capture(post, true) + add_3ds_auth(post, options) commit(:authorize, post) end @@ -120,7 +122,6 @@ def scrub(transcript) gsub(%r((&?cvc=)\d*(&?)), '\1[FILTERED]\2') end - private def add_amount(post, money, options) @@ -159,21 +160,35 @@ def add_instant_capture(post, option) post[:instantcapture] = option ? 1 : 0 end + def add_3ds_auth(post, options) + if options[:three_d_secure] + post[:eci] = options.dig(:three_d_secure, :eci) + post[:xid] = options.dig(:three_d_secure, :xid) + post[:cavv] = options.dig(:three_d_secure, :cavv) + post[:threeds_version] = options.dig(:three_d_secure, :version) + post[:ds_transaction_id] = options.dig(:three_d_secure, :ds_transaction_id) + end + end + def commit(action, params) response = send("do_#{action}", params) if action == :authorize - Response.new response['accept'].to_i == 1, - response['errortext'], - response, - :test => test?, - :authorization => response['tid'] + Response.new( + response['accept'].to_i == 1, + response['errortext'], + response, + test: test?, + authorization: response['tid'] + ) else - Response.new response['result'] == 'true', - messages(response['epay'], response['pbs']), - response, - :test => test?, - :authorization => params[:transaction] + Response.new( + response['result'] == 'true', + messages(response['epay'], response['pbs']), + response, + test: test?, + authorization: params[:transaction] + ) end end @@ -194,7 +209,6 @@ def do_authorize(params) headers['Referer'] = (options[:password] || 'activemerchant.org') response = raw_ssl_request(:post, live_url + 'auth/default.aspx', authorize_post_data(params), headers) - # Authorize gives the response back by redirecting with the values in # the URL query if location = response['Location'] @@ -209,7 +223,7 @@ def do_authorize(params) end result = {} - query.each_pair do |k,v| + query.each_pair do |k, v| result[k] = v.is_a?(Array) && v.size == 1 ? v[0] : v # make values like ['v'] into 'v' end result @@ -251,25 +265,25 @@ def make_headers(data, soap_call) end def xml_builder(params, soap_call) - xml = Builder::XmlMarkup.new(:indent => 2) + xml = Builder::XmlMarkup.new(indent: 2) xml.instruct! - xml.tag! 'soap:Envelope', { 'xmlns:xsi' => 'http://schemas.xmlsoap.org/soap/envelope/', - 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema', - 'xmlns:soap' => 'http://schemas.xmlsoap.org/soap/envelope/' } do - xml.tag! 'soap:Body' do - xml.tag! soap_call, { 'xmlns' => "#{self.live_url}remote/payment" } do - xml.tag! 'merchantnumber', @options[:login] - xml.tag! 'transactionid', params[:transaction] - xml.tag! 'amount', params[:amount].to_s if soap_call != 'delete' - end + xml.tag! 'soap:Envelope', { 'xmlns:xsi' => 'http://schemas.xmlsoap.org/soap/envelope/', + 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema', + 'xmlns:soap' => 'http://schemas.xmlsoap.org/soap/envelope/' } do + xml.tag! 'soap:Body' do + xml.tag! soap_call, { 'xmlns' => "#{self.live_url}remote/payment" } do + xml.tag! 'merchantnumber', @options[:login] + xml.tag! 'transactionid', params[:transaction] + xml.tag! 'amount', params[:amount].to_s if soap_call != 'delete' end end + end xml.target! end def authorize_post_data(params = {}) params[:language] = '2' - params[:cms] = 'activemerchant' + params[:cms] = 'activemerchant_3ds' params[:accepturl] = live_url + 'auth/default.aspx?accept=1' params[:declineurl] = live_url + 'auth/default.aspx?decline=1' params[:merchantnumber] = @options[:login] diff --git a/lib/active_merchant/billing/gateways/evo_ca.rb b/lib/active_merchant/billing/gateways/evo_ca.rb index b5f976b7cee..59e525ce755 100644 --- a/lib/active_merchant/billing/gateways/evo_ca.rb +++ b/lib/active_merchant/billing/gateways/evo_ca.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # === EVO Canada payment gateway. # # EVO returns two different identifiers for most transactions, the @@ -36,7 +36,7 @@ class EvoCaGateway < Gateway self.live_url = 'https://secure.evoepay.com/api/transact.php' self.supported_countries = ['CA'] - self.supported_cardtypes = [:visa, :master, :american_express, :jcb, :discover] + self.supported_cardtypes = %i[visa master american_express jcb discover] self.money_format = :dollars self.homepage_url = 'http://www.evocanada.com/' self.display_name = 'EVO Canada' @@ -139,8 +139,8 @@ def authorize(money, credit_card, options = {}) # options. def capture(money, authorization, options = {}) post = { - :amount => amount(money), - :transactionid => authorization + amount: amount(money), + transactionid: authorization } add_order(post, options) commit('capture', money, post) @@ -153,7 +153,7 @@ def capture(money, authorization, options = {}) # The identification parameter is the transaction ID, retrieved # from {Response#authorization}. def refund(money, identification) - post = {:transactionid => identification} + post = { transactionid: identification } commit('refund', money, post) end @@ -181,7 +181,7 @@ def credit(money, credit_card, options = {}) # The identification parameter is the transaction ID, retrieved # from {Response#authorization}. def void(identification) - post = {:transactionid => identification} + post = { transactionid: identification } commit('void', nil, post) end @@ -192,7 +192,7 @@ def void(identification) # The identification parameter is the transaction ID, retrieved # from {Response#authorization}. def update(identification, options) - post = {:transactionid => identification} + post = { transactionid: identification } add_order(post, options) commit('update', nil, post) end @@ -245,7 +245,7 @@ def add_invoice(post, options) end def add_paymentmethod(post, payment) - if card_brand(payment)=='check' + if card_brand(payment) == 'check' post[:payment] = 'check' post[:checkname] = payment.name post[:checkaba] = payment.routing_number @@ -279,11 +279,14 @@ def commit(action, money, parameters) response = parse(data) message = message_from(response) - Response.new(success?(response), message, response, - :test => test?, - :authorization => response['transactionid'], - :avs_result => { :code => response['avsresponse'] }, - :cvv_result => response['cvvresponse'] + Response.new( + success?(response), + message, + response, + test: test?, + authorization: response['transactionid'], + avs_result: { code: response['avsresponse'] }, + cvv_result: response['cvvresponse'] ) end @@ -292,7 +295,7 @@ def message_from(response) end def post_data(action, parameters = {}) - post = {:type => action} + post = { type: action } if test? post[:username] = 'demo' diff --git a/lib/active_merchant/billing/gateways/eway.rb b/lib/active_merchant/billing/gateways/eway.rb index 1c24fb8032f..5ee0518c62f 100644 --- a/lib/active_merchant/billing/gateways/eway.rb +++ b/lib/active_merchant/billing/gateways/eway.rb @@ -1,7 +1,7 @@ require 'rexml/document' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # Public: For more information on the Eway Gateway please visit their # {Developers Area}[http://www.eway.com.au/developers/api/direct-payments] class EwayGateway < Gateway @@ -9,7 +9,7 @@ class EwayGateway < Gateway self.money_format = :cents self.supported_countries = ['AU'] - self.supported_cardtypes = [:visa, :master, :american_express, :diners_club] + self.supported_cardtypes = %i[visa master american_express diners_club] self.homepage_url = 'http://www.eway.com.au/' self.display_name = 'eWAY' @@ -38,7 +38,7 @@ def purchase(money, creditcard, options = {}) commit(purchase_url(post[:CVN]), money, post) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) post = {} add_customer_id(post) @@ -64,13 +64,14 @@ def scrub(transcript) end private + def requires_address!(options) - raise ArgumentError.new('Missing eWay required parameters: address or billing_address') unless (options.has_key?(:address) or options.has_key?(:billing_address)) + raise ArgumentError.new('Missing eWay required parameters: address or billing_address') unless options.has_key?(:address) || options.has_key?(:billing_address) end def add_creditcard(post, creditcard) - post[:CardNumber] = creditcard.number - post[:CardExpiryMonth] = sprintf('%.2i', creditcard.month) + post[:CardNumber] = creditcard.number + post[:CardExpiryMonth] = sprintf('%.2i', creditcard.month) post[:CardExpiryYear] = sprintf('%.4i', creditcard.year)[-2..-1] post[:CustomerFirstName] = creditcard.first_name post[:CustomerLastName] = creditcard.last_name @@ -81,7 +82,7 @@ def add_creditcard(post, creditcard) def add_address(post, options) if address = options[:billing_address] || options[:address] - post[:CustomerAddress] = [ address[:address1], address[:address2], address[:city], address[:state], address[:country] ].compact.join(', ') + post[:CustomerAddress] = [address[:address1], address[:address2], address[:city], address[:state], address[:country]].compact.join(', ') post[:CustomerPostcode] = address[:zip] end end @@ -110,11 +111,12 @@ 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? + authorization: response[:ewaytrxnnumber], + test: test? ) end @@ -144,7 +146,8 @@ def post_data(parameters = {}) def message_from(message) return '' if message.blank? - MESSAGES[message[0,2]] || message + + MESSAGES[message[0, 2]] || message end def purchase_url(cvn) diff --git a/lib/active_merchant/billing/gateways/eway_managed.rb b/lib/active_merchant/billing/gateways/eway_managed.rb index 72e45021922..356588d0567 100644 --- a/lib/active_merchant/billing/gateways/eway_managed.rb +++ b/lib/active_merchant/billing/gateways/eway_managed.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class EwayManagedGateway < Gateway self.test_url = 'https://www.eway.com.au/gateway/ManagedPaymentService/test/managedCreditCardPayment.asmx' self.live_url = 'https://www.eway.com.au/gateway/ManagedPaymentService/managedCreditCardPayment.asmx' @@ -8,11 +8,11 @@ class EwayManagedGateway < Gateway self.supported_countries = ['AU'] # The card types supported by the payment gateway - self.supported_cardtypes = [:visa, :master] + self.supported_cardtypes = %i[visa master] self.default_currency = 'AUD' - #accepted money format + # accepted money format self.money_format = :cents # The homepage URL of the gateway @@ -49,7 +49,7 @@ def store(creditcard, options = {}) commit('CreateCustomer', post) end - def update(billing_id, creditcard, options={}) + def update(billing_id, creditcard, options = {}) post = {} # Handle our required fields @@ -59,7 +59,7 @@ def update(billing_id, creditcard, options={}) billing_address = options[:billing_address] eway_requires!(billing_address) - post[:managedCustomerID]=billing_id + post[:managedCustomerID] = billing_id add_creditcard(post, creditcard) add_address(post, billing_address) add_misc_fields(post, options) @@ -80,10 +80,10 @@ def update(billing_id, creditcard, options={}) # * :order_id -- The order number, passed to eWay as the "Invoice Reference" # * :invoice -- The invoice number, passed to eWay as the "Invoice Reference" unless :order_id is also given # * :description -- A description of the payment, passed to eWay as the "Invoice Description" - def purchase(money, billing_id, options={}) + def purchase(money, billing_id, options = {}) post = {} post[:managedCustomerID] = billing_id.to_s - post[:amount]=money + post[:amount] = money add_invoice(post, options) commit('ProcessPayment', post) @@ -122,13 +122,13 @@ def add_address(post, address) end def add_misc_fields(post, options) - post[:CustomerRef]=options[:billing_address][:customer_ref] || options[:customer] - post[:Title]=options[:billing_address][:title] - post[:Company]=options[:billing_address][:company] - post[:JobDesc]=options[:billing_address][:job_desc] - post[:Email]=options[:billing_address][:email] || options[:email] - post[:URL]=options[:billing_address][:url] - post[:Comments]=options[:description] + post[:CustomerRef] = options[:billing_address][:customer_ref] || options[:customer] + post[:Title] = options[:billing_address][:title] + post[:Company] = options[:billing_address][:company] + post[:JobDesc] = options[:billing_address][:job_desc] + post[:Email] = options[:billing_address][:email] || options[:email] + post[:URL] = options[:billing_address][:url] + post[:Comments] = options[:description] end def add_invoice(post, options) @@ -136,11 +136,10 @@ def add_invoice(post, options) post[:invoiceDescription] = options[:description] end - # add credit card details to be stored by eway. NOTE eway requires "title" field def add_creditcard(post, creditcard) - post[:CCNumber] = creditcard.number - post[:CCExpiryMonth] = sprintf('%.2i', creditcard.month) + post[:CCNumber] = creditcard.number + post[:CCExpiryMonth] = sprintf('%.2i', creditcard.month) post[:CCExpiryYear] = sprintf('%.4i', creditcard.year)[-2..-1] post[:CCNameOnCard] = creditcard.name post[:FirstName] = creditcard.first_name @@ -151,24 +150,24 @@ def parse(body) reply = {} xml = REXML::Document.new(body) if root = REXML::XPath.first(xml, '//soap:Fault') then - reply=parse_fault(root) + reply = parse_fault(root) else if root = REXML::XPath.first(xml, '//ProcessPaymentResponse/ewayResponse') then # Successful payment - reply=parse_purchase(root) + reply = parse_purchase(root) else if root = REXML::XPath.first(xml, '//QueryCustomerResult') then - reply=parse_query_customer(root) + reply = parse_query_customer(root) else if root = REXML::XPath.first(xml, '//CreateCustomerResult') then - reply[:message]='OK' - reply[:CreateCustomerResult]=root.text - reply[:success]=true + reply[:message] = 'OK' + reply[:CreateCustomerResult] = root.text + reply[:success] = true else if root = REXML::XPath.first(xml, '//UpdateCustomerResult') then - if root.text.downcase == 'true' then - reply[:message]='OK' - reply[:success]=true + if root.text.casecmp('true').zero? then + reply[:message] = 'OK' + reply[:success] = true else # ERROR: This state should never occur. If there is a problem, # a soap:Fault will be returned. The presence of this @@ -188,43 +187,47 @@ def parse(body) end def parse_fault(node) - reply={} - reply[:message]=REXML::XPath.first(node, '//soap:Reason/soap:Text').text - reply[:success]=false + reply = {} + reply[:message] = REXML::XPath.first(node, '//soap:Reason/soap:Text').text + reply[:success] = false reply end def parse_purchase(node) - reply={} - reply[:message]=REXML::XPath.first(node, '//ewayTrxnError').text - reply[:success]=(REXML::XPath.first(node, '//ewayTrxnStatus').text == 'True') - reply[:auth_code]=REXML::XPath.first(node, '//ewayAuthCode').text - reply[:transaction_number]=REXML::XPath.first(node, '//ewayTrxnNumber').text + reply = {} + reply[:message] = REXML::XPath.first(node, '//ewayTrxnError').text + reply[:success] = (REXML::XPath.first(node, '//ewayTrxnStatus').text == 'True') + reply[:auth_code] = REXML::XPath.first(node, '//ewayAuthCode').text + reply[:transaction_number] = REXML::XPath.first(node, '//ewayTrxnNumber').text reply end def parse_query_customer(node) - reply={} - reply[:message]='OK' - reply[:success]=true - reply[:CCNumber]=REXML::XPath.first(node, '//CCNumber').text - reply[:CCName]=REXML::XPath.first(node, '//CCName').text - reply[:CCExpiryMonth]=REXML::XPath.first(node, '//CCExpiryMonth').text - reply[:CCExpiryYear]=REXML::XPath.first(node, '//CCExpiryYear').text + reply = {} + reply[:message] = 'OK' + reply[:success] = true + reply[:CCNumber] = REXML::XPath.first(node, '//CCNumber').text + reply[:CCName] = REXML::XPath.first(node, '//CCName').text + reply[:CCExpiryMonth] = REXML::XPath.first(node, '//CCExpiryMonth').text + reply[:CCExpiryYear] = REXML::XPath.first(node, '//CCExpiryYear').text reply end def commit(action, post) - raw = begin - ssl_post(test? ? self.test_url : self.live_url, soap_request(post, action), 'Content-Type' => 'application/soap+xml; charset=utf-8') - rescue ResponseError => e - e.response.body - end + raw = + begin + ssl_post(test? ? self.test_url : self.live_url, soap_request(post, action), 'Content-Type' => 'application/soap+xml; charset=utf-8') + rescue ResponseError => e + e.response.body + end response = parse(raw) - EwayResponse.new(response[:success], response[:message], response, - :test => test?, - :authorization => response[:auth_code] + EwayResponse.new( + response[:success], + response[:message], + response, + test: test?, + authorization: response[:auth_code] ) end @@ -232,49 +235,49 @@ def commit(action, post) def soap_request(arguments, action) # eWay demands all fields be sent, but contain an empty string if blank post = case action - when 'QueryCustomer' - arguments - when 'ProcessPayment' - default_payment_fields.merge(arguments) - when 'CreateCustomer' - default_customer_fields.merge(arguments) - when 'UpdateCustomer' - default_customer_fields.merge(arguments) + when 'QueryCustomer' + arguments + when 'ProcessPayment' + default_payment_fields.merge(arguments) + when 'CreateCustomer' + default_customer_fields.merge(arguments) + when 'UpdateCustomer' + default_customer_fields.merge(arguments) end - xml = Builder::XmlMarkup.new :indent => 2 - xml.instruct! - xml.tag! 'soap12:Envelope', {'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema', 'xmlns:soap12' => 'http://www.w3.org/2003/05/soap-envelope'} do - xml.tag! 'soap12:Header' do - xml.tag! 'eWAYHeader', {'xmlns' => 'https://www.eway.com.au/gateway/managedpayment'} do - xml.tag! 'eWAYCustomerID', @options[:login] - xml.tag! 'Username', @options[:username] - xml.tag! 'Password', @options[:password] - end + xml = Builder::XmlMarkup.new indent: 2 + xml.instruct! + xml.tag! 'soap12:Envelope', { 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema', 'xmlns:soap12' => 'http://www.w3.org/2003/05/soap-envelope' } do + xml.tag! 'soap12:Header' do + xml.tag! 'eWAYHeader', { 'xmlns' => 'https://www.eway.com.au/gateway/managedpayment' } do + xml.tag! 'eWAYCustomerID', @options[:login] + xml.tag! 'Username', @options[:username] + xml.tag! 'Password', @options[:password] end - xml.tag! 'soap12:Body' do |x| - x.tag! "#{action}", {'xmlns' => 'https://www.eway.com.au/gateway/managedpayment'} do |y| - post.each do |key, value| - y.tag! "#{key}", "#{value}" - end + end + xml.tag! 'soap12:Body' do |x| + x.tag! action, { 'xmlns' => 'https://www.eway.com.au/gateway/managedpayment' } do |y| + post.each do |key, value| + y.tag! key, value end end end + end xml.target! end def default_customer_fields - hash={} - %w( CustomerRef Title FirstName LastName Company JobDesc Email Address Suburb State PostCode Country Phone Mobile Fax URL Comments CCNumber CCNameOnCard CCExpiryMonth CCExpiryYear ).each do |field| - hash[field.to_sym]='' + hash = {} + %w(CustomerRef Title FirstName LastName Company JobDesc Email Address Suburb State PostCode Country Phone Mobile Fax URL Comments CCNumber CCNameOnCard CCExpiryMonth CCExpiryYear).each do |field| + hash[field.to_sym] = '' end return hash end def default_payment_fields - hash={} - %w( managedCustomerID amount invoiceReference invoiceDescription ).each do |field| - hash[field.to_sym]='' + hash = {} + %w(managedCustomerID amount invoiceReference invoiceDescription).each do |field| + hash[field.to_sym] = '' end return hash end @@ -285,7 +288,6 @@ def token @params['CreateCustomerResult'] end end - end end end diff --git a/lib/active_merchant/billing/gateways/eway_rapid.rb b/lib/active_merchant/billing/gateways/eway_rapid.rb index c8fb53e0aa0..c3c59a93f9f 100644 --- a/lib/active_merchant/billing/gateways/eway_rapid.rb +++ b/lib/active_merchant/billing/gateways/eway_rapid.rb @@ -1,14 +1,14 @@ require 'json' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class EwayRapidGateway < Gateway self.test_url = 'https://api.sandbox.ewaypayments.com/' self.live_url = 'https://api.ewaypayments.com/' self.money_format = :cents - self.supported_countries = ['AU', 'NZ', 'GB', 'SG', 'MY', 'HK'] - self.supported_cardtypes = [:visa, :master, :american_express, :diners_club, :jcb] + self.supported_countries = %w[AU NZ GB SG MY HK] + self.supported_cardtypes = %i[visa master american_express diners_club jcb] self.homepage_url = 'http://www.eway.com.au/' self.display_name = 'eWAY Rapid 3.1' self.default_currency = 'AUD' @@ -47,21 +47,22 @@ def initialize(options = {}) # (default: "https://github.com/activemerchant/active_merchant") # # Returns an ActiveMerchant::Billing::Response object where authorization is the Transaction ID on success - def purchase(amount, payment_method, options={}) + def purchase(amount, payment_method, options = {}) params = {} add_metadata(params, options) add_invoice(params, amount, options) - add_customer_data(params, options) + add_customer_data(params, options, payment_method) add_credit_card(params, payment_method, options) + add_3ds_authenticated_data(params, options) if options[:three_d_secure] params['Method'] = payment_method.respond_to?(:number) ? 'ProcessPayment' : 'TokenPayment' commit(url_for('Transaction'), params) end - def authorize(amount, payment_method, options={}) + def authorize(amount, payment_method, options = {}) params = {} add_metadata(params, options) add_invoice(params, amount, options) - add_customer_data(params, options) + add_customer_data(params, options, payment_method) add_credit_card(params, payment_method, options) params['Method'] = 'Authorise' commit(url_for('Authorisation'), params) @@ -137,7 +138,7 @@ def store(payment_method, options = {}) params = {} add_metadata(params, options) add_invoice(params, 0, options) - add_customer_data(params, options) + add_customer_data(params, options, payment_method) add_credit_card(params, payment_method, options) params['Method'] = 'CreateTokenCustomer' commit(url_for('Transaction'), params) @@ -166,7 +167,7 @@ def update(customer_token, payment_method, options = {}) params = {} add_metadata(params, options) add_invoice(params, 0, options) - add_customer_data(params, options) + add_customer_data(params, options, payment_method) add_credit_card(params, payment_method, options) add_customer_token(params, customer_token) params['Method'] = 'UpdateTokenCustomer' @@ -197,6 +198,18 @@ def add_metadata(params, options) params end + def add_3ds_authenticated_data(params, options) + three_d_secure_options = options[:three_d_secure] + params['PaymentInstrument'] ||= {} if params['PaymentInstrument'].nil? + threed_secure_auth = params['PaymentInstrument']['ThreeDSecureAuth'] = {} + threed_secure_auth['Cryptogram'] = three_d_secure_options[:cavv] + threed_secure_auth['ECI'] = three_d_secure_options[:eci] + threed_secure_auth['XID'] = three_d_secure_options[:xid] + threed_secure_auth['AuthStatus'] = three_d_secure_options[:authentication_response_status] + threed_secure_auth['dsTransactionId'] = three_d_secure_options[:ds_transaction_id] + threed_secure_auth['Version'] = three_d_secure_options[:version] + end + def add_invoice(params, money, options, key = 'Payment') currency_code = options[:currency] || currency(money) params[key] = { @@ -204,7 +217,7 @@ def add_invoice(params, money, options, key = 'Payment') 'InvoiceReference' => truncate(options[:order_id], 50), 'InvoiceNumber' => truncate(options[:invoice] || options[:order_id], 12), 'InvoiceDescription' => truncate(options[:description], 64), - 'CurrencyCode' => currency_code, + 'CurrencyCode' => currency_code } end @@ -212,17 +225,48 @@ def add_reference(params, reference) params['TransactionID'] = reference end - def add_customer_data(params, options) - params['Customer'] ||= {} - add_address(params['Customer'], (options[:billing_address] || options[:address]), {:email => options[:email]}) - params['ShippingAddress'] = {} - add_address(params['ShippingAddress'], options[:shipping_address], {:skip_company => true}) + def add_customer_data(params, options, payment_method = nil) + add_customer_fields(params, options, payment_method) + add_shipping_fields(params, options) + end + + def add_customer_fields(params, options, payment_method) + key = 'Customer' + params[key] ||= {} + + customer_address = options[:billing_address] || options[:address] + + add_name_and_email(params[key], customer_address, options[:email], payment_method) + add_address(params[key], customer_address) + end + + def add_shipping_fields(params, options) + key = 'ShippingAddress' + params[key] = {} + + add_name_and_email(params[key], options[:shipping_address], options[:email]) + add_address(params[key], options[:shipping_address], { skip_company: true }) end - def add_address(params, address, options={}) + def add_name_and_email(params, address, email, payment_method = nil) + if address.present? + params['FirstName'], params['LastName'] = split_names(address[:name]) + elsif payment_method_name_available?(payment_method) + params['FirstName'] = payment_method.first_name + params['LastName'] = payment_method.last_name + end + + params['Email'] = email + end + + def payment_method_name_available?(payment_method) + payment_method.respond_to?(:first_name) && payment_method.respond_to?(:last_name) && + payment_method.first_name.present? && payment_method.last_name.present? + end + + def add_address(params, address, options = {}) return unless address - params['FirstName'], params['LastName'] = split_names(address[:name]) params['Title'] = address[:title] params['CompanyName'] = address[:company] unless options[:skip_company] params['Street1'] = truncate(address[:address1], 50) @@ -231,13 +275,13 @@ def add_address(params, address, options={}) params['State'] = address[:state] params['PostalCode'] = address[:zip] params['Country'] = address[:country].to_s.downcase - params['Phone'] = address[:phone] + params['Phone'] = address[:phone] || address[:phone_number] params['Fax'] = address[:fax] - params['Email'] = options[:email] end def add_credit_card(params, credit_card, options) return unless credit_card + params['Customer'] ||= {} if credit_card.respond_to? :number card_details = params['Customer']['CardDetails'] = {} @@ -273,13 +317,13 @@ def commit(url, params) succeeded, message_from(succeeded, raw), raw, - :authorization => authorization_from(raw), - :test => test?, - :avs_result => avs_result_from(raw), - :cvv_result => cvv_result_from(raw) + authorization: authorization_from(raw), + test: test?, + avs_result: avs_result_from(raw), + cvv_result: cvv_result_from(raw) ) rescue ActiveMerchant::ResponseError => e - return ActiveMerchant::Billing::Response.new(false, e.response.message, {:status_code => e.response.code}, :test => test?) + return ActiveMerchant::Billing::Response.new(false, e.response.message, { status_code: e.response.code }, test: test?) end def parse(data) @@ -299,7 +343,7 @@ def success?(response) end def parse_errors(message) - errors = message.split(',').collect{|code| MESSAGES[code.strip]}.flatten.join(',') + errors = message.split(',').collect { |code| MESSAGES[code.strip] }.flatten.join(',') errors.presence || message end @@ -325,15 +369,16 @@ def authorization_from(response) def avs_result_from(response) verification = response['Verification'] || {} - code = case verification['Address'] - when 'Valid' - 'M' - when 'Invalid' - 'N' - else - 'I' - end - {:code => code} + code = + case verification['Address'] + when 'Valid' + 'M' + when 'Invalid' + 'N' + else + 'I' + end + { code: } end def cvv_result_from(response) @@ -526,7 +571,7 @@ def cvv_result_from(response) 'V6150' => 'Invalid Refund Amount', 'V6151' => 'Refund amount greater than original transaction', 'V6152' => 'Original transaction already refunded for total amount', - 'V6153' => 'Card type not support by merchant', + 'V6153' => 'Card type not support by merchant' } end end diff --git a/lib/active_merchant/billing/gateways/exact.rb b/lib/active_merchant/billing/gateways/exact.rb index f72ba4247af..e4428cbe75e 100644 --- a/lib/active_merchant/billing/gateways/exact.rb +++ b/lib/active_merchant/billing/gateways/exact.rb @@ -1,42 +1,37 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class ExactGateway < Gateway self.live_url = self.test_url = 'https://secure2.e-xact.com/vplug-in/transaction/rpc-enc/service.asmx' API_VERSION = '8.5' - TEST_LOGINS = [ {:login => 'A00049-01', :password => 'test1'}, - {:login => 'A00427-01', :password => 'testus'} ] - - TRANSACTIONS = { :sale => '00', - :authorization => '01', - :capture => '32', - :credit => '34' } + TEST_LOGINS = [{ login: 'A00049-01', password: 'test1' }, + { login: 'A00427-01', password: 'testus' }] + TRANSACTIONS = { sale: '00', + authorization: '01', + capture: '32', + credit: '34' } ENVELOPE_NAMESPACES = { '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' - } + 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance' } SEND_AND_COMMIT_ATTRIBUTES = { 'xmlns:n1' => 'http://secure2.e-xact.com/vplug-in/transaction/rpc-enc/Request', - 'env:encodingStyle' => 'http://schemas.xmlsoap.org/soap/encoding/' - } + 'env:encodingStyle' => 'http://schemas.xmlsoap.org/soap/encoding/' } SEND_AND_COMMIT_SOURCE_ATTRIBUTES = { 'xmlns:n2' => 'http://secure2.e-xact.com/vplug-in/transaction/rpc-enc/encodedTypes', - 'xsi:type' => 'n2:Transaction' - } + 'xsi:type' => 'n2:Transaction' } POST_HEADERS = { 'soapAction' => 'http://secure2.e-xact.com/vplug-in/transaction/rpc-enc/SendAndCommit', - 'Content-Type' => 'text/xml' - } + 'Content-Type' => 'text/xml' } SUCCESS = 'true' - SENSITIVE_FIELDS = [ :verification_str2, :expiry_date, :card_number ] + SENSITIVE_FIELDS = %i[verification_str2 expiry_date card_number] - self.supported_cardtypes = [:visa, :master, :american_express, :jcb, :discover] - self.supported_countries = ['CA', 'US'] + self.supported_countries = %w[CA US] + self.supported_cardtypes = %i[visa master american_express jcb discover] self.homepage_url = 'http://www.e-xact.com' self.display_name = 'E-xact' @@ -161,19 +156,21 @@ def expdate(credit_card) end 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, - :test => test?, - :authorization => authorization_from(response), - :avs_result => { :code => response[:avs] }, - :cvv_result => response[:cvv2] - ) - + response = parse(ssl_post(self.live_url, build_request(action, request), POST_HEADERS)) + + Response.new( + successful?(response), + message_from(response), + response, + test: test?, + authorization: authorization_from(response), + avs_result: { code: response[:avs] }, + cvv_result: response[:cvv2] + ) rescue ResponseError => e case e.response.code when '401' - return Response.new(false, "Invalid Login: #{e.response.body}", {}, :test => test?) + return Response.new(false, "Invalid Login: #{e.response.body}", {}, test: test?) else raise end @@ -185,9 +182,9 @@ def successful?(response) def authorization_from(response) if response[:authorization_num] && response[:transaction_tag] - "#{response[:authorization_num]};#{response[:transaction_tag]}" + "#{response[:authorization_num]};#{response[:transaction_tag]}" else - '' + '' end end @@ -213,7 +210,7 @@ def parse(xml) parse_elements(response, root) end - response.delete_if{ |k,v| SENSITIVE_FIELDS.include?(k) } + response.delete_if { |k, _v| SENSITIVE_FIELDS.include?(k) } end def parse_elements(response, root) @@ -224,4 +221,3 @@ def parse_elements(response, root) end end end - diff --git a/lib/active_merchant/billing/gateways/ezic.rb b/lib/active_merchant/billing/gateways/ezic.rb index 15f81847ef4..480c3313cd8 100644 --- a/lib/active_merchant/billing/gateways/ezic.rb +++ b/lib/active_merchant/billing/gateways/ezic.rb @@ -5,17 +5,17 @@ class EzicGateway < Gateway self.supported_countries = %w(AU CA CN FR DE GI IL MT MU MX NL NZ PA PH RU SG KR ES KN GB US) self.default_currency = 'USD' - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :jcb, :diners_club] + self.supported_cardtypes = %i[visa master american_express discover jcb diners_club] self.homepage_url = 'http://www.ezic.com/' self.display_name = 'Ezic' - def initialize(options={}) + def initialize(options = {}) requires!(options, :account_id) super end - def purchase(money, payment, options={}) + def purchase(money, payment, options = {}) post = {} add_account_id(post) @@ -26,7 +26,7 @@ def purchase(money, payment, options={}) commit('S', post) end - def authorize(money, payment, options={}) + def authorize(money, payment, options = {}) post = {} add_account_id(post) @@ -37,7 +37,7 @@ def authorize(money, payment, options={}) commit('A', post) end - def capture(money, authorization, options={}) + def capture(money, authorization, options = {}) post = {} add_account_id(post) @@ -48,7 +48,7 @@ def capture(money, authorization, options={}) commit('D', post) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) post = {} add_account_id(post) @@ -59,7 +59,7 @@ def refund(money, authorization, options={}) commit('R', post) end - def void(authorization, options={}) + def void(authorization, options = {}) post = {} add_account_id(post) @@ -69,7 +69,7 @@ def void(authorization, options={}) commit('U', post) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -187,10 +187,9 @@ def post_data(parameters = {}) def headers { - 'User-Agent' => "ActiveMerchantBindings/#{ActiveMerchant::VERSION}", + 'User-Agent' => "ActiveMerchantBindings/#{ActiveMerchant::VERSION}" } end end - end end diff --git a/lib/active_merchant/billing/gateways/fat_zebra.rb b/lib/active_merchant/billing/gateways/fat_zebra.rb index aa64f4dad68..579ab9fdcf3 100644 --- a/lib/active_merchant/billing/gateways/fat_zebra.rb +++ b/lib/active_merchant/billing/gateways/fat_zebra.rb @@ -1,7 +1,7 @@ require 'json' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class FatZebraGateway < Gateway self.live_url = 'https://gateway.fatzebra.com.au/v1.0' self.test_url = 'https://gateway.sandbox.fatzebra.com.au/v1.0' @@ -9,7 +9,7 @@ class FatZebraGateway < Gateway self.supported_countries = ['AU'] self.default_currency = 'AUD' self.money_format = :cents - self.supported_cardtypes = [:visa, :master, :american_express, :jcb] + self.supported_cardtypes = %i[visa master american_express jcb] self.homepage_url = 'https://www.fatzebra.com.au/' self.display_name = 'Fat Zebra' @@ -27,6 +27,8 @@ def purchase(money, creditcard, options = {}) add_extra_options(post, options) add_order_id(post, options) add_ip(post, options) + add_metadata(post, options) + add_three_ds(post, options) commit(:post, 'purchases', post) end @@ -39,6 +41,8 @@ def authorize(money, creditcard, options = {}) add_extra_options(post, options) add_order_id(post, options) add_ip(post, options) + add_metadata(post, options) + add_three_ds(post, options) post[:capture] = false @@ -46,14 +50,17 @@ def authorize(money, creditcard, options = {}) end def capture(money, authorization, options = {}) + txn_id, = authorization.to_s.split('|') post = {} + add_amount(post, money, options) add_extra_options(post, options) - commit(:post, "purchases/#{CGI.escape(authorization)}/capture", post) + commit(:post, "purchases/#{CGI.escape(txn_id)}/capture", post) end - def refund(money, txn_id, options={}) + def refund(money, authorization, options = {}) + txn_id, = authorization.to_s.split('|') post = {} add_extra_options(post, options) @@ -64,9 +71,17 @@ def refund(money, txn_id, options={}) commit(:post, 'refunds', post) end - def store(creditcard, options={}) + def void(authorization, options = {}) + txn_id, endpoint = authorization.to_s.split('|') + + commit(:post, "#{endpoint}/void?id=#{txn_id}", {}) + end + + def store(creditcard, options = {}) post = {} + add_creditcard(post, creditcard) + post[:is_billing] = true if options[:recurring] commit(:post, 'credit_cards', post) end @@ -97,7 +112,8 @@ def add_creditcard(post, creditcard, options = {}) post[:cvv] = creditcard.verification_value if creditcard.verification_value? post[:card_holder] = creditcard.name if creditcard.name elsif creditcard.is_a?(String) - post[:card_token] = creditcard + id, = creditcard.to_s.split('|') + post[:card_token] = id post[:cvv] = options[:cvv] elsif creditcard.is_a?(Hash) ActiveMerchant.deprecated 'Passing the credit card as a Hash is deprecated. Use a String and put the (optional) CVV in the options hash instead.' @@ -111,14 +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] if options[:cavv] - extra[:xid] = options[:xid] if options[:xid] - extra[:sli] = options[:sli] if options[:sli] 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], + directory_server_txn_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 @@ -127,21 +171,27 @@ def add_ip(post, options) post[:customer_ip] = options[:ip] || '127.0.0.1' end - def commit(method, uri, parameters=nil) - response = begin - parse(ssl_request(method, get_url(uri), parameters.to_json, headers)) - rescue ResponseError => e - return Response.new(false, 'Invalid Login') if(e.response.code == '401') - parse(e.response.body) - end + def add_metadata(post, options) + post[:metadata] = options.fetch(:metadata, {}) + end + + def commit(method, uri, parameters = nil) + response = + begin + parse(ssl_request(method, get_url(uri), parameters.to_json, headers)) + rescue ResponseError => e + return Response.new(false, 'Invalid Login') if e.response.code == '401' + + parse(e.response.body) + end success = success_from(response) Response.new( success, message_from(response), response, - :test => response['test'], - :authorization => authorization_from(response, success) + test: response['test'], + authorization: authorization_from(response, success, uri) ) end @@ -149,13 +199,15 @@ def success_from(response) ( response['successful'] && response['response'] && - (response['response']['successful'] || response['response']['token']) + (response['response']['successful'] || response['response']['token'] || response['response']['response_code'] == '00') ) end - def authorization_from(response, success) + def authorization_from(response, success, uri) + endpoint = uri.split('/')[0] if success - (response['response']['id'] || response['response']['token']) + id = response['response']['id'] || response['response']['token'] + "#{id}|#{endpoint}" else nil end @@ -172,17 +224,15 @@ def message_from(response) end def parse(response) - begin - JSON.parse(response) - rescue JSON::ParserError - msg = 'Invalid JSON response received from Fat Zebra. Please contact support@fatzebra.com.au if you continue to receive this message.' - msg += " (The raw response returned by the API was #{response.inspect})" - { - 'successful' => false, - 'response' => {}, - 'errors' => [msg] - } - end + JSON.parse(response) + rescue JSON::ParserError + msg = 'Invalid JSON response received from Fat Zebra. Please contact support@fatzebra.com.au 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 get_url(uri) diff --git a/lib/active_merchant/billing/gateways/federated_canada.rb b/lib/active_merchant/billing/gateways/federated_canada.rb index d434f8307d2..ce7453b50a8 100644 --- a/lib/active_merchant/billing/gateways/federated_canada.rb +++ b/lib/active_merchant/billing/gateways/federated_canada.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class FederatedCanadaGateway < Gateway # Same URL for both test and live, testing is done by using the test username (demo) and password (password). self.live_url = self.test_url = 'https://secure.federatedgateway.com/api/transact.php' @@ -12,7 +12,7 @@ class FederatedCanadaGateway < Gateway self.default_currency = 'CAD' # The card types supported by the payment gateway - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] # The homepage URL of the gateway self.homepage_url = 'http://www.federatedcanada.com/' @@ -54,7 +54,7 @@ def void(authorization, options = {}) end def refund(money, authorization, options = {}) - commit('refund', money, options.merge(:transactionid => authorization)) + commit('refund', money, options.merge(transactionid: authorization)) end def credit(money, authorization, options = {}) @@ -121,11 +121,14 @@ def commit(action, money, parameters) response = parse(data) message = message_from(response) - Response.new(success?(response), message, response, - :test => test?, - :authorization => response['transactionid'], - :avs_result => {:code => response['avsresponse']}, - :cvv_result => response['cvvresponse'] + Response.new( + success?(response), + message, + response, + test: test?, + authorization: response['transactionid'], + avs_result: { code: response['avsresponse'] }, + cvv_result: response['cvvresponse'] ) end @@ -134,7 +137,7 @@ def success?(response) end def test? - (@options[:login].eql?('demo')) && (@options[:password].eql?('password')) + @options[:login].eql?('demo') && @options[:password].eql?('password') end def message_from(response) @@ -152,9 +155,8 @@ def post_data(action, parameters = {}) parameters[:type] = action parameters[:username] = @options[:login] parameters[:password] = @options[:password] - parameters.map{|k, v| "#{k}=#{CGI.escape(v.to_s)}"}.join('&') + parameters.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&') end end end end - diff --git a/lib/active_merchant/billing/gateways/finansbank.rb b/lib/active_merchant/billing/gateways/finansbank.rb index 144f798abfe..51c2c401d71 100644 --- a/lib/active_merchant/billing/gateways/finansbank.rb +++ b/lib/active_merchant/billing/gateways/finansbank.rb @@ -1,16 +1,16 @@ require 'active_merchant/billing/gateways/cc5' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class FinansbankGateway < CC5Gateway - self.live_url = 'https://www.fbwebpos.com/servlet/cc5ApiServer' + self.live_url = 'https://www.fbwebpos.com/servlet/cc5ApiServer' self.test_url = 'https://entegrasyon.asseco-see.com.tr/fim/api' # The countries the gateway supports merchants from as 2 digit ISO country codes - self.supported_countries = ['US', 'TR'] + self.supported_countries = %w[US TR] # The card types supported by the payment gateway - self.supported_cardtypes = [:visa, :master] + self.supported_cardtypes = %i[visa master] # The homepage URL of the gateway self.homepage_url = 'https://www.fbwebpos.com/' @@ -20,4 +20,3 @@ class FinansbankGateway < CC5Gateway end end end - diff --git a/lib/active_merchant/billing/gateways/first_giving.rb b/lib/active_merchant/billing/gateways/first_giving.rb index 3adb5ab8d85..5f3f9f1575c 100644 --- a/lib/active_merchant/billing/gateways/first_giving.rb +++ b/lib/active_merchant/billing/gateways/first_giving.rb @@ -1,13 +1,13 @@ require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class FirstGivingGateway < Gateway self.test_url = 'http://usapisandbox.fgdev.net' self.live_url = 'https://api.firstgiving.com' self.supported_countries = ['US'] - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'http://www.firstgiving.com/' self.default_currency = 'USD' self.display_name = 'FirstGiving' @@ -30,7 +30,7 @@ def purchase(money, creditcard, options = {}) def refund(money, identifier, options = {}) get = {} get[:transactionId] = identifier - get[:tranType] = 'REFUNDREQUEST' + get[:tranType] = 'REFUNDREQUEST' commit('/transaction/refundrequest?' + encode(get)) end @@ -49,7 +49,7 @@ def add_customer_data(post, options) end def add_address(post, options) - if(billing_address = (options[:billing_address] || options[:address])) + if (billing_address = (options[:billing_address] || options[:address])) post[:billToAddressLine1] = billing_address[:address1] post[:billToCity] = billing_address[:city] post[:billToState] = billing_address[:state] @@ -83,13 +83,14 @@ def parse(body) end element.children.each do |child| next if child.text? + response[child.name] = child.text end response end - def commit(action, post=nil) + def commit(action, post = nil) url = (test? ? self.test_url : self.live_url) + action begin @@ -107,7 +108,7 @@ def commit(action, post=nil) (response['friendlyErrorMessage'] || response['verboseErrorMessage'] || response['acknowledgement']), response, authorization: response['transactionId'], - test: test?, + test: test? ) end @@ -116,7 +117,7 @@ def post_data(post) end def encode(hash) - hash.collect{|(k,v)| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}"}.join('&') + hash.collect { |(k, v)| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join('&') end def creditcard_brand(brand) @@ -133,11 +134,10 @@ def creditcard_brand(brand) def headers { 'User-Agent' => "ActiveMerchantBindings/#{ActiveMerchant::VERSION}", - 'JG_APPLICATIONKEY' => "#{@options[:application_key]}", - 'JG_SECURITYTOKEN' => "#{@options[:security_token]}" + 'JG_APPLICATIONKEY' => @options[:application_key].to_s, + 'JG_SECURITYTOKEN' => @options[:security_token].to_s } end end end end - diff --git a/lib/active_merchant/billing/gateways/first_pay.rb b/lib/active_merchant/billing/gateways/first_pay.rb index b218557b4c7..ba375a18090 100644 --- a/lib/active_merchant/billing/gateways/first_pay.rb +++ b/lib/active_merchant/billing/gateways/first_pay.rb @@ -1,164 +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: +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 = [: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 - - 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[:recurringStartDate] = options[:recurring_start_date] if options[:recurring_start_date] - post[:recurringEndDate] = options[:recurring_end_date] if options[:recurring_end_date] - 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 unless doc.root.nil? - - 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), - 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 if msg - 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..daaa97d000b --- /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..c64c27fcf52 --- /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 58d18a49a52..cf0b1f93b62 100755 --- a/lib/active_merchant/billing/gateways/firstdata_e4.rb +++ b/lib/active_merchant/billing/gateways/firstdata_e4.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class FirstdataE4Gateway < Gateway # TransArmor support requires v11 or lower self.test_url = 'https://api.demo.globalgatewaye4.firstdata.com/transaction/v11' @@ -22,28 +22,28 @@ class FirstdataE4Gateway < Gateway SUCCESS = 'true' - SENSITIVE_FIELDS = [:verification_str2, :expiry_date, :card_number] + SENSITIVE_FIELDS = %i[verification_str2 expiry_date card_number] BRANDS = { - :visa => 'Visa', - :master => 'Mastercard', - :american_express => 'American Express', - :jcb => 'JCB', - :discover => 'Discover' + visa: 'Visa', + master: 'Mastercard', + american_express: 'American Express', + jcb: 'JCB', + discover: 'Discover' } - E4_BRANDS = BRANDS.merge({:mastercard => 'Mastercard'}) + E4_BRANDS = BRANDS.merge({ mastercard: 'Mastercard' }) DEFAULT_ECI = '07' self.supported_cardtypes = BRANDS.keys - self.supported_countries = ['CA', 'US'] + self.supported_countries = %w[CA US] self.default_currency = 'USD' self.homepage_url = 'http://www.firstdata.com' self.display_name = 'FirstData Global Gateway e4' STANDARD_ERROR_CODE_MAPPING = { - # Bank error codes: https://firstdata.zendesk.com/entries/471297-First-Data-Global-Gateway-e4-Bank-Response-Codes + # Bank error codes: https://firstdata.zendesk.com/entries/471297-First-Data-Global-Gateway-e4-Bank-Response-Codes '201' => STANDARD_ERROR_CODE[:incorrect_number], '531' => STANDARD_ERROR_CODE[:invalid_cvc], '503' => STANDARD_ERROR_CODE[:invalid_cvc], @@ -55,7 +55,7 @@ class FirstdataE4Gateway < Gateway '401' => STANDARD_ERROR_CODE[:call_issuer], '402' => STANDARD_ERROR_CODE[:call_issuer], '501' => STANDARD_ERROR_CODE[:pickup_card], - # Ecommerce error codes -- https://firstdata.zendesk.com/entries/451980-ecommerce-response-codes-etg-codes + # Ecommerce error codes -- https://firstdata.zendesk.com/entries/451980-ecommerce-response-codes-etg-codes '22' => STANDARD_ERROR_CODE[:invalid_number], '25' => STANDARD_ERROR_CODE[:invalid_expiry_date], '31' => STANDARD_ERROR_CODE[:incorrect_cvc], @@ -141,12 +141,12 @@ def supports_scrubbing? end def scrub(transcript) - transcript - .gsub(%r(().+()), '\1[FILTERED]\2') - .gsub(%r(().+()), '\1[FILTERED]\2') - .gsub(%r(().+())i, '\1[FILTERED]\2') - .gsub(%r(().+()), '\1[FILTERED]\2') - .gsub(%r((Card Number : ).*\d)i, '\1[FILTERED]') + transcript. + gsub(%r(().+()), '\1[FILTERED]\2'). + gsub(%r(().+()), '\1[FILTERED]\2'). + gsub(%r(().+())i, '\1[FILTERED]\2'). + gsub(%r(().+()), '\1[FILTERED]\2'). + gsub(%r((Card Number : ).*\d)i, '\1[FILTERED]') end def supports_network_tokenization? @@ -217,7 +217,7 @@ def add_transaction_type(xml, action) end def add_identification(xml, identification) - authorization_num, transaction_tag, _ = identification.split(';') + authorization_num, transaction_tag, = identification.split(';') xml.tag! 'Authorization_Num', authorization_num xml.tag! 'Transaction_Tag', transaction_tag @@ -245,23 +245,24 @@ def add_credit_card(xml, credit_card, options) end def add_credit_card_eci(xml, credit_card, options) - eci = if credit_card.is_a?(NetworkTokenizationCreditCard) && credit_card.source == :apple_pay && card_brand(credit_card) == 'discover' - # Discover requires any Apple Pay transaction, regardless of in-app - # or web, and regardless of the ECI contained in the PKPaymentToken, - # to have an ECI value explicitly of 04. - '04' - else - (credit_card.respond_to?(:eci) ? credit_card.eci : nil) || options[:eci] || DEFAULT_ECI - end + eci = + if credit_card.is_a?(NetworkTokenizationCreditCard) && credit_card.source == :apple_pay && card_brand(credit_card) == 'discover' + # Discover requires any Apple Pay transaction, regardless of in-app + # or web, and regardless of the ECI contained in the PKPaymentToken, + # to have an ECI value explicitly of 04. + '04' + else + (credit_card.respond_to?(:eci) ? credit_card.eci : nil) || options[:eci] || DEFAULT_ECI + end - xml.tag! 'Ecommerce_Flag', eci.to_s =~ /^[0-9]+$/ ? eci.to_s.rjust(2, '0') : eci + xml.tag! 'Ecommerce_Flag', /^[0-9]+$/.match?(eci.to_s) ? eci.to_s.rjust(2, '0') : eci end def add_credit_card_verification_strings(xml, credit_card, options) address = options[:billing_address] || options[:address] if address address_values = [] - [:address1, :zip, :city, :state, :country].each { |part| address_values << address[part].to_s } + %i[address1 zip city state country].each { |part| address_values << address[part].to_s.tr("\r\n", ' ').strip } xml.tag! 'VerificationStr1', address_values.join('|') end @@ -297,11 +298,12 @@ def add_card_authentication_data(xml, options) def add_credit_card_token(xml, store_authorization, options) params = store_authorization.split(';') credit_card = CreditCard.new( - :brand => params[1], - :first_name => params[2], - :last_name => params[3], - :month => params[4], - :year => params[5]) + brand: params[1], + first_name: params[2], + last_name: params[3], + month: params[4], + year: params[5] + ) xml.tag! 'TransarmorToken', params[0] xml.tag! 'Expiry_Date', expdate(credit_card) @@ -352,12 +354,15 @@ def commit(action, request, credit_card = nil) response = parse_error(e.response) end - 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) + 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) ) end @@ -394,7 +399,7 @@ def store_authorization_from(response, credit_card) credit_card.last_name, credit_card.month, credit_card.year - ].map { |value| value.to_s.gsub(/;/, '') }.join(';') + ].map { |value| value.to_s.delete(';') }.join(';') else raise StandardError, "TransArmor support is not enabled on your #{display_name} account" end @@ -406,9 +411,9 @@ def money_from_authorization(auth) end def message_from(response) - if(response[:faultcode] && response[:faultstring]) + if response[:faultcode] && response[:faultstring] response[:faultstring] - elsif(response[:error_number] && response[:error_number] != '0') + elsif response[:error_number] && response[:error_number] != '0' response[:error_description] else result = (response[:exact_message] || '') @@ -419,10 +424,10 @@ def message_from(response) def parse_error(error) { - :transaction_approved => 'false', - :error_number => error.code, - :error_description => error.body, - :ecommerce_error_code => error.body.gsub(/[^\d]/, '') + transaction_approved: 'false', + error_number: error.code, + error_description: error.body, + ecommerce_error_code: error.body.gsub(/[^\d]/, '') } end @@ -438,7 +443,7 @@ def parse(xml) parse_elements(response, root) end - response.delete_if{ |k,v| SENSITIVE_FIELDS.include?(k) } + response.delete_if { |k, _v| SENSITIVE_FIELDS.include?(k) } end def parse_elements(response, root) diff --git a/lib/active_merchant/billing/gateways/firstdata_e4_v27.rb b/lib/active_merchant/billing/gateways/firstdata_e4_v27.rb new file mode 100644 index 00000000000..4b7f3e9e67c --- /dev/null +++ b/lib/active_merchant/billing/gateways/firstdata_e4_v27.rb @@ -0,0 +1,509 @@ +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class FirstdataE4V27Gateway < Gateway + self.test_url = 'https://api.demo.globalgatewaye4.firstdata.com/transaction/v28' + self.live_url = 'https://api.globalgatewaye4.firstdata.com/transaction/v28' + + TRANSACTIONS = { + sale: '00', + authorization: '01', + verify: '05', + capture: '32', + void: '33', + credit: '34', + store: '05' + } + + SUCCESS = 'true' + + SENSITIVE_FIELDS = %i[cvdcode expiry_date card_number] + + BRANDS = { + visa: 'Visa', + master: 'Mastercard', + american_express: 'American Express', + jcb: 'JCB', + discover: 'Discover' + } + + DEFAULT_ECI = '07' + + self.supported_cardtypes = BRANDS.keys + self.supported_countries = %w[CA US] + self.default_currency = 'USD' + self.homepage_url = 'http://www.firstdata.com' + self.display_name = 'FirstData Global Gateway e4 v27' + + STANDARD_ERROR_CODE_MAPPING = { + # Bank error codes: https://support.payeezy.com/hc/en-us/articles/203730509-First-Data-Global-Gateway-e4-Bank-Response-Codes + '201' => STANDARD_ERROR_CODE[:incorrect_number], + '531' => STANDARD_ERROR_CODE[:invalid_cvc], + '503' => STANDARD_ERROR_CODE[:invalid_cvc], + '811' => STANDARD_ERROR_CODE[:invalid_cvc], + '605' => STANDARD_ERROR_CODE[:invalid_expiry_date], + '522' => STANDARD_ERROR_CODE[:expired_card], + '303' => STANDARD_ERROR_CODE[:card_declined], + '530' => STANDARD_ERROR_CODE[:card_declined], + '401' => STANDARD_ERROR_CODE[:call_issuer], + '402' => STANDARD_ERROR_CODE[:call_issuer], + '501' => STANDARD_ERROR_CODE[:pickup_card], + # Ecommerce error codes: https://support.payeezy.com/hc/en-us/articles/203730499-eCommerce-Response-Codes-ETG-e4-Transaction-Gateway-Codes + '22' => STANDARD_ERROR_CODE[:invalid_number], + '25' => STANDARD_ERROR_CODE[:invalid_expiry_date], + '31' => STANDARD_ERROR_CODE[:incorrect_cvc], + '44' => STANDARD_ERROR_CODE[:incorrect_zip], + '42' => STANDARD_ERROR_CODE[:processing_error] + } + + def initialize(options = {}) + requires!(options, :login, :password, :key_id, :hmac_key) + @options = options + + super + end + + def authorize(money, credit_card_or_store_authorization, options = {}) + commit(:authorization, build_sale_or_authorization_request(money, credit_card_or_store_authorization, options)) + end + + def purchase(money, credit_card_or_store_authorization, options = {}) + commit(:sale, build_sale_or_authorization_request(money, credit_card_or_store_authorization, options)) + end + + def capture(money, authorization, options = {}) + commit(:capture, build_capture_or_credit_request(money, authorization, options)) + end + + def void(authorization, options = {}) + commit(:void, build_capture_or_credit_request(money_from_authorization(authorization), authorization, options)) + end + + def refund(money, authorization, options = {}) + commit(:credit, build_capture_or_credit_request(money, authorization, options)) + end + + def verify(credit_card, options = {}) + commit(:verify, build_sale_or_authorization_request(0, credit_card, options)) + end + + # Tokenize a credit card with TransArmor + # + # The TransArmor token and other card data necessary for subsequent + # transactions is stored in the response's +authorization+ attribute. + # The authorization string may be passed to +authorize+ and +purchase+ + # instead of a +ActiveMerchant::Billing::CreditCard+ instance. + # + # TransArmor support must be explicitly activated on your gateway + # account by FirstData. If your authorization string is empty, contact + # FirstData support for account setup assistance. + # + # https://support.payeezy.com/hc/en-us/articles/203731189-TransArmor-Tokenization + def store(credit_card, options = {}) + commit(:store, build_store_request(credit_card, options), credit_card) + end + + def verify_credentials + response = void('0') + response.message != 'Unauthorized Request. Bad or missing credentials.' + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r(().+()), '\1[FILTERED]\2'). + gsub(%r(().+()), '\1[FILTERED]\2'). + gsub(%r(().+())i, '\1[FILTERED]\2'). + gsub(%r(().+()), '\1[FILTERED]\2'). + gsub(%r((CARD NUMBER\s+: )#+\d+), '\1[FILTERED]') + end + + def supports_network_tokenization? + true + end + + private + + def build_request(action, body) + xml = Builder::XmlMarkup.new + + xml.instruct! + xml.tag! 'Transaction', xmlns: 'http://secure2.e-xact.com/vplug-in/transaction/rpc-enc/encodedTypes' do + add_credentials(xml) + add_transaction_type(xml, action) + xml << body + end + + xml.target! + end + + def build_sale_or_authorization_request(money, credit_card_or_store_authorization, options) + xml = Builder::XmlMarkup.new + + add_amount(xml, money, options) + + if credit_card_or_store_authorization.is_a? String + add_credit_card_token(xml, credit_card_or_store_authorization, options) + else + add_credit_card(xml, credit_card_or_store_authorization, options) + add_stored_credentials(xml, credit_card_or_store_authorization, options) + end + + add_address(xml, options) + add_customer_data(xml, options) + add_invoice(xml, options) + add_tax_fields(xml, options) + add_level_3(xml, options) + + xml.target! + end + + def build_capture_or_credit_request(money, identification, options) + xml = Builder::XmlMarkup.new + + add_identification(xml, identification) + add_amount(xml, money, options) + add_customer_data(xml, options) + add_card_authentication_data(xml, options) + + xml.target! + end + + def build_store_request(credit_card, options) + xml = Builder::XmlMarkup.new + + add_credit_card(xml, credit_card, options) + add_address(xml, options) + add_customer_data(xml, options) + + xml.target! + end + + def add_credentials(xml) + xml.tag! 'ExactID', @options[:login] + xml.tag! 'Password', @options[:password] + end + + def add_transaction_type(xml, action) + xml.tag! 'Transaction_Type', TRANSACTIONS[action] + end + + def add_identification(xml, identification) + authorization_num, transaction_tag, = identification.split(';') + + xml.tag! 'Authorization_Num', authorization_num + xml.tag! 'Transaction_Tag', transaction_tag + end + + def add_amount(xml, money, options) + currency_code = options[:currency] || default_currency + xml.tag! 'DollarAmount', localized_amount(money, currency_code) + xml.tag! 'Currency', currency_code + end + + def add_credit_card(xml, credit_card, options) + if credit_card.respond_to?(:track_data) && credit_card.track_data.present? + xml.tag! 'Track1', credit_card.track_data + xml.tag! 'Ecommerce_Flag', 'R' + else + xml.tag! 'Card_Number', credit_card.number + xml.tag! 'Expiry_Date', expdate(credit_card) + xml.tag! 'CardHoldersName', credit_card.name + xml.tag! 'CardType', card_type(credit_card.brand) + + add_wallet_provider_id(xml, credit_card, options) + add_credit_card_eci(xml, credit_card, options) + add_credit_card_verification_strings(xml, credit_card, options) + end + end + + def add_credit_card_eci(xml, credit_card, options) + eci = if credit_card.is_a?(NetworkTokenizationCreditCard) && credit_card.source == :apple_pay && card_brand(credit_card) == 'discover' + # Payeezy requires an ECI of 5 for apple pay transactions + # See: https://support.payeezy.com/hc/en-us/articles/203730589-Ecommerce-Flag-Values + '05' + else + (credit_card.respond_to?(:eci) ? credit_card.eci : nil) || options[:eci] || DEFAULT_ECI + end + + xml.tag! 'Ecommerce_Flag', /^[0-9]+$/.match?(eci.to_s) ? eci.to_s.rjust(2, '0') : eci + end + + def add_credit_card_verification_strings(xml, credit_card, options) + if credit_card.is_a?(NetworkTokenizationCreditCard) + add_network_tokenization_credit_card(xml, credit_card) + else + if credit_card.verification_value? + xml.tag! 'CVD_Presence_Ind', '1' + xml.tag! 'CVDCode', credit_card.verification_value + end + + add_card_authentication_data(xml, options) + end + end + + def add_network_tokenization_credit_card(xml, credit_card) + case card_brand(credit_card).to_sym + when :american_express + cryptogram = Base64.decode64(credit_card.payment_cryptogram) + xml.tag!('XID', Base64.encode64(cryptogram[20...40])) + xml.tag!('CAVV', Base64.encode64(cryptogram[0...20])) + else + xml.tag!('XID', credit_card.transaction_id) if credit_card.transaction_id + xml.tag!('CAVV', credit_card.payment_cryptogram) + end + end + + def add_card_authentication_data(xml, options) + xml.tag! 'CAVV', options[:cavv] + xml.tag! 'XID', options[:xid] + end + + def add_credit_card_token(xml, store_authorization, options) + params = store_authorization.split(';') + credit_card = CreditCard.new( + brand: params[1], + first_name: params[2], + last_name: params[3], + month: params[4], + year: params[5] + ) + + xml.tag! 'TransarmorToken', params[0] + xml.tag! 'Expiry_Date', expdate(credit_card) + xml.tag! 'CardHoldersName', credit_card.name + xml.tag! 'CardType', card_type(credit_card.brand) + + add_wallet_provider_id(xml, credit_card, options) + add_card_authentication_data(xml, options) + end + + def add_wallet_provider_id(xml, credit_card, options) + provider_id = if options[:wallet_provider_id] + options[:wallet_provider_id] + elsif credit_card.is_a?(NetworkTokenizationCreditCard) && credit_card.source == :apple_pay + # See: https://support.payeezy.com/hc/en-us/articles/206601408-First-Data-Payeezy-Gateway-Web-Service-API-Reference-Guide#3.9 + 4 + end + + xml.tag! 'WalletProviderID', provider_id if provider_id + end + + def add_customer_data(xml, options) + xml.tag! 'Customer_Ref', options[:customer] if options[:customer] + xml.tag! 'Client_IP', options[:ip] if options[:ip] + xml.tag! 'Client_Email', options[:email] if options[:email] + end + + def add_address(xml, options) + if (address = options[:billing_address] || options[:address]) + address = strip_line_breaks(address) + + xml.tag! 'Address' do + xml.tag! 'Address1', address[:address1] + xml.tag! 'Address2', address[:address2] if address[:address2] + xml.tag! 'City', address[:city] + xml.tag! 'State', address[:state] + xml.tag! 'Zip', address[:zip] + xml.tag! 'CountryCode', address[:country] + end + xml.tag! 'ZipCode', address[:zip] + end + end + + def strip_line_breaks(address) + return unless address.is_a?(Hash) + + address.map { |k, s| [k, s&.tr("\r\n", ' ')&.strip] }.to_h + end + + def add_invoice(xml, options) + xml.tag! 'Reference_No', options[:order_id] + xml.tag! 'Reference_3', options[:description] if options[:description] + end + + def add_tax_fields(xml, options) + xml.tag! 'Tax1Amount', options[:tax1_amount] if options[:tax1_amount] + xml.tag! 'Tax1Number', options[:tax1_number] if options[:tax1_number] + end + + def add_level_3(xml, options) + xml.tag!('Level3') { |x| x << options[:level_3] } if options[:level_3] + end + + def add_stored_credentials(xml, card, options) + return unless options[:stored_credential] + + xml.tag! 'StoredCredentials' do + xml.tag! 'Indicator', stored_credential_indicator(xml, card, options) + if initiator = options.dig(:stored_credential, :initiator) + xml.tag! 'Initiation', initiator == 'merchant' ? 'M' : 'C' + end + if reason_type = options.dig(:stored_credential, :reason_type) + xml.tag! 'Schedule', reason_type == 'unscheduled' ? 'U' : 'S' + end + xml.tag! 'AuthorizationTypeOverride', options[:authorization_type_override] if options[:authorization_type_override] + if network_transaction_id = options[:stored_credential][:network_transaction_id] + xml.tag! 'TransactionId', network_transaction_id + else + xml.tag! 'TransactionId', 'new' + end + xml.tag! 'OriginalAmount', options[:original_amount] if options[:original_amount] + xml.tag! 'ProtectbuyIndicator', options[:protectbuy_indicator] if options[:protectbuy_indicator] + end + end + + def stored_credential_indicator(xml, card, options) + if card.brand == 'master' || options.dig(:stored_credential, :initial_transaction) == false + 'S' + else + '1' + end + end + + def expdate(credit_card) + "#{format(credit_card.month, :two_digits)}#{format(credit_card.year, :two_digits)}" + end + + def card_type(credit_card_brand) + BRANDS[credit_card_brand.to_sym] if credit_card_brand + end + + def commit(action, data, credit_card = nil) + url = (test? ? self.test_url : self.live_url) + request = build_request(action, data) + begin + response = parse(ssl_post(url, request, headers('POST', url, request))) + rescue ResponseError => e + response = parse_error(e.response) + end + + 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) + ) + end + + def headers(method, url, request) + content_type = 'application/xml' + content_digest = Digest::SHA1.hexdigest(request) + sending_time = Time.now.utc.iso8601 + payload = [method, content_type, content_digest, sending_time, url.split('.com')[1]].join("\n") + hmac = OpenSSL::HMAC.digest('sha1', @options[:hmac_key], payload) + encoded = Base64.strict_encode64(hmac) + + { + 'x-gge4-date' => sending_time, + 'x-gge4-content-sha1' => content_digest, + 'Authorization' => 'GGE4_API ' + @options[:key_id].to_s + ':' + encoded, + 'Accepts' => content_type, + 'Content-Type' => content_type + } + end + + def successful?(response) + response[:transaction_approved] == SUCCESS + end + + def response_authorization(action, response, credit_card) + if action == :store + store_authorization_from(response, credit_card) + else + authorization_from(response) + end + end + + def authorization_from(response) + if response[:authorization_num] && response[:transaction_tag] + [ + response[:authorization_num], + response[:transaction_tag], + (response[:dollar_amount].to_f * 100).round + ].join(';') + else + '' + end + end + + def store_authorization_from(response, credit_card) + if response[:transarmor_token].present? + [ + response[:transarmor_token], + credit_card.brand, + credit_card.first_name, + credit_card.last_name, + credit_card.month, + credit_card.year + ].map { |value| value.to_s.tr(';', '') }.join(';') + else + raise StandardError, "TransArmor support is not enabled on your #{display_name} account" + end + end + + def money_from_authorization(auth) + _, _, amount = auth.split(/;/, 3) + amount.to_i + end + + def message_from(response) + if response[:faultcode] && response[:faultstring] + response[:faultstring] + elsif response[:error_number] && response[:error_number] != '0' + response[:error_description] + else + result = (response[:exact_message] || '') + result << " - #{response[:bank_message]}" if response[:bank_message].present? + result + end + end + + def parse_error(error) + { + transaction_approved: 'false', + error_number: error.code, + error_description: error.body, + ecommerce_error_code: error.body.gsub(/[^\d]/, '') + } + end + + def standard_error_code(response) + STANDARD_ERROR_CODE_MAPPING[response[:bank_resp_code] || response[:ecommerce_error_code]] + end + + def parse(xml) + response = {} + xml = REXML::Document.new(xml) + + if (root = REXML::XPath.first(xml, '//TransactionResult')) + parse_elements(response, root) + end + + SENSITIVE_FIELDS.each { |key| response.delete(key) } + response + end + + def parse_elements(response, root) + root.elements.to_a.each do |node| + if node.has_elements? + parse_elements(response, node) + else + response[name_node(root, node)] = (node.text || '').strip + end + end + end + + def name_node(root, node) + parent = root.name unless root.name == 'TransactionResult' + "#{parent}#{node.name}".gsub(/EXact/, 'Exact').underscore.to_sym + end + end + end +end 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..b678c916563 --- /dev/null +++ b/lib/active_merchant/billing/gateways/flex_charge.rb @@ -0,0 +1,355 @@ +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.flexfactor.io/' + self.display_name = 'FlexCharge' + + ENDPOINTS_MAPPING = { + authenticate: 'oauth2/token', + purchase: 'evaluate', + sync: 'outcome', + refund: 'orders/%s/refund', + store: 'tokenize', + inquire: 'orders/%s', + capture: 'capture', + void: 'orders/%s/cancel' + } + + SUCCESS_MESSAGES = %w(APPROVED CHALLENGE SUBMITTED SUCCESS PROCESSING CAPTUREREQUIRED).freeze + + NO_ERROR_KEYS = %w(TraceId access_token token_expires).freeze + + def initialize(options = {}) + requires!(options, :app_key, :app_secret, :site_id, :mid) + super + end + + def purchase(money, credit_card, options = {}) + post = { transactionType: options.fetch(:transactionType, 'Purchase') } + + 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), options) + add_address(post, credit_card, address(options), :billingInformation) + add_address(post, credit_card, options[:shipping_address], :shippingInformation) + add_customer_data(post, options) + add_three_ds(post, options) + add_metadata(post, options) + + commit(:purchase, post) + end + + def authorize(money, credit_card, options = {}) + options[:transactionType] = 'Authorization' + purchase(money, credit_card, options) + end + + def capture(money, authorization, options = {}) + order_id, currency = authorization.split('#') + post = { + idempotencyKey: options[:idempotency_key] || SecureRandom.uuid, + orderId: order_id, + amount: money, + currency: + } + + commit(:capture, post, authorization) + end + + def refund(money, authorization, options = {}) + order_id, _currency = authorization.split('#') + self.money_format = :dollars + commit(:refund, { amountToRefund: localized_amount(money, 2).to_f }, order_id) + end + + def void(authorization, options = {}) + order_id, _currency = authorization.split('#') + commit(:void, {}, order_id) + end + + def store(credit_card, options = {}) + first_name, last_name = names_from_address(address(options), credit_card) + + post = { + payment_method: { + credit_card: { + first_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 = {}) + order_id, _currency = authorization.split('#') + commit(:inquire, {}, order_id, :get) + end + + def add_metadata(post, options) + post[:Source] = 'Spreedly' + post[:ExtraData] = options[:extra_data] if options[:extra_data].present? + end + + private + + def address(options) + options[:billing_address] || options[:address] || {} + end + + 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] || SecureRandom.uuid + post[:senseKey] = options[:sense_key] + 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, address_type) + return unless address.present? + + first_name, last_name = names_from_address(address, payment) + + post[address_type] = { + 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 } + when CreditCard + if credit_card.number + { + 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 + } + else + {} + end + end + post[:paymentMethod] = payment_method.compact + end + + def names_from_address(address, payment_method) + split_names(address[:name]).tap do |names| + names[0] = payment_method&.first_name unless names[0].present? || payment_method.is_a?(String) + names[1] = payment_method&.last_name unless names[1].present? || payment_method.is_a?(String) + 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 + success = response[:accessToken].present? + Response.new( + success, + message_from(response, success), + 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) + body = '{}' if body.blank? + + 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) + success = success_from(action, response) + Response.new( + success, + message_from(response, success), + response, + authorization: authorization_from(action, response, post), + test: test?, + error_code: error_code_from(success, 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, false), 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]) + when :void then response.empty? + else + response[:success] && SUCCESS_MESSAGES.include?(response[:status]) + end + end + + def message_from(response, success_status) + return extract_error(response) unless success_status || response['TraceId'].nil? + + response[:title] || response[:responseMessage] || response[:statusName] || response[:status] + end + + def authorization_from(action, response, options) + if action == :store + response.dig(:transaction, :payment_method, :token) + elsif success_from(action, response) + [response[:orderId], options[:currency] || default_currency].compact.join('#') + end + end + + def error_code_from(success, response) + (response[:statusName] || response[:status]) unless success + end + + def cast_bool(value) + ![false, 0, '', '0', 'f', 'F', 'false', 'FALSE'].include?(value) + end + + def extract_error(response) + response.except(*NO_ERROR_KEYS).to_json + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/flo2cash.rb b/lib/active_merchant/billing/gateways/flo2cash.rb index 83bdb83f50f..a6ffba7f99e 100644 --- a/lib/active_merchant/billing/gateways/flo2cash.rb +++ b/lib/active_merchant/billing/gateways/flo2cash.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class Flo2cashGateway < Gateway self.display_name = 'Flo2Cash' self.homepage_url = 'http://www.flo2cash.co.nz/' @@ -10,7 +10,7 @@ class Flo2cashGateway < Gateway self.supported_countries = ['NZ'] self.default_currency = 'NZD' self.money_format = :dollars - self.supported_cardtypes = [:visa, :master, :american_express, :diners_club] + self.supported_cardtypes = %i[visa master american_express diners_club] BRAND_MAP = { 'visa' => 'VISA', @@ -19,19 +19,19 @@ class Flo2cashGateway < Gateway 'diners_club' => 'DINERS' } - def initialize(options={}) + def initialize(options = {}) requires!(options, :username, :password, :account_id) super end - def purchase(amount, payment_method, options={}) + def purchase(amount, payment_method, options = {}) MultiResponse.run do |r| r.process { authorize(amount, payment_method, options) } r.process { capture(amount, r.authorization, options) } end end - def authorize(amount, payment_method, options={}) + def authorize(amount, payment_method, options = {}) post = {} add_invoice(post, amount, options) add_payment_method(post, payment_method) @@ -40,7 +40,7 @@ def authorize(amount, payment_method, options={}) commit('ProcessAuthorise', post) end - def capture(amount, authorization, options={}) + def capture(amount, authorization, options = {}) post = {} add_invoice(post, amount, options) add_reference(post, authorization) @@ -49,7 +49,7 @@ def capture(amount, authorization, options={}) commit('ProcessCapture', post) end - def refund(amount, authorization, options={}) + def refund(amount, authorization, options = {}) post = {} add_invoice(post, amount, options) add_reference(post, authorization) @@ -71,7 +71,7 @@ def scrub(transcript) private - CURRENCY_CODES = Hash.new{|h,k| raise ArgumentError.new("Unsupported currency: #{k}")} + CURRENCY_CODES = Hash.new { |_h, k| raise ArgumentError.new("Unsupported currency: #{k}") } CURRENCY_CODES['NZD'] = '554' def add_invoice(post, money, options) @@ -89,7 +89,7 @@ def add_payment_method(post, payment_method) end def add_customer_data(post, options) - if(billing_address = (options[:billing_address] || options[:address])) + if (billing_address = (options[:billing_address] || options[:address])) post[:Email] = billing_address[:email] end end @@ -107,7 +107,7 @@ def commit(action, post) begin raw = parse(ssl_post(url, data, headers(action)), action) rescue ActiveMerchant::ResponseError => e - if(e.response.code == '500' && e.response.body.start_with?(' authorization_from(action, raw[:transaction_id], post[:OriginalTransactionId]), - :error_code => error_code_from(succeeded, raw), - :test => test? + authorization: authorization_from(action, raw[:transaction_id], post[:OriginalTransactionId]), + error_code: error_code_from(succeeded, raw), + test: test? ) end @@ -133,7 +133,7 @@ def headers(action) end def build_request(action, post) - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 post.each do |field, value| xml.tag!(field, value) end @@ -142,16 +142,16 @@ def build_request(action, post) end def envelope_wrap(action, body) - <<-EOS - - - - <#{action} xmlns="http://www.flo2cash.co.nz/webservices/paymentwebservice"> - #{body} - - - - EOS + <<~XML + + + + <#{action} xmlns="http://www.flo2cash.co.nz/webservices/paymentwebservice"> + #{body} + + + + XML end def url @@ -172,7 +172,7 @@ def parse(body, action) def parse_element(response, node) if node.has_elements? - node.elements.each{|element| parse_element(response, element) } + node.elements.each { |element| parse_element(response, element) } else response[node.name.underscore.to_sym] = node.text end @@ -204,7 +204,7 @@ def authorization_from(action, current, original) 'Bank Declined Transaction' => STANDARD_ERROR_CODE[:card_declined], 'Insufficient Funds' => STANDARD_ERROR_CODE[:card_declined], 'Transaction Declined - Bank Error' => STANDARD_ERROR_CODE[:processing_error], - 'No Reply from Bank' => STANDARD_ERROR_CODE[:processing_error], + 'No Reply from Bank' => STANDARD_ERROR_CODE[:processing_error] } def error_code_from(succeeded, response) diff --git a/lib/active_merchant/billing/gateways/flo2cash_simple.rb b/lib/active_merchant/billing/gateways/flo2cash_simple.rb index f0662ff463c..88ab86294a8 100644 --- a/lib/active_merchant/billing/gateways/flo2cash_simple.rb +++ b/lib/active_merchant/billing/gateways/flo2cash_simple.rb @@ -1,9 +1,9 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class Flo2cashSimpleGateway < Flo2cashGateway self.display_name = 'Flo2Cash Simple' - def purchase(amount, payment_method, options={}) + def purchase(amount, payment_method, options = {}) post = {} add_invoice(post, amount, options) add_payment_method(post, payment_method) diff --git a/lib/active_merchant/billing/gateways/forte.rb b/lib/active_merchant/billing/gateways/forte.rb index 12b2e6e3308..6308d2ade9a 100644 --- a/lib/active_merchant/billing/gateways/forte.rb +++ b/lib/active_merchant/billing/gateways/forte.rb @@ -1,7 +1,7 @@ require 'json' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class ForteGateway < Gateway include Empty @@ -10,41 +10,45 @@ class ForteGateway < Gateway self.supported_countries = ['US'] self.default_currency = 'USD' - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'https://www.forte.net' self.display_name = 'Forte' - def initialize(options={}) + def initialize(options = {}) requires!(options, :api_key, :secret, :location_id, :account_id) super end - def purchase(money, payment_method, options={}) + def purchase(money, payment_method, options = {}) post = {} add_amount(post, money, options) + add_service_fee(post, options) add_invoice(post, options) - add_payment_method(post, payment_method) + add_payment_method(post, payment_method, options) add_billing_address(post, payment_method, options) add_shipping_address(post, options) + add_xdata(post, options) post[:action] = 'sale' commit(:post, post) end - def authorize(money, payment_method, options={}) + def authorize(money, payment_method, options = {}) post = {} add_amount(post, money, options) + add_service_fee(post, options) add_invoice(post, options) - add_payment_method(post, payment_method) + add_payment_method(post, payment_method, options) add_billing_address(post, payment_method, options) add_shipping_address(post, options) + add_xdata(post, options) post[:action] = 'authorize' commit(:post, post) end - def capture(money, authorization, options={}) + def capture(money, authorization, options = {}) post = {} post[:transaction_id] = transaction_id_from(authorization) post[:authorization_code] = authorization_code_from(authorization) || '' @@ -53,18 +57,18 @@ def capture(money, authorization, options={}) commit(:put, post) end - def credit(money, payment_method, options={}) + def credit(money, payment_method, options = {}) post = {} add_amount(post, money, options) add_invoice(post, options) - add_payment_method(post, payment_method) + add_payment_method(post, payment_method, options) add_billing_address(post, payment_method, options) post[:action] = 'disburse' commit(:post, post) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) post = {} add_amount(post, money, options) post[:original_transaction_id] = transaction_id_from(authorization) @@ -74,7 +78,7 @@ def refund(money, authorization, options={}) commit(:post, post) end - def void(authorization, options={}) + def void(authorization, options = {}) post = {} post[:transaction_id] = transaction_id_from(authorization) post[:authorization_code] = authorization_code_from(authorization) @@ -83,7 +87,7 @@ def void(authorization, options={}) commit(:put, post) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -116,6 +120,20 @@ def add_amount(post, money, options) post[:authorization_amount] = amount(money) end + def add_service_fee(post, options) + post[:service_fee_amount] = options[:service_fee_amount] if options[:service_fee_amount] + end + + def add_xdata(post, options) + post[:xdata] = {} + if xdata = options[:xdata] + (1..9).each do |n| + field = "xdata_#{n}".to_sym + post[:xdata][field] = xdata[field] if xdata[field] + end + end + end + def add_billing_address(post, payment, options) post[:billing_address] = {} if address = options[:billing_address] || options[:address] @@ -130,17 +148,14 @@ def add_billing_address(post, payment, options) post[:billing_address][:physical_address][:locality] = address[:city] if address[:city] end - if empty?(post[:billing_address][:first_name]) && payment.first_name - post[:billing_address][:first_name] = payment.first_name - end + post[:billing_address][:first_name] = payment.first_name if empty?(post[:billing_address][:first_name]) && payment.first_name - if empty?(post[:billing_address][:last_name]) && payment.last_name - post[:billing_address][:last_name] = payment.last_name - end + post[:billing_address][:last_name] = payment.last_name if empty?(post[:billing_address][:last_name]) && payment.last_name end def add_shipping_address(post, options) return unless options[:shipping_address] + address = options[:shipping_address] post[:shipping_address] = {} @@ -154,21 +169,22 @@ def add_shipping_address(post, options) post[:shipping_address][:physical_address][:locality] = address[:city] if address[:city] end - def add_payment_method(post, payment_method) + def add_payment_method(post, payment_method, options) if payment_method.respond_to?(:brand) add_credit_card(post, payment_method) else - add_echeck(post, payment_method) + add_echeck(post, payment_method, options) end end - def add_echeck(post, payment) + def add_echeck(post, payment, options) post[:echeck] = {} post[:echeck][:account_holder] = payment.name post[:echeck][:account_number] = payment.account_number post[:echeck][:routing_number] = payment.routing_number post[:echeck][:account_type] = payment.account_type post[:echeck][:check_number] = payment.number + post[:echeck][:sec_code] = options[:sec_code] || 'PPD' end def add_credit_card(post, payment) @@ -191,7 +207,7 @@ def commit(type, parameters) success_from(response), message_from(response), response, - authorization: authorization_from(response), + authorization: authorization_from(response, parameters), avs_result: AVSResult.new(code: response['response']['avs_result']), cvv_result: CVVResult.new(response['response']['cvv_code']), test: test? @@ -219,8 +235,12 @@ def message_from(response) response['response']['response_desc'] end - def authorization_from(response) - [response.try(:[], 'transaction_id'), response.try(:[], 'response').try(:[], 'authorization_code')].join('#') + def authorization_from(response, parameters) + if parameters[:action] == 'capture' + [response['transaction_id'], response.dig('response', 'authorization_code'), parameters[:transaction_id], parameters[:authorization_code]].join('#') + else + [response['transaction_id'], response.dig('response', 'authorization_code')].join('#') + end end def endpoint @@ -253,13 +273,13 @@ def split_authorization(authorization) end def authorization_code_from(authorization) - _, authorization_code = split_authorization(authorization) - authorization_code + _, authorization_code, _, original_auth_authorization_code = split_authorization(authorization) + original_auth_authorization_code.present? ? original_auth_authorization_code : authorization_code end def transaction_id_from(authorization) - transaction_id, _ = split_authorization(authorization) - transaction_id + transaction_id, _, original_auth_transaction_id, = split_authorization(authorization) + original_auth_transaction_id.present? ? original_auth_transaction_id : transaction_id end end end diff --git a/lib/active_merchant/billing/gateways/fortis.rb b/lib/active_merchant/billing/gateways/fortis.rb new file mode 100644 index 00000000000..9590ee66a25 --- /dev/null +++ b/lib/active_merchant/billing/gateways/fortis.rb @@ -0,0 +1,347 @@ +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class FortisGateway < Gateway + self.test_url = 'https://api.sandbox.fortis.tech/v1' + self.live_url = 'https://api.fortis.tech/v1' + + self.supported_countries = %w{US CA} + self.default_currency = 'USD' + self.supported_cardtypes = %i[visa master american_express discover jbc unionpay] + self.money_format = :cents + self.homepage_url = 'https://fortispay.com' + self.display_name = 'Fortis' + + STATUS_MAPPING = { + 101 => 'Sale cc Approved', + 102 => 'Sale cc AuthOnly', + 111 => 'Refund cc Refunded', + 121 => 'Credit/Debit/Refund cc AvsOnly', + 131 => 'Credit/Debit/Refund ach Pending Origination', + 132 => 'Credit/Debit/Refund ach Originating', + 133 => 'Credit/Debit/Refund ach Originated', + 134 => 'Credit/Debit/Refund ach Settled', + 191 => 'Settled (deprecated - batches are now settled on the /v2/transactionbatches endpoint)', + 201 => 'All cc/ach Voided', + 301 => 'All cc/ach Declined', + 331 => 'Credit/Debit/Refund ach Charged Back' + } + + REASON_MAPPING = { + 0 => 'N/A', + 1000 => 'CC - Approved / ACH - Accepted', + 1001 => 'AuthCompleted', + 1002 => 'Forced', + 1003 => 'AuthOnly Declined', + 1004 => 'Validation Failure (System Run Trx)', + 1005 => 'Processor Response Invalid', + 1200 => 'Voided', + 1201 => 'Partial Approval', + 1240 => 'Approved, optional fields are missing (Paya ACH only)', + 1301 => 'Account Deactivated for Fraud', + 1500 => 'Generic Decline', + 1510 => 'Call', + 1518 => 'Transaction Not Permitted - Terminal', + 1520 => 'Pickup Card', + 1530 => 'Retry Trx', + 1531 => 'Communication Error', + 1540 => 'Setup Issue, contact Support', + 1541 => 'Device is not signature capable', + 1588 => 'Data could not be de-tokenized', + 1599 => 'Other Reason', + 1601 => 'Generic Decline', + 1602 => 'Call', + 1603 => 'No Reply', + 1604 => 'Pickup Card - No Fraud', + 1605 => 'Pickup Card - Fraud', + 1606 => 'Pickup Card - Lost', + 1607 => 'Pickup Card - Stolen', + 1608 => 'Account Error', + 1609 => 'Already Reversed', + 1610 => 'Bad PIN', + 1611 => 'Cashback Exceeded', + 1612 => 'Cashback Not Available', + 1613 => 'CID Error', + 1614 => 'Date Error', + 1615 => 'Do Not Honor', + 1616 => 'NSF', + 1618 => 'Invalid Service Code', + 1619 => 'Exceeded activity limit', + 1620 => 'Violation', + 1621 => 'Encryption Error', + 1622 => 'Card Expired', + 1623 => 'Renter', + 1624 => 'Security Violation', + 1625 => 'Card Not Permitted', + 1626 => 'Trans Not Permitted', + 1627 => 'System Error', + 1628 => 'Bad Merchant ID', + 1629 => 'Duplicate Batch (Already Closed)', + 1630 => 'Batch Rejected', + 1631 => 'Account Closed', + 1632 => 'PIN tries exceeded', + 1640 => 'Required fields are missing (ACH only)', + 1641 => 'Previously declined transaction (1640)', + 1650 => 'Contact Support', + 1651 => 'Max Sending - Throttle Limit Hit (ACH only)', + 1652 => 'Max Attempts Exceeded', + 1653 => 'Contact Support', + 1654 => 'Voided - Online Reversal Failed', + 1655 => 'Decline (AVS Auto Reversal)', + 1656 => 'Decline (CVV Auto Reversal)', + 1657 => 'Decline (Partial Auth Auto Reversal)', + 1658 => 'Expired Authorization', + 1659 => 'Declined - Partial Approval not Supported', + 1660 => 'Bank Account Error, please delete and re-add Token', + 1661 => 'Declined AuthIncrement', + 1662 => 'Auto Reversal - Processor cant settle', + 1663 => 'Manager Needed (Needs override transaction)', + 1664 => 'Token Not Found: Sharing Group Unavailable', + 1665 => 'Contact Not Found: Sharing Group Unavailable', + 1666 => 'Amount Error', + 1667 => 'Action Not Allowed in Current State', + 1668 => 'Original Authorization Not Valid', + 1701 => 'Chip Reject', + 1800 => 'Incorrect CVV', + 1801 => 'Duplicate Transaction', + 1802 => 'MID/TID Not Registered', + 1803 => 'Stop Recurring', + 1804 => 'No Transactions in Batch', + 1805 => 'Batch Does Not Exist' + } + + def initialize(options = {}) + requires!(options, :user_id, :user_api_key, :developer_id) + super + end + + def authorize(money, payment, options = {}) + commit path(:authorize, payment_type(payment)), auth_purchase_request(money, payment, options) + end + + def purchase(money, payment, options = {}) + commit path(:purchase, payment_type(payment)), auth_purchase_request(money, payment, options) + end + + def capture(money, authorization, options = {}) + commit path(:capture, authorization), { transaction_amount: money }, :patch + end + + def void(authorization, options = {}) + commit path(:void, authorization), {}, :put + end + + def refund(money, authorization, options = {}) + commit path(:refund, authorization), { transaction_amount: money }, :patch + end + + def credit(money, payment, options = {}) + commit path(:credit), auth_purchase_request(money, payment, options) + end + + def store(payment, options = {}) + post = {} + add_payment(post, payment, include_cvv: false) + add_address(post, payment, options) + + commit path(:store), post, :post, options + end + + def unstore(authorization, options = {}) + commit path(:unstore, authorization), nil, :delete + end + + def verify(credit_card, options = {}) + MultiResponse.run(:use_first_response) do |r| + r.process { authorize(100, credit_card, options) } + r.process(:ignore_result) { void(r.authorization, options) } + end + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(/(\\?"account_number\\?":\\?")\d+/, '\1[FILTERED]'). + gsub(/(\\?"cvv\\?":\\?")\d+/, '\1[FILTERED]'). + gsub(%r((user-id: )[\w =]+), '\1[FILTERED]'). + gsub(%r((user-api-key: )[\w =]+), '\1[FILTERED]'). + gsub(%r((developer-id: )[\w =]+), '\1[FILTERED]') + end + + private + + def path(action, value = '') + { + authorize: '/transactions/cc/auth-only/{placeholder}', + purchase: '/transactions/cc/sale/{placeholder}', + capture: '/transactions/{placeholder}/auth-complete', + void: '/transactions/{placeholder}/void', + refund: '/transactions/{placeholder}/refund', + credit: '/transactions/cc/refund/keyed', + store: '/tokens/cc', + unstore: '/tokens/{placeholder}' + }[action]&.gsub('{placeholder}', value.to_s.split('|').first || '') + end + + def payment_type(payment) + if payment.is_a?(String) + payment.split('|').last == 'txn' ? 'prev-trxn' : 'token' + else + 'keyed' + end + end + + def auth_purchase_request(money, payment, options = {}) + {}.tap do |post| + add_invoice(post, money, options) + add_payment(post, payment) + add_address(post, payment, options) + end + end + + def add_address(post, payment_method, options) + address = address_from_options(options) + return unless address.present? + + post[:billing_address] = { + postal_code: address[:zip], + street: address[:address1], + city: address[:city], + state: address[:state], + phone: address[:phone], + country: lookup_country_code(address[:country]) + }.compact + end + + def address_from_options(options) + options[:billing_address] || options[:address] || {} + end + + def lookup_country_code(country_field) + return unless country_field.present? + + country_code = Country.find(country_field) + country_code&.code(:alpha3)&.value + end + + def add_invoice(post, money, options) + post[:transaction_amount] = amount(money) + post[:order_number] = options[:order_id] + post[:transaction_api_id] = options[:order_id] + post[:notification_email_address] = options[:email] + end + + def add_payment(post, payment, include_cvv: true) + case payment + when CreditCard + post[:account_number] = payment.number + post[:exp_date] = expdate(payment) + post[:cvv] = payment.verification_value if include_cvv + post[:account_holder_name] = payment.name + when String + key = { 'prev-trxn' => :previous_transaction_id, 'token' => :token_id }[payment_type(payment)] + post[key] = payment.split('|').first + end + end + + def parse(body) + JSON.parse(body).with_indifferent_access + rescue JSON::ParserError, TypeError => e + { + errors: body, + status: 'Unable to parse JSON response', + message: e.message + }.with_indifferent_access + end + + def request_headers + CaseSensitiveHeaders.new.reverse_merge!({ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + 'user-id' => @options[:user_id], + 'user-api-key' => @options[:user_api_key], + 'developer-id' => @options[:developer_id] + }) + end + + def add_location_id(post, options) + post[:location_id] = @options[:location_id] || options[:location_id] + end + + def commit(path, post, method = :post, options = {}) + add_location_id(post, options) if post.present? + + http_code, raw_response = ssl_request(method, url(path), post&.compact&.to_json, request_headers) + response = parse(raw_response) + + Response.new( + success_from(http_code, response), + message_from(response), + response, + authorization: authorization_from(response, token?(path)), + avs_result: AVSResult.new(code: response.dig(:data, :avs_enhanced)), + cvv_result: CVVResult.new(response.dig(:data, :cvv_response)), + test: test?, + error_code: error_code_from(http_code, response) + ) + rescue ResponseError => e + response = parse(e.response.body) + Response.new(false, message_from(response), response, test: test?) + end + + def handle_response(response) + case response.code.to_i + when 200...300 + return response.code.to_i, response.body + else + raise ResponseError.new(response) + end + end + + def url(path) + "#{test? ? test_url : live_url}#{path}" + end + + def success_from(http_code, response) + return true if http_code == 204 + return response[:data][:active] == true if response[:type] == 'Token' + return false if response.dig(:data, :status_code) == 301 + + STATUS_MAPPING[response.dig(:data, :status_code)].present? + end + + def message_from(response) + return '' if response.blank? + + response[:type] == 'Error' ? error_message_from(response) : success_message_from(response) + end + + def error_message_from(response) + response[:detail] || response[:title] + end + + def success_message_from(response) + response.dig(:data, :verbiaje) || get_reason_description_from(response) || STATUS_MAPPING[response.dig(:data, :status_code)] || response.dig(:data, :status_code) + end + + def get_reason_description_from(response) + code_id = response.dig(:data, :reason_code_id) + REASON_MAPPING[code_id] || ((1302..1399).include?(code_id) ? 'Reserved for Future Fraud Reason Codes' : nil) + end + + def authorization_from(response, is_token) + "#{response.dig(:data, :id)}|#{is_token ? 'token' : 'txn'}" + end + + def token?(path) + path.match?(/tokens/) + end + + def error_code_from(http_code, response) + [response.dig(:data, :status_code), response.dig(:data, :reason_code_id)].compact.join(' - ') unless success_from(http_code, response) + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/garanti.rb b/lib/active_merchant/billing/gateways/garanti.rb index cd567c73d6c..ec9f3cfc6d9 100644 --- a/lib/active_merchant/billing/gateways/garanti.rb +++ b/lib/active_merchant/billing/gateways/garanti.rb @@ -1,14 +1,14 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class GarantiGateway < Gateway self.live_url = 'https://sanalposprov.garanti.com.tr/VPServlet' self.test_url = 'https://sanalposprovtest.garanti.com.tr/VPServlet' # The countries the gateway supports merchants from as 2 digit ISO country codes - self.supported_countries = ['US','TR'] + self.supported_countries = %w[US TR] # The card types supported by the payment gateway - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] # The homepage URL of the gateway self.homepage_url = 'https://sanalposweb.garanti.com.tr' @@ -30,24 +30,23 @@ class GarantiGateway < Gateway 'JPY' => 392 } - def initialize(options = {}) requires!(options, :login, :password, :terminal_id, :merchant_id) super end def purchase(money, credit_card, options = {}) - options = options.merge(:gvp_order_type => 'sales') + options = options.merge(gvp_order_type: 'sales') commit(money, build_sale_request(money, credit_card, options)) end def authorize(money, credit_card, options = {}) - options = options.merge(:gvp_order_type => 'preauth') + options = options.merge(gvp_order_type: 'preauth') commit(money, build_authorize_request(money, credit_card, options)) end def capture(money, ref_id, options = {}) - options = options.merge(:gvp_order_type => 'postauth') + options = options.merge(gvp_order_type: 'postauth') commit(money, build_capture_request(money, ref_id, options)) end @@ -67,8 +66,8 @@ def build_xml_request(money, credit_card, options, &block) card_number = credit_card.respond_to?(:number) ? credit_card.number : '' hash_data = generate_hash_data(format_order_id(options[:order_id]), @options[:terminal_id], card_number, amount(money), security_data) - xml = Builder::XmlMarkup.new(:indent => 2) - xml.instruct! :xml, :version => '1.0', :encoding => 'UTF-8' + xml = Builder::XmlMarkup.new(indent: 2) + xml.instruct! :xml, version: '1.0', encoding: 'UTF-8' xml.tag! 'GVPSRequest' do xml.tag! 'Mode', test? ? 'TEST' : 'PROD' @@ -105,7 +104,7 @@ def build_sale_request(money, credit_card, options) def build_authorize_request(money, credit_card, options) build_xml_request(money, credit_card, options) do |xml| add_customer_data(xml, options) - add_order_data(xml, options) do + add_order_data(xml, options) do add_addresses(xml, options) end add_credit_card(xml, credit_card) @@ -116,7 +115,7 @@ def build_authorize_request(money, credit_card, options) end def build_capture_request(money, ref_id, options) - options = options.merge(:order_id => ref_id) + options = options.merge(order_id: ref_id) build_xml_request(money, ref_id, options) do |xml| add_customer_data(xml, options) add_order_data(xml, options) @@ -138,9 +137,7 @@ def add_order_data(xml, options, &block) xml.tag! 'OrderID', format_order_id(options[:order_id]) xml.tag! 'GroupID' - if block_given? - yield xml - end + yield xml if block_given? end end @@ -182,7 +179,7 @@ def add_addresses(xml, options) def add_address(xml, address) xml.tag! 'Name', normalize(address[:name]) address_text = address[:address1] - address_text << " #{ address[:address2]}" if address[:address2] + address_text << " #{address[:address2]}" if address[:address2] xml.tag! 'Text', normalize(address_text) xml.tag! 'City', normalize(address[:city]) xml.tag! 'District', normalize(address[:state]) @@ -196,7 +193,7 @@ def normalize(text) return unless text if ActiveSupport::Inflector.method(:transliterate).arity == -2 - ActiveSupport::Inflector.transliterate(text,'') + ActiveSupport::Inflector.transliterate(text, '') else text.gsub(/[^\x00-\x7F]+/, '') end @@ -215,18 +212,20 @@ def currency_code(currency) CURRENCY_CODES[currency] || CURRENCY_CODES[default_currency] end - def commit(money,request) + def commit(money, request) url = test? ? self.test_url : self.live_url raw_response = ssl_post(url, 'data=' + request) response = parse(raw_response) success = success?(response) - Response.new(success, - success ? 'Approved' : "Declined (Reason: #{response[:reason_code]} - #{response[:error_msg]} - #{response[:sys_err_msg]})", - response, - :test => test?, - :authorization => response[:order_id]) + Response.new( + success, + success ? 'Approved' : "Declined (Reason: #{response[:reason_code]} - #{response[:error_msg]} - #{response[:sys_err_msg]})", + response, + test: test?, + authorization: response[:order_id] + ) end def parse(body) @@ -241,7 +240,7 @@ def parse(body) def parse_element(response, node) if node.has_elements? - node.elements.each{|element| parse_element(response, element) } + node.elements.each { |element| parse_element(response, element) } else response[node.name.underscore.to_sym] = node.text end @@ -254,8 +253,6 @@ def success?(response) def strip_invalid_xml_chars(xml) xml.gsub(/&(?!(?:[a-z]+|#[0-9]+|x[a-zA-Z0-9]+);)/, '&') end - end end end - diff --git a/lib/active_merchant/billing/gateways/global_collect.rb b/lib/active_merchant/billing/gateways/global_collect.rb index 3f37eb85177..f52fed1404f 100644 --- a/lib/active_merchant/billing/gateways/global_collect.rb +++ b/lib/active_merchant/billing/gateways/global_collect.rb @@ -1,30 +1,40 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class GlobalCollectGateway < Gateway - self.display_name = 'GlobalCollect' + class_attribute :preproduction_url + class_attribute :ogone_direct_test + class_attribute :ogone_direct_live + + self.display_name = 'Worldline (formerly GlobalCollect)' self.homepage_url = 'http://www.globalcollect.com/' - self.test_url = 'https://api-sandbox.globalcollect.com/' - self.live_url = 'https://api.globalcollect.com/' + self.test_url = 'https://eu.sandbox.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 = ['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.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 = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover naranja cabal tuya patagonia_365 tarjeta_sol] + + version 'v1' + version 'v2', :ogone_direct - def initialize(options={}) + def initialize(options = {}) requires!(options, :merchant_id, :api_key_id, :secret_api_key) super end - def purchase(money, payment, options={}) + def purchase(money, payment, options = {}) MultiResponse.run do |r| r.process { authorize(money, payment, options) } - r.process { capture(money, r.authorization, options) } unless capture_requested?(r) + r.process { capture(money, r.authorization, options) } if should_request_capture?(r, options[:requires_approval]) end end - def authorize(money, payment, options={}) + def authorize(money, payment, options = {}) post = nestable_hash add_order(post, money, options) add_payment(post, payment, options) @@ -32,39 +42,50 @@ def authorize(money, payment, options={}) add_address(post, 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) + action = options[:action] || :authorize - commit(:authorize, post) + commit(:post, action, post, options:) end - def capture(money, authorization, options={}) + def capture(money, authorization, options = {}) post = nestable_hash - add_order(post, money, options) + add_order(post, money, options, capture: true) add_customer_data(post, options) add_creator_info(post, options) - commit(:capture, post, authorization) + commit(:post, :capture, post, authorization:) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) post = nestable_hash add_amount(post, money, options) add_refund_customer_data(post, options) add_creator_info(post, options) - commit(:refund, post, authorization) + commit(:post, :refund, post, authorization:) end - def void(authorization, options={}) + def void(authorization, options = {}) post = nestable_hash add_creator_info(post, options) - commit(:void, post, authorization) + commit(:post, :void, post, authorization:) end - def verify(payment, options={}) - MultiResponse.run(:use_first_response) do |r| - r.process { authorize(100, payment, options) } - r.process { void(r.authorization, options) } + def verify(payment, options = {}) + if ogone_direct? + MultiResponse.run(:use_first_response) do |r| + r.process { authorize(100, payment, options) } + r.process { void(r.authorization, options) } + end + else + authorize(0, payment, options.merge(action: :verify)) end end + def inquire(authorization, options = {}) + commit(:get, :inquire, nil, authorization:) + end + def supports_scrubbing? true end @@ -72,8 +93,11 @@ def supports_scrubbing? def scrub(transcript) transcript. gsub(%r((Authorization: )[^\\]*)i, '\1[FILTERED]'). - gsub(%r(("cardNumber\\":\\")\d+), '\1[FILTERED]'). - gsub(%r(("cvv\\":\\")\d+), '\1[FILTERED]') + gsub(%r(("cardNumber\\+":\\+")\d+), '\1[FILTERED]'). + gsub(%r(("cvv\\+":\\+")\d+), '\1[FILTERED]'). + gsub(%r(("dpan\\+":\\+")\d+), '\1[FILTERED]'). + gsub(%r(("pan\\+":\\+")\d+), '\1[FILTERED]'). + gsub(%r(("cryptogram\\+":\\+"|("cavv\\+" : \\+"))[^\\]*), '\1[FILTERED]') end private @@ -84,14 +108,19 @@ def scrub(transcript) 'master' => '3', 'discover' => '128', 'jcb' => '125', - 'diners_club' => '132' + 'diners_club' => '132', + 'cabal' => '135', + 'naranja' => '136', + apple_pay: '302', + google_pay: '320' } - def add_order(post, money, options) - post['order']['amountOfMoney'] = { - 'amount' => amount(money), - 'currencyCode' => options[:currency] || currency(money) - } + def add_order(post, money, options, capture: false) + if capture + post['amount'] = amount(money) + else + add_amount(post['order'], money, options) + end post['order']['references'] = { 'merchantReference' => options[:order_id], 'descriptor' => options[:description] # Max 256 chars @@ -99,6 +128,127 @@ def add_order(post, money, options) post['order']['references']['invoiceData'] = { 'invoiceNumber' => options[:invoice] } + 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 + + def add_airline_data(post, options) + return unless airline_options = options[:airline_data] + + airline_data = {} + + airline_data['flightDate'] = airline_options[:flight_date] if airline_options[:flight_date] + airline_data['passengerName'] = airline_options[:passenger_name] if airline_options[:passenger_name] + airline_data['code'] = airline_options[:code] if airline_options[:code] + airline_data['name'] = airline_options[:name] if airline_options[:name] + airline_data['invoiceNumber'] = options[:airline_data][:invoice_number] if options[:airline_data][:invoice_number] + airline_data['isETicket'] = options[:airline_data][:is_eticket] if options[:airline_data][:is_eticket] + airline_data['isRestrictedTicket'] = options[:airline_data][:is_restricted_ticket] if options[:airline_data][:is_restricted_ticket] + 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) + + post['order']['additionalInput']['airlineData'] = airline_data + end + + def add_flight_legs(airline_options) + flight_legs = [] + airline_options[:flight_legs]&.each do |fl| + leg = {} + leg['airlineClass'] = fl[:airline_class] if fl[:airline_class] + leg['arrivalAirport'] = fl[:arrival_airport] if fl[:arrival_airport] + leg['arrivalTime'] = fl[:arrival_time] if fl[:arrival_time] + leg['carrierCode'] = fl[:carrier_code] if fl[:carrier_code] + leg['conjunctionTicket'] = fl[:conjunction_ticket] if fl[:conjunction_ticket] + leg['couponNumber'] = fl[:coupon_number] if fl[:coupon_number] + leg['date'] = fl[:date] if fl[:date] + leg['departureTime'] = fl[:departure_time] if fl[:departure_time] + leg['endorsementOrRestriction'] = fl[:endorsement_or_restriction] if fl[:endorsement_or_restriction] + leg['exchangeTicket'] = fl[:exchange_ticket] if fl[:exchange_ticket] + leg['fare'] = fl[:fare] if fl[:fare] + leg['fareBasis'] = fl[:fare_basis] if fl[:fare_basis] + leg['fee'] = fl[:fee] if fl[:fee] + leg['flightNumber'] = fl[:flight_number] if fl[:flight_number] + leg['number'] = fl[:number] if fl[:number] + leg['originAirport'] = fl[:origin_airport] if fl[:origin_airport] + leg['passengerClass'] = fl[:passenger_class] if fl[:passenger_class] + leg['stopoverCode'] = fl[:stopover_code] if fl[:stopover_code] + leg['taxes'] = fl[:taxes] if fl[:taxes] + flight_legs << leg + end + flight_legs + end + + def add_passengers(airline_options) + passengers = [] + airline_options[:passengers]&.each do |flyer| + passenger = {} + passenger['firstName'] = flyer[:first_name] if flyer[:first_name] + passenger['surname'] = flyer[:surname] if flyer[:surname] + passenger['surnamePrefix'] = flyer[:surname_prefix] if flyer[:surname_prefix] + passenger['title'] = flyer[:title] if flyer[:title] + passengers << passenger + end + passengers + end + + def add_lodging_data(post, options) + return unless lodging_options = options[:lodging_data] + + lodging_data = {} + + lodging_data['charges'] = add_charges(lodging_options) + lodging_data['checkInDate'] = lodging_options[:check_in_date] if lodging_options[:check_in_date] + lodging_data['checkOutDate'] = lodging_options[:check_out_date] if lodging_options[:check_out_date] + lodging_data['folioNumber'] = lodging_options[:folio_number] if lodging_options[:folio_number] + lodging_data['isConfirmedReservation'] = lodging_options[:is_confirmed_reservation] if lodging_options[:is_confirmed_reservation] + lodging_data['isFacilityFireSafetyConform'] = lodging_options[:is_facility_fire_safety_conform] if lodging_options[:is_facility_fire_safety_conform] + lodging_data['isNoShow'] = lodging_options[:is_no_show] if lodging_options[:is_no_show] + lodging_data['isPreferenceSmokingRoom'] = lodging_options[:is_preference_smoking_room] if lodging_options[:is_preference_smoking_room] + lodging_data['numberOfAdults'] = lodging_options[:number_of_adults] if lodging_options[:number_of_adults] + lodging_data['numberOfNights'] = lodging_options[:number_of_nights] if lodging_options[:number_of_nights] + lodging_data['numberOfRooms'] = lodging_options[:number_of_rooms] if lodging_options[:number_of_rooms] + lodging_data['programCode'] = lodging_options[:program_code] if lodging_options[:program_code] + lodging_data['propertyCustomerServicePhoneNumber'] = lodging_options[:property_customer_service_phone_number] if lodging_options[:property_customer_service_phone_number] + lodging_data['propertyPhoneNumber'] = lodging_options[:property_phone_number] if lodging_options[:property_phone_number] + lodging_data['renterName'] = lodging_options[:renter_name] if lodging_options[:renter_name] + lodging_data['rooms'] = add_rooms(lodging_options) + + post['order']['additionalInput']['lodgingData'] = lodging_data + end + + def add_charges(lodging_options) + charges = [] + lodging_options[:charges]&.each do |item| + charge = {} + charge['chargeAmount'] = item[:charge_amount] if item[:charge_amount] + charge['chargeAmountCurrencyCode'] = item[:charge_amount_currency_code] if item[:charge_amount_currency_code] + charge['chargeType'] = item[:charge_type] if item[:charge_type] + charges << charge + end + charges + end + + def add_rooms(lodging_options) + rooms = [] + lodging_options[:rooms]&.each do |item| + room = {} + room['dailyRoomRate'] = item[:daily_room_rate] if item[:daily_room_rate] + room['dailyRoomRateCurrencyCode'] = item[:daily_room_rate_currency_code] if item[:daily_room_rate_currency_code] + room['dailyRoomTaxAmount'] = item[:daily_room_tax_amount] if item[:daily_room_tax_amount] + room['dailyRoomTaxAmountCurrencyCode'] = item[:daily_room_tax_amount_currency_code] if item[:daily_room_tax_amount_currency_code] + room['numberOfNightsAtRoomRate'] = item[:number_of_nights_at_room_rate] if item[:number_of_nights_at_room_rate] + room['roomLocation'] = item[:room_location] if item[:room_location] + room['roomNumber'] = item[:room_number] if item[:room_number] + room['typeOfBed'] = item[:type_of_bed] if item[:type_of_bed] + room['typeOfRoom'] = item[:type_of_room] if item[:type_of_room] + rooms << room + end + rooms end def add_creator_info(post, options) @@ -112,55 +262,84 @@ def add_creator_info(post, options) post['shoppingCartExtension']['extensionID'] = options[:extension_ID] if options[:extension_ID] end - def add_amount(post, money, options={}) + 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 def add_payment(post, payment, options) year = format(payment.year, :two_digits) month = format(payment.month, :two_digits) - expirydate = "#{month}#{year}" + expirydate = "#{month}#{year}" pre_authorization = options[:pre_authorization] ? 'PRE_AUTHORIZATION' : 'FINAL_AUTHORIZATION' - - post['cardPaymentMethodSpecificInput'] = { - 'paymentProductId' => BRAND_MAP[payment.brand], - 'skipAuthentication' => 'true', # refers to 3DSecure - 'skipFraudService' => 'true', - 'authorizationMode' => pre_authorization + product_id = options[:payment_product_id] || BRAND_MAP[payment.brand] + specifics_inputs = { + 'paymentProductId' => product_id, + 'skipAuthentication' => options[:skip_authentication] || 'true', # refers to 3DSecure + 'skipFraudService' => 'true', + 'authorizationMode' => pre_authorization } - post['cardPaymentMethodSpecificInput']['card'] = { + specifics_inputs['requiresApproval'] = options[:requires_approval] unless options[:requires_approval].nil? + if payment.is_a?(NetworkTokenizationCreditCard) + add_mobile_credit_card(post, payment, options, specifics_inputs, expirydate) + elsif payment.is_a?(CreditCard) + add_credit_card(post, payment, specifics_inputs, expirydate) + end + end + + def add_credit_card(post, payment, specifics_inputs, expirydate) + post['cardPaymentMethodSpecificInput'] = specifics_inputs.merge({ + 'card' => { 'cvv' => payment.verification_value, 'cardNumber' => payment.number, 'expiryDate' => expirydate, 'cardholderName' => payment.name - } + } + }) + end + + def add_mobile_credit_card(post, payment, options, specifics_inputs, expirydate) + specifics_inputs['paymentProductId'] = BRAND_MAP[payment.source] + post['mobilePaymentMethodSpecificInput'] = specifics_inputs + + if options[:use_encrypted_payment_data] + post['mobilePaymentMethodSpecificInput']['encryptedPaymentData'] = payment.payment_data.to_s&.gsub('=>', ':') + else + add_decrypted_payment_data(post, payment, options, expirydate) + end + end + + def add_decrypted_payment_data(post, payment, options, expirydate) + 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.to_s&.gsub('=>', ':') + end + + post['mobilePaymentMethodSpecificInput']["#{data_type}PaymentData"] = data if data end def add_customer_data(post, options, payment = nil) - post['order']['customer'] = { - 'merchantCustomerId' => options[:customer] - } if payment - post['order']['customer']['personalInformation'] = { - 'name' => { - 'firstName' => payment.first_name[0..14], - 'surname' => payment.last_name[0..69] - } - } + post['order']['customer']['personalInformation']['name']['firstName'] = payment.first_name[0..14] if payment.first_name + post['order']['customer']['personalInformation']['name']['surname'] = payment.last_name[0..69] if payment.last_name end - post['order']['companyInformation'] = { - 'name' => options[:company] - } - post['order']['contactDetails'] = { - 'emailAddress' => options[:email] - } - if address = options[:billing_address] || options[:address] - post['order']['contactDetails'] = { - 'phoneNumber' => address[:phone] - } + 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] && (address[:phone])) + post['order']['customer']['contactDetails']['phoneNumber'] = address[:phone] end end @@ -169,33 +348,34 @@ def add_refund_customer_data(post, options) post['customer']['address'] = { 'countryCode' => address[:country] } - post['customer']['contactDetails'] = { - 'emailAddress' => options[:email], - 'phoneNumber' => address[:phone] - } + post['customer']['contactDetails']['emailAddress'] = options[:email] if options[:email] + if address = options[:billing_address] || (options[:address] && (address[:phone])) + post['customer']['contactDetails']['phoneNumber'] = address[:phone] + end end end def add_address(post, creditcard, options) - billing_address = options[:billing_address] || options[:address] shipping_address = options[:shipping_address] if billing_address = options[:billing_address] || options[:address] post['order']['customer']['billingAddress'] = { - 'street' => billing_address[:address1], - 'additionalInfo' => billing_address[:address2], + '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], - 'state' => billing_address[:state], + 'state' => truncate(billing_address[:state], 35), 'countryCode' => billing_address[:country] } end if shipping_address post['order']['customer']['shippingAddress'] = { - 'street' => shipping_address[:address1], - 'additionalInfo' => shipping_address[:address2], + '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], - 'state' => shipping_address[:state], + 'state' => truncate(shipping_address[:state], 35), 'countryCode' => shipping_address[:country] } post['order']['customer']['shippingAddress']['name'] = { @@ -208,122 +388,207 @@ 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.merge!({customerIpAddress: options[:ip]}) if options[:ip] post['fraudFields'] = fraud_fields unless fraud_fields.empty? end + def add_external_cardholder_authentication_data(post, options) + return unless threeds_2_options = options[:three_d_secure] + + 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 + def parse(body) JSON.parse(body) end 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? ? fetch_version(:ogone_direct) : fetch_version + uri = "/#{version}/#{@options[:merchant_id]}/" case action - when :authorize + when :authorize, :verify uri + 'payments' when :capture - uri + "payments/#{authorization}/approve" + capture_name = ogone_direct? ? 'capture' : 'approve' + uri + "payments/#{authorization}/#{capture_name}" when :refund uri + "payments/#{authorization}/refund" when :void uri + "payments/#{authorization}/cancel" + when :inquire + uri + "payments/#{authorization}" end end - def commit(action, post, authorization = nil) + def idempotency_key_for_signature(options) + "x-gcs-idempotence-key:#{options[:idempotency_key]}" if options[:idempotency_key] && !ogone_direct? + end + + def commit(method, action, post, authorization: nil, options: {}) begin - response = parse(ssl_post(url(action, authorization), post.to_json, headers(action, post, authorization))) + raw_response = ssl_request(method, url(action, authorization), post&.to_json, headers(method, action, post, authorization, options)) + response = parse(raw_response) rescue ResponseError => e - if e.response.code.to_i >= 400 - response = parse(e.response.body) - end + response = parse(e.response.body) if e.response.code.to_i >= 400 + rescue JSON::ParserError + response = json_error(raw_response) end - succeeded = success_from(response) + succeeded = success_from(action, response) Response.new( - succeeded, - message_from(succeeded, response), - response, - authorization: authorization_from(succeeded, response), - error_code: error_code_from(succeeded, response), - test: test? + succeeded, + message_from(succeeded, response), + response, + authorization: authorization_from(response), + error_code: error_code_from(succeeded, response), + test: test? ) - end - def headers(action, post, authorization = nil) + def json_error(raw_response) { - 'Content-Type' => content_type, - 'Authorization' => auth_digest(action, post, authorization), + 'error_message' => 'Invalid response received from the Ingenico ePayments (formerly GlobalCollect) API. Please contact Ingenico ePayments if you continue to receive this message.' \ + " (The raw response returned by the API was #{raw_response.inspect})" + } + end + + def headers(method, action, post, authorization = nil, options = {}) + headers = { + 'Content-Type' => content_type, + 'Authorization' => auth_digest(method, action, post, authorization, options), 'Date' => date } + + headers['X-GCS-Idempotence-Key'] = options[:idempotency_key] if options[:idempotency_key] && !ogone_direct? + headers end - def auth_digest(action, post, authorization = nil) - data = <<-EOS -POST -#{content_type} -#{date} -#{uri(action, authorization)} -EOS - digest = OpenSSL::Digest.new('sha256') + def auth_digest(method, action, post, authorization = nil, options = {}) + data = <<~REQUEST + #{method.to_s.upcase} + #{content_type} + #{date} + #{idempotency_key_for_signature(options)} + #{uri(action, authorization)} + REQUEST + data = data.each_line.reject { |line| line.strip == '' }.join + digest = OpenSSL::Digest.new('SHA256') key = @options[:secret_api_key] - "GCS v1HMAC:#{@options[:api_key_id]}:#{Base64.strict_encode64(OpenSSL::HMAC.digest(digest, key, data))}" + "GCS v1HMAC:#{@options[:api_key_id]}:#{Base64.strict_encode64(OpenSSL::HMAC.digest(digest, key, data)).strip}" end def date - @date ||= Time.now.strftime('%a, %d %b %Y %H:%M:%S %Z') # Must be same in digest and HTTP header + @date ||= Time.now.gmtime.strftime('%a, %d %b %Y %H:%M:%S GMT') end def content_type 'application/json' end - def success_from(response) - !response['errorId'] && response['status'] != 'REJECTED' - end + def success_from(action, response) + return false if response['errorId'] || response['error_message'] - def message_from(succeeded, response) - if succeeded - 'Succeeded' + case action + when :authorize + response.dig('payment', 'statusOutput', 'isAuthorized') + when :capture + capture_status = response.dig('status') || response.dig('payment', 'status') + %w(CAPTURED CAPTURE_REQUESTED).include?(capture_status) + when :void + void_response_id = response.dig('cardPaymentMethodSpecificOutput', 'voidResponseId') || response.dig('mobilePaymentMethodSpecificOutput', 'voidResponseId') + %w(00 0 8 11).include?(void_response_id) || response.dig('payment', 'status') == 'CANCELLED' + when :refund + refund_status = response.dig('status') || response.dig('payment', 'status') + %w(REFUNDED REFUND_REQUESTED).include?(refund_status) + when :verify + response.dig('payment', 'statusOutput', 'statusCategory') == 'ACCOUNT_VERIFIED' else - if errors = response['errors'] - errors.first.try(:[], 'message') - elsif status = response['status'] - 'Status: ' + status - else - 'No message available' - end + response['status'] != 'REJECTED' end end - def authorization_from(succeeded, response) - if succeeded - response['id'] || response['payment']['id'] || response['paymentResult']['payment']['id'] + def message_from(succeeded, response) + return 'Succeeded' if succeeded + + if errors = response['errors'] + errors.first.try(:[], 'message') + elsif response['error_message'] + response['error_message'] + elsif response['status'] + 'Status: ' + response['status'] else - response['errorId'] + 'No message available' end end + def authorization_from(response) + response.dig('id') || response.dig('payment', 'id') || response.dig('paymentResult', 'payment', 'id') + end + def error_code_from(succeeded, response) - unless succeeded - if errors = response['errors'] - errors.first.try(:[], 'code') - elsif status = response.try(:[], 'statusOutput').try(:[], 'statusCode') - status.to_s - else - 'No error code available' - end + return if succeeded + + if errors = response['errors'] + errors.first.try(:[], 'code') + elsif status = response.try(:[], 'statusOutput').try(:[], 'statusCode') + status.to_s + else + 'No error code available' end end def nestable_hash - Hash.new {|h,k| h[k] = Hash.new(&h.default_proc) } + Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) } + end + + # Capture hasn't already been requested, + # and + # `requires_approval` is not false + def should_request_capture?(response, requires_approval) + !capture_requested?(response) && requires_approval != false end def capture_requested?(response) diff --git a/lib/active_merchant/billing/gateways/global_transport.rb b/lib/active_merchant/billing/gateways/global_transport.rb index 94087b23e17..e3f3113f55e 100644 --- a/lib/active_merchant/billing/gateways/global_transport.rb +++ b/lib/active_merchant/billing/gateways/global_transport.rb @@ -1,14 +1,14 @@ require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class GlobalTransportGateway < Gateway self.test_url = 'https://certapia.globalpay.com/GlobalPay/transact.asmx/ProcessCreditCard' self.live_url = 'https://api.globalpay.com/GlobalPay/transact.asmx/ProcessCreditCard' self.supported_countries = %w(CA PR US) self.default_currency = 'USD' - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb] + self.supported_cardtypes = %i[visa master american_express discover diners_club jcb] self.homepage_url = 'https://www.globalpaymentsinc.com' self.display_name = 'Global Transport' @@ -20,12 +20,12 @@ class GlobalTransportGateway < Gateway # :global_password - Your Global password # :term_type - 3 character field assigned by Global Transport after # - your application is certified. - def initialize(options={}) + def initialize(options = {}) requires!(options, :global_user_name, :global_password, :term_type) super end - def purchase(money, payment_method, options={}) + def purchase(money, payment_method, options = {}) post = {} add_invoice(post, money, options) add_payment_method(post, payment_method) @@ -34,7 +34,7 @@ def purchase(money, payment_method, options={}) commit('Sale', post, options) end - def authorize(money, payment_method, options={}) + def authorize(money, payment_method, options = {}) post = {} add_invoice(post, money, options) add_payment_method(post, payment_method) @@ -43,7 +43,7 @@ def authorize(money, payment_method, options={}) commit('Auth', post, options) end - def capture(money, authorization, options={}) + def capture(money, authorization, options = {}) post = {} add_invoice(post, money, options) add_auth(post, authorization) @@ -51,7 +51,7 @@ def capture(money, authorization, options={}) commit('Force', post, options) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) post = {} add_invoice(post, money, options) add_auth(post, authorization) @@ -59,14 +59,14 @@ def refund(money, authorization, options={}) commit('Return', post, options) end - def void(authorization, options={}) + def void(authorization, options = {}) post = {} add_auth(post, authorization) commit('Void', post, options) end - def verify(payment_method, options={}) + def verify(payment_method, options = {}) post = {} add_payment_method(post, payment_method) add_address(post, options) @@ -188,7 +188,6 @@ def default_params ExtData: '' } end - end end end diff --git a/lib/active_merchant/billing/gateways/hdfc.rb b/lib/active_merchant/billing/gateways/hdfc.rb index 01404cd8d00..2bb0578715b 100644 --- a/lib/active_merchant/billing/gateways/hdfc.rb +++ b/lib/active_merchant/billing/gateways/hdfc.rb @@ -1,7 +1,7 @@ require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class HdfcGateway < Gateway self.display_name = 'HDFC' self.homepage_url = 'http://www.hdfcbank.com/sme/sme-details/merchant-services/guzh6m0i' @@ -12,14 +12,14 @@ class HdfcGateway < Gateway self.supported_countries = ['IN'] self.default_currency = 'INR' self.money_format = :dollars - self.supported_cardtypes = [:visa, :master, :discover, :diners_club] + self.supported_cardtypes = %i[visa master discover diners_club] - def initialize(options={}) + def initialize(options = {}) requires!(options, :login, :password) super end - def purchase(amount, payment_method, options={}) + def purchase(amount, payment_method, options = {}) post = {} add_invoice(post, amount, options) add_payment_method(post, payment_method) @@ -28,7 +28,7 @@ def purchase(amount, payment_method, options={}) commit('purchase', post) end - def authorize(amount, payment_method, options={}) + def authorize(amount, payment_method, options = {}) post = {} add_invoice(post, amount, options) add_payment_method(post, payment_method) @@ -37,7 +37,7 @@ def authorize(amount, payment_method, options={}) commit('authorize', post) end - def capture(amount, authorization, options={}) + def capture(amount, authorization, options = {}) post = {} add_invoice(post, amount, options) add_reference(post, authorization) @@ -46,7 +46,7 @@ def capture(amount, authorization, options={}) commit('capture', post) end - def refund(amount, authorization, options={}) + def refund(amount, authorization, options = {}) post = {} add_invoice(post, amount, options) add_reference(post, authorization) @@ -57,7 +57,7 @@ def refund(amount, authorization, options={}) private - CURRENCY_CODES = Hash.new{|h,k| raise ArgumentError.new("Unsupported currency for HDFC: #{k}")} + CURRENCY_CODES = Hash.new { |_h, k| raise ArgumentError.new("Unsupported currency for HDFC: #{k}") } CURRENCY_CODES['AED'] = '784' CURRENCY_CODES['AUD'] = '036' CURRENCY_CODES['CAD'] = '124' @@ -81,14 +81,14 @@ def add_customer_data(post, options) post[:udf2] = escape(options[:email]) if options[:email] if address = (options[:billing_address] || options[:address]) post[:udf3] = escape(address[:phone]) if address[:phone] - post[:udf4] = escape(< '1', 'refund' => '2', 'authorize' => '4', - 'capture' => '5', + 'capture' => '5' } def commit(action, post) @@ -149,13 +149,13 @@ def commit(action, post) succeeded, message_from(succeeded, raw), raw, - :authorization => authorization_from(post, raw), - :test => test? + authorization: authorization_from(post, raw), + test: test? ) end def build_request(post) - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 xml.instruct! post.each do |field, value| xml.tag!(field, value) @@ -194,14 +194,12 @@ def split_authorization(authorization) [tranid, member] end - def escape(string, max_length=250) + def escape(string, max_length = 250) return '' unless string - if max_length - string = string[0...max_length] - end + + string = string[0...max_length] if max_length string.gsub(/[^A-Za-z0-9 \-_@\.\n]/, '') end end end end - 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..2559bf1dac7 --- /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:, + 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: }, 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[:execute_threed] && options[:three_ds_2] + + 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 07f4fb26597..5dbc4103110 100644 --- a/lib/active_merchant/billing/gateways/hps.rb +++ b/lib/active_merchant/billing/gateways/hps.rb @@ -1,75 +1,97 @@ require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class HpsGateway < Gateway - self.live_url = 'https://posgateway.secureexchange.net/Hps.Exchange.PosGateway/PosGatewayService.asmx?wsdl' - self.test_url = 'https://posgateway.cert.secureexchange.net/Hps.Exchange.PosGateway/PosGatewayService.asmx?wsdl' + self.live_url = 'https://api2.heartlandportico.com/hps.exchange.posgateway/posgatewayservice.asmx' + self.test_url = 'https://cert.api2.heartlandportico.com/Hps.Exchange.PosGateway/PosGatewayService.asmx' self.supported_countries = ['US'] self.default_currency = 'USD' - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :jbc, :diners_club] + self.supported_cardtypes = %i[visa master american_express discover jbc diners_club] self.homepage_url = 'http://developer.heartlandpaymentsystems.com/SecureSubmit/' self.display_name = 'Heartland Payment Systems' self.money_format = :dollars - def initialize(options={}) + PAYMENT_DATA_SOURCE_MAPPING = { + apple_pay: 'ApplePay', + google_pay: 'GooglePayApp' + } + + def initialize(options = {}) requires!(options, :secret_api_key) super end - def authorize(money, card_or_token, options={}) + def authorize(money, payment_method, options = {}) commit('CreditAuth') do |xml| add_amount(xml, money) add_allow_dup(xml) - add_customer_data(xml, card_or_token, options) + add_card_or_token_customer_data(xml, payment_method, options) add_details(xml, options) add_descriptor_name(xml, options) - add_payment(xml, card_or_token, options) + add_card_or_token_payment(xml, payment_method, options) + add_wallet_data(xml, payment_method, options) + add_three_d_secure(xml, payment_method, options) + add_stored_credentials(xml, options) end end - def capture(money, transaction_id, options={}) - commit('CreditAddToBatch') do |xml| + def capture(money, transaction_id, options = {}) + commit('CreditAddToBatch', transaction_id) do |xml| add_amount(xml, money) add_reference(xml, transaction_id) end end - def purchase(money, card_or_token, options={}) - commit('CreditSale') do |xml| + def purchase(money, payment_method, options = {}) + if payment_method.is_a?(Check) + commit_check_sale(money, payment_method, options) + elsif options.dig(:stored_credential, :reason_type) == 'recurring' + commit_recurring_billing_sale(money, payment_method, options) + else + commit_credit_sale(money, payment_method, options) + end + end + + def refund(money, transaction_id, options = {}) + commit('CreditReturn') do |xml| add_amount(xml, money) add_allow_dup(xml) - add_customer_data(xml, card_or_token,options) + add_reference(xml, transaction_id) + add_card_or_token_customer_data(xml, transaction_id, options) add_details(xml, options) - add_descriptor_name(xml, options) - add_payment(xml, card_or_token, options) end end - def refund(money, transaction_id, options={}) + def credit(money, payment_method, options = {}) commit('CreditReturn') do |xml| add_amount(xml, money) add_allow_dup(xml) - add_reference(xml, transaction_id) - add_customer_data(xml, transaction_id,options) + add_card_or_token_payment(xml, payment_method, options) add_details(xml, options) end end - def verify(card_or_token, options={}) + def verify(card_or_token, options = {}) commit('CreditAccountVerify') do |xml| - add_customer_data(xml, card_or_token, options) + add_card_or_token_customer_data(xml, card_or_token, options) add_descriptor_name(xml, options) - add_payment(xml, card_or_token, options) + add_card_or_token_payment(xml, card_or_token, options) end end - def void(transaction_id, options={}) - commit('CreditVoid') do |xml| - add_reference(xml, transaction_id) + def void(transaction_id, options = {}) + if options[:check_void] + commit('CheckVoid') do |xml| + add_reference(xml, transaction_id) + end + else + commit('CreditVoid') do |xml| + add_reference(xml, transaction_id) + end end end @@ -81,20 +103,64 @@ def scrub(transcript) transcript. gsub(%r(()[^<]*(<\/hps:CardNbr>))i, '\1[FILTERED]\2'). gsub(%r(()[^<]*(<\/hps:CVV2>))i, '\1[FILTERED]\2'). - gsub(%r(()[^<]*(<\/hps:SecretAPIKey>))i, '\1[FILTERED]\2') + gsub(%r(()[^<]*(<\/hps:SecretAPIKey>))i, '\1[FILTERED]\2'). + gsub(%r(()[^<]*(<\/hps:PaymentData>))i, '\1[FILTERED]\2'). + gsub(%r(()[^<]*(<\/hps:RoutingNumber>))i, '\1[FILTERED]\2'). + gsub(%r(()[^<]*(<\/hps:AccountNumber>))i, '\1[FILTERED]\2'). + gsub(%r(()[^<]*(<\/hps:Cryptogram>))i, '\1[FILTERED]\2') end private + def commit_check_sale(money, check, options) + commit('CheckSale') do |xml| + add_check_payment(xml, check, options) + add_amount(xml, money) + add_sec_code(xml, options) + add_check_customer_data(xml, check, options) + add_details(xml, options) + end + end + + def commit_credit_sale(money, payment_method, options) + commit('CreditSale') do |xml| + add_amount(xml, money) + add_allow_dup(xml) + add_card_or_token_customer_data(xml, payment_method, options) + add_details(xml, options) + add_descriptor_name(xml, options) + add_card_or_token_payment(xml, payment_method, options) + add_wallet_data(xml, payment_method, options) + add_three_d_secure(xml, payment_method, options) + add_stored_credentials(xml, options) + end + end + + def commit_recurring_billing_sale(money, payment_method, options) + commit('RecurringBilling') do |xml| + add_amount(xml, money) + add_allow_dup(xml) + add_card_or_token_customer_data(xml, payment_method, options) + add_details(xml, options) + add_descriptor_name(xml, options) + add_card_or_token_payment(xml, payment_method, options) + add_wallet_data(xml, payment_method, options) + add_three_d_secure(xml, payment_method, options) + add_stored_credentials(xml, options) + add_stored_credentials_for_recurring_billing(xml, options) + end + end + def add_reference(xml, transaction_id) - xml.hps :GatewayTxnId, transaction_id + reference = transaction_id.to_s.include?('|') ? transaction_id.split('|').first : transaction_id + xml.hps :GatewayTxnId, reference end def add_amount(xml, money) xml.hps :Amt, amount(money) if money end - def add_customer_data(xml, credit_card, options) + def add_card_or_token_customer_data(xml, credit_card, options) xml.hps :CardHolderData do if credit_card.respond_to?(:number) xml.hps :CardHolderFirstName, credit_card.first_name if credit_card.first_name @@ -104,20 +170,28 @@ def add_customer_data(xml, credit_card, options) xml.hps :CardHolderEmail, options[:email] if options[:email] xml.hps :CardHolderPhone, options[:phone] if options[:phone] - if(billing_address = (options[:billing_address] || options[:address])) + if (billing_address = (options[:billing_address] || options[:address])) xml.hps :CardHolderAddr, billing_address[:address1] if billing_address[:address1] xml.hps :CardHolderCity, billing_address[:city] if billing_address[:city] xml.hps :CardHolderState, billing_address[:state] if billing_address[:state] - xml.hps :CardHolderZip, billing_address[:zip] if billing_address[:zip] + xml.hps :CardHolderZip, alphanumeric_zip(billing_address[:zip]) if billing_address[:zip] end end end - def add_payment(xml, card_or_token, options) + def add_check_customer_data(xml, check, options) + xml.hps :ConsumerInfo do + xml.hps :FirstName, check.first_name + xml.hps :LastName, check.last_name + xml.hps :CheckName, options[:company_name] if options[:company_name] + end + end + + def add_card_or_token_payment(xml, card_or_token, options) xml.hps :CardData do if card_or_token.respond_to?(:number) if card_or_token.track_data - xml.tag!('hps:TrackData', 'method'=>'swipe') do + xml.tag!('hps:TrackData', 'method' => 'swipe') do xml.text! card_or_token.track_data end if options[:encryption_type] @@ -148,14 +222,29 @@ def add_payment(xml, card_or_token, options) end end + def add_check_payment(xml, check, options) + xml.hps :CheckAction, 'SALE' + xml.hps :AccountInfo do + xml.hps :RoutingNumber, check.routing_number + xml.hps :AccountNumber, check.account_number + xml.hps :CheckNumber, check.number + xml.hps :AccountType, check.account_type&.upcase + end + xml.hps :CheckType, check.account_holder_type&.upcase + end + def add_details(xml, options) xml.hps :AdditionalTxnFields do xml.hps :Description, options[:description] if options[:description] - xml.hps :InvoiceNbr, options[:order_id] if options[:order_id] + xml.hps :InvoiceNbr, options[:order_id][0..59] if options[:order_id] xml.hps :CustomerID, options[:customer_id] if options[:customer_id] end end + def add_sec_code(xml, options) + xml.hps :SECCode, options[:sec_code] || 'WEB' + end + def add_allow_dup(xml) xml.hps :AllowDup, 'Y' end @@ -164,15 +253,75 @@ def add_descriptor_name(xml, options) xml.hps :TxnDescriptor, options[:descriptor_name] if options[:descriptor_name] end + def add_wallet_data(xml, payment_method, options) + return unless payment_method.is_a?(NetworkTokenizationCreditCard) + + xml.hps :WalletData do + xml.hps :PaymentSource, PAYMENT_DATA_SOURCE_MAPPING[payment_method.source] + xml.hps :Cryptogram, payment_method.payment_cryptogram + xml.hps :ECI, strip_leading_zero(payment_method.eci) if payment_method.eci + end + end + + def add_three_d_secure(xml, card_or_token, options) + return unless (three_d_secure = options[:three_d_secure]) + + xml.hps :Secure3D do + xml.hps :Version, three_d_secure[:version] + xml.hps :AuthenticationValue, three_d_secure[:cavv] if three_d_secure[:cavv] + xml.hps :ECI, strip_leading_zero(three_d_secure[:eci]) if three_d_secure[:eci] + xml.hps :DirectoryServerTxnId, three_d_secure[:ds_transaction_id] if three_d_secure[:ds_transaction_id] + end + end + + # We do not currently support installments on this gateway. + # The HPS gateway treats recurring transactions as a seperate transaction type + def add_stored_credentials(xml, options) + return unless options[:stored_credential] + + xml.hps :CardOnFileData do + if options[:stored_credential][:initiator] == 'customer' + xml.hps :CardOnFile, 'C' + elsif options[:stored_credential][:initiator] == 'merchant' + xml.hps :CardOnFile, 'M' + else + return + end + + if options[:stored_credential][:network_transaction_id] + xml.hps :CardBrandTxnId, options[:stored_credential][:network_transaction_id] + else + return + end + end + end + + def add_stored_credentials_for_recurring_billing(xml, options) + xml.hps :RecurringData do + if options[:stored_credential][:reason_type] = 'recurring' + xml.hps :OneTime, 'N' + else + xml.hps :OneTime, 'Y' + end + end + end + + def strip_leading_zero(value) + return value unless value[0] == '0' + + value[1, 1] + end + def build_request(action) xml = Builder::XmlMarkup.new(encoding: 'UTF-8') xml.instruct!(:xml, encoding: 'UTF-8') xml.SOAP :Envelope, { - 'xmlns:SOAP' => 'http://schemas.xmlsoap.org/soap/envelope/', - 'xmlns:hps' => 'http://Hps.Exchange.PosGateway' } do + 'xmlns:SOAP' => 'http://schemas.xmlsoap.org/soap/envelope/', + 'xmlns:hps' => 'http://Hps.Exchange.PosGateway' + } 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] @@ -202,9 +351,9 @@ def parse(raw) doc = Nokogiri::XML(raw) doc.remove_namespaces! - if(header = doc.xpath('//Header').first) + if (header = doc.xpath('//Header').first) header.elements.each do |node| - if (node.elements.size == 0) + if node.elements.size == 0 response[node.name] = node.text else node.elements.each do |childnode| @@ -213,33 +362,34 @@ def parse(raw) end end end - if(transaction = doc.xpath('//Transaction/*[1]').first) + if (transaction = doc.xpath('//Transaction/*[1]').first) transaction.elements.each do |node| response[node.name] = node.text end end - if(fault = doc.xpath('//Fault/Reason/Text').first) + if (fault = doc.xpath('//Fault/Reason/Text').first) response['Fault'] = fault.text end response end - def commit(action, &request) - data = build_request(action, &request) + def commit(action, reference = nil, &) + data = build_request(action, &) - response = begin - parse(ssl_post((test? ? test_url : live_url), data, 'Content-Type' => 'text/xml')) - rescue ResponseError => e - parse(e.response.body) - end + response = + begin + parse(ssl_post((test? ? test_url : live_url), data, 'Content-Type' => 'text/xml')) + rescue ResponseError => e + parse(e.response.body) + end ActiveMerchant::Billing::Response.new( successful?(response), message_from(response), response, test: test?, - authorization: authorization_from(response), + authorization: authorization_from(response, reference), avs_result: { code: response['AVSRsltCode'], message: response['AVSRsltText'] @@ -248,33 +398,40 @@ def commit(action, &request) ) end + SUCCESSFUL_RESPONSE_CODES = %w(0 00 85) def successful?(response) ( (response['GatewayRspCode'] == '0') && - ((response['RspCode'] || '00') == '00' || response['RspCode'] == '85') + ((SUCCESSFUL_RESPONSE_CODES.include? response['RspCode']) || !response['RspCode']) ) end def message_from(response) - if(response['Fault']) + if response['Fault'] response['Fault'] - elsif(response['GatewayRspCode'] == '0') - if(response['RspCode'] != '00' && response['RspCode'] != '85') - issuer_message(response['RspCode']) - else + elsif response['GatewayRspCode'] == '0' + if SUCCESSFUL_RESPONSE_CODES.include? response['RspCode'] response['GatewayRspMsg'] + else + issuer_message(response['RspCode']) end else (GATEWAY_MESSAGES[response['GatewayRspCode']] || response['GatewayRspMsg']) end end - def authorization_from(response) + def authorization_from(response, reference) + return [reference, response['GatewayTxnId']].join('|') if reference + response['GatewayTxnId'] end def test? - (@options[:secret_api_key] && @options[:secret_api_key].include?('_cert_')) + @options[:secret_api_key]&.include?('_cert_') + end + + def alphanumeric_zip(zip) + zip.gsub(/[^0-9a-z]/i, '') end ISSUER_MESSAGES = { @@ -290,6 +447,7 @@ def issuer_message(code) return 'The card was declined.' if %w(02 03 04 05 41 43 44 51 56 61 62 63 65 78).include?(code) return 'An error occurred while processing the card.' if %w(06 07 12 15 19 12 52 53 57 58 76 77 91 96 EC).include?(code) return "The card's security code is incorrect." if %w(EB N7).include?(code) + ISSUER_MESSAGES[code] end diff --git a/lib/active_merchant/billing/gateways/iats_payments.rb b/lib/active_merchant/billing/gateways/iats_payments.rb index dcc43765847..0bc0bf5a8e9 100644 --- a/lib/active_merchant/billing/gateways/iats_payments.rb +++ b/lib/active_merchant/billing/gateways/iats_payments.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class IatsPaymentsGateway < Gateway class_attribute :live_na_url, :live_uk_url @@ -8,22 +8,23 @@ class IatsPaymentsGateway < Gateway self.supported_countries = %w(AU BR CA CH DE DK ES FI FR GR HK IE IT NL NO PT SE SG TR GB US TH ID PH BE) self.default_currency = 'USD' - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'http://home.iatspayments.com/' self.display_name = 'iATS Payments' ACTIONS = { - purchase: 'ProcessCreditCardV1', - purchase_check: 'ProcessACHEFTV1', - refund: 'ProcessCreditCardRefundWithTransactionIdV1', - refund_check: 'ProcessACHEFTRefundWithTransactionIdV1', - store: 'CreateCreditCardCustomerCodeV1', - unstore: 'DeleteCustomerCodeV1' + purchase: 'ProcessCreditCard', + purchase_check: 'ProcessACHEFT', + purchase_customer_code: 'ProcessCreditCardWithCustomerCode', + refund: 'ProcessCreditCardRefundWithTransactionId', + refund_check: 'ProcessACHEFTRefundWithTransactionId', + store: 'CreateCreditCardCustomerCode', + unstore: 'DeleteCustomerCode' } - def initialize(options={}) - if(options[:login]) + def initialize(options = {}) + if options[:login] ActiveMerchant.deprecated("The 'login' option is deprecated in favor of 'agent_code' and will be removed in a future version.") options[:agent_code] = options[:login] end @@ -34,18 +35,19 @@ def initialize(options={}) super end - def purchase(money, payment, options={}) + def purchase(money, payment, options = {}) post = {} add_invoice(post, money, options) add_payment(post, payment) add_address(post, options) add_ip(post, options) add_description(post, options) + add_customer_details(post, options) - commit((payment.is_a?(Check) ? :purchase_check : :purchase), post) + commit(determine_purchase_type(payment), post) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) post = {} transaction_id, payment_type = split_authorization(authorization) post[:transaction_id] = transaction_id @@ -90,17 +92,30 @@ def scrub(transcript) private + def determine_purchase_type(payment) + if payment.is_a?(String) + :purchase_customer_code + elsif payment.is_a?(Check) + :purchase_check + else + :purchase + end + end + def add_ip(post, options) post[:customer_ip_address] = options[:ip] if options.has_key?(:ip) end def add_address(post, options) billing_address = options[:billing_address] || options[:address] - if(billing_address) + if billing_address post[:address] = billing_address[:address1] post[:city] = billing_address[:city] post[:state] = billing_address[:state] post[:zip_code] = billing_address[:zip] + post[:phone] = billing_address[:phone] if billing_address[:phone] + post[:email] = billing_address[:email] if billing_address[:email] + post[:country] = billing_address[:country] if billing_address[:country] end end @@ -114,7 +129,9 @@ def add_description(post, options) end def add_payment(post, payment) - if payment.is_a?(Check) + if payment.is_a?(String) + post[:customer_code] = payment + elsif payment.is_a?(Check) add_check(post, payment) else add_credit_card(post, payment) @@ -144,6 +161,10 @@ def add_store_defaults(post) post[:amount] = 0 end + def add_customer_details(post, options) + post[:email] = options[:email] if options[:email] + end + def expdate(creditcard) year = sprintf('%.4i', creditcard.year) month = sprintf('%.2i', creditcard.month) @@ -164,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), @@ -178,12 +204,13 @@ def commit(action, parameters) def endpoints { - purchase: 'ProcessLink.asmx', - purchase_check: 'ProcessLink.asmx', - refund: 'ProcessLink.asmx', - refund_check: 'ProcessLink.asmx', - store: 'CustomerLink.asmx', - unstore: 'CustomerLink.asmx' + purchase: 'ProcessLinkv3.asmx', + purchase_check: 'ProcessLinkv3.asmx', + purchase_customer_code: 'ProcessLinkv3.asmx', + refund: 'ProcessLinkv3.asmx', + refund_check: 'ProcessLinkv3.asmx', + store: 'CustomerLinkv3.asmx', + unstore: 'CustomerLinkv3.asmx' } end @@ -217,7 +244,7 @@ def hashify_xml!(xml, response) end def recursively_parse_element(node, response) - if(node.has_elements?) + if node.has_elements? node.elements.each { |n| recursively_parse_element(n, response) } else response[dexmlize_param_name(node.name)] = (node.text ? node.text.strip : nil) @@ -235,7 +262,7 @@ def success_from(response) def message_from(response) if !successful_result_message?(response) && response[:authorization_result] return response[:authorization_result].strip - elsif(response[:status] == 'Failure') + elsif response[:status] == 'Failure' return response[:errors] else response[:status] @@ -243,7 +270,7 @@ def message_from(response) end def authorization_from(action, response) - if [:store, :unstore].include?(action) + if %i[store unstore].include?(action) response[:customercode] elsif [:purchase_check].include?(action) response[:transaction_id] ? "#{response[:transaction_id]}|check" : nil @@ -266,7 +293,7 @@ def envelope_namespaces def post_data(action, parameters = {}) xml = Builder::XmlMarkup.new - xml.instruct!(:xml, :version => '1.0', :encoding => 'utf-8') + xml.instruct!(:xml, version: '1.0', encoding: 'utf-8') xml.tag! 'soap12:Envelope', envelope_namespaces do xml.tag! 'soap12:Body' do xml.tag! ACTIONS[action], { 'xmlns' => 'https://www.iatspayments.com/NetGate/' } do diff --git a/lib/active_merchant/billing/gateways/in_context_paypal_express.rb b/lib/active_merchant/billing/gateways/in_context_paypal_express.rb index e746d978ca0..69ed4c2bf54 100644 --- a/lib/active_merchant/billing/gateways/in_context_paypal_express.rb +++ b/lib/active_merchant/billing/gateways/in_context_paypal_express.rb @@ -1,11 +1,11 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class InContextPaypalExpressGateway < PaypalExpressGateway self.test_redirect_url = 'https://www.sandbox.paypal.com/checkoutnow' self.live_redirect_url = 'https://www.paypal.com/checkoutnow' def redirect_url_for(token, options = {}) - options = {review: true}.update(options) + options = { review: true }.update(options) url = "#{redirect_url}?token=#{token}" url += '&useraction=commit' unless options[:review] url diff --git a/lib/active_merchant/billing/gateways/inspire.rb b/lib/active_merchant/billing/gateways/inspire.rb index 52122d4d0b6..1c8087f58d0 100644 --- a/lib/active_merchant/billing/gateways/inspire.rb +++ b/lib/active_merchant/billing/gateways/inspire.rb @@ -1,11 +1,11 @@ require File.join(File.dirname(__FILE__), '..', 'check.rb') -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class InspireGateway < Gateway self.live_url = self.test_url = 'https://secure.inspiregateway.net/api/transact.php' self.supported_countries = ['US'] - self.supported_cardtypes = [:visa, :master, :american_express] + self.supported_cardtypes = %i[visa master american_express] self.homepage_url = 'http://www.inspiregateway.com' self.display_name = 'Inspire Commerce' @@ -33,7 +33,7 @@ def initialize(options = {}) def authorize(money, creditcard, options = {}) post = {} add_invoice(post, options) - add_payment_source(post, creditcard,options) + add_payment_source(post, creditcard, options) add_address(post, creditcard, options) add_customer_data(post, options) @@ -51,13 +51,13 @@ def purchase(money, payment_source, options = {}) end def capture(money, authorization, options = {}) - post ={} + post = {} post[:transactionid] = authorization commit('capture', money, post) end def void(authorization, options = {}) - post ={} + post = {} post[:transactionid] = authorization commit('void', nil, post) end @@ -93,32 +93,29 @@ def delete(vault_id) # store and unstore need to be defined def store(creditcard, options = {}) billing_id = options.delete(:billing_id).to_s || true - authorize(100, creditcard, options.merge(:store => billing_id)) + authorize(100, creditcard, options.merge(store: billing_id)) end - alias_method :unstore, :delete + alias unstore delete private + def add_customer_data(post, options) - if options.has_key? :email - post[:email] = options[:email] - end + post[:email] = options[:email] if options.has_key? :email - if options.has_key? :ip - post[:ipaddress] = options[:ip] - end + post[:ipaddress] = options[:ip] if options.has_key? :ip end def add_address(post, creditcard, options) if address = options[:billing_address] || options[:address] post[:address1] = address[:address1].to_s post[:address2] = address[:address2].to_s unless address[:address2].blank? - post[:company] = address[:company].to_s - post[:phone] = address[:phone].to_s - post[:zip] = address[:zip].to_s - post[:city] = address[:city].to_s - post[:country] = address[:country].to_s - post[:state] = address[:state].blank? ? 'n/a' : address[:state] + post[:company] = address[:company].to_s + post[:phone] = address[:phone].to_s + post[:zip] = address[:zip].to_s + post[:city] = address[:city].to_s + post[:country] = address[:country].to_s + post[:state] = address[:state].blank? ? 'n/a' : address[:state] end end @@ -127,7 +124,7 @@ def add_invoice(post, options) post[:orderdescription] = options[:description] end - def add_payment_source(params, source, options={}) + def add_payment_source(params, source, options = {}) case determine_funding_source(source) when :vault then add_customer_vault_id(params, source) when :credit_card then add_creditcard(params, source, options) @@ -135,18 +132,18 @@ def add_payment_source(params, source, options={}) end end - def add_customer_vault_id(params,vault_id) + def add_customer_vault_id(params, vault_id) params[:customer_vault_id] = vault_id end - def add_creditcard(post, creditcard,options) + def add_creditcard(post, creditcard, options) if options[:store] post[:customer_vault] = 'add_customer' post[:customer_vault_id] = options[:store] unless options[:store] == true end - post[:ccnumber] = creditcard.number + post[:ccnumber] = creditcard.number post[:cvv] = creditcard.verification_value if creditcard.verification_value? - post[:ccexp] = expdate(creditcard) + post[:ccexp] = expdate(creditcard) post[:firstname] = creditcard.first_name post[:lastname] = creditcard.last_name end @@ -163,7 +160,7 @@ def add_check(post, check) def parse(body) results = {} body.split(/&/).each do |pair| - key,val = pair.split(%r{=}) + key, val = pair.split(%r{=}) results[key] = val end @@ -171,22 +168,23 @@ def parse(body) end def commit(action, money, parameters) - parameters[:amount] = amount(money) if money + parameters[:amount] = amount(money) if money - response = parse( ssl_post(self.live_url, post_data(action,parameters)) ) + response = parse(ssl_post(self.live_url, post_data(action, parameters))) - Response.new(response['response'] == '1', message_from(response), response, - :authorization => response['transactionid'], - :test => test?, - :cvv_result => response['cvvresponse'], - :avs_result => { :code => response['avsresponse'] } + Response.new( + response['response'] == '1', + message_from(response), response, + authorization: response['transactionid'], + test: test?, + cvv_result: response['cvvresponse'], + avs_result: { code: response['avsresponse'] } ) - end def message_from(response) case response['responsetext'] - when 'SUCCESS','Approved' + when 'SUCCESS', 'Approved' 'This transaction has been approved' when 'DECLINE' 'This transaction has been declined' @@ -197,18 +195,17 @@ def message_from(response) def post_data(action, parameters = {}) post = {} - post[:username] = @options[:login] + post[:username] = @options[:login] 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) case when source.is_a?(String) then :vault - when CreditCard.card_companies.keys.include?(card_brand(source)) then :credit_card + when CreditCard.card_companies.include?(card_brand(source)) then :credit_card when card_brand(source) == 'check' then :check else raise ArgumentError, 'Unsupported funding source provided' end @@ -216,4 +213,3 @@ def determine_funding_source(source) end end end - diff --git a/lib/active_merchant/billing/gateways/instapay.rb b/lib/active_merchant/billing/gateways/instapay.rb index 13a41a76593..08154f8eaaa 100644 --- a/lib/active_merchant/billing/gateways/instapay.rb +++ b/lib/active_merchant/billing/gateways/instapay.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class InstapayGateway < Gateway self.live_url = 'https://trans.instapaygateway.com/cgi-bin/process.cgi' @@ -8,7 +8,7 @@ class InstapayGateway < Gateway self.money_format = :dollars self.default_currency = 'USD' # The card types supported by the payment gateway - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] # The homepage URL of the gateway self.homepage_url = 'http://www.instapayllc.com' @@ -140,24 +140,23 @@ def commit(action, parameters) data = ssl_post self.live_url, post_data(action, parameters) response = parse(data) - Response.new(response[:success] , response[:message], response, - :authorization => response[:transaction_id], - :avs_result => { :code => response[:avs_result] }, - :cvv_result => response[:cvv_result] + Response.new( + response[:success], + response[:message], + response, + authorization: response[:transaction_id], + avs_result: { code: response[:avs_result] }, + cvv_result: response[:cvv_result] ) end def post_data(action, parameters = {}) post = {} post[:acctid] = @options[:login] - if(@options[:password]) - post[:merchantpin] = @options[:password] - end + 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 end - diff --git a/lib/active_merchant/billing/gateways/ipg.rb b/lib/active_merchant/billing/gateways/ipg.rb new file mode 100644 index 00000000000..00bbe134640 --- /dev/null +++ b/lib/active_merchant/billing/gateways/ipg.rb @@ -0,0 +1,424 @@ +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/ipgapi/services' + + self.supported_countries = %w(AR) + self.default_currency = 'ARS' + self.supported_cardtypes = %i[visa master american_express discover] + + self.homepage_url = 'https://www.ipg-online.com' + self.display_name = 'IPG' + + CURRENCY_CODES = { + 'ARS' => '032' + } + + ACTION_REQUEST_ITEMS = %w(vault unstore) + + def initialize(options = {}) + requires!(options, :user_id, :password, :pem, :pem_password) + @credentials = options + @hosted_data_id = nil + super + end + + def purchase(money, payment, options = {}) + xml = build_purchase_and_authorize_request(money, payment, options) + + commit('sale', xml, options) + end + + def authorize(money, payment, options = {}) + xml = build_purchase_and_authorize_request(money, payment, options) + + commit('preAuth', xml, options) + end + + def capture(money, authorization, options = {}) + xml = build_capture_and_refund_request(money, authorization, options) + + commit('postAuth', xml, options) + end + + def refund(money, authorization, options = {}) + xml = build_capture_and_refund_request(money, authorization, options) + + commit('return', xml, options) + end + + def void(authorization, options = {}) + xml = Builder::XmlMarkup.new(indent: 2) + add_transaction_details(xml, options.merge!({ order_id: authorization })) + + commit('void', xml, options) + end + + def store(credit_card, options = {}) + @hosted_data_id = options[:hosted_data_id] || generate_unique_id + xml = Builder::XmlMarkup.new(indent: 2) + add_storage_item(xml, credit_card, options) + + commit('vault', xml, options) + end + + def unstore(hosted_data_id) + xml = Builder::XmlMarkup.new(indent: 2) + add_unstore_item(xml, hosted_data_id) + + commit('unstore', xml) + end + + def verify(credit_card, options = {}) + options[:currency] = self.default_currency unless options[:currency] && !options[:currency].empty? + MultiResponse.run(:use_first_response) do |r| + r.process { authorize(100, credit_card, options) } + r.process(:ignore_result) { void(r.authorization, options) } + end + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). + gsub(%r(().+()), '\1[FILTERED]\2'). + gsub(%r(().+()), '\1[FILTERED]\2') + end + + private + + NAMESPACE_BASE_URL = 'http://ipg-online.com' + + def build_purchase_and_authorize_request(money, payment, options) + xml = Builder::XmlMarkup.new(indent: 2) + + add_credit_card(xml, payment, options) + add_sub_merchant(xml, options[:submerchant]) if options[:submerchant] + add_three_d_secure(xml, options[:three_d_secure]) if options[:three_d_secure] + add_stored_credentials(xml, options) if options[:stored_credential] || options[:recurring_type] + add_payment(xml, money, payment, options) + add_transaction_details(xml, options) + add_billing(xml, options[:billing]) if options[:billing] + add_shipping(xml, options[:shipping]) if options[:shipping] + xml + end + + def build_capture_and_refund_request(money, authorization, options) + xml = Builder::XmlMarkup.new(indent: 2) + add_payment(xml, money, nil, options) + add_transaction_details(xml, options.merge!({ order_id: authorization }), true) + xml + end + + def build_order_request(xml, action, body) + xml.tag!('ipg:IPGApiOrderRequest') do + xml.tag!('v1:Transaction') do + add_transaction_type(xml, action) + xml << body.target! + end + end + end + + def build_action_request(xml, action, body) + xml.tag!('ns4:IPGApiActionRequest', ipg_action_namespaces) do + xml.tag!('ns2:Action') do + xml << body.target! + end + end + end + + def build_soap_request(action, body) + xml = Builder::XmlMarkup.new(indent: 2) + xml.tag!('soapenv:Envelope', envelope_namespaces) do + xml.tag!('soapenv:Header') + xml.tag!('soapenv:Body') do + build_order_request(xml, action, body) unless ACTION_REQUEST_ITEMS.include?(action) + build_action_request(xml, action, body) if ACTION_REQUEST_ITEMS.include?(action) + end + end + xml.target! + end + + def add_stored_credentials(xml, params) + recurring_type = params[:stored_credential][:initial_transaction] ? 'FIRST' : 'REPEAT' if params[:stored_credential] + recurring_type = params[:recurring_type] if params[:recurring_type] + xml.tag!('v1:recurringType', recurring_type) + end + + def add_storage_item(xml, credit_card, options) + requires!(options.merge!({ credit_card:, hosted_data_id: @hosted_data_id }), :credit_card, :hosted_data_id) + xml.tag!('ns2:StoreHostedData') do + xml.tag!('ns2:DataStorageItem') do + add_credit_card(xml, credit_card, {}, 'ns2') + add_three_d_secure(xml, options[:three_d_secure]) if options[:three_d_secure] + xml.tag!('ns2:HostedDataID', @hosted_data_id) if @hosted_data_id + end + end + end + + def add_unstore_item(xml, hosted_data_id) + requires!({}.merge!({ hosted_data_id: }), :hosted_data_id) + xml.tag!('ns2:StoreHostedData') do + xml.tag!('ns2:DataStorageItem') do + xml.tag!('ns2:Function', 'delete') + xml.tag!('ns2:HostedDataID', hosted_data_id) + end + end + end + + def add_transaction_type(xml, type) + xml.tag!('v1:CreditCardTxType') do + xml.tag!('v1:StoreId', @credentials[:store_id]) + xml.tag!('v1:Type', type) + end + end + + def add_credit_card(xml, payment, options = {}, credit_envelope = 'v1') + if payment&.is_a?(CreditCard) + requires!(options.merge!({ card_number: payment.number, month: payment.month, year: payment.year }), :card_number, :month, :year) + + xml.tag!("#{credit_envelope}:CreditCardData") do + xml.tag!('v1:CardNumber', payment.number) if payment.number + xml.tag!('v1:ExpMonth', format(payment.month, :two_digits)) if payment.month + xml.tag!('v1:ExpYear', format(payment.year, :two_digits)) if payment.year + xml.tag!('v1:CardCodeValue', payment.verification_value) if payment.verification_value + xml.tag!('v1:Brand', options[:brand]) if options[:brand] + end + end + + if options[:card_function_type] + xml.tag!('v1:cardFunction') do + xml.tag!('v1:Type', options[:card_function_type]) + end + end + + if options[:track_data] + xml.tag!("#{credit_envelope}:CreditCardData") do + xml.tag!('v1:TrackData', options[:track_data]) + end + end + end + + def add_sub_merchant(xml, submerchant) + xml.tag!('v1:SubMerchant') do + xml.tag!('v1:Mcc', submerchant[:mcc]) if submerchant[:mcc] + xml.tag!('v1:LegalName', submerchant[:legal_name]) if submerchant[:legal_name] + add_address(xml, submerchant[:address]) if submerchant[:address] + add_document(xml, submerchant[:document]) if submerchant[:document] + xml.tag!('v1:MerchantID', submerchant[:merchant_id]) if submerchant[:merchant_id] + end + end + + def add_address(xml, address) + xml.tag!('v1:Address') do + xml.tag!('v1:Address1', address[:address1]) if address[:address1] + xml.tag!('v1:Address2', address[:address2]) if address[:address2] + xml.tag!('v1:Zip', address[:zip]) if address[:zip] + xml.tag!('v1:City', address[:city]) if address[:city] + xml.tag!('v1:State', address[:state]) if address[:state] + xml.tag!('v1:Country', address[:country]) if address[:country] + end + end + + def add_document(xml, document) + xml.tag!('v1:Document') do + xml.tag!('v1:Type', document[:type]) if document[:type] + xml.tag!('v1:Number', document[:number]) if document[:number] + end + end + + def add_three_d_secure(xml, three_d_secure) + xml.tag!('v1:CreditCard3DSecure') do + xml.tag!('v1:AuthenticationValue', three_d_secure[:cavv]) if three_d_secure[:cavv] + xml.tag!('v1:XID', three_d_secure[:xid]) if three_d_secure[:xid] + xml.tag!('v1:Secure3D2TransactionStatus', three_d_secure[:directory_response_status]) if three_d_secure[:directory_response_status] + xml.tag!('v1:Secure3D2AuthenticationResponse', three_d_secure[:authentication_response_status]) if three_d_secure[:authentication_response_status] + xml.tag!('v1:Secure3DProtocolVersion', three_d_secure[:version]) if three_d_secure[:version] + xml.tag!('v1:DirectoryServerTransactionId', three_d_secure[:ds_transaction_id]) if three_d_secure[:ds_transaction_id] + end + end + + def add_transaction_details(xml, options, pre_order = false) + requires!(options, :order_id) if pre_order + xml.tag!('v1:TransactionDetails') do + xml.tag!('v1:OrderId', options[:order_id]) if options[:order_id] + xml.tag!('v1:MerchantTransactionId', options[:merchant_transaction_id]) if options[:merchant_transaction_id] + xml.tag!('v1:Ip', options[:ip]) if options[:ip] + xml.tag!('v1:Tdate', options[:t_date]) if options[:t_date] + xml.tag!('v1:IpgTransactionId', options[:ipg_transaction_id]) if options[:ipg_transaction_id] + xml.tag!('v1:ReferencedMerchantTransactionId', options[:referenced_merchant_transaction_id]) if options[:referenced_merchant_transaction_id] + xml.tag!('v1:TransactionOrigin', options[:transaction_origin]) if options[:transaction_origin] + xml.tag!('v1:InvoiceNumber', options[:invoice_number]) if options[:invoice_number] + xml.tag!('v1:DynamicMerchantName', options[:dynamic_merchant_name]) if options[:dynamic_merchant_name] + xml.tag!('v1:Comments', options[:comments]) if options[:comments] + if options[:terminal_id] + xml.tag!('v1:Terminal') do + xml.tag!('v1:TerminalID', options[:terminal_id]) if options[:terminal_id] + end + end + end + end + + def add_payment(xml, money, payment, options) + requires!(options.merge!({ money: }), :currency, :money) + xml.tag!('v1:Payment') do + xml.tag!('v1:HostedDataID', payment) if payment&.is_a?(String) + xml.tag!('v1:HostedDataStoreID', options[:hosted_data_store_id]) if options[:hosted_data_store_id] + xml.tag!('v1:DeclineHostedDataDuplicates', options[:decline_hosted_data_duplicates]) if options[:decline_hosted_data_duplicates] + 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', amount(money)) + xml.tag!('v1:Currency', CURRENCY_CODES[options[:currency]]) + xml.tag!('v1:numberOfInstallments', options[:number_of_installments]) if options[:number_of_installments] + end + end + + def add_billing(xml, billing) + xml.tag!('v1:Billing') do + xml.tag!('v1:CustomerID', billing[:customer_id]) if billing[:customer_id] + xml.tag!('v1:Name', billing[:name]) if billing[:name] + xml.tag!('v1:Company', billing[:company]) if billing[:company] + xml.tag!('v1:Address1', billing[:address_1]) if billing[:address_1] + xml.tag!('v1:Address2', billing[:address_2]) if billing[:address_2] + xml.tag!('v1:City', billing[:city]) if billing[:city] + xml.tag!('v1:State', billing[:state]) if billing[:state] + xml.tag!('v1:Zip', billing[:zip]) if billing[:zip] + xml.tag!('v1:Country', billing[:country]) if billing[:country] + xml.tag!('v1:Phone', billing[:phone]) if billing[:phone] + xml.tag!('v1:Fax', billing[:fax]) if billing[:fax] + xml.tag!('v1:Email', billing[:email]) if billing[:email] + end + end + + def add_shipping(xml, shipping) + xml.tag!('v1:Shipping') do + xml.tag!('v1:Type', shipping[:type]) if shipping[:type] + xml.tag!('v1:Name', shipping[:name]) if shipping[:name] + xml.tag!('v1:Address1', shipping[:address_1]) if shipping[:address_1] + xml.tag!('v1:Address2', shipping[:address_2]) if shipping[:address_2] + xml.tag!('v1:City', shipping[:city]) if shipping[:city] + xml.tag!('v1:State', shipping[:state]) if shipping[:state] + xml.tag!('v1:Zip', shipping[:zip]) if shipping[:zip] + xml.tag!('v1:Country', shipping[:country]) if shipping[:country] + end + end + + def build_header + { + 'Content-Type' => 'text/xml; charset=utf-8', + 'Authorization' => "Basic #{encoded_credentials}" + } + end + + def encoded_credentials + # 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 + { + 'xmlns:soapenv' => 'http://schemas.xmlsoap.org/soap/envelope/', + 'xmlns:ipg' => "#{NAMESPACE_BASE_URL}/ipgapi/schemas/ipgapi", + 'xmlns:v1' => "#{NAMESPACE_BASE_URL}/ipgapi/schemas/v1" + } + end + + def ipg_order_namespaces + { + 'xmlns:v1' => "#{NAMESPACE_BASE_URL}/ipgapi/schemas/v1", + 'xmlns:ipgapi' => "#{NAMESPACE_BASE_URL}/ipgapi/schemas/ipgapi" + } + end + + def ipg_action_namespaces + { + 'xmlns:ns4' => "#{NAMESPACE_BASE_URL}/ipgapi/schemas/ipgapi", + 'xmlns:ns2' => "#{NAMESPACE_BASE_URL}/ipgapi/schemas/a1", + 'xmlns:ns3' => "#{NAMESPACE_BASE_URL}/ipgapi/schemas/v1" + } + 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 + + def commit(action, request, options = {}) + override_store_id(options) + url = (test? ? test_url : live_url) + soap_request = build_soap_request(action, request) + response = parse(ssl_post(url, soap_request, build_header)) + Response.new( + response[:success], + message_from(response), + response, + authorization: authorization_from(action, response), + avs_result: AVSResult.new(code: response[:AVSResponse]), + cvv_result: CVVResult.new(response[:ProcessorCCVResponse]), + test: test?, + error_code: error_code_from(response) + ) + end + + def parse(xml) + reply = {} + xml = REXML::Document.new(xml) + root = REXML::XPath.first(xml, '//ipgapi:IPGApiOrderResponse') || REXML::XPath.first(xml, '//ipgapi:IPGApiActionResponse') || REXML::XPath.first(xml, '//SOAP-ENV:Fault') || REXML::XPath.first(xml, '//ns4:IPGApiActionResponse') + reply[:success] = REXML::XPath.first(xml, '//faultcode') ? false : true + if REXML::XPath.first(xml, '//ns4:IPGApiActionResponse') + reply[:tpv_error_code] = REXML::XPath.first(root, '//ns2:Error').attributes['Code'] + reply[:tpv_error_msg] = REXML::XPath.first(root, '//ns2:ErrorMessage').text + reply[:success] = false + end + root.elements.to_a.each do |node| + parse_element(reply, node) + end + reply[:hosted_data_id] = @hosted_data_id if @hosted_data_id + return reply + end + + def parse_element(reply, node) + if node.has_elements? + node.elements.each { |e| parse_element(reply, e) } + else + if /item/.match?(node.parent.name) + parent = node.parent.name + parent += '_' + node.parent.attributes['id'] if node.parent.attributes['id'] + parent += '_' + end + reply["#{parent}#{node.name}".to_sym] ||= node.text + end + return reply + end + + def message_from(response) + [response[:TransactionResult], response[:ErrorMessage]&.split(':')&.last&.strip].compact.join(', ') + end + + def authorization_from(action, response) + return (action == 'vault' ? response[:hosted_data_id] : response[:OrderId]) + end + + def error_code_from(response) + response[:ErrorMessage]&.split(':')&.first unless response[:success] + end + + def handle_response(response) + case response.code.to_i + when 200...300, 500 + response.body + else + raise ResponseError.new(response) + end + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/ipp.rb b/lib/active_merchant/billing/gateways/ipp.rb index a7595b82388..d693f8f021c 100644 --- a/lib/active_merchant/billing/gateways/ipp.rb +++ b/lib/active_merchant/billing/gateways/ipp.rb @@ -1,13 +1,13 @@ require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class IppGateway < Gateway self.live_url = 'https://www.ippayments.com.au/interface/api/dts.asmx' self.test_url = 'https://demo.ippayments.com.au/interface/api/dts.asmx' self.supported_countries = ['AU'] - self.supported_cardtypes = [:visa, :master, :american_express, :diners_club, :jcb] + self.supported_cardtypes = %i[visa master american_express diners_club jcb] self.homepage_url = 'http://www.ippayments.com.au/' self.display_name = 'IPP' @@ -18,15 +18,16 @@ class IppGateway < Gateway '05' => STANDARD_ERROR_CODE[:card_declined], '06' => STANDARD_ERROR_CODE[:processing_error], '14' => STANDARD_ERROR_CODE[:invalid_number], - '54' => STANDARD_ERROR_CODE[:expired_card], + '54' => STANDARD_ERROR_CODE[:expired_card] } - def initialize(options={}) + def initialize(options = {}) + ActiveMerchant.deprecated('IPP gateway is now named Bambora Asia-Pacific') requires!(options, :username, :password) super end - def purchase(money, payment, options={}) + def purchase(money, payment, options = {}) commit('SubmitSinglePayment') do |xml| xml.Transaction do xml.CustRef options[:order_id] @@ -39,7 +40,7 @@ def purchase(money, payment, options={}) end end - def authorize(money, payment, options={}) + def authorize(money, payment, options = {}) commit('SubmitSinglePayment') do |xml| xml.Transaction do xml.CustRef options[:order_id] @@ -52,7 +53,7 @@ def authorize(money, payment, options={}) end end - def capture(money, authorization, options={}) + def capture(money, authorization, options = {}) commit('SubmitSingleCapture') do |xml| xml.Capture do xml.Receipt authorization @@ -62,7 +63,7 @@ def capture(money, authorization, options={}) end end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) commit('SubmitSingleRefund') do |xml| xml.Refund do xml.Receipt authorization @@ -97,7 +98,7 @@ def add_amount(xml, money) end def add_credit_card(xml, payment) - xml.CreditCard :Registered => 'False' do + xml.CreditCard Registered: 'False' do xml.CardNumber payment.number xml.ExpM format(payment.month, :two_digits) xml.ExpY format(payment.year, :four_digits) @@ -120,7 +121,7 @@ def parse(body) def commit(action, &block) headers = { 'Content-Type' => 'text/xml; charset=utf-8', - 'SOAPAction' => "http://www.ippayments.com.au/interface/api/dts/#{action}", + 'SOAPAction' => "http://www.ippayments.com.au/interface/api/dts/#{action}" } response = parse(ssl_post(commit_url, new_submit_xml(action, &block), headers)) @@ -130,7 +131,7 @@ def commit(action, &block) response, authorization: authorization_from(response), error_code: error_code_from(response), - test: test?, + test: test? ) end diff --git a/lib/active_merchant/billing/gateways/iridium.rb b/lib/active_merchant/billing/gateways/iridium.rb index 102a1677029..e1fcbca3516 100644 --- a/lib/active_merchant/billing/gateways/iridium.rb +++ b/lib/active_merchant/billing/gateways/iridium.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # For more information on the Iridium Gateway please download the # documentation from their Merchant Management System. # @@ -10,12 +10,12 @@ class IridiumGateway < Gateway self.live_url = self.test_url = 'https://gw1.iridiumcorp.net/' # The countries the gateway supports merchants from as 2 digit ISO country codes - self.supported_countries = ['GB', 'ES'] + self.supported_countries = %w[GB ES] self.default_currency = 'EUR' self.money_format = :cents # The card types supported by the payment gateway - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :maestro, :jcb, :solo, :diners_club] + self.supported_cardtypes = %i[visa master american_express discover maestro jcb diners_club] # The homepage URL of the gateway self.homepage_url = 'http://www.iridiumcorp.co.uk/' @@ -203,7 +203,7 @@ class IridiumGateway < Gateway 'YER' => '886', 'ZAR' => '710', 'ZMK' => '894', - 'ZWD' => '716', + 'ZWD' => '716' } AVS_CODE = { @@ -251,16 +251,16 @@ def capture(money, authorization, options = {}) commit(build_reference_request('COLLECTION', money, authorization, options), options) end - def credit(money, authorization, options={}) + def credit(money, authorization, options = {}) ActiveMerchant.deprecated CREDIT_DEPRECATION_MESSAGE refund(money, authorization, options) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) commit(build_reference_request('REFUND', money, authorization, options), options) end - def void(authorization, options={}) + def void(authorization, options = {}) commit(build_reference_request('VOID', nil, authorization, options), options) end @@ -278,7 +278,7 @@ def scrub(transcript) private def build_purchase_request(type, money, creditcard, options) - options.merge!(:action => 'CardDetailsTransaction') + options[:action] = 'CardDetailsTransaction' build_request(options) do |xml| add_purchase_data(xml, type, money, options) add_creditcard(xml, creditcard) @@ -287,16 +287,17 @@ def build_purchase_request(type, money, creditcard, options) end def build_reference_request(type, money, authorization, options) - options.merge!(:action => 'CrossReferenceTransaction') - order_id, cross_reference, _ = authorization.split(';') + options[:action] = 'CrossReferenceTransaction' + order_id, cross_reference, = authorization.split(';') build_request(options) do |xml| if money - details = {'CurrencyCode' => currency_code(options[:currency] || default_currency), 'Amount' => amount(money)} + currency = options[:currency] || currency(money) + details = { 'CurrencyCode' => currency_code(currency), 'Amount' => localized_amount(money, currency) } else - details = {'CurrencyCode' => currency_code(default_currency), 'Amount' => '0'} + details = { 'CurrencyCode' => currency_code(default_currency), 'Amount' => '0' } end xml.tag! 'TransactionDetails', details do - xml.tag! 'MessageDetails', {'TransactionType' => type, 'CrossReference' => cross_reference} + xml.tag! 'MessageDetails', { 'TransactionType' => type, 'CrossReference' => cross_reference } xml.tag! 'OrderID', (options[:order_id] || order_id) end end @@ -304,13 +305,13 @@ def build_reference_request(type, money, authorization, options) def build_request(options) requires!(options, :action) - xml = Builder::XmlMarkup.new :indent => 2 - xml.instruct!(:xml, :version => '1.0', :encoding => 'utf-8') + xml = Builder::XmlMarkup.new indent: 2 + xml.instruct!(:xml, version: '1.0', encoding: 'utf-8') xml.tag! 'soap:Envelope', { 'xmlns:soap' => 'http://schemas.xmlsoap.org/soap/envelope/', 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', - 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema'} do + 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema' } do xml.tag! 'soap:Body' do - xml.tag! options[:action], {'xmlns' => 'https://www.thepaymentgateway.net/'} do + xml.tag! options[:action], { 'xmlns' => 'https://www.thepaymentgateway.net/' } do xml.tag! 'PaymentMessage' do add_merchant_data(xml, options) yield(xml) @@ -327,9 +328,10 @@ def setup_address_hash(options) end def add_purchase_data(xml, type, money, options) + currency = options[:currency] || currency(money) requires!(options, :order_id) - xml.tag! 'TransactionDetails', {'Amount' => amount(money), 'CurrencyCode' => currency_code(options[:currency] || currency(money))} do - xml.tag! 'MessageDetails', {'TransactionType' => type} + xml.tag! 'TransactionDetails', { 'Amount' => localized_amount(money, currency), 'CurrencyCode' => currency_code(currency) } do + xml.tag! 'MessageDetails', { 'TransactionType' => type } xml.tag! 'OrderID', options[:order_id] xml.tag! 'TransactionControl' do xml.tag! 'ThreeDSecureOverridePolicy', 'FALSE' @@ -342,9 +344,7 @@ def add_purchase_data(xml, type, money, options) def add_customerdetails(xml, creditcard, address, options, shipTo = false) xml.tag! 'CustomerDetails' do if address - unless address[:country].blank? - country_code = Country.find(address[:country]).code(:numeric) - end + country_code = Country.find(address[:country]).code(:numeric) unless address[:country].blank? xml.tag! 'BillingAddress' do xml.tag! 'Address1', address[:address1] xml.tag! 'Address2', address[:address2] @@ -371,35 +371,44 @@ def add_creditcard(xml, creditcard) end def add_merchant_data(xml, options) - xml.tag! 'MerchantAuthentication', {'MerchantID' => @options[:login], 'Password' => @options[:password]} + xml.tag! 'MerchantAuthentication', { 'MerchantID' => @options[:login], 'Password' => @options[:password] } end 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, - :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] ], + 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, + test: test?, + 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) reply = {} xml = REXML::Document.new(xml) - if (root = REXML::XPath.first(xml, '//CardDetailsTransactionResponse')) or - (root = REXML::XPath.first(xml, '//CrossReferenceTransactionResponse')) + if (root = REXML::XPath.first(xml, '//CardDetailsTransactionResponse')) || + (root = REXML::XPath.first(xml, '//CrossReferenceTransactionResponse')) root.elements.to_a.each do |node| case node.name when 'Message' @@ -419,39 +428,39 @@ def parse_element(reply, node) case node.name when 'CrossReferenceTransactionResult' reply[:transaction_result] = {} - node.attributes.each do |a,b| + node.attributes.each do |a, b| reply[:transaction_result][a.underscore.to_sym] = b end - node.elements.each{|e| parse_element(reply[:transaction_result], e) } if node.has_elements? + node.elements.each { |e| parse_element(reply[:transaction_result], e) } if node.has_elements? when 'CardDetailsTransactionResult' reply[:transaction_result] = {} - node.attributes.each do |a,b| + node.attributes.each do |a, b| reply[:transaction_result][a.underscore.to_sym] = b end - node.elements.each{|e| parse_element(reply[:transaction_result], e) } if node.has_elements? + node.elements.each { |e| parse_element(reply[:transaction_result], e) } if node.has_elements? when 'TransactionOutputData' reply[:transaction_output_data] = {} - node.attributes.each{|a,b| reply[:transaction_output_data][a.underscore.to_sym] = b } - node.elements.each{|e| parse_element(reply[:transaction_output_data], e) } if node.has_elements? + node.attributes.each { |a, b| reply[:transaction_output_data][a.underscore.to_sym] = b } + node.elements.each { |e| parse_element(reply[:transaction_output_data], e) } if node.has_elements? when 'CustomVariables' reply[:custom_variables] = {} - node.attributes.each{|a,b| reply[:custom_variables][a.underscore.to_sym] = b } - node.elements.each{|e| parse_element(reply[:custom_variables], e) } if node.has_elements? + node.attributes.each { |a, b| reply[:custom_variables][a.underscore.to_sym] = b } + node.elements.each { |e| parse_element(reply[:custom_variables], e) } if node.has_elements? when 'GatewayEntryPoints' reply[:gateway_entry_points] = {} - node.attributes.each{|a,b| reply[:gateway_entry_points][a.underscore.to_sym] = b } - node.elements.each{|e| parse_element(reply[:gateway_entry_points], e) } if node.has_elements? + node.attributes.each { |a, b| reply[:gateway_entry_points][a.underscore.to_sym] = b } + node.elements.each { |e| parse_element(reply[:gateway_entry_points], e) } if node.has_elements? else k = node.name.underscore.to_sym if node.has_elements? reply[k] = {} - node.elements.each{|e| parse_element(reply[k], e) } + node.elements.each { |e| parse_element(reply[k], e) } else if node.has_attributes? reply[k] = {} - node.attributes.each{|a,b| reply[k][a.underscore.to_sym] = b } + node.attributes.each { |a, b| reply[k][a.underscore.to_sym] = b } else reply[k] = node.text end diff --git a/lib/active_merchant/billing/gateways/itransact.rb b/lib/active_merchant/billing/gateways/itransact.rb index 0d96f7556e6..972542b920e 100644 --- a/lib/active_merchant/billing/gateways/itransact.rb +++ b/lib/active_merchant/billing/gateways/itransact.rb @@ -1,7 +1,7 @@ require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # iTransact, Inc. is an authorized reseller of the PaymentClearing gateway. If your merchant service provider uses PaymentClearing.com to process payments, you can use this module. # # @@ -38,7 +38,7 @@ class ItransactGateway < Gateway self.supported_countries = ['US'] # The card types supported by the payment gateway - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] # The homepage URL of the gateway self.homepage_url = 'http://www.itransact.com/' @@ -301,8 +301,8 @@ def add_customer_data(xml, payment_source, options) def add_invoice(xml, money, options) xml.AuthCode options[:force] if options[:force] if options[:order_items].blank? - xml.Total(amount(money)) unless(money.nil? || money < 0.01) - xml.Description(options[:description]) unless( options[:description].blank?) + xml.Total(amount(money)) unless money.nil? || money < 0.01 + xml.Description(options[:description]) unless options[:description].blank? else xml.OrderItems { options[:order_items].each do |item| @@ -336,7 +336,7 @@ def add_creditcard(xml, creditcard) xml.AccountInfo { xml.CardAccount { xml.AccountNumber(creditcard.number.to_s) - xml.ExpirationMonth(creditcard.month.to_s.rjust(2,'0')) + xml.ExpirationMonth(creditcard.month.to_s.rjust(2, '0')) xml.ExpirationYear(creditcard.year.to_s) xml.CVVNumber(creditcard.verification_value.to_s) unless creditcard.verification_value.blank? } @@ -371,8 +371,9 @@ def add_transaction_control(xml, options) def add_vendor_data(xml, options) return if options[:vendor_data].blank? + xml.VendorData { - options[:vendor_data].each do |k,v| + options[:vendor_data].each do |k, v| xml.Element { xml.Name(k) xml.Key(v) @@ -386,15 +387,19 @@ 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, - :test => test?, - :authorization => response[:xid], - :avs_result => { :code => response[:avs_response] }, - :cvv_result => response[:cvv_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] + ) end def post_data(payload) - payload_xml = payload.root.to_xml(:indent => 0) + payload_xml = payload.root.to_xml(indent: 0) payload_signature = sign_payload(payload_xml) @@ -409,7 +414,7 @@ def post_data(payload) end.doc request.root.children.first.after payload.root - request.to_xml(:indent => 0) + request.to_xml(indent: 0) end def parse(raw_xml) @@ -424,7 +429,7 @@ def parse(raw_xml) def successful?(response) # Turns out the PaymentClearing gateway is not consistent... - response[:status].downcase =='ok' + response[:status].casecmp('ok').zero? end def test_mode?(response) @@ -438,11 +443,10 @@ def message_from(response) def sign_payload(payload) key = @options[:password].to_s - digest=OpenSSL::HMAC.digest(OpenSSL::Digest::SHA1.new(key), key, payload) + digest = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA1.new(key), key, payload) signature = Base64.encode64(digest) signature.chomp! end end end end - diff --git a/lib/active_merchant/billing/gateways/iveri.rb b/lib/active_merchant/billing/gateways/iveri.rb index 64f08999b39..9f72d701a45 100644 --- a/lib/active_merchant/billing/gateways/iveri.rb +++ b/lib/active_merchant/billing/gateways/iveri.rb @@ -1,24 +1,27 @@ require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class IveriGateway < Gateway + class_attribute :iveri_url + self.live_url = self.test_url = 'https://portal.nedsecure.co.za/iVeriWebService/Service.asmx' + self.iveri_url = 'https://portal.host.iveri.com/iVeriWebService/Service.asmx' - self.supported_countries = ['US', 'ZA', 'GB'] + self.supported_countries = %w[US ZA GB] self.default_currency = 'ZAR' self.money_format = :cents - self.supported_cardtypes = [:visa, :master, :american_express] + self.supported_cardtypes = %i[visa master american_express] self.homepage_url = 'http://www.iveri.com' self.display_name = 'iVeri' - def initialize(options={}) + def initialize(options = {}) requires!(options, :app_id, :cert_id) super end - def purchase(money, payment_method, options={}) + def purchase(money, payment_method, options = {}) post = build_vxml_request('Debit', options) do |xml| add_auth_purchase_params(xml, money, payment_method, options) end @@ -26,7 +29,7 @@ def purchase(money, payment_method, options={}) commit(post) end - def authorize(money, payment_method, options={}) + def authorize(money, payment_method, options = {}) post = build_vxml_request('Authorisation', options) do |xml| add_auth_purchase_params(xml, money, payment_method, options) end @@ -34,7 +37,7 @@ def authorize(money, payment_method, options={}) commit(post) end - def capture(money, authorization, options={}) + def capture(money, authorization, options = {}) post = build_vxml_request('Debit', options) do |xml| add_authorization(xml, authorization, options) end @@ -42,7 +45,7 @@ def capture(money, authorization, options={}) commit(post) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) post = build_vxml_request('Credit', options) do |xml| add_amount(xml, money, options) add_authorization(xml, authorization, options) @@ -51,21 +54,26 @@ def refund(money, authorization, options={}) commit(post) end - def void(authorization, options={}) - post = build_vxml_request('Void', options) do |xml| + def void(authorization, options = {}) + txn_type = options[:reference_type] == :authorize ? 'AuthorisationReversal' : 'Void' + post = build_vxml_request(txn_type, options) do |xml| add_authorization(xml, authorization, options) end commit(post) end - def verify(credit_card, options={}) - authorize(0, credit_card, options) + def verify(credit_card, options = {}) + MultiResponse.run(:use_first_response) do |r| + r.process { authorize(100, credit_card, options) } + r.process(:ignore_result) { void(r.authorization, options.merge(reference_type: :authorize)) } + end end def verify_credentials void = void('', options) - return true if void.message == 'Missing OriginalMerchantTrace' + return true if void.message == 'Missing OriginalMerchantTrace' + false end @@ -83,11 +91,11 @@ def scrub(transcript) private def build_xml_envelope(vxml) - builder = Nokogiri::XML::Builder.new(:encoding => 'UTF-8') do |xml| + builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml| xml[:soap].Envelope 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema', 'xmlns:soap' => 'http://schemas.xmlsoap.org/soap/envelope/' do xml[:soap].Body do xml.Execute 'xmlns' => 'http://iveri.com/' do - xml.validateRequest 'true' + xml.validateRequest('false') xml.protocol 'V_XML' xml.protocolVersion '2.0' xml.request vxml @@ -114,8 +122,9 @@ def build_vxml_request(action, options) def add_auth_purchase_params(post, money, payment_method, options) add_card_holder_authentication(post, options) add_amount(post, money, options) - add_electronic_commerce_indicator(post, options) + add_electronic_commerce_indicator(post, options) unless options[:three_d_secure] add_payment_method(post, payment_method, options) + add_three_ds(post, options) end def add_amount(post, money, options) @@ -150,11 +159,12 @@ def add_card_holder_authentication(post, options) end def commit(post) - raw_response = begin - ssl_post(live_url, build_xml_envelope(post), headers(post)) - rescue ActiveMerchant::ResponseError => e - e.response.body - end + raw_response = + begin + ssl_post(url, build_xml_envelope(post), headers(post)) + rescue ActiveMerchant::ResponseError => e + e.response.body + end parsed = parse(raw_response) succeeded = success_from(parsed) @@ -173,6 +183,10 @@ def mode test? ? 'Test' : 'Live' end + def url + @options[:url_override].to_s == 'iveri' ? iveri_url : live_url + end + def headers(post) { 'Content-Type' => 'text/xml; charset=utf-8', @@ -187,7 +201,7 @@ def parse(body) vxml = Nokogiri::XML(body).remove_namespaces!.xpath('//Envelope/Body/ExecuteResponse/ExecuteResult').inner_text doc = Nokogiri::XML(vxml) doc.xpath('*').each do |node| - if (node.elements.empty?) + if node.elements.empty? parsed[underscore(node.name)] = node.text else node.elements.each do |childnode| @@ -201,14 +215,14 @@ def parse(body) def parse_element(parsed, node) if !node.attributes.empty? node.attributes.each do |a| - parsed[underscore(node.name)+ '_' + underscore(a[1].name)] = a[1].value + parsed[underscore(node.name) + '_' + underscore(a[1].name)] = a[1].value 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 @@ -234,18 +248,44 @@ def split_auth(authorization) end def error_code_from(response, succeeded) - unless succeeded - response['result_code'] - end + response['result_code'] unless succeeded end def underscore(camel_cased_word) camel_cased_word.to_s.gsub(/::/, '/'). - gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). - gsub(/([a-z\d])([A-Z])/,'\1_\2'). + gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2'). + gsub(/([a-z\d])([A-Z])/, '\1_\2'). tr('-', '_'). downcase end + + def add_three_ds(post, options) + return unless three_d_secure = options[:three_d_secure] + + post.ElectronicCommerceIndicator(formatted_three_ds_eci(three_d_secure[:eci])) if three_d_secure[:eci] + post.CardHolderAuthenticationID(three_d_secure[:xid]) if three_d_secure[:xid] + post.CardHolderAuthenticationData(three_d_secure[:cavv]) if three_d_secure[:cavv] + post.ThreeDSecure_ProtocolVersion(three_d_secure[:version]) if three_d_secure[:version] + post.ThreeDSecure_DSTransID(three_d_secure[:ds_transaction_id]) if three_d_secure[:ds_transaction_id] + post.ThreeDSecure_VEResEnrolled(formatted_enrollment(three_d_secure[:enrolled])) if three_d_secure[:enrolled] + end + + def formatted_enrollment(val) + case val + when 'Y', 'N', 'U' then val + when true, 'true' then 'Y' + when false, 'false' then 'N' + end + end + + def formatted_three_ds_eci(val) + case val + when '05', '02' then 'ThreeDSecure' + when '06', '01' then 'ThreeDSecureAttempted' + when '07' then 'SecureChannel' + else val + end + end end end end diff --git a/lib/active_merchant/billing/gateways/ixopay.rb b/lib/active_merchant/billing/gateways/ixopay.rb new file mode 100644 index 00000000000..fa2fcaa7635 --- /dev/null +++ b/lib/active_merchant/billing/gateways/ixopay.rb @@ -0,0 +1,320 @@ +require 'nokogiri' + +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class IxopayGateway < Gateway + self.test_url = 'https://secure.ixopay.com/transaction' + self.live_url = 'https://secure.ixopay.com/transaction' + + self.supported_countries = %w(AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BQ BR BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ EC EE EG EH ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HM HN HR HT HU ID IE IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM KN KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ TC TD TF TG TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI VN VU WF WS YE YT ZA ZM ZW) + self.default_currency = 'EUR' + self.currencies_with_three_decimal_places = %w(BHD IQD JOD KWD LWD OMR TND) + self.supported_cardtypes = %i[visa master american_express discover diners_club jcb maestro] + + self.homepage_url = 'https://www.ixopay.com' + self.display_name = 'Ixopay' + + def initialize(options = {}) + requires!(options, :username, :password, :secret, :api_key) + @secret = options[:secret] + super + end + + def purchase(money, payment_method, options = {}) + request = build_xml_request do |xml| + add_card_data(xml, payment_method) + add_debit(xml, money, options) + end + + commit(request) + end + + def authorize(money, payment_method, options = {}) + request = build_xml_request do |xml| + add_card_data(xml, payment_method) + add_preauth(xml, money, options) + end + + commit(request) + end + + def capture(money, authorization, options = {}) + request = build_xml_request do |xml| + add_capture(xml, money, authorization, options) + end + + commit(request) + end + + def refund(money, authorization, options = {}) + request = build_xml_request do |xml| + add_refund(xml, money, authorization, options) + end + + commit(request) + end + + def void(authorization, options = {}) + request = build_xml_request do |xml| + add_void(xml, authorization) + end + + commit(request) + end + + def verify(credit_card, options = {}) + MultiResponse.run(:use_first_response) do |r| + r.process { authorize(100, credit_card, options) } + r.process(:ignore_result) { void(r.authorization, options) } + end + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + clean_transcript = remove_invalid_utf_8_byte_sequences(transcript) + + clean_transcript. + gsub(%r((Authorization: Gateway )(.*)(:)), '\1[FILTERED]\3'). + gsub(%r(()(.*)()), '\1[FILTERED]\3'). + gsub(%r(()(.*)()), '\1[FILTERED]\3'). + gsub(%r(()\d+()), '\1[FILTERED]\2') + end + + private + + def remove_invalid_utf_8_byte_sequences(text) + text.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '') + end + + def headers(xml) + timestamp = Time.now.httpdate + signature = generate_signature('POST', xml, timestamp) + + { + 'Authorization' => "Gateway #{options[:api_key]}:#{signature}", + 'Date' => timestamp, + 'Content-Type' => 'text/xml; charset=utf-8' + } + end + + def generate_signature(http_method, xml, timestamp) + content_type = 'text/xml; charset=utf-8' + message = "#{http_method}\n#{Digest::MD5.hexdigest(xml)}\n#{content_type}\n#{timestamp}\n\n/transaction" + digest = OpenSSL::Digest.new('sha512') + hmac = OpenSSL::HMAC.digest(digest, @secret, message) + + Base64.encode64(hmac).delete("\n") + end + + def parse(body) + xml = Nokogiri::XML(body) + response = Hash.from_xml(xml.to_s)['result'] + + response.deep_transform_keys(&:underscore).transform_keys(&:to_sym) + end + + def build_xml_request + builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml| + xml.transactionWithCard 'xmlns' => 'http://secure.ixopay.com/Schema/V2/TransactionWithCard' do + xml.username @options[:username] + xml.password Digest::SHA1.hexdigest(@options[:password]) + yield(xml) + end + end + + builder.to_xml + end + + def add_card_data(xml, payment_method) + xml.cardData do + xml.cardHolder payment_method.name + xml.pan payment_method.number + xml.cvv payment_method.verification_value + xml.expirationMonth format(payment_method.month, :two_digits) + xml.expirationYear format(payment_method.year, :four_digits) + end + end + + def add_debit(xml, money, options) + currency = options[:currency] || currency(money) + description = options[:description].blank? ? 'Purchase' : options[:description] + + xml.debit do + xml.transactionId new_transaction_id + + add_customer_data(xml, options) + add_extra_data(xml, options[:extra_data]) if options[:extra_data] + + xml.amount localized_amount(money, currency) + xml.currency currency + xml.description description + xml.callbackUrl(options[:callback_url]) + add_stored_credentials(xml, options) + end + end + + def add_preauth(xml, money, options) + description = options[:description].blank? ? 'Preauthorize' : options[:description] + currency = options[:currency] || currency(money) + callback_url = options[:callback_url] + + xml.preauthorize do + xml.transactionId new_transaction_id + + add_customer_data(xml, options) + add_extra_data(xml, options[:extra_data]) if options[:extra_data] + + xml.amount localized_amount(money, currency) + xml.currency currency + xml.description description + xml.callbackUrl callback_url + add_stored_credentials(xml, options) + end + end + + def add_refund(xml, money, authorization, options) + currency = options[:currency] || currency(money) + + xml.refund do + xml.transactionId new_transaction_id + add_extra_data(xml, options[:extra_data]) if options[:extra_data] + xml.referenceTransactionId authorization&.split('|')&.first + xml.amount localized_amount(money, currency) + xml.currency currency + end + end + + def add_void(xml, authorization) + xml.void do + xml.transactionId new_transaction_id + add_extra_data(xml, options[:extra_data]) if options[:extra_data] + xml.referenceTransactionId authorization&.split('|')&.first + end + end + + def add_capture(xml, money, authorization, options) + currency = options[:currency] || currency(money) + + xml.capture_ do + xml.transactionId new_transaction_id + add_extra_data(xml, options[:extra_data]) if options[:extra_data] + xml.referenceTransactionId authorization&.split('|')&.first + xml.amount localized_amount(money, currency) + xml.currency currency + end + end + + def add_customer_data(xml, options) + # Ixopay returns an error if the elements are not added in the order used here. + xml.customer do + add_billing_address(xml, options[:billing_address]) if options[:billing_address] + add_shipping_address(xml, options[:shipping_address]) if options[:shipping_address] + + xml.company options[:billing_address][:company] if options.dig(:billing_address, :company) + xml.email options[:email] + xml.ipAddress(options[:ip] || '127.0.0.1') + end + end + + def add_billing_address(xml, address) + if address[:name] + xml.firstName split_names(address[:name])[0] + xml.lastName split_names(address[:name])[1] + end + + xml.billingAddress1 address[:address1] + xml.billingAddress2 address[:address2] + xml.billingCity address[:city] + xml.billingPostcode address[:zip] + xml.billingState address[:state] + xml.billingCountry address[:country] + xml.billingPhone address[:phone] + end + + def add_shipping_address(xml, address) + if address[:name] + xml.shippingFirstName split_names(address[:name])[0] + xml.shippingLastName split_names(address[:name])[1] + end + + xml.shippingCompany address[:company] + xml.shippingAddress1 address[:address1] + xml.shippingAddress2 address[:address2] + xml.shippingCity address[:city] + xml.shippingPostcode address[:zip] + xml.shippingState address[:state] + xml.shippingCountry address[:country] + xml.shippingPhone address[:phone] + end + + def new_transaction_id + SecureRandom.uuid + end + + # Ixopay does not pass any parameters for cardholder/merchant initiated. + # Ixopay also doesn't support installment transactions, only recurring + # ("RECURRING") and unscheduled ("CARDONFILE"). + # + # Furthermore, Ixopay is slightly unusual in its application of stored + # credentials in that the gateway does not return a true + # network_transaction_id that can be sent on subsequent transactions. + def add_stored_credentials(xml, options) + return unless stored_credential = options[:stored_credential] + + if stored_credential[:initial_transaction] + xml.transactionIndicator 'INITIAL' + elsif stored_credential[:reason_type] == 'recurring' + xml.transactionIndicator 'RECURRING' + elsif stored_credential[:reason_type] == 'unscheduled' + xml.transactionIndicator 'CARDONFILE' + end + end + + def add_extra_data(xml, extra_data) + extra_data.each do |k, v| + xml.extraData(v, key: k) + end + end + + def commit(request) + url = (test? ? test_url : live_url) + + # ssl_post raises an exception for any non-2xx HTTP status from the gateway + response = + begin + parse(ssl_post(url, request, headers(request))) + rescue StandardError => e + parse(e.response.body) + end + + Response.new( + success_from(response), + message_from(response), + response, + authorization: authorization_from(response), + test: test?, + error_code: error_code_from(response) + ) + end + + def success_from(response) + response[:success] == 'true' + end + + def message_from(response) + response.dig(:errors, 'error', 'message') || response[:return_type] + end + + def authorization_from(response) + response[:reference_id] ? "#{response[:reference_id]}|#{response[:purchase_id]}" : nil + end + + def error_code_from(response) + response.dig(:errors, 'error', 'code') unless success_from(response) + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/jetpay.rb b/lib/active_merchant/billing/gateways/jetpay.rb index 948c4799941..f576b76ad10 100644 --- a/lib/active_merchant/billing/gateways/jetpay.rb +++ b/lib/active_merchant/billing/gateways/jetpay.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class JetpayGateway < Gateway class_attribute :live_us_url, :live_ca_url @@ -8,10 +8,10 @@ class JetpayGateway < Gateway self.live_ca_url = 'https://gateway17.jetpay.com/canada-bb' # The countries the gateway supports merchants from as 2 digit ISO country codes - self.supported_countries = ['US', 'CA'] + self.supported_countries = %w[US CA] # The card types supported by the payment gateway - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] # The homepage URL of the gateway self.homepage_url = 'http://www.jetpay.com/' @@ -213,9 +213,7 @@ def build_xml_request(transaction_type, options = {}, transaction_id = nil, &blo xml.tag! 'TerminalID', @options[:login] xml.tag! 'TransactionType', transaction_type xml.tag! 'TransactionID', transaction_id.nil? ? generate_unique_id.slice(0, 18) : transaction_id - if options && options[:origin] - xml.tag! 'Origin', options[:origin] - end + xml.tag! 'Origin', options[:origin] if options && options[:origin] if block_given? yield xml @@ -286,13 +284,14 @@ 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] + test: test?, + authorization: authorization_from(response, money, token), + avs_result: { code: response[:avs] }, + cvv_result: response[:cvv2] ) end @@ -315,7 +314,7 @@ def parse(body) def parse_element(response, node) if node.has_elements? - node.elements.each{|element| parse_element(response, element) } + node.elements.each { |element| parse_element(response, element) } else response[node.name.underscore.to_sym] = node.text end @@ -335,7 +334,7 @@ def message_from(response) def authorization_from(response, money, previous_token) original_amount = amount(money) if money - [ response[:transaction_id], response[:approval], original_amount, (response[:token] || previous_token)].join(';') + [response[:transaction_id], response[:approval], original_amount, (response[:token] || previous_token)].join(';') end def add_credit_card(xml, credit_card) @@ -343,13 +342,9 @@ def add_credit_card(xml, credit_card) xml.tag! 'CardExpMonth', format_exp(credit_card.month) xml.tag! 'CardExpYear', format_exp(credit_card.year) - if credit_card.first_name || credit_card.last_name - xml.tag! 'CardName', [credit_card.first_name,credit_card.last_name].compact.join(' ') - end + xml.tag! 'CardName', [credit_card.first_name, credit_card.last_name].compact.join(' ') if credit_card.first_name || credit_card.last_name - unless credit_card.verification_value.nil? || (credit_card.verification_value.length == 0) - xml.tag! 'CVV2', credit_card.verification_value - end + xml.tag! 'CVV2', credit_card.verification_value unless credit_card.verification_value.nil? || (credit_card.verification_value.length == 0) end def add_addresses(xml, options) @@ -395,7 +390,7 @@ def add_user_defined_fields(xml, options) def lookup_country_code(code) country = Country.find(code) rescue nil - country && country.code(:alpha3) + country&.code(:alpha3) end end end diff --git a/lib/active_merchant/billing/gateways/jetpay_v2.rb b/lib/active_merchant/billing/gateways/jetpay_v2.rb index 0eb900d2aea..1ce991efe99 100644 --- a/lib/active_merchant/billing/gateways/jetpay_v2.rb +++ b/lib/active_merchant/billing/gateways/jetpay_v2.rb @@ -1,13 +1,13 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class JetpayV2Gateway < Gateway self.test_url = 'https://test1.jetpay.com/jetpay' self.live_url = 'https://gateway20.jetpay.com/jetpay' self.money_format = :cents self.default_currency = 'USD' - self.supported_countries = ['US', 'CA'] - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_countries = %w[US CA] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'http://www.jetpay.com' self.display_name = 'JetPay' @@ -210,8 +210,8 @@ def build_xml_request(transaction_type, options = {}, transaction_id = nil, &blo xml.tag! 'TransactionID', transaction_id.nil? ? generate_unique_id.slice(0, 18) : transaction_id xml.tag! 'Origin', options[:origin] || 'INTERNET' xml.tag! 'IndustryInfo', 'Type' => options[:industry_info] || 'ECOMMERCE' - xml.tag! 'Application', (options[:application] || 'n/a'), {'Version' => options[:application_version] || '1.0'} - xml.tag! 'Device', (options[:device] || 'n/a'), {'Version' => options[:device_version] || '1.0'} + xml.tag! 'Application', (options[:application] || 'n/a'), { 'Version' => options[:application_version] || '1.0' } + xml.tag! 'Device', (options[:device] || 'n/a'), { 'Version' => options[:device_version] || '1.0' } xml.tag! 'Library', 'VirtPOS SDK', 'Version' => '1.5' xml.tag! 'Gateway', 'JetPay' xml.tag! 'DeveloperID', options[:developer_id] || 'n/a' @@ -295,14 +295,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 => AVSResult.new(:code => response[:avs]), - :cvv_result => CVVResult.new(response[:cvv2]), - :error_code => success ? nil : error_code_from(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) ) end @@ -324,7 +325,7 @@ def parse(body) def parse_element(response, node) if node.has_elements? - node.elements.each{|element| parse_element(response, element) } + node.elements.each { |element| parse_element(response, element) } else response[node.name.underscore.to_sym] = node.text end @@ -344,7 +345,7 @@ def message_from(response) def authorization_from(response, money, previous_token) original_amount = amount(money) if money - [ response[:transaction_id], response[:approval], original_amount, (response[:token] || previous_token)].join(';') + [response[:transaction_id], response[:approval], original_amount, (response[:token] || previous_token)].join(';') end def error_code_from(response) @@ -368,13 +369,9 @@ def add_credit_card(xml, credit_card) xml.tag! 'CardExpMonth', format_exp(credit_card.month) xml.tag! 'CardExpYear', format_exp(credit_card.year) - if credit_card.first_name || credit_card.last_name - xml.tag! 'CardName', [credit_card.first_name,credit_card.last_name].compact.join(' ') - end + xml.tag! 'CardName', [credit_card.first_name, credit_card.last_name].compact.join(' ') if credit_card.first_name || credit_card.last_name - unless credit_card.verification_value.nil? || (credit_card.verification_value.length == 0) - xml.tag! 'CVV2', credit_card.verification_value - end + xml.tag! 'CVV2', credit_card.verification_value unless credit_card.verification_value.nil? || (credit_card.verification_value.length == 0) end def add_addresses(xml, options) @@ -410,7 +407,7 @@ def add_customer_data(xml, options) def add_invoice_data(xml, options) xml.tag! 'OrderNumber', options[:order_id] if options[:order_id] if tax_amount = options[:tax_amount] - xml.tag! 'TaxAmount', tax_amount, {'ExemptInd' => options[:tax_exempt] || 'false'} + xml.tag! 'TaxAmount', tax_amount, { 'ExemptInd' => options[:tax_exempt] || 'false' } end end @@ -430,7 +427,7 @@ def add_user_defined_fields(xml, options) def lookup_country_code(code) country = Country.find(code) rescue nil - country && country.code(:alpha3) + country&.code(:alpha3) end end end diff --git a/lib/active_merchant/billing/gateways/komoju.rb b/lib/active_merchant/billing/gateways/komoju.rb index 8aa3fc1b636..7c93a573d1f 100644 --- a/lib/active_merchant/billing/gateways/komoju.rb +++ b/lib/active_merchant/billing/gateways/komoju.rb @@ -1,16 +1,16 @@ require 'json' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class KomojuGateway < Gateway - self.test_url = 'https://sandbox.komoju.com/api/v1' + self.test_url = 'https://komoju.com/api/v1' self.live_url = 'https://komoju.com/api/v1' self.supported_countries = ['JP'] self.default_currency = 'JPY' self.money_format = :cents self.homepage_url = 'https://www.komoju.com/' self.display_name = 'Komoju' - self.supported_cardtypes = [:visa, :master, :american_express, :jcb] + self.supported_cardtypes = %i[visa master american_express jcb] STANDARD_ERROR_CODE_MAPPING = { 'bad_verification_value' => 'incorrect_cvc', diff --git a/lib/active_merchant/billing/gateways/kushki.rb b/lib/active_merchant/billing/gateways/kushki.rb index 14598237a74..a9dc8d0dca9 100644 --- a/lib/active_merchant/billing/gateways/kushki.rb +++ b/lib/active_merchant/billing/gateways/kushki.rb @@ -1,43 +1,64 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class KushkiGateway < Gateway self.display_name = 'Kushki' self.homepage_url = 'https://www.kushkipagos.com' - self.test_url = 'https://api-uat.kushkipagos.com/v1/' - self.live_url = 'https://api.kushkipagos.com/v1/' + self.test_url = 'https://api-uat.kushkipagos.com/' + self.live_url = 'https://api.kushkipagos.com/' - self.supported_countries = ['CO', 'EC'] + self.supported_countries = %w[BR CL CO EC MX PE] self.default_currency = 'USD' self.money_format = :dollars - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club] + self.supported_cardtypes = %i[visa master american_express discover diners_club alia] - def initialize(options={}) + def initialize(options = {}) requires!(options, :public_merchant_id, :private_merchant_id) super end - def purchase(amount, payment_method, 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 refund(amount, authorization, options={}) - action = 'refund' + def authorize(amount, payment_method, options = {}) + MultiResponse.run() do |r| + r.process { tokenize(amount, payment_method, options) } + r.process { preauthorize(amount, r.authorization, options, payment_method) } + end + end + + def capture(amount, authorization, options = {}) + action = 'capture' post = {} post[:ticketNumber] = authorization + add_invoice(action, post, amount, options) + add_full_response(post, options) commit(action, post) end - def void(authorization, options={}) + def refund(amount, authorization, options = {}) + action = 'refund' + + post = {} + post[:ticketNumber] = authorization + add_full_response(post, options) + add_invoice(action, post, amount, options) + + commit(action, post, options) + end + + def void(authorization, options = {}) action = 'void' post = {} post[:ticketNumber] = authorization + add_full_response(post, options) commit(action, post) end @@ -61,16 +82,42 @@ def tokenize(amount, payment_method, options) post = {} add_invoice(action, post, amount, 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 = {} add_reference(post, authorization, options) add_invoice(action, post, amount, 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, payment_method = {}) + action = 'preAuthorization' + + post = {} + add_reference(post, 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 @@ -90,13 +137,11 @@ 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 - if sum[:currency] != 'COP' - sum[:ice] = 0 - end + sum[:ice] = 0 if sum[:currency] != 'COP' end def add_amount_by_country(sum, options) @@ -105,7 +150,7 @@ def add_amount_by_country(sum, options) sum[:iva] = amount[:iva].to_f if amount[:iva] sum[:subtotalIva0] = amount[:subtotal_iva_0].to_f if amount[:subtotal_iva_0] sum[:ice] = amount[:ice].to_f if amount[:ice] - if (extra_taxes = amount[:extra_taxes]) && sum[:currency] == 'COP' + if (extra_taxes = amount[:extra_taxes]) sum[:extraTaxes] ||= Hash.new sum[:extraTaxes][:propina] = extra_taxes[:propina].to_f if extra_taxes[:propina] sum[:extraTaxes][:tasaAeroportuaria] = extra_taxes[:tasa_aeroportuaria].to_f if extra_taxes[:tasa_aeroportuaria] @@ -129,19 +174,110 @@ def add_reference(post, authorization, options) post[:token] = authorization end + def add_contact_details(post, contact_details_options) + contact_details = {} + contact_details[:documentType] = contact_details_options[:document_type] if contact_details_options[:document_type] + contact_details[:documentNumber] = contact_details_options[:document_number] if contact_details_options[:document_number] + contact_details[:email] = contact_details_options[:email] if contact_details_options[:email] + contact_details[:firstName] = contact_details_options[:first_name] if contact_details_options[:first_name] + contact_details[:lastName] = contact_details_options[:last_name] if contact_details_options[:last_name] + contact_details[:secondLastName] = contact_details_options[:second_last_name] if contact_details_options[:second_last_name] + contact_details[:phoneNumber] = contact_details_options[:phone_number] if contact_details_options[:phone_number] + post[:contactDetails] = contact_details + end + + def add_full_response(post, options) + # 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', 'void' => 'charges', - 'refund' => 'refund' + 'refund' => 'refund', + 'preAuthorization' => 'preAuthorization', + 'capture' => 'capture' } - def commit(action, params) - response = begin - parse(ssl_invoke(action, params)) - rescue ResponseError => e - parse(e.response.body) - end + def commit(action, params, options = {}) + response = + begin + parse(ssl_invoke(action, params, options)) + rescue ResponseError => e + parse(e.response.body) + end success = success_from(response) @@ -155,9 +291,11 @@ def commit(action, params) ) end - def ssl_invoke(action, params) - if ['void', 'refund'].include?(action) - ssl_request(:delete, url(action, params), nil, headers(action)) + def ssl_invoke(action, params, options) + if %w[void refund].include?(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 @@ -178,23 +316,21 @@ def post_data(params) def url(action, params) base_url = test? ? test_url : live_url - if ['void', 'refund'].include?(action) - base_url + ENDPOINT[action] + '/' + params[:ticketNumber].to_s + if %w[void refund].include?(action) + base_url + 'v1/' + ENDPOINT[action] + '/' + params[:ticketNumber].to_s else - base_url + ENDPOINT[action] + base_url + 'card/v1/' + ENDPOINT[action] end end def parse(body) - begin - JSON.parse(body) - rescue JSON::ParserError - message = 'Invalid JSON response received from KushkiGateway. Please contact KushkiGateway if you continue to receive this message.' - message += " (The raw response returned by the API was #{body.inspect})" - { - 'message' => message - } - end + JSON.parse(body) + rescue JSON::ParserError + message = 'Invalid JSON response received from KushkiGateway. Please contact KushkiGateway if you continue to receive this message.' + message += " (The raw response returned by the API was #{body.inspect})" + { + 'message' => message + } end def success_from(response) diff --git a/lib/active_merchant/billing/gateways/latitude19.rb b/lib/active_merchant/billing/gateways/latitude19.rb index b380d1a1c17..79d5027e7f2 100644 --- a/lib/active_merchant/billing/gateways/latitude19.rb +++ b/lib/active_merchant/billing/gateways/latitude19.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class Latitude19Gateway < Gateway self.display_name = 'Latitude19 Gateway' self.homepage_url = 'http://www.l19tech.com' @@ -7,10 +7,10 @@ class Latitude19Gateway < Gateway self.live_url = 'https://gateway.l19tech.com/payments/' self.test_url = 'https://gateway-sb.l19tech.com/payments/' - self.supported_countries = ['US', 'CA'] + self.supported_countries = %w[US CA] self.default_currency = 'USD' - self.money_format = :cents - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb] + self.money_format = :dollars + self.supported_cardtypes = %i[visa master american_express discover diners_club jcb] RESPONSE_CODE_MAPPING = { '100' => 'Approved', @@ -51,12 +51,12 @@ class Latitude19Gateway < Gateway 'jcb' => 'JC' } - def initialize(options={}) + def initialize(options = {}) requires!(options, :account_number, :configuration_id, :secret) super end - def purchase(amount, payment_method, options={}) + def purchase(amount, payment_method, options = {}) if payment_method.is_a?(String) auth_or_sale('sale', payment_method, amount, nil, options) else @@ -68,7 +68,7 @@ def purchase(amount, payment_method, options={}) end end - def authorize(amount, payment_method, options={}) + def authorize(amount, payment_method, options = {}) if payment_method.is_a?(String) auth_or_sale('auth', payment_method, amount, nil, options) else @@ -80,7 +80,7 @@ def authorize(amount, payment_method, options={}) end end - def capture(amount, authorization, options={}) + def capture(amount, authorization, options = {}) post = {} post[:method] = 'deposit' add_request_id(post) @@ -96,7 +96,7 @@ def capture(amount, authorization, options={}) commit('v1/', post) end - def void(authorization, options={}) + def void(authorization, options = {}) method, pgwTID = split_authorization(authorization) case method when 'auth' @@ -109,7 +109,7 @@ def void(authorization, options={}) end end - def credit(amount, payment_method, options={}) + def credit(amount, payment_method, options = {}) if payment_method.is_a?(String) refundWithCard(payment_method, amount, nil, options) else @@ -121,7 +121,7 @@ def credit(amount, payment_method, options={}) end end - def verify(payment_method, options={}, action=nil) + def verify(payment_method, options = {}, action = nil) if payment_method.is_a?(String) verifyOnly(action, payment_method, nil, options) else @@ -133,7 +133,7 @@ def verify(payment_method, options={}, action=nil) end end - def store(payment_method, options={}) + def store(payment_method, options = {}) verify(payment_method, options, 'store') end @@ -147,14 +147,13 @@ def scrub(transcript) gsub(%r((\"cvv\\\":\\\")\d+), '\1[FILTERED]') end - private def add_request_id(post) post[:id] = SecureRandom.hex(16) end - def add_timestamp() + def add_timestamp Time.now.getutc.strftime('%Y%m%d%H%M%S') end @@ -202,7 +201,7 @@ def add_customer_data(params, options) end end - def get_session(options={}) + def get_session(options = {}) post = {} post[:method] = 'getSession' add_request_id(post) @@ -214,7 +213,7 @@ def get_session(options={}) commit('session', post) end - def get_token(authorization, payment_method, options={}) + def get_token(authorization, payment_method, options = {}) post = {} post[:method] = 'tokenize' add_request_id(post) @@ -227,7 +226,7 @@ def get_token(authorization, payment_method, options={}) commit('token', post) end - def auth_or_sale(method, authorization, amount, credit_card, options={}) + def auth_or_sale(method, authorization, amount, credit_card, options = {}) post = {} post[:method] = method add_request_id(post) @@ -247,7 +246,7 @@ def auth_or_sale(method, authorization, amount, credit_card, options={}) commit('v1/', post) end - def verifyOnly(action, authorization, credit_card, options={}) + def verifyOnly(action, authorization, credit_card, options = {}) post = {} post[:method] = 'verifyOnly' add_request_id(post) @@ -268,7 +267,7 @@ def verifyOnly(action, authorization, credit_card, options={}) commit('v1/', post) end - def refundWithCard(authorization, amount, credit_card, options={}) + def refundWithCard(authorization, amount, credit_card, options = {}) post = {} post[:method] = 'refundWithCard' add_request_id(post) @@ -287,7 +286,7 @@ def refundWithCard(authorization, amount, credit_card, options={}) commit('v1/', post) end - def reverse_or_void(method, pgwTID, options={}) + def reverse_or_void(method, pgwTID, options = {}) post = {} post[:method] = method add_request_id(post) @@ -302,32 +301,30 @@ def reverse_or_void(method, pgwTID, options={}) end def commit(endpoint, post) - begin - raw_response = ssl_post(url() + endpoint, post_data(post), headers) - response = parse(raw_response) - rescue ResponseError => e - raw_response = e.response.body - response_error(raw_response) - rescue JSON::ParserError - unparsable_response(raw_response) - else - success = success_from(response) - Response.new( - success, - message_from(response), - response, - authorization: success ? authorization_from(response, post[:method]) : nil, - avs_result: success ? avs_from(response) : nil, - cvv_result: success ? cvv_from(response) : nil, - error_code: success ? nil : error_from(response), - test: test? - ) - end + raw_response = ssl_post(url() + endpoint, post_data(post), headers) + response = parse(raw_response) + rescue ResponseError => e + raw_response = e.response.body + response_error(raw_response) + rescue JSON::ParserError + unparsable_response(raw_response) + else + success = success_from(response) + Response.new( + success, + message_from(response), + response, + authorization: success ? authorization_from(response, post[:method]) : nil, + avs_result: success ? avs_from(response) : nil, + cvv_result: success ? cvv_from(response) : nil, + error_code: success ? nil : error_from(response), + test: test? + ) end def headers { - 'Content-Type' => 'application/json' + 'Content-Type' => 'application/json' } end @@ -367,6 +364,7 @@ def message_from(response) def error_from(response) return response['error'] if response['error'] return 'Failed' unless response.key?('result') + return response['result']['pgwResponseCode'] || response['result']['processor']['responseCode'] || 'Failed' end @@ -392,18 +390,16 @@ def cvv_from(response) end def response_error(raw_response) - begin - response = parse(raw_response) - rescue JSON::ParserError - unparsable_response(raw_response) - else - return Response.new( - false, - message_from(response), - response, - :test => test? - ) - end + response = parse(raw_response) + rescue JSON::ParserError + unparsable_response(raw_response) + else + return Response.new( + false, + message_from(response), + response, + test: test? + ) end def unparsable_response(raw_response) diff --git a/lib/active_merchant/billing/gateways/linkpoint.rb b/lib/active_merchant/billing/gateways/linkpoint.rb index 9c63d503254..534020f4eaf 100644 --- a/lib/active_merchant/billing/gateways/linkpoint.rb +++ b/lib/active_merchant/billing/gateways/linkpoint.rb @@ -1,8 +1,7 @@ require 'rexml/document' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: - +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # Initialization Options # :login Your store number # :pem The text of your linkpoint PEM file. Note @@ -131,7 +130,7 @@ class LinkpointGateway < Gateway self.live_url = 'https://secure.linkpt.net:1129/' self.supported_countries = ['US'] - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :jcb, :diners_club] + self.supported_cardtypes = %i[visa master american_express discover jcb diners_club] self.homepage_url = 'http://www.linkpoint.com/' self.display_name = 'LinkPoint' @@ -139,8 +138,8 @@ def initialize(options = {}) requires!(options, :login) @options = { - :result => 'LIVE', - :pem => LinkpointGateway.pem_file + result: 'LIVE', + pem: LinkpointGateway.pem_file }.update(options) raise ArgumentError, "You need to pass in your pem file using the :pem parameter or set it globally using ActiveMerchant::Billing::LinkpointGateway.pem_file = File.read( File.dirname(__FILE__) + '/../mycert.pem' ) or similar" if @options[:pem].blank? @@ -167,28 +166,28 @@ def initialize(options = {}) # :threshold Tells how many times to retry the transaction (if it fails) before contacting the merchant. # :comments Uh... comments # - def recurring(money, creditcard, options={}) + def recurring(money, creditcard, options = {}) ActiveMerchant.deprecated RECURRING_DEPRECATION_MESSAGE - requires!(options, [:periodicity, :bimonthly, :monthly, :biweekly, :weekly, :yearly, :daily], :installments, :order_id ) + requires!(options, %i[periodicity bimonthly monthly biweekly weekly yearly daily], :installments, :order_id) options.update( - :ordertype => 'SALE', - :action => options[:action] || 'SUBMIT', - :installments => options[:installments] || 12, - :startdate => options[:startdate] || 'immediate', - :periodicity => options[:periodicity].to_s || 'monthly', - :comments => options[:comments] || nil, - :threshold => options[:threshold] || 3 + ordertype: 'SALE', + action: options[:action] || 'SUBMIT', + installments: options[:installments] || 12, + startdate: options[:startdate] || 'immediate', + periodicity: options[:periodicity].to_s || 'monthly', + comments: options[:comments] || nil, + threshold: options[:threshold] || 3 ) commit(money, creditcard, options) end # Buy the thing - def purchase(money, creditcard, options={}) + def purchase(money, creditcard, options = {}) requires!(options, :order_id) options.update( - :ordertype => 'SALE' + ordertype: 'SALE' ) commit(money, creditcard, options) end @@ -201,7 +200,7 @@ def purchase(money, creditcard, options={}) def authorize(money, creditcard, options = {}) requires!(options, :order_id) options.update( - :ordertype => 'PREAUTH' + ordertype: 'PREAUTH' ) commit(money, creditcard, options) end @@ -214,8 +213,8 @@ def authorize(money, creditcard, options = {}) # def capture(money, authorization, options = {}) options.update( - :order_id => authorization, - :ordertype => 'POSTAUTH' + order_id: authorization, + ordertype: 'POSTAUTH' ) commit(money, nil, options) end @@ -223,8 +222,8 @@ def capture(money, authorization, options = {}) # Void a previous transaction def void(identification, options = {}) options.update( - :order_id => identification, - :ordertype => 'VOID' + order_id: identification, + ordertype: 'VOID' ) commit(nil, nil, options) end @@ -236,8 +235,8 @@ def void(identification, options = {}) # def refund(money, identification, options = {}) options.update( - :ordertype => 'CREDIT', - :order_id => identification + ordertype: 'CREDIT', + order_id: identification ) commit(money, nil, options) end @@ -259,15 +258,19 @@ def scrub(transcript) end private + # Commit the transaction by posting the XML file to the LinkPoint server 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, - :test => test?, - :authorization => response[:ordernum], - :avs_result => { :code => response[:avs].to_s[2,1] }, - :cvv_result => response[:avs].to_s[3,1] + 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] ) end @@ -311,11 +314,11 @@ def build_items(element, items) options_element = item_element.add_element('options') for option in value opt_element = options_element.add_element('option') - opt_element.add_element('name').text = option[:name] unless option[:name].blank? - opt_element.add_element('value').text = option[:value] unless option[:value].blank? + opt_element.add_element('name').text = option[:name] unless option[:name].blank? + opt_element.add_element('value').text = option[:value] unless option[:value].blank? end else - item_element.add_element(key.to_s).text = item[key].to_s unless item[key].blank? + item_element.add_element(key.to_s).text = item[key].to_s unless item[key].blank? end end end @@ -324,57 +327,56 @@ def build_items(element, items) # Set up the parameters hash just once so we don't have to do it # for every action. def parameters(money, creditcard, options = {}) - params = { - :payment => { - :subtotal => amount(options[:subtotal]), - :tax => amount(options[:tax]), - :vattax => amount(options[:vattax]), - :shipping => amount(options[:shipping]), - :chargetotal => amount(money) + payment: { + subtotal: amount(options[:subtotal]), + tax: amount(options[:tax]), + vattax: amount(options[:vattax]), + shipping: amount(options[:shipping]), + chargetotal: amount(money) }, - :transactiondetails => { - :transactionorigin => options[:transactionorigin] || 'ECI', - :oid => options[:order_id], - :ponumber => options[:ponumber], - :taxexempt => options[:taxexempt], - :terminaltype => options[:terminaltype], - :ip => options[:ip], - :reference_number => options[:reference_number], - :recurring => options[:recurring] || 'NO', #DO NOT USE if you are using the periodic billing option. - :tdate => options[:tdate] + transactiondetails: { + transactionorigin: options[:transactionorigin] || 'ECI', + oid: options[:order_id], + ponumber: options[:ponumber], + taxexempt: options[:taxexempt], + terminaltype: options[:terminaltype], + ip: options[:ip], + reference_number: options[:reference_number], + recurring: options[:recurring] || 'NO', # DO NOT USE if you are using the periodic billing option. + tdate: options[:tdate] }, - :orderoptions => { - :ordertype => options[:ordertype], - :result => @options[:result] + orderoptions: { + ordertype: options[:ordertype], + result: @options[:result] }, - :periodic => { - :action => options[:action], - :installments => options[:installments], - :threshold => options[:threshold], - :startdate => options[:startdate], - :periodicity => options[:periodicity], - :comments => options[:comments] + periodic: { + action: options[:action], + installments: options[:installments], + threshold: options[:threshold], + startdate: options[:startdate], + periodicity: options[:periodicity], + comments: options[:comments] }, - :telecheck => { - :routing => options[:telecheck_routing], - :account => options[:telecheck_account], - :checknumber => options[:telecheck_checknumber], - :bankname => options[:telecheck_bankname], - :dl => options[:telecheck_dl], - :dlstate => options[:telecheck_dlstate], - :void => options[:telecheck_void], - :accounttype => options[:telecheck_accounttype], - :ssn => options[:telecheck_ssn], + telecheck: { + routing: options[:telecheck_routing], + account: options[:telecheck_account], + checknumber: options[:telecheck_checknumber], + bankname: options[:telecheck_bankname], + dl: options[:telecheck_dl], + dlstate: options[:telecheck_dlstate], + void: options[:telecheck_void], + accounttype: options[:telecheck_accounttype], + ssn: options[:telecheck_ssn] } } if creditcard params[:creditcard] = { - :cardnumber => creditcard.number, - :cardexpmonth => creditcard.month, - :cardexpyear => format_creditcard_expiry_year(creditcard.year), - :track => nil + cardnumber: creditcard.number, + cardexpmonth: creditcard.month, + cardexpyear: format_creditcard_expiry_year(creditcard.year), + track: nil } if creditcard.verification_value? @@ -396,8 +398,8 @@ def parameters(money, creditcard, options = {}) params[:billing][:zip] = billing_address[:zip] unless billing_address[:zip].blank? params[:billing][:country] = billing_address[:country] unless billing_address[:country].blank? params[:billing][:company] = billing_address[:company] unless billing_address[:company].blank? - params[:billing][:phone] = billing_address[:phone] unless billing_address[:phone].blank? - params[:billing][:email] = options[:email] unless options[:email].blank? + params[:billing][:phone] = billing_address[:phone] unless billing_address[:phone].blank? + params[:billing][:email] = options[:email] unless options[:email].blank? end if shipping_address = options[:shipping_address] @@ -418,7 +420,6 @@ def parameters(money, creditcard, options = {}) end def parse(xml) - # For reference, a typical response... # # @@ -433,12 +434,12 @@ def parse(xml) # APPROVED # - response = {:message => 'Global Error Receipt', :complete => false} + response = { message: 'Global Error Receipt', complete: false } xml = REXML::Document.new("#{xml}") - xml.root.elements.each do |node| + xml.root&.elements&.each do |node| response[node.name.downcase.sub(/^r_/, '').to_sym] = normalize(node.text) - end unless xml.root.nil? + end response end diff --git a/lib/active_merchant/billing/gateways/litle.rb b/lib/active_merchant/billing/gateways/litle.rb index e702dc297ed..19df4f93572 100644 --- a/lib/active_merchant/billing/gateways/litle.rb +++ b/lib/active_merchant/billing/gateways/litle.rb @@ -1,26 +1,30 @@ require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class LitleGateway < Gateway - SCHEMA_VERSION = '9.12' + SCHEMA_VERSION = '9.14' + + 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' self.supported_countries = ['US'] self.default_currency = 'USD' - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb] + self.supported_cardtypes = %i[visa master american_express discover diners_club jcb] - self.homepage_url = 'http://www.vantiv.com/' + self.homepage_url = 'https://www.fisglobal.com/' self.display_name = 'Vantiv eCommerce' - def initialize(options={}) + def initialize(options = {}) requires!(options, :login, :password, :merchant_id) super end - def purchase(money, payment_method, options={}) + def purchase(money, payment_method, options = {}) request = build_xml_request do |doc| add_authentication(doc) if check?(payment_method) @@ -33,10 +37,101 @@ def purchase(money, payment_method, options={}) end end end - check?(payment_method) ? commit(:echeckSales, request, money) : commit(:sale, request, money) + check?(payment_method) ? commit(:echeckSales, request, money) : commit(:sale, request, money) + end + + def add_level_two_data(doc, payment_method, options = {}) + level_2_data = options[:level_2_data] + if level_2_data + doc.enhancedData do + case payment_method.brand + when 'visa' + doc.salesTax(level_2_data[:sales_tax]) if level_2_data[:sales_tax] + when 'master' + doc.customerReference(level_2_data[:customer_code]) if level_2_data[:customer_code] + doc.salesTax(level_2_data[:total_tax_amount]) if level_2_data[:total_tax_amount] + doc.detailTax do + doc.taxIncludedInTotal(level_2_data[:tax_included_in_total]) if level_2_data[:tax_included_in_total] + doc.taxAmount(level_2_data[:tax_amount]) if level_2_data[:tax_amount] + doc.cardAcceptorTaxId(level_2_data[:card_acceptor_tax_id]) if level_2_data[:card_acceptor_tax_id] + end + end + end + end end - def authorize(money, payment_method, options={}) + def add_level_three_data(doc, payment_method, options = {}) + level_3_data = options[:level_3_data] + if level_3_data + doc.enhancedData do + case payment_method.brand + when 'visa' + add_level_three_information_tags_visa(doc, payment_method, level_3_data) + when 'master' + add_level_three_information_tags_master(doc, payment_method, level_3_data) + end + end + end + end + + def add_level_three_information_tags_visa(doc, payment_method, level_3_data) + doc.discountAmount(level_3_data[:discount_amount]) if level_3_data[:discount_amount] + doc.shippingAmount(level_3_data[:shipping_amount]) if level_3_data[:shipping_amount] + doc.dutyAmount(level_3_data[:duty_amount]) if level_3_data[:duty_amount] + doc.detailTax do + doc.taxIncludedInTotal(level_3_data[:tax_included_in_total]) if level_3_data[:tax_included_in_total] + doc.taxAmount(level_3_data[:tax_amount]) if level_3_data[:tax_amount] + doc.taxRate(level_3_data[:tax_rate]) if level_3_data[:tax_rate] + doc.taxTypeIdentifier(level_3_data[:tax_type_identifier]) if level_3_data[:tax_type_identifier] + doc.cardAcceptorTaxId(level_3_data[:card_acceptor_tax_id]) if level_3_data[:card_acceptor_tax_id] + end + add_line_item_information_for_level_three_visa(doc, payment_method, level_3_data) + end + + def add_level_three_information_tags_master(doc, payment_method, level_3_data) + doc.customerReference :customerReference, level_3_data[:customer_code] if level_3_data[:customer_code] + doc.salesTax(level_3_data[:total_tax_amount]) if level_3_data[:total_tax_amount] + doc.detailTax do + doc.taxIncludedInTotal(level_3_data[:tax_included_in_total]) if level_3_data[:tax_included_in_total] + doc.taxAmount(level_3_data[:tax_amount]) if level_3_data[:tax_amount] + doc.cardAcceptorTaxId :cardAcceptorTaxId, level_3_data[:card_acceptor_tax_id] if level_3_data[:card_acceptor_tax_id] + end + doc.lineItemData do + level_3_data[:line_items].each do |line_item| + 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.lineItemTotal(line_item[:line_item_total]) if line_item[:line_item_total] + end + end + end + + def add_line_item_information_for_level_three_visa(doc, payment_method, level_3_data) + 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.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.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] + doc.taxRate(line_item[:tax_rate]) if line_item[:tax_rate] + doc.taxTypeIdentifier(line_item[:tax_type_identifier]) if line_item[:tax_type_identifier] + doc.cardAcceptorTaxId(line_item[:card_acceptor_tax_id]) if line_item[:card_acceptor_tax_id] + end + end + end + end + + def authorize(money, payment_method, options = {}) request = build_xml_request do |doc| add_authentication(doc) if check?(payment_method) @@ -52,8 +147,8 @@ def authorize(money, payment_method, options={}) check?(payment_method) ? commit(:echeckVerification, request, money) : commit(:authorization, request, money) end - def capture(money, authorization, options={}) - transaction_id, _, _ = split_authorization(authorization) + def capture(money, authorization, options = {}) + transaction_id, = split_authorization(authorization) request = build_xml_request do |doc| add_authentication(doc) @@ -72,19 +167,19 @@ def credit(money, authorization, options = {}) refund(money, authorization, options) end - def refund(money, payment, options={}) + def refund(money, payment, options = {}) request = build_xml_request do |doc| add_authentication(doc) add_descriptor(doc, options) doc.send(refund_type(payment), transaction_attributes(options)) do if payment.is_a?(String) - transaction_id, _, _ = split_authorization(payment) + transaction_id, = split_authorization(payment) doc.litleTxnId(transaction_id) doc.amount(money) if money elsif check?(payment) add_echeck_purchase_params(doc, money, payment, options) else - add_auth_purchase_params(doc, money, payment, options) + add_credit_params(doc, money, payment, options) end end end @@ -99,7 +194,7 @@ def verify(creditcard, options = {}) end end - def void(authorization, options={}) + def void(authorization, options = {}) transaction_id, kind, money = split_authorization(authorization) request = build_xml_request do |doc| @@ -164,21 +259,21 @@ def scrub(transcript) } AVS_RESPONSE_CODE = { - '00' => 'Y', - '01' => 'X', - '02' => 'D', - '10' => 'Z', - '11' => 'W', - '12' => 'A', - '13' => 'A', - '14' => 'P', - '20' => 'N', - '30' => 'S', - '31' => 'R', - '32' => 'U', - '33' => 'R', - '34' => 'I', - '40' => 'E' + '00' => 'Y', + '01' => 'X', + '02' => 'D', + '10' => 'Z', + '11' => 'W', + '12' => 'A', + '13' => 'A', + '14' => 'P', + '20' => 'N', + '30' => 'S', + '31' => 'R', + '32' => 'U', + '33' => 'R', + '34' => 'I', + '40' => 'E' } def void_type(kind) @@ -192,8 +287,8 @@ def void_type(kind) end def refund_type(payment) - _, kind, _ = split_authorization(payment) - if check?(payment) || kind == 'echeckSales' + _, kind, = split_authorization(payment) + if check?(payment) || kind == 'echeckSales' :echeckCredit else :credit @@ -202,6 +297,7 @@ def refund_type(payment) def check?(payment_method) return false if payment_method.is_a?(String) + card_brand(payment_method) == 'check' end @@ -215,17 +311,40 @@ 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) add_pos(doc, payment_method) add_descriptor(doc, options) + add_level_two_data(doc, payment_method, options) + add_level_three_data(doc, payment_method, options) add_merchant_data(doc, options) add_debt_repayment(doc, options) + add_stored_credential_params(doc, options) + add_fraud_filter_override(doc, options) end - def add_merchant_data(doc, options={}) + def add_credit_params(doc, money, payment_method, options) + doc.orderId(truncate(options[:order_id], 24)) + doc.amount(money) + add_order_source(doc, payment_method, options) + add_billing_address(doc, payment_method, options) + add_payment_method(doc, payment_method, options) + add_pos(doc, payment_method) + add_descriptor(doc, options) + add_merchant_data(doc, options) + end + + def add_merchant_data(doc, options = {}) if options[:affiliate] || options[:campaign] || options[:merchant_grouping_id] doc.merchantData do doc.affiliate(options[:affiliate]) if options[:affiliate] @@ -257,21 +376,27 @@ def add_debt_repayment(doc, options) doc.debtRepayment(true) if options[:debt_repayment] == true end + def add_fraud_filter_override(doc, options) + doc.fraudFilterOverride(options[:fraud_filter_override]) if options[:fraud_filter_override] + end + def add_payment_method(doc, payment_method, options) if payment_method.is_a?(String) doc.token do doc.litleToken(payment_method) + doc.expDate(format_exp_date(options[:basis_expiration_month], options[:basis_expiration_year])) if options[:basis_expiration_month] && options[:basis_expiration_year] end elsif payment_method.respond_to?(:track_data) && payment_method.track_data.present? doc.card do 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) + doc.accType(account_type&.capitalize) doc.accNum(payment_method.account_number) doc.routingNum(payment_method.routing_number) - doc.checkNum(payment_method.number) + doc.checkNum(payment_method.number) if payment_method.number end else doc.card do @@ -284,7 +409,7 @@ def add_payment_method(doc, payment_method, options) doc.cardholderAuthentication do doc.authenticationValue(payment_method.payment_cryptogram) end - elsif options[:order_source] && options[:order_source].start_with?('3ds') + elsif options[:order_source]&.start_with?('3ds') doc.cardholderAuthentication do doc.authenticationValue(options[:cavv]) if options[:cavv] doc.authenticationTransactionId(options[:xid]) if options[:xid] @@ -293,6 +418,39 @@ def add_payment_method(doc, payment_method, options) end end + def add_stored_credential_params(doc, options = {}) + return unless stored_credential = options[:stored_credential] + + if stored_credential[:initial_transaction] + add_stored_credential_for_initial_txn(doc, options) + else + 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_for_initial_txn(doc, options) + case options[:stored_credential][:reason_type] + when 'unscheduled' + doc.processingType('initialCOF') + when 'installment' + doc.processingType('initialInstallment') + when 'recurring' + doc.processingType('initialRecurring') + end + 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 + end + def add_billing_address(doc, payment_method, options) return if payment_method.is_a?(String) @@ -322,9 +480,9 @@ def add_address(doc, address) return unless address doc.companyName(address[:company]) unless address[:company].blank? - doc.addressLine1(address[:address1]) unless address[:address1].blank? - doc.addressLine2(address[:address2]) unless address[:address2].blank? - doc.city(address[:city]) unless address[:city].blank? + doc.addressLine1(truncate(address[:address1], 35)) unless address[:address1].blank? + doc.addressLine2(truncate(address[:address2], 35)) unless address[:address2].blank? + doc.city(truncate(address[:city], 35)) unless address[:city].blank? doc.state(address[:state]) unless address[:state].blank? doc.zip(address[:zip]) unless address[:zip].blank? doc.country(address[:country]) unless address[:country].blank? @@ -332,11 +490,11 @@ def add_address(doc, address) end def add_order_source(doc, payment_method, options) - if options[:order_source] - doc.orderSource(options[: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') - elsif payment_method.is_a?(NetworkTokenizationCreditCard) && payment_method.source == :android_pay + elsif payment_method.is_a?(NetworkTokenizationCreditCard) && %i[google_pay android_pay].include?(payment_method.source) doc.orderSource('androidpay') elsif payment_method.respond_to?(:track_data) && payment_method.track_data.present? doc.orderSource('retail') @@ -356,15 +514,21 @@ def add_pos(doc, payment_method) end def exp_date(payment_method) - "#{format(payment_method.month, :two_digits)}#{format(payment_method.year, :two_digits)}" + format_exp_date(payment_method.month, payment_method.year) + end + + def format_exp_date(month, year) + "#{format(month, :two_digits)}#{format(year, :two_digits)}" end def parse(kind, xml) parsed = {} - doc = Nokogiri::XML(xml).remove_namespaces! + + parsed['duplicate'] = doc.at_xpath('//saleResponse').try(:[], 'duplicate') == 'true' if kind == :sale + doc.xpath("//litleOnlineResponse/#{kind}Response/*").each do |node| - if (node.elements.empty?) + if node.elements.empty? parsed[node.name.to_sym] = node.text else node.elements.each do |childnode| @@ -383,26 +547,38 @@ def parse(kind, xml) parsed end - def commit(kind, request, money=nil) + def commit(kind, request, money = nil) parsed = parse(kind, ssl_post(url, request, headers)) options = { authorization: authorization_from(kind, parsed, money), test: test?, - :avs_result => { :code => AVS_RESPONSE_CODE[parsed[:fraudResult_avsResult]] }, - :cvv_result => parsed[:fraudResult_cardValidationResult] + avs_result: { code: AVS_RESPONSE_CODE[parsed[:fraudResult_avsResult]] }, + 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 136 141 142 470 473).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}" + kind == :registerToken ? parsed[:litleToken] : "#{parsed[:litleTxnId]};#{kind};#{money}" end def split_authorization(authorization) @@ -414,8 +590,8 @@ def transaction_attributes(options) attributes = {} attributes[:id] = truncate(options[:id] || options[:order_id], 24) attributes[:reportGroup] = options[:merchant] || 'Default Report Group' - attributes[:customerId] = options[:customer] - attributes.delete_if { |key, value| value == nil } + attributes[:customerId] = options[:customer_id] + attributes.delete_if { |_key, value| value == nil } attributes end @@ -427,15 +603,16 @@ 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 d15bb6cbfc6..894ad2be3f5 100644 --- a/lib/active_merchant/billing/gateways/mastercard.rb +++ b/lib/active_merchant/billing/gateways/mastercard.rb @@ -1,23 +1,33 @@ module ActiveMerchant module Billing module MastercardGateway - - def initialize(options={}) + def initialize(options = {}) requires!(options, :userid, :password) super end - def purchase(amount, payment_method, options={}) - MultiResponse.run do |r| - r.process { authorize(amount, payment_method, options) } - r.process { capture(amount, r.authorization, options) } + def purchase(amount, payment_method, options = {}) + if options[:pay_mode] + post = new_post + add_invoice(post, amount, options) + add_reference(post, *new_authorization(options)) + add_payment_method(post, payment_method) + add_customer_data(post, payment_method, options) + add_3dsecure_id(post, options) + + commit('pay', post) + else + MultiResponse.run do |r| + r.process { authorize(amount, payment_method, options) } + r.process { capture(amount, r.authorization, options) } + end end end - def authorize(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) @@ -25,7 +35,7 @@ def authorize(amount, payment_method, options={}) commit('authorize', post) end - def capture(amount, authorization, options={}) + def capture(amount, authorization, options = {}) post = new_post add_invoice(post, amount, options, :transaction) add_reference(post, *next_authorization(authorization)) @@ -35,7 +45,7 @@ def capture(amount, authorization, options={}) commit('capture', post) end - def refund(amount, authorization, options={}) + def refund(amount, authorization, options = {}) post = new_post add_invoice(post, amount, options, :transaction) add_reference(post, *next_authorization(authorization)) @@ -44,14 +54,14 @@ def refund(amount, authorization, options={}) commit('refund', post) end - def void(authorization, options={}) + def void(authorization, options = {}) post = new_post add_reference(post, *next_authorization(authorization), :targetTransactionId) commit('void', post) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -81,6 +91,7 @@ def scrub(transcript) end private + def new_post { order: {}, @@ -94,16 +105,16 @@ def new_post billing: {}, device: {}, shipping: {}, - transaction: {}, + transaction: {} } end - def add_invoice(post, amount, options, node=:order) + def add_invoice(post, amount, options, node = :order) post[node][:amount] = amount(amount) post[node][:currency] = (options[:currency] || currency(amount)) end - def add_reference(post, orderid, transactionid, transaction_reference, reference_key=:reference) + def add_reference(post, orderid, transactionid, transaction_reference, reference_key = :reference) post[:orderid] = orderid post[:transactionid] = transactionid post[:transaction][reference_key] = transaction_reference if transaction_reference @@ -164,7 +175,8 @@ def add_customer_data(post, payment_method, options) def add_3dsecure_id(post, options) return unless options[:threed_secure_id] - post.merge!({'3DSecureId' => options[:threed_secure_id]}) + + post.merge!({ '3DSecureId' => options[:threed_secure_id] }) end def country_code(country) @@ -178,7 +190,7 @@ def country_code(country) def headers { 'Authorization' => 'Basic ' + Base64.encode64("merchant.#{@options[:userid]}:#{@options[:password]}").strip.delete("\r\n"), - 'Content-Type' => 'application/json', + 'Content-Type' => 'application/json' } end @@ -195,8 +207,8 @@ def commit(action, post) succeeded, message_from(succeeded, raw), raw, - :authorization => authorization_from(post, raw), - :test => test? + authorization: authorization_from(post, raw), + test: test? ) end @@ -206,9 +218,16 @@ def build_url(orderid, transactionid) def base_url if test? - @options[:region] == 'asia_pacific' ? test_ap_url : test_na_url + test_url else - @options[:region] == 'asia_pacific' ? live_ap_url : live_na_url + case @options[:region] + when 'asia_pacific' + live_ap_url + when 'europe' + live_eu_url + when 'north_america', nil + live_na_url + end end end @@ -245,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' @@ -262,7 +281,6 @@ def next_authorization(authorization) next_transactionid = SecureRandom.uuid [orderid, next_transactionid, prev_transactionid] end - end end end diff --git a/lib/active_merchant/billing/gateways/maxipago.rb b/lib/active_merchant/billing/gateways/maxipago.rb index d029d261b93..1eb46f93dc0 100644 --- a/lib/active_merchant/billing/gateways/maxipago.rb +++ b/lib/active_merchant/billing/gateways/maxipago.rb @@ -1,7 +1,7 @@ require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class MaxipagoGateway < Gateway API_VERSION = '3.1.1.15' @@ -11,7 +11,7 @@ class MaxipagoGateway < Gateway self.supported_countries = ['BR'] self.default_currency = 'BRL' self.money_format = :dollars - self.supported_cardtypes = [:visa, :master, :discover, :american_express, :diners_club] + self.supported_cardtypes = %i[visa master discover american_express diners_club] self.homepage_url = 'http://www.maxipago.com/' self.display_name = 'maxiPago!' @@ -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( @@ -142,7 +142,7 @@ def parse(body) def parse_element(response, node) if node.has_elements? - node.elements.each{|element| parse_element(response, element) } + node.elements.each { |element| parse_element(response, element) } else response[node.name.underscore.to_sym] = node.text end @@ -212,7 +212,7 @@ def add_billing_address(xml, creditcard, options) end def add_order_id(xml, authorization) - order_id, _ = split_authorization(authorization) + order_id, = split_authorization(authorization) xml.orderID order_id end end diff --git a/lib/active_merchant/billing/gateways/mercado_pago.rb b/lib/active_merchant/billing/gateways/mercado_pago.rb index ad5f4a555d8..4d8ab67faad 100644 --- a/lib/active_merchant/billing/gateways/mercado_pago.rb +++ b/lib/active_merchant/billing/gateways/mercado_pago.rb @@ -1,71 +1,71 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class MercadoPagoGateway < Gateway self.live_url = self.test_url = 'https://api.mercadopago.com/v1' - self.supported_countries = ['AR', 'BR', 'CL', 'CO', 'MX', 'PE', 'UY'] - self.supported_cardtypes = [:visa, :master, :american_express] + self.supported_countries = %w[AR BR CL CO MX PE UY] + self.supported_cardtypes = %i[visa master american_express elo cabal naranja creditel patagonia_365 tarjeta_sol] self.homepage_url = 'https://www.mercadopago.com/' self.display_name = 'Mercado Pago' self.money_format = :dollars - CARD_BRAND = { - 'american_express' => 'amex', - 'diners_club' => 'diners' - } - - def initialize(options={}) + def initialize(options = {}) requires!(options, :access_token) super end - def purchase(money, payment, options={}) + def purchase(money, payment, options = {}) MultiResponse.run do |r| r.process { commit('tokenize', 'card_tokens', card_token_request(money, payment, options)) } - options.merge!(card_brand: (CARD_BRAND[payment.brand] || payment.brand)) - options.merge!(card_token: r.authorization.split('|').first) - r.process { commit('purchase', 'payments', purchase_request(money, payment, options) ) } + options[:card_token] = r.authorization.split('|').first + r.process { commit('purchase', 'payments', purchase_request(money, payment, options)) } end end - def authorize(money, payment, options={}) + def authorize(money, payment, options = {}) MultiResponse.run do |r| r.process { commit('tokenize', 'card_tokens', card_token_request(money, payment, options)) } - options.merge!(card_brand: (CARD_BRAND[payment.brand] || payment.brand)) - options.merge!(card_token: r.authorization.split('|').first) - r.process { commit('authorize', 'payments', authorize_request(money, payment, options) ) } + options[:card_token] = r.authorization.split('|').first + r.process { commit('authorize', 'payments', authorize_request(money, payment, options)) } end end - def capture(money, authorization, options={}) + def capture(money, authorization, options = {}) post = {} - authorization, _ = authorization.split('|') + authorization, = authorization.split('|') post[:capture] = true post[:transaction_amount] = amount(money).to_f commit('capture', "payments/#{authorization}", post) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) post = {} authorization, original_amount = authorization.split('|') post[:amount] = amount(money).to_f if original_amount && original_amount.to_f > amount(money).to_f + add_idempotency_key(post, options) commit('refund', "payments/#{authorization}/refunds", post) end - def void(authorization, options={}) - authorization, _ = authorization.split('|') + def void(authorization, options = {}) + authorization, = authorization.split('|') post = { status: 'cancelled' } commit('void', "payments/#{authorization}", post) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) + verify_amount = 100 + verify_amount = options[:amount].to_i if options[:amount] MultiResponse.run(:use_first_response) do |r| - r.process { authorize(100, credit_card, options) } + r.process { authorize(verify_amount, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } end end + def inquire(authorization, options = {}) + commit('inquire', inquire_path(authorization, options), {}) + end + def supports_scrubbing? true end @@ -102,24 +102,48 @@ def purchase_request(money, payment, options = {}) add_additional_data(post, options) add_customer_data(post, payment, options) add_address(post, options) - post[:binary_mode] = (options[:binary_mode].nil? ? true : options[:binary_mode]) + add_processing_mode(post, options) + add_net_amount(post, options) + add_taxes(post, options) + add_notification_url(post, options) + add_idempotency_key(post, options) + add_3ds(post, options) + post[:binary_mode] = options.fetch(:binary_mode, true) unless options[:execute_threed] post end def authorize_request(money, payment, options = {}) post = purchase_request(money, payment, options) - post.merge!(capture: false) + post[:capture] = options[:capture] || false post end + def add_processing_mode(post, options) + return unless options[:processing_mode] + + post[:processing_mode] = options[:processing_mode] + post[:merchant_account_id] = options[:merchant_account_id] if options[:merchant_account_id] + post[:payment_method_option_id] = options[:payment_method_option_id] if options[:payment_method_option_id] + add_merchant_services(post, options) + end + + def add_merchant_services(post, options) + return unless options[:fraud_scoring] || options[:fraud_manual_review] + + merchant_services = {} + merchant_services[:fraud_scoring] = options[:fraud_scoring] if options[:fraud_scoring] + merchant_services[:fraud_manual_review] = options[:fraud_manual_review] if options[:fraud_manual_review] + post[:merchant_services] = merchant_services + end + def add_additional_data(post, options) - post[:sponsor_id] = options[:sponsor_id] + post[:sponsor_id] = options[:sponsor_id] unless test? + post[:metadata] = options[:metadata] if options[:metadata] post[:device_id] = options[:device_id] if options[:device_id] post[:additional_info] = { ip_address: options[:ip_address] }.merge(options[:additional_info] || {}) - add_address(post, options) add_shipping_address(post, options) end @@ -129,7 +153,7 @@ def add_customer_data(post, payment, options) email: options[:email], first_name: payment.first_name, last_name: payment.last_name - } + }.merge(options[:payer] || {}) end def add_address(post, options) @@ -177,21 +201,81 @@ def add_invoice(post, money, options) post[:description] = options[:description] post[:installments] = options[:installments] ? options[:installments].to_i : 1 post[:statement_descriptor] = options[:statement_descriptor] if options[:statement_descriptor] - post[:external_reference] = options[:order_id] || SecureRandom.hex(16) + post[:external_reference] = options[:order_id] || options[:external_reference] || SecureRandom.hex(16) end def add_payment(post, options) post[:token] = options[:card_token] - post[:payment_method_id] = options[:card_brand] + post[:issuer_id] = options[:issuer_id] if options[:issuer_id] + post[:payment_method_id] = options[:payment_method_id] if options[:payment_method_id] + end + + def add_net_amount(post, options) + post[:net_amount] = Float(options[:net_amount]) if options[:net_amount] + end + + def add_idempotency_key(post, options) + post[:idempotency_key] = options[:idempotency_key] if options[:idempotency_key] + end + + def add_notification_url(post, options) + post[:notification_url] = options[:notification_url] if options[:notification_url] + end + + def add_taxes(post, options) + return unless (tax_object = options[:taxes]) + + if tax_object.is_a?(Array) + post[:taxes] = process_taxes_array(tax_object) + elsif tax_object.is_a?(Hash) + post[:taxes] = process_taxes_hash(tax_object) + else + raise taxes_error + end + end + + def process_taxes_hash(tax_object) + [sanitize_taxes_hash(tax_object)] + end + + def process_taxes_array(taxes_array) + taxes_array.map do |tax_object| + raise taxes_error unless tax_object.is_a?(Hash) + + sanitize_taxes_hash(tax_object) + end + end + + def sanitize_taxes_hash(tax_object) + tax_value = tax_object['value'] || tax_object[:value] + tax_type = tax_object['type'] || tax_object[:type] + + raise taxes_error if tax_value.nil? || tax_type.nil? + + { value: Float(tax_value), type: tax_type } + end + + def taxes_error + ArgumentError.new("Taxes should be a single object or array of objects with the shape: { value: 500, type: 'IVA' }") end def parse(body) JSON.parse(body) + rescue JSON::ParserError + { + 'status' => 'error', + 'status_detail' => 'json_parse_error', + 'message' => "A non-JSON response was received from Mercado Pago where one was expected. The raw response was:\n\n#{body}" + } end def commit(action, path, parameters) - if ['capture', 'void'].include?(action) + if %w[capture void].include?(action) response = parse(ssl_request(:put, url(path), post_data(parameters), headers)) + elsif action == 'inquire' + response = parse(ssl_get(url(path), headers)) + + response = response[0]['results'][0] if response.is_a?(Array) else response = parse(ssl_post(url(path), post_data(parameters), headers(parameters))) end @@ -208,9 +292,9 @@ def commit(action, path, parameters) def success_from(action, response) if action == 'refund' - response['error'].nil? + response['status'] != 404 && response['error'].nil? else - ['active', 'approved', 'authorized', 'cancelled', 'in_process'].include?(response['status']) + %w[active approved authorized cancelled in_process pending].include?(response['status']) end end @@ -223,7 +307,20 @@ def authorization_from(response, params) end def post_data(parameters = {}) - parameters.clone.tap { |p| p.delete(:device_id) }.to_json + params = parameters.clone.tap do |p| + p.delete(:device_id) + p.delete(:idempotency_key) + end + params.to_json + end + + def inquire_path(authorization, options) + if authorization + authorization, = authorization.split('|') + "payments/#{authorization}" + else + "payments/search?external_reference=#{options[:order_id] || options[:external_reference]}" + end end def error_code_from(action, response) @@ -236,6 +333,13 @@ def error_code_from(action, response) end end + def add_3ds(post, options) + return unless options[:execute_threed] + + post[:three_d_secure_mode] = options[:three_ds_mode] == 'mandatory' ? 'mandatory' : 'optional' + post[:notification_url] = options[:notification_url] if options[:notification_url] + end + def url(action) full_url = (test? ? test_url : live_url) full_url + "/#{action}?access_token=#{CGI.escape(@options[:access_token])}" @@ -245,7 +349,8 @@ def headers(options = {}) headers = { 'Content-Type' => 'application/json' } - headers['X-Device-Session-ID'] = options[:device_id] if options[:device_id] + headers['X-meli-session-id'] = options[:device_id] if options[:device_id] + headers['X-Idempotency-Key'] = options[:idempotency_key] if options[:idempotency_key] headers end diff --git a/lib/active_merchant/billing/gateways/merchant_e_solutions.rb b/lib/active_merchant/billing/gateways/merchant_e_solutions.rb index 8af50c307e6..60bcdd6808f 100644 --- a/lib/active_merchant/billing/gateways/merchant_e_solutions.rb +++ b/lib/active_merchant/billing/gateways/merchant_e_solutions.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class MerchantESolutionsGateway < Gateway include Empty @@ -10,7 +10,7 @@ class MerchantESolutionsGateway < Gateway self.supported_countries = ['US'] # The card types supported by the payment gateway - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :jcb] + self.supported_cardtypes = %i[visa master american_express discover jcb] # The homepage URL of the gateway self.homepage_url = 'http://www.merchante-solutions.com/' @@ -18,6 +18,8 @@ class MerchantESolutionsGateway < Gateway # The name of the gateway self.display_name = 'Merchant e-Solutions' + SUCCESS_RESPONSE_CODES = %w(000 085) + def initialize(options = {}) requires!(options, :login, :password) super @@ -25,28 +27,26 @@ def initialize(options = {}) def authorize(money, creditcard_or_card_id, options = {}) post = {} - post[:client_reference_number] = options[:customer] if options.has_key?(:customer) - post[:moto_ecommerce_ind] = options[:moto_ecommerce_ind] if options.has_key?(:moto_ecommerce_ind) add_invoice(post, options) add_payment_source(post, creditcard_or_card_id, options) add_address(post, options) add_3dsecure_params(post, options) + add_stored_credentials(post, options) commit('P', money, post) end def purchase(money, creditcard_or_card_id, options = {}) post = {} - post[:client_reference_number] = options[:customer] if options.has_key?(:customer) - post[:moto_ecommerce_ind] = options[:moto_ecommerce_ind] if options.has_key?(:moto_ecommerce_ind) add_invoice(post, options) add_payment_source(post, creditcard_or_card_id, options) add_address(post, options) add_3dsecure_params(post, options) + add_stored_credentials(post, options) commit('D', money, post) end def capture(money, transaction_id, options = {}) - post ={} + post = {} post[:transaction_id] = transaction_id post[:client_reference_number] = options[:customer] if options.has_key?(:customer) add_invoice(post, options) @@ -55,10 +55,10 @@ def capture(money, transaction_id, options = {}) end def store(creditcard, options = {}) - post = {} - post[:client_reference_number] = options[:customer] if options.has_key?(:customer) - add_creditcard(post, creditcard, options) - commit('T', nil, post) + MultiResponse.run do |r| + r.process { temporary_store(creditcard, options) } + r.process { verify(r.authorization, { store_card: 'y' }) } + end end def unstore(card_id, options = {}) @@ -94,6 +94,13 @@ def void(transaction_id, options = {}) commit('V', nil, options.merge(post)) end + def verify(credit_card, options = {}) + post = {} + post[:store_card] = options[:store_card] if options[:store_card] + add_payment_source(post, credit_card, options) + commit('A', 0, post) + end + def supports_scrubbing? true end @@ -107,6 +114,13 @@ def scrub(transcript) private + def temporary_store(creditcard, options = {}) + post = {} + post[:client_reference_number] = options[:customer] if options.has_key?(:customer) + add_creditcard(post, creditcard, options) + commit('T', nil, post) + end + def add_address(post, options) if address = options[:billing_address] || options[:address] post[:cardholder_street_address] = address[:address1].to_s.gsub(/[^\w.]/, '+') @@ -133,9 +147,9 @@ def add_payment_source(post, creditcard_or_card_id, options) end def add_creditcard(post, creditcard, options) - post[:card_number] = creditcard.number + post[:card_number] = creditcard.number post[:cvv2] = creditcard.verification_value if creditcard.verification_value? - post[:card_exp_date] = expdate(creditcard) + post[:card_exp_date] = expdate(creditcard) end def add_3dsecure_params(post, options) @@ -145,10 +159,20 @@ def add_3dsecure_params(post, options) post[:ucaf_auth_data] = options[:ucaf_auth_data] unless empty?(options[:ucaf_auth_data]) end + def add_stored_credentials(post, options) + post[:client_reference_number] = options[:client_reference_number] if options[:client_reference_number] + post[:moto_ecommerce_ind] = options[:moto_ecommerce_ind] if options[:moto_ecommerce_ind] + post[:recurring_pmt_num] = options[:recurring_pmt_num] if options[:recurring_pmt_num] + post[:recurring_pmt_count] = options[:recurring_pmt_count] if options[:recurring_pmt_count] + post[:card_on_file] = options[:card_on_file] if options[:card_on_file] + post[:cit_mit_indicator] = options[:cit_mit_indicator] if options[:cit_mit_indicator] + post[:account_data_source] = options[:account_data_source] if options[:account_data_source] + end + def parse(body) results = {} body.split(/&/).each do |pair| - key,val = pair.split(/=/) + key, val = pair.split(/=/) results[key] = val end results @@ -156,21 +180,34 @@ 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 + parse(ssl_post(url, post_data(action, parameters))) + rescue ActiveMerchant::ResponseError => e + { 'error_code' => '404', 'auth_response_text' => e.to_s } + end + + 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'] } + ) + end + def authorization_from(response) + return response['card_id'] if response['card_id'] - response = begin - parse( ssl_post(url, post_data(action,parameters)) ) - rescue ActiveMerchant::ResponseError => e - { 'error_code' => '404', 'auth_response_text' => e.to_s } - end + response['transaction_id'] + end - Response.new(response['error_code'] == '000', message_from(response), response, - :authorization => response['transaction_id'], - :test => test?, - :cvv_result => response['cvv2_result'], - :avs_result => { :code => response['avs_result'] } - ) + def success_from(response) + SUCCESS_RESPONSE_CODES.include?(response['error_code']) end def message_from(response) @@ -187,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_one.rb b/lib/active_merchant/billing/gateways/merchant_one.rb index 1a2c457fec5..68951b1724c 100644 --- a/lib/active_merchant/billing/gateways/merchant_one.rb +++ b/lib/active_merchant/billing/gateways/merchant_one.rb @@ -1,9 +1,8 @@ require 'cgi' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class MerchantOneGateway < Gateway - class MerchantOneSslConnection < ActiveMerchant::Connection def configure_ssl(http) super(http) @@ -14,7 +13,7 @@ def configure_ssl(http) BASE_URL = 'https://secure.merchantonegateway.com/api/transact.php' self.supported_countries = ['US'] - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'http://merchantone.com/' self.display_name = 'Merchant One Gateway' self.money_format = :dollars @@ -46,7 +45,7 @@ def purchase(money, creditcard, options = {}) def capture(money, authorization, options = {}) post = {} - post.merge!(:transactionid => authorization) + post[:transactionid] = authorization add_amount(post, money, options) commit('capture', money, post) end @@ -75,40 +74,37 @@ def add_address(post, creditcard, options) end def add_creditcard(post, creditcard) - post['cvv'] = creditcard.verification_value - post['ccnumber'] = creditcard.number - post['ccexp'] = "#{sprintf("%02d", creditcard.month)}#{"#{creditcard.year}"[-2, 2]}" + post['cvv'] = creditcard.verification_value + post['ccnumber'] = creditcard.number + post['ccexp'] = "#{sprintf('%02d', creditcard.month)}#{creditcard.year.to_s[-2, 2]}" end - def commit(action, money, parameters={}) + def commit(action, money, parameters = {}) parameters['username'] = @options[:username] parameters['password'] = @options[:password] - parse(ssl_post(BASE_URL,post_data(action, parameters))) + parse(ssl_post(BASE_URL, post_data(action, parameters))) end def post_data(action, parameters = {}) - parameters.merge!({:type => action}) + parameters[:type] = action ret = '' for key in parameters.keys ret += "#{key}=#{CGI.escape(parameters[key].to_s)}" - if key != parameters.keys.last - ret += '&' - end + ret += '&' if key != parameters.keys.last end ret.to_s end def parse(data) - responses = CGI.parse(data).inject({}){|h,(k, v)| h[k] = v.first; h} + responses = CGI.parse(data).inject({}) { |h, (k, v)| h[k] = v.first; h } Response.new( (responses['response'].to_i == 1), responses['responsetext'], responses, - :test => test?, - :authorization => responses['transactionid'] + test: test?, + authorization: responses['transactionid'] ) end end end end - diff --git a/lib/active_merchant/billing/gateways/merchant_partners.rb b/lib/active_merchant/billing/gateways/merchant_partners.rb index 3af4a301cdf..51a366cb197 100644 --- a/lib/active_merchant/billing/gateways/merchant_partners.rb +++ b/lib/active_merchant/billing/gateways/merchant_partners.rb @@ -1,7 +1,7 @@ require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class MerchantPartnersGateway < Gateway self.display_name = 'Merchant Partners' self.homepage_url = 'http://www.merchantpartners.com/' @@ -11,14 +11,14 @@ class MerchantPartnersGateway < Gateway self.supported_countries = ['US'] self.default_currency = 'USD' self.money_format = :dollars - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb] + self.supported_cardtypes = %i[visa master american_express discover diners_club jcb] - def initialize(options={}) + def initialize(options = {}) requires!(options, :account_id, :merchant_pin) super end - def purchase(amount, payment_method, options={}) + def purchase(amount, payment_method, options = {}) post = {} add_invoice(post, amount, options) add_payment_method(post, payment_method) @@ -27,7 +27,7 @@ def purchase(amount, payment_method, options={}) commit(payment_method.is_a?(String) ? :stored_purchase : :purchase, post) end - def authorize(amount, payment_method, options={}) + def authorize(amount, payment_method, options = {}) post = {} add_invoice(post, amount, options) add_payment_method(post, payment_method) @@ -36,7 +36,7 @@ def authorize(amount, payment_method, options={}) commit(:authorize, post) end - def capture(amount, authorization, options={}) + def capture(amount, authorization, options = {}) post = {} add_invoice(post, amount, options) add_reference(post, authorization) @@ -45,14 +45,14 @@ def capture(amount, authorization, options={}) commit(:capture, post) end - def void(authorization, options={}) + def void(authorization, options = {}) post = {} add_reference(post, authorization) commit(:void, post) end - def refund(amount, authorization, options={}) + def refund(amount, authorization, options = {}) post = {} add_invoice(post, amount, options) add_reference(post, authorization) @@ -61,7 +61,7 @@ def refund(amount, authorization, options={}) commit(:refund, post) end - def credit(amount, payment_method, options={}) + def credit(amount, payment_method, options = {}) post = {} add_invoice(post, amount, options) add_payment_method(post, payment_method) @@ -69,7 +69,7 @@ def credit(amount, payment_method, options={}) commit(payment_method.is_a?(String) ? :stored_credit : :credit, post) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -110,10 +110,10 @@ def add_invoice(post, money, options) end def add_payment_method(post, payment_method) - if(payment_method.is_a?(String)) - user_profile_id, last_4 = split_authorization(payment_method) + if payment_method.is_a?(String) + user_profile_id, last4 = split_authorization(payment_method) post[:userprofileid] = user_profile_id - post[:last4digits] = last_4 + post[:last4digits] = last4 else post[:ccname] = payment_method.name post[:ccnum] = payment_method.number @@ -127,13 +127,13 @@ def add_payment_method(post, payment_method) def add_customer_data(post, options) post[:email] = options[:email] if options[:email] post[:ipaddress] = options[:ip] if options[:ip] - if(billing_address = options[:billing_address]) + if (billing_address = options[:billing_address]) post[:billaddr1] = billing_address[:address1] post[:billaddr2] = billing_address[:address2] post[:billcity] = billing_address[:city] post[:billstate] = billing_address[:state] post[:billcountry] = billing_address[:country] - post[:bilzip] = billing_address[:zip] + post[:bilzip] = billing_address[:zip] post[:phone] = billing_address[:phone] end end @@ -172,20 +172,20 @@ def commit(action, post) message_from(succeeded, response_data), response_data, authorization: authorization_from(post, response_data), - :avs_result => AVSResult.new(code: response_data['avs_response']), - :cvv_result => CVVResult.new(response_data['cvv2_response']), + avs_result: AVSResult.new(code: response_data['avs_response']), + cvv_result: CVVResult.new(response_data['cvv2_response']), test: test? ) end def headers { - 'Content-Type' => 'application/xml' + 'Content-Type' => 'application/xml' } end def build_request(post) - Nokogiri::XML::Builder.new(:encoding => 'utf-8') do |xml| + Nokogiri::XML::Builder.new(encoding: 'utf-8') do |xml| xml.interface_driver { xml.trans_catalog { xml.transaction(name: 'creditcard') { @@ -212,7 +212,7 @@ def parse_element(response, node) if node.elements.size == 0 response[node.name.downcase.underscore.to_sym] = node.text else - node.elements.each{|element| parse_element(response, element) } + node.elements.each { |element| parse_element(response, element) } end end @@ -235,7 +235,7 @@ def split_authorization(authorization) end def error_message_from(response) - if(response[:status] == 'Declined') + if response[:status] == 'Declined' match = response[:result].match(/DECLINED:\d{10}:(.+):/) match[1] if match end diff --git a/lib/active_merchant/billing/gateways/merchant_ware.rb b/lib/active_merchant/billing/gateways/merchant_ware.rb index 3b8d4a20b0e..bc10d169a59 100644 --- a/lib/active_merchant/billing/gateways/merchant_ware.rb +++ b/lib/active_merchant/billing/gateways/merchant_ware.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class MerchantWareGateway < Gateway class_attribute :v4_live_url @@ -7,29 +7,27 @@ class MerchantWareGateway < Gateway self.v4_live_url = 'https://ps1.merchantware.net/Merchantware/ws/RetailTransaction/v4/Credit.asmx' self.supported_countries = ['US'] - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'http://merchantwarehouse.com/merchantware' self.display_name = 'MerchantWARE' ENV_NAMESPACES = { 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema', - 'xmlns:env' => 'http://schemas.xmlsoap.org/soap/envelope/' - } + 'xmlns:env' => 'http://schemas.xmlsoap.org/soap/envelope/' } ENV_NAMESPACES_V4 = { '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/' - } + 'xmlns:soap' => 'http://schemas.xmlsoap.org/soap/envelope/' } TX_NAMESPACE = 'http://merchantwarehouse.com/MerchantWARE/Client/TransactionRetail' TX_NAMESPACE_V4 = 'http://schemas.merchantwarehouse.com/merchantware/40/Credit/' ACTIONS = { - :purchase => 'IssueKeyedSale', - :authorize => 'IssueKeyedPreAuth', - :capture => 'IssuePostAuth', - :void => 'VoidPreAuthorization', - :credit => 'IssueKeyedRefund', - :reference_credit => 'IssueRefundByReference' + purchase: 'IssueKeyedSale', + authorize: 'IssueKeyedPreAuth', + capture: 'IssuePostAuth', + void: 'VoidPreAuthorization', + credit: 'IssueKeyedRefund', + reference_credit: 'IssueRefundByReference' } # Creates a new MerchantWareGateway @@ -119,7 +117,7 @@ def refund(money, reference, options = {}) private def soap_request(action) - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 xml.instruct! xml.tag! 'env:Envelope', ENV_NAMESPACES do xml.tag! 'env:Body' do @@ -133,7 +131,7 @@ def soap_request(action) end def v4_soap_request(action) - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 xml.instruct! xml.tag! 'soap:Envelope', ENV_NAMESPACES_V4 do xml.tag! 'soap:Body' do @@ -226,10 +224,10 @@ def add_credit_card(xml, credit_card) if credit_card.respond_to?(:track_data) && credit_card.track_data.present? xml.tag! 'trackData', credit_card.track_data else - xml.tag! 'strPAN', credit_card.number - xml.tag! 'strExpDate', expdate(credit_card) - xml.tag! 'strCardHolder', credit_card.name - xml.tag! 'strCVCode', credit_card.verification_value if credit_card.verification_value? + xml.tag! 'strPAN', credit_card.number + xml.tag! 'strExpDate', expdate(credit_card) + xml.tag! 'strCardHolder', credit_card.name + xml.tag! 'strCVCode', credit_card.verification_value if credit_card.verification_value? end end @@ -274,7 +272,7 @@ def parse_error(http_response) response[element.name] = element.text end - response[:message] = response['faultstring'].to_s.gsub("\n", ' ') + response[:message] = response['faultstring'].to_s.tr("\n", ' ') response rescue REXML::ParseException response[:http_body] = http_response.body @@ -292,27 +290,30 @@ def url(v4 = false) def commit(action, request, v4 = false) begin - data = ssl_post(url(v4), request, - 'Content-Type' => 'text/xml; charset=utf-8', - 'SOAPAction' => soap_action(action, v4) - ) + data = ssl_post( + url(v4), + request, + 'Content-Type' => 'text/xml; charset=utf-8', + '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, - :test => test?, - :authorization => authorization_from(response), - :avs_result => { :code => response['AVSResponse'] }, - :cvv_result => response['CVResponse'] + Response.new( + response[:success], + response[:message], + response, + test: test?, + authorization: authorization_from(response), + avs_result: { code: response['AVSResponse'] }, + cvv_result: response['CVResponse'] ) end def authorization_from(response) - if response[:success] - [ response['ReferenceID'], response['OrderNumber'] ].join(';') - end + [response['ReferenceID'], response['OrderNumber']].join(';') if response[:success] end end end 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 109d00fce00..a793892e117 100644 --- a/lib/active_merchant/billing/gateways/merchant_ware_version_four.rb +++ b/lib/active_merchant/billing/gateways/merchant_ware_version_four.rb @@ -1,11 +1,11 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class MerchantWareVersionFourGateway < Gateway self.live_url = 'https://ps1.merchantware.net/Merchantware/ws/RetailTransaction/v4/Credit.asmx' self.test_url = 'https://ps1.merchantware.net/Merchantware/ws/RetailTransaction/v4/Credit.asmx' self.supported_countries = ['US'] - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'http://merchantwarehouse.com/merchantware' self.display_name = 'MerchantWARE' @@ -16,12 +16,12 @@ class MerchantWareVersionFourGateway < Gateway TX_NAMESPACE = 'http://schemas.merchantwarehouse.com/merchantware/40/Credit/' ACTIONS = { - :purchase => 'SaleKeyed', - :reference_purchase => 'RepeatSale', - :authorize => 'PreAuthorizationKeyed', - :capture => 'PostAuthorization', - :void => 'Void', - :refund => 'Refund' + purchase: 'SaleKeyed', + reference_purchase: 'RepeatSale', + authorize: 'PreAuthorizationKeyed', + capture: 'PostAuthorization', + void: 'Void', + refund: 'Refund' } # Creates a new MerchantWareVersionFourGateway @@ -108,7 +108,7 @@ def refund(money, identification, options = {}) commit(:refund, request) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -129,7 +129,7 @@ def scrub(transcript) private def soap_request(action) - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 xml.instruct! xml.tag! 'soap:Envelope', ENV_NAMESPACES do xml.tag! 'soap:Body' do @@ -243,7 +243,7 @@ def parse_error(http_response, action) response[element.name] = element.text end - response[:message] = response['ErrorMessage'].to_s.gsub("\n", ' ') + response[:message] = response['ErrorMessage'].to_s.tr("\n", ' ') response rescue REXML::ParseException response[:http_body] = http_response.body @@ -261,20 +261,25 @@ def url def commit(action, request) begin - data = ssl_post(url, request, - 'Content-Type' => 'text/xml; charset=utf-8', - 'SOAPAction' => soap_action(action) - ) + data = ssl_post( + url, + request, + 'Content-Type' => 'text/xml; charset=utf-8', + '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, - :test => test?, - :authorization => authorization_from(response), - :avs_result => { :code => response['AvsResponse'] }, - :cvv_result => response['CvResponse'] + Response.new( + response[:success], + response[:message], + response, + test: test?, + authorization: authorization_from(response), + avs_result: { code: response['AvsResponse'] }, + cvv_result: response['CvResponse'] ) end diff --git a/lib/active_merchant/billing/gateways/merchant_warrior.rb b/lib/active_merchant/billing/gateways/merchant_warrior.rb index 701b44ab049..62a1dbca207 100644 --- a/lib/active_merchant/billing/gateways/merchant_warrior.rb +++ b/lib/active_merchant/billing/gateways/merchant_warrior.rb @@ -1,8 +1,8 @@ require 'digest/md5' require 'rexml/document' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class MerchantWarriorGateway < Gateway TOKEN_TEST_URL = 'https://base.merchantwarrior.com/token/' TOKEN_LIVE_URL = 'https://api.merchantwarrior.com/token/' @@ -11,8 +11,8 @@ class MerchantWarriorGateway < Gateway POST_LIVE_URL = 'https://api.merchantwarrior.com/post/' self.supported_countries = ['AU'] - self.supported_cardtypes = [:visa, :master, :american_express, - :diners_club, :discover, :jcb] + self.supported_cardtypes = %i[visa master american_express + diners_club discover jcb] self.homepage_url = 'https://www.merchantwarrior.com/' self.display_name = 'Merchant Warrior' @@ -30,6 +30,10 @@ def authorize(money, payment_method, options = {}) add_order_id(post, options) add_address(post, options) add_payment_method(post, payment_method) + add_recurring_flag(post, options) + add_soft_descriptors(post, options) + add_three_ds(post, options) + post['storeID'] = options[:store_id] if options[:store_id] commit('processAuth', post) end @@ -39,6 +43,10 @@ def purchase(money, payment_method, options = {}) add_order_id(post, options) add_address(post, options) add_payment_method(post, payment_method) + add_recurring_flag(post, options) + add_soft_descriptors(post, options) + add_three_ds(post, options) + post['storeID'] = options[:store_id] if options[:store_id] commit('processCard', post) end @@ -46,7 +54,8 @@ def capture(money, identification, options = {}) post = {} add_amount(post, money, options) add_transaction(post, identification) - post.merge!('captureAmount' => amount(money)) + add_soft_descriptors(post, options) + post['captureAmount'] = amount(money) commit('processCapture', post) end @@ -54,10 +63,21 @@ def refund(money, identification, options = {}) post = {} add_amount(post, money, options) add_transaction(post, identification) + add_soft_descriptors(post, options) post['refundAmount'] = amount(money) commit('refundCard', post) end + def void(identification, options = {}) + post = {} + # The amount parameter is required for void transactions + # on the Merchant Warrior gateway. + post['transactionAmount'] = options[:amount] + post['hash'] = void_verification_hash(identification) + add_transaction(post, identification) + commit('processVoid', post) + end + def store(creditcard, options = {}) post = { 'cardName' => scrub_name(creditcard.name), @@ -87,7 +107,7 @@ def add_transaction(post, identification) end def add_address(post, options) - return unless(address = (options[:billing_address] || options[:address])) + return unless (address = (options[:billing_address] || options[:address])) post['customerName'] = scrub_name(address[:name]) post['customerCountry'] = address[:country] @@ -95,9 +115,9 @@ def add_address(post, options) post['customerCity'] = address[:city] post['customerAddress'] = address[:address1] post['customerPostCode'] = address[:zip] - post['customerIP'] = address[:ip] - post['customerPhone'] = address[:phone] - post['customerEmail'] = address[:email] + post['customerIP'] = address[:ip] || options[:ip] + post['customerPhone'] = address[:phone] || address[:phone_number] + post['customerEmail'] = address[:email] || options[:email] end def add_order_id(post, options) @@ -135,6 +155,18 @@ def add_amount(post, money, options) post['hash'] = verification_hash(amount(money), currency) end + def add_recurring_flag(post, options) + return if options[:recurring_flag].nil? + + post['recurringFlag'] = options[:recurring_flag] + end + + def add_soft_descriptors(post, options) + post['descriptorName'] = options[:descriptor_name] if options[:descriptor_name] + post['descriptorCity'] = options[:descriptor_city] if options[:descriptor_city] + post['descriptorState'] = options[:descriptor_state] if options[:descriptor_state] + end + def verification_hash(money, currency) Digest::MD5.hexdigest( ( @@ -146,9 +178,33 @@ def verification_hash(money, currency) ) end + def void_verification_hash(transaction_id) + Digest::MD5.hexdigest( + ( + @options[:api_passphrase].to_s + + @options[:merchant_uuid].to_s + + transaction_id + ).downcase + ) + 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) + return { response_message: 'Invalid gateway response' } unless xml.root.present? + response = {} xml.root.elements.to_a.each do |node| parse_element(response, node) @@ -158,7 +214,7 @@ def parse(body) def parse_element(response, node) if node.has_elements? - node.elements.each{|element| parse_element(response, element)} + node.elements.each { |element| parse_element(response, element) } else response[node.name.underscore.to_sym] = node.text end @@ -173,17 +229,15 @@ def commit(action, post) success?(response), response[:response_message], response, - :test => test?, - :authorization => (response[:card_id] || response[:transaction_id]) + test: test?, + authorization: (response[:card_id] || response[:transaction_id]) ) end def add_auth(action, post) post['merchantUUID'] = @options[:merchant_uuid] post['apiKey'] = @options[:api_key] - unless token?(post) - post['method'] = action - end + post['method'] = action unless token?(post) end def url_for(action, post) @@ -203,7 +257,7 @@ def success?(response) end def post_data(post) - post.collect{|k,v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&') + post.collect { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&') end end end diff --git a/lib/active_merchant/billing/gateways/mercury.rb b/lib/active_merchant/billing/gateways/mercury.rb index 54f51af876c..f54c257a8e1 100644 --- a/lib/active_merchant/billing/gateways/mercury.rb +++ b/lib/active_merchant/billing/gateways/mercury.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # The Mercury gateway integration by default requires that the Mercury # account being used has tokenization turned. This enables the use of # capture/refund/void without having to pass the credit card back in each @@ -12,14 +12,14 @@ module Billing #:nodoc: # and +refund+ will become mandatory. class MercuryGateway < Gateway URLS = { - :test => 'https://w1.mercurycert.net/ws/ws.asmx', - :live => 'https://w1.mercurypay.com/ws/ws.asmx' + test: 'https://w1.mercurycert.net/ws/ws.asmx', + live: 'https://w1.mercurypay.com/ws/ws.asmx' } self.homepage_url = 'http://www.mercurypay.com' self.display_name = 'Mercury' - self.supported_countries = ['US','CA'] - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb] + self.supported_countries = %w[US CA] + self.supported_cardtypes = %i[visa master american_express discover diners_club jcb] self.default_currency = 'USD' STANDARD_ERROR_CODE_MAPPING = { @@ -51,14 +51,14 @@ def credit(money, credit_card, options = {}) def authorize(money, credit_card, options = {}) requires!(options, :order_id) - request = build_non_authorized_request('PreAuth', money, credit_card, options.merge(:authorized => money)) + request = build_non_authorized_request('PreAuth', money, credit_card, options.merge(authorized: money)) commit('PreAuth', request) end def capture(money, authorization, options = {}) requires!(options, :credit_card) unless @use_tokenization - request = build_authorized_request('PreAuthCapture', money, authorization, options[:credit_card], options.merge(:authorized => money)) + request = build_authorized_request('PreAuthCapture', money, authorization, options[:credit_card], options.merge(authorized: money)) commit('PreAuthCapture', request) end @@ -69,14 +69,14 @@ def refund(money, authorization, options = {}) commit('Return', request) end - def void(authorization, options={}) + def void(authorization, options = {}) requires!(options, :credit_card) unless @use_tokenization request = build_authorized_request('VoidSale', nil, authorization, options[:credit_card], options) commit('VoidSale', request) end - def store(credit_card, options={}) + def store(credit_card, options = {}) request = build_card_lookup_request(credit_card, options) commit('CardLookup', request) end @@ -103,9 +103,7 @@ def build_non_authorized_request(action, money, credit_card, options) xml.tag! 'Transaction' do xml.tag! 'TranType', 'Credit' xml.tag! 'TranCode', action - if options[:allow_partial_auth] && (action == 'PreAuth' || action == 'Sale') - xml.tag! 'PartialAuth', 'Allow' - end + xml.tag! 'PartialAuth', 'Allow' if options[:allow_partial_auth] && %w[PreAuth Sale].include?(action) add_invoice(xml, options[:order_id], nil, options) add_reference(xml, 'RecordNumberRequested') add_customer_data(xml, options) @@ -126,9 +124,7 @@ def build_authorized_request(action, money, authorization, credit_card, options) xml.tag! 'TStream' do xml.tag! 'Transaction' do xml.tag! 'TranType', 'Credit' - if options[:allow_partial_auth] && (action == 'PreAuthCapture') - xml.tag! 'PartialAuth', 'Allow' - end + xml.tag! 'PartialAuth', 'Allow' if options[:allow_partial_auth] && (action == 'PreAuthCapture') xml.tag! 'TranCode', (@use_tokenization ? (action + 'ByRecordNo') : action) add_invoice(xml, invoice_no, ref_no, options) add_reference(xml, record_no) @@ -212,14 +208,14 @@ def add_credit_card(xml, credit_card, action) # Track 1 and 2 have identical end sentinels (ETX) of '?' # Tracks may or may not have checksum (LRC) after the ETX # If the track has no STX or is corrupt, we send it as track 1, to let Mercury - #handle with the validation error as it sees fit. + # handle with the validation error as it sees fit. # Track 2 requires having the STX and ETX stripped. Track 1 does not. # Max-length track 1s require having the STX and ETX stripped. Max is 79 bytes including LRC. - is_track_2 = credit_card.track_data[0] == ';' + is_track2 = credit_card.track_data[0] == ';' etx_index = credit_card.track_data.rindex('?') || credit_card.track_data.length is_max_track1 = etx_index >= 77 - if is_track_2 + if is_track2 xml.tag! 'Track2', credit_card.track_data[1...etx_index] elsif is_max_track1 xml.tag! 'Track1', credit_card.track_data[1...etx_index] @@ -234,7 +230,7 @@ def add_credit_card(xml, credit_card, action) xml.tag! 'CardType', CARD_CODES[credit_card.brand] if credit_card.brand include_cvv = !%w(Return PreAuthCapture).include?(action) && !credit_card.track_data.present? - xml.tag! 'CVVData', credit_card.verification_value if(include_cvv && credit_card.verification_value) + xml.tag! 'CVVData', credit_card.verification_value if include_cvv && credit_card.verification_value end def add_address(xml, options) @@ -298,7 +294,7 @@ def build_header } end - SUCCESS_CODES = [ 'Approved', 'Success' ] + SUCCESS_CODES = %w[Approved Success] def commit(action, request) response = parse(action, ssl_post(endpoint_url, build_soap_request(request), build_header)) @@ -306,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, - :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]]) + 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]] + ) end def message_from(response) @@ -319,7 +319,7 @@ def message_from(response) end def authorization_from(response) - dollars, cents = (response[:purchase] || '').split('.').collect{|e| e.to_i} + dollars, cents = (response[:purchase] || '').split('.').collect(&:to_i) dollars ||= 0 cents ||= 0 [ @@ -349,7 +349,9 @@ def escape_xml(xml) end def unescape_xml(escaped_xml) - escaped_xml.gsub(/\>/,'>').gsub(/\</,'<') + xml = escaped_xml.gsub(/\>/, '>').gsub(/\r/, '').gsub(/\n/, '').gsub(/\t/, '').gsub('<', '<') + xml.slice! "" # rubocop:disable Style/StringLiterals + xml end end end diff --git a/lib/active_merchant/billing/gateways/metrics_global.rb b/lib/active_merchant/billing/gateways/metrics_global.rb index de046d9932b..d8bb62e4903 100644 --- a/lib/active_merchant/billing/gateways/metrics_global.rb +++ b/lib/active_merchant/billing/gateways/metrics_global.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # For more information on the Metrics Global Payment Gateway, visit the {Metrics Global website}[www.metricsglobal.com]. # Further documentation on AVS and CVV response codes are available under the support section of the Metrics Global # control panel. @@ -31,12 +31,12 @@ class MetricsGlobalGateway < Gateway AVS_RESULT_CODE, TRANSACTION_ID, CARD_CODE_RESPONSE_CODE = 5, 6, 38 self.supported_countries = ['US'] - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb] + self.supported_cardtypes = %i[visa master american_express discover diners_club jcb] self.homepage_url = 'http://www.metricsglobal.com' self.display_name = 'Metrics Global' - CARD_CODE_ERRORS = %w( N S ) - AVS_ERRORS = %w( A E N R W Z ) + CARD_CODE_ERRORS = %w(N S) + AVS_ERRORS = %w(A E N R W Z) AVS_REASON_CODES = %w(27 45) # Creates a new MetricsGlobalGateway @@ -99,7 +99,7 @@ def purchase(money, creditcard, options = {}) # * money -- The amount to be captured as an Integer value in cents. # * authorization -- The authorization returned from the previous authorize request. def capture(money, authorization, options = {}) - post = {:trans_id => authorization} + post = { trans_id: authorization } add_customer_data(post, options) commit('PRIOR_AUTH_CAPTURE', money, post) end @@ -110,7 +110,7 @@ def capture(money, authorization, options = {}) # # * authorization - The authorization returned from the previous authorize request. def void(authorization, options = {}) - post = {:trans_id => authorization} + post = { trans_id: authorization } add_duplicate_window(post) commit('VOID', nil, post) end @@ -135,9 +135,8 @@ def void(authorization, options = {}) def refund(money, identification, options = {}) requires!(options, :card_number) - post = { :trans_id => identification, - :card_num => options[:card_number] - } + post = { trans_id: identification, + card_num: options[:card_number] } post[:first_name] = options[:first_name] if options[:first_name] post[:last_name] = options[:last_name] if options[:last_name] @@ -176,12 +175,15 @@ def commit(action, money, parameters) # (TESTMODE) Successful Sale test_mode = test? || message =~ /TESTMODE/ - 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] + 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] ) end @@ -196,15 +198,14 @@ 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], - :avs_result_code => fields[AVS_RESULT_CODE], - :transaction_id => fields[TRANSACTION_ID], - :card_code => fields[CARD_CODE_RESPONSE_CODE] + { + response_code: fields[RESPONSE_CODE].to_i, + response_reason_code: fields[RESPONSE_REASON_CODE], + response_reason_text: fields[RESPONSE_REASON_TEXT], + avs_result_code: fields[AVS_RESULT_CODE], + transaction_id: fields[TRANSACTION_ID], + card_code: fields[CARD_CODE_RESPONSE_CODE] } - results end def post_data(action, parameters = {}) @@ -220,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) @@ -243,21 +243,15 @@ def add_customer_data(post, options) post[:email_customer] = false end - if options.has_key? :customer - post[:cust_id] = options[:customer] - end + post[:cust_id] = options[:customer] if options.has_key? :customer - if options.has_key? :ip - post[:customer_ip] = options[:ip] - end + post[:customer_ip] = options[:ip] if options.has_key? :ip end # x_duplicate_window won't be sent by default, because sending it changes the response. # "If this field is present in the request with or without a value, an enhanced duplicate transaction response will be sent." def add_duplicate_window(post) - unless duplicate_window.nil? - post[:duplicate_window] = duplicate_window - end + post[:duplicate_window] = duplicate_window unless duplicate_window.nil? end def add_address(post, options) @@ -268,7 +262,7 @@ def add_address(post, options) post[:zip] = address[:zip].to_s post[:city] = address[:city].to_s post[:country] = address[:country].to_s - post[:state] = address[:state].blank? ? 'n/a' : address[:state] + post[:state] = address[:state].blank? ? 'n/a' : address[:state] end if address = options[:shipping_address] @@ -280,16 +274,14 @@ def add_address(post, options) post[:ship_to_zip] = address[:zip].to_s post[:ship_to_city] = address[:city].to_s post[:ship_to_country] = address[:country].to_s - post[:ship_to_state] = address[:state].blank? ? 'n/a' : address[:state] + post[:ship_to_state] = address[:state].blank? ? 'n/a' : address[:state] end end def message_from(results) if results[:response_code] == DECLINED - return CVVResult.messages[ results[:card_code] ] if CARD_CODE_ERRORS.include?(results[:card_code]) - if AVS_REASON_CODES.include?(results[:response_reason_code]) && AVS_ERRORS.include?(results[:avs_result_code]) - return AVSResult.messages[ results[:avs_result_code] ] - end + return CVVResult.messages[results[:card_code]] if CARD_CODE_ERRORS.include?(results[:card_code]) + return AVSResult.messages[results[:avs_result_code]] if AVS_REASON_CODES.include?(results[:response_reason_code]) && AVS_ERRORS.include?(results[:avs_result_code]) end (results[:response_reason_text] ? results[:response_reason_text].chomp('.') : '') diff --git a/lib/active_merchant/billing/gateways/micropayment.rb b/lib/active_merchant/billing/gateways/micropayment.rb index 3172e3ec4f1..03765f415cf 100644 --- a/lib/active_merchant/billing/gateways/micropayment.rb +++ b/lib/active_merchant/billing/gateways/micropayment.rb @@ -1,7 +1,6 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class MicropaymentGateway < Gateway - self.display_name = 'micropayment' self.homepage_url = 'https://www.micropayment.de/' @@ -10,15 +9,14 @@ class MicropaymentGateway < Gateway self.supported_countries = %w(DE) self.default_currency = 'EUR' self.money_format = :cents - self.supported_cardtypes = [:visa, :master, :american_express] - + self.supported_cardtypes = %i[visa master american_express] - def initialize(options={}) + def initialize(options = {}) requires!(options, :access_key) super end - def purchase(amount, payment_method, options={}) + def purchase(amount, payment_method, options = {}) post = {} add_invoice(post, amount, options) add_payment_method(post, payment_method, options) @@ -27,7 +25,7 @@ def purchase(amount, payment_method, options={}) commit('purchase', post) end - def authorize(amount, payment_method, options={}) + def authorize(amount, payment_method, options = {}) post = {} add_invoice(post, amount, options) add_payment_method(post, payment_method, options) @@ -36,28 +34,27 @@ def authorize(amount, payment_method, options={}) commit('authorize', post) end - def capture(amount, authorization, options={}) + def capture(amount, authorization, options = {}) post = {} add_reference(post, authorization) add_invoice(post, amount, options) commit('capture', post) end - def void(authorization, options={}) + def void(authorization, options = {}) post = {} add_reference(post, authorization) commit('void', post) end - def refund(amount, authorization, options={}) + def refund(amount, authorization, options = {}) post = {} add_reference(post, authorization) add_invoice(post, amount, options) commit('refund', post) end - def verify(credit_card, options={}) - + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(250, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -86,7 +83,7 @@ def add_invoice(post, money, options) post['params[title]'] = options[:description] if options[:description] end - def add_payment_method(post, payment_method, options={}) + def add_payment_method(post, payment_method, options = {}) post[:number] = payment_method.number post[:recurring] = 1 if options[:recurring] == true post[:cvc2] = payment_method.verification_value @@ -138,11 +135,11 @@ def commit(action, params) end def headers - { 'Content-Type' => 'application/x-www-form-urlencoded;charset=UTF-8' } + { 'Content-Type' => 'application/x-www-form-urlencoded;charset=UTF-8' } end def post_data(action, params) - params.map {|k, v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}"}.join('&') + params.map { |k, v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join('&') end def url(action) @@ -178,7 +175,7 @@ def split_authorization(authorization) def authorization_from(response, request_params) session_id = response['sessionId'] || request_params[:sessionId] - "#{session_id}|#{response["transactionId"]}" + "#{session_id}|#{response['transactionId']}" end end end diff --git a/lib/active_merchant/billing/gateways/migs.rb b/lib/active_merchant/billing/gateways/migs.rb index d0fa0d72411..51b9cbf8517 100644 --- a/lib/active_merchant/billing/gateways/migs.rb +++ b/lib/active_merchant/billing/gateways/migs.rb @@ -1,7 +1,7 @@ require 'active_merchant/billing/gateways/migs/migs_codes' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class MigsGateway < Gateway include MigsCodes @@ -17,10 +17,10 @@ class MigsGateway < Gateway # MiGS is supported throughout Asia Pacific, Middle East and Africa # MiGS is used in Australia (AU) by ANZ (eGate), CBA (CommWeb) and more # Source of Country List: http://www.scribd.com/doc/17811923 - self.supported_countries = %w(AU AE BD BN EG HK ID IN JO KW LB LK MU MV MY NZ OM PH QA SA SG TT VN) + self.supported_countries = %w(AU AE BD BN EG HK ID JO KW LB LK MU MV MY NZ OM PH QA SA SG TT VN) # The card types supported by the payment gateway - self.supported_cardtypes = [:visa, :master, :american_express, :diners_club, :jcb] + self.supported_cardtypes = %i[visa master american_express diners_club jcb] self.money_format = :cents self.currencies_without_fractions = %w(IDR) @@ -63,13 +63,14 @@ def purchase(money, creditcard, options = {}) add_creditcard(post, creditcard) add_standard_parameters('pay', post, options[:unique_id]) add_3ds(post, options) + add_tx_source(post, options) commit(post) end # MiGS works by merchants being either purchase only or authorize/capture # So authorize is the same as purchase when in authorize mode - alias_method :authorize, :purchase + alias authorize purchase # ==== Options # @@ -78,11 +79,12 @@ def purchase(money, creditcard, options = {}) def capture(money, authorization, options = {}) requires!(@options, :advanced_login, :advanced_password) - post = options.merge(:TransNo => authorization) + post = options.merge(TransNo: authorization) add_amount(post, money, options) add_advanced_user(post) add_standard_parameters('capture', post, options[:unique_id]) + add_tx_source(post, options) commit(post) end @@ -94,11 +96,12 @@ def capture(money, authorization, options = {}) def refund(money, authorization, options = {}) requires!(@options, :advanced_login, :advanced_password) - post = options.merge(:TransNo => authorization) + post = options.merge(TransNo: authorization) add_amount(post, money, options) add_advanced_user(post) add_standard_parameters('refund', post, options[:unique_id]) + add_tx_source(post, options) commit(post) end @@ -106,10 +109,11 @@ def refund(money, authorization, options = {}) def void(authorization, options = {}) requires!(@options, :advanced_login, :advanced_password) - post = options.merge(:TransNo => authorization) + post = options.merge(TransNo: authorization) add_advanced_user(post) add_standard_parameters('voidAuthorisation', post, options[:unique_id]) + add_tx_source(post, options) commit(post) end @@ -119,7 +123,7 @@ def credit(money, authorization, options = {}) refund(money, authorization, options) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run do |r| r.process { authorize(100, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -169,10 +173,8 @@ def purchase_offsite_url(money, options = {}) add_invoice(post, options) add_creditcard_type(post, options[:card_type]) if options[:card_type] - post.merge!( - :Locale => options[:locale] || 'en', - :ReturnURL => options[:return_url] - ) + post[:Locale] = options[:locale] || 'en' + post[:ReturnURL] = options[:return_url] add_standard_parameters('pay', post, options[:unique_id]) @@ -193,9 +195,7 @@ def purchase_offsite_response(data) response_hash = parse(data) expected_secure_hash = calculate_secure_hash(response_hash, @options[:secure_hash]) - unless response_hash[:SecureHash] == expected_secure_hash - raise SecurityError, 'Secure Hash mismatch, response may be tampered with' - end + raise SecurityError, 'Secure Hash mismatch, response may be tampered with' unless response_hash[:SecureHash] == expected_secure_hash response_object(response_hash) end @@ -243,6 +243,10 @@ def add_3ds(post, options) post['3DSstatus'] = options[:three_ds_status] if options[:three_ds_status] end + def add_tx_source(post, options) + post[:TxSource] = options[:tx_source] if options[:tx_source] + end + def add_creditcard(post, creditcard) post[:CardNum] = creditcard.number post[:CardSecurityCode] = creditcard.verification_value if creditcard.verification_value? @@ -250,8 +254,8 @@ def add_creditcard(post, creditcard) end def add_creditcard_type(post, card_type) - post[:Gateway] = 'ssl' - post[:card] = CARD_TYPES.detect{|ct| ct.am_code == card_type}.migs_long_code + post[:Gateway] = 'ssl' + post[:card] = CARD_TYPES.detect { |ct| ct.am_code == card_type }.migs_long_code end def parse(body) @@ -277,12 +281,15 @@ 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, - :test => test?, - :authorization => response[:TransactionNo], - :fraud_review => fraud_review?(response), - :avs_result => { :code => avs_response_code }, - :cvv_result => cvv_result_code + 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 ) end @@ -296,11 +303,11 @@ def fraud_review?(response) def add_standard_parameters(action, post, unique_id = nil) post.merge!( - :Version => API_VERSION, - :Merchant => @options[:login], - :AccessCode => @options[:password], - :Command => action, - :MerchTxnRef => unique_id || generate_unique_id.slice(0, 40) + Version: API_VERSION, + Merchant: @options[:login], + AccessCode: @options[:password], + Command: action, + MerchTxnRef: unique_id || generate_unique_id.slice(0, 40) ) end @@ -314,11 +321,11 @@ def add_secure_hash(post) end def calculate_secure_hash(post, secure_hash) - input = post - .reject { |k| %i[SecureHash SecureHashType].include?(k) } - .sort - .map { |(k, v)| "vpc_#{k}=#{v}" } - .join('&') + input = post. + reject { |k| %i[SecureHash SecureHashType].include?(k) }. + sort. + map { |(k, v)| "vpc_#{k}=#{v}" }. + join('&') OpenSSL::HMAC.hexdigest('SHA256', [secure_hash].pack('H*'), input).upcase end end diff --git a/lib/active_merchant/billing/gateways/migs/migs_codes.rb b/lib/active_merchant/billing/gateways/migs/migs_codes.rb index 9eae5f4bdfc..234c851d80b 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 @@ -85,13 +86,13 @@ def initialize(am_code, migs_code, migs_long_code, name) # migs_code: Used in response for purchase/authorize/status # migs_long_code: Used to pre-select card for server_purchase_url # name: The nice display name - %w(american_express AE Amex American\ Express), - %w(diners_club DC Dinersclub Diners\ Club), - %w(jcb JC JCB JCB\ Card), - %w(maestro MS Maestro Maestro\ Card), - %w(master MC Mastercard MasterCard), - %w(na PL PrivateLabelCard Private\ Label\ Card), - %w(visa VC Visa Visa\ Card') + ['american_express', 'AE', 'Amex', 'American Express'], + ['diners_club', 'DC', 'Dinersclub', 'Diners Club'], + ['jcb', 'JC', 'JCB', 'JCB Card'], + ['maestro', 'MS', 'Maestro', 'Maestro Card'], + %w(master MC Mastercard MasterCard), + ['na', 'PL', 'PrivateLabelCard', 'Private Label Card'], + ['visa', 'VC', 'Visa', 'Visa Card'] ].map do |am_code, migs_code, migs_long_code, name| CreditCardType.new(am_code, migs_code, migs_long_code, name) end diff --git a/lib/active_merchant/billing/gateways/mit.rb b/lib/active_merchant/billing/gateways/mit.rb new file mode 100644 index 00000000000..b6e37bf2cc6 --- /dev/null +++ b/lib/active_merchant/billing/gateways/mit.rb @@ -0,0 +1,265 @@ +require 'json' +require 'openssl' +require 'digest' +require 'base64' + +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class MitGateway < Gateway + self.live_url = 'https://wpy.mitec.com.mx/ModuloUtilWS/activeCDP.htm' + self.test_url = 'https://shoppingrc.mitec.com.mx/ModuloUtilWS/activeCDP.htm' + + self.supported_countries = ['MX'] + self.default_currency = 'MXN' + self.supported_cardtypes = %i[visa master] + + self.homepage_url = 'http://www.centrodepagos.com.mx/' + self.display_name = 'MIT Centro de pagos' + + self.money_format = :dollars + + def initialize(options = {}) + requires!(options, :commerce_id, :user, :api_key, :key_session) + super + end + + def purchase(money, payment, options = {}) + MultiResponse.run do |r| + r.process { authorize(money, payment, options) } + r.process { capture(money, r.authorization, options) } + end + end + + def cipher_key + @options[:key_session] + end + + def decrypt(val, keyinhex) + # Splits the first 16 bytes (the IV bytes) in array format + unpacked = val.unpack('m') + iv_base64 = unpacked[0].bytes.slice(0, 16) + # Splits the second bytes (the encrypted text bytes) these would be the + # original message + full_data = unpacked[0].bytes.slice(16, unpacked[0].bytes.length) + # Creates the engine + engine = OpenSSL::Cipher.new('aes-128-cbc') + # Set engine as decrypt mode + engine.decrypt + # Converts the key from hex to bytes + engine.key = [keyinhex].pack('H*') + # Converts the ivBase64 array into bytes + engine.iv = iv_base64.pack('c*') + # Decrypts the texts and returns the original string + engine.update(full_data.pack('c*')) + engine.final + end + + def encrypt(val, keyinhex) + # Creates the engine motor + engine = OpenSSL::Cipher.new('aes-128-cbc') + # Set engine as encrypt mode + engine.encrypt + # Converts the key from hex to bytes + engine.key = [keyinhex].pack('H*') + # Generates a random iv with this settings + iv_rand = engine.random_iv + # Packs IV as a Base64 string + iv_base64 = [iv_rand].pack('m') + # Converts the packed key into bytes + unpacked = iv_base64.unpack('m') + iv = unpacked[0] + # Sets the IV into the engine + engine.iv = iv + # Encrypts the texts and stores the bytes + encrypted_bytes = engine.update(val) + engine.final + # Concatenates the (a) IV bytes and (b) the encrypted bytes then returns a + # base64 representation + [iv << encrypted_bytes].pack('m') + end + + def authorize(money, payment, options = {}) + post = { + operation: 'Authorize', + commerce_id: @options[:commerce_id], + user: @options[:user], + apikey: @options[:api_key], + testMode: (test? ? 'YES' : 'NO') + } + add_invoice(post, money, options) + # Payments contains the card information + add_payment(post, payment) + add_customer_data(post, options) + post[:key_session] = @options[:key_session] + + post_to_json = post.to_json + post_to_json_encrypt = encrypt(post_to_json, @options[:key_session]) + + final_post = '' + post_to_json_encrypt + '' + @options[:user] + '' + json_post = final_post + commit('sale', json_post) + end + + def capture(money, authorization, options = {}) + post = { + operation: 'Capture', + commerce_id: @options[:commerce_id], + user: @options[:user], + apikey: @options[:api_key], + testMode: (test? ? 'YES' : 'NO'), + transaction_id: authorization, + amount: amount(money) + } + post[:key_session] = @options[:key_session] + + post_to_json = post.to_json + post_to_json_encrypt = encrypt(post_to_json, @options[:key_session]) + + final_post = '' + post_to_json_encrypt + '' + @options[:user] + '' + json_post = final_post + commit('capture', json_post) + end + + def refund(money, authorization, options = {}) + post = { + operation: 'Refund', + commerce_id: @options[:commerce_id], + user: @options[:user], + apikey: @options[:api_key], + testMode: (test? ? 'YES' : 'NO'), + transaction_id: authorization, + auth: authorization, + amount: amount(money) + } + post[:key_session] = @options[:key_session] + + post_to_json = post.to_json + post_to_json_encrypt = encrypt(post_to_json, @options[:key_session]) + + final_post = '' + post_to_json_encrypt + '' + @options[:user] + '' + json_post = final_post + commit('refund', json_post) + end + + 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]' + auth_json['cvv'] = '[FILTERED]' + auth_json['apikey'] = '[FILTERED]' + auth_json['key_session'] = '[FILTERED]' + auth_to_json = auth_json.to_json + auth_tagged = '' + auth_to_json + '' + ret_transcript = ret_transcript.gsub(/(.*?)<\/authorization>/, auth_tagged) + end + + 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]' + cap_json['key_session'] = '[FILTERED]' + cap_to_json = cap_json.to_json + cap_tagged = '' + cap_to_json + '' + ret_transcript = ret_transcript.gsub(/(.*?)<\/capture>/, cap_tagged) + end + + 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]' + ref_json['key_session'] = '[FILTERED]' + ref_to_json = ref_json.to_json + ref_tagged = '' + ref_to_json + '' + ret_transcript = ret_transcript.gsub(/(.*?)<\/refund>/, ref_tagged) + end + + 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 + end + + private + + def add_customer_data(post, options) + post[:email] = options[:email] || 'nadie@mit.test' + end + + def add_invoice(post, money, options) + post[:amount] = amount(money) + post[:currency] = (options[:currency] || currency(money)) + post[:reference] = options[:order_id] + post[:transaction_id] = options[:order_id] + end + + def add_payment(post, payment) + post[:installments] = 1 + post[:card] = payment.number + post[:expmonth] = payment.month + post[:expyear] = payment.year + post[:cvv] = payment.verification_value + post[:name_client] = [payment.first_name, payment.last_name].join(' ') + end + + def url + test? ? test_url : live_url + end + + def commit(action, parameters) + raw_response = ssl_post(url, parameters, { 'Content-type' => 'text/plain' }) + response = JSON.parse(decrypt(raw_response, @options[:key_session])) + + Response.new( + success_from(response), + message_from(response), + response, + authorization: authorization_from(response), + avs_result: AVSResult.new(code: response['some_avs_response_key']), + cvv_result: CVVResult.new(response['some_cvv_response_key']), + test: test?, + error_code: error_code_from(response) + ) + end + + def success_from(response) + response['response'] == 'approved' + end + + def message_from(response) + response['response'] + end + + def authorization_from(response) + response['reference'] + end + + def post_data(action, parameters = {}) + parameters.collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&') + end + + def error_code_from(response) + response['message'].split(' -- ', 2)[0] unless success_from(response) + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/modern_payments.rb b/lib/active_merchant/billing/gateways/modern_payments.rb index 340e3e7a0c7..48a3756ae68 100644 --- a/lib/active_merchant/billing/gateways/modern_payments.rb +++ b/lib/active_merchant/billing/gateways/modern_payments.rb @@ -1,7 +1,7 @@ require 'active_merchant/billing/gateways/modern_payments_cim' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class ModernPaymentsGateway < Gateway self.supported_countries = ModernPaymentsCimGateway.supported_countries self.supported_cardtypes = ModernPaymentsCimGateway.supported_cardtypes @@ -28,10 +28,10 @@ def purchase(money, credit_card, options = {}) end private + def cim @cim ||= ModernPaymentsCimGateway.new(@options) end end end end - diff --git a/lib/active_merchant/billing/gateways/modern_payments_cim.rb b/lib/active_merchant/billing/gateways/modern_payments_cim.rb index 688598fd8d9..51d0cce03ab 100644 --- a/lib/active_merchant/billing/gateways/modern_payments_cim.rb +++ b/lib/active_merchant/billing/gateways/modern_payments_cim.rb @@ -1,6 +1,6 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: - class ModernPaymentsCimGateway < Gateway #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class ModernPaymentsCimGateway < Gateway # :nodoc: self.test_url = 'https://secure.modpay.com/netservices/test/ModpayTest.asmx' self.live_url = 'https://secure.modpay.com/ws/modpay.asmx' @@ -8,7 +8,7 @@ class ModernPaymentsCimGateway < Gateway #:nodoc: TEST_XMLNS = 'https://secure.modpay.com/netservices/test/' self.supported_countries = ['US'] - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'http://www.modpay.com' self.display_name = 'Modern Payments' @@ -17,8 +17,8 @@ class ModernPaymentsCimGateway < Gateway #:nodoc: ERROR_MESSAGE = 'Transaction error' PAYMENT_METHOD = { - :check => 1, - :credit_card => 2 + check: 1, + credit_card: 2 } def initialize(options = {}) @@ -66,6 +66,7 @@ def create_payment(customer_id, amount, options = {}) end private + def add_payment_details(post, options) post[:pmtDate] = (options[:payment_date] || Time.now.utc).strftime('%Y-%m-%dT%H:%M:%SZ') post[:pmtType] = PAYMENT_METHOD[options[:payment_method] || :credit_card] @@ -80,7 +81,7 @@ def add_customer_id(post, customer_id) end def add_customer_data(post, options) - post[:acctNum] = options[:customer] + post[:acctNum] = options[:customer] end def add_address(post, options) @@ -110,18 +111,17 @@ def add_credit_card(post, credit_card) end def build_request(action, params) - xml = Builder::XmlMarkup.new :indent => 2 + 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] xml.tag! 'clientCode', @options[:password] - params.each {|key, value| xml.tag! key, value } + params.each { |key, value| xml.tag! key, value } end end end @@ -145,16 +145,23 @@ 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, - :test => test?, - :authorization => authorization_from(action, response), - :avs_result => { :code => response[:avs_code] } + Response.new( + successful?(action, response), + message_from(action, response), + response, + test: test?, + authorization: authorization_from(action, response), + avs_result: { code: response[:avs_code] } ) end @@ -206,7 +213,7 @@ def parse(action, xml) def parse_element(response, node) if node.has_elements? - node.elements.each{|e| parse_element(response, e) } + node.elements.each { |e| parse_element(response, e) } else response[node.name.underscore.to_sym] = node.text.to_s.strip end @@ -214,4 +221,3 @@ def parse_element(response, node) end end end - diff --git a/lib/active_merchant/billing/gateways/moka.rb b/lib/active_merchant/billing/gateways/moka.rb new file mode 100644 index 00000000000..af849f70e37 --- /dev/null +++ b/lib/active_merchant/billing/gateways/moka.rb @@ -0,0 +1,290 @@ +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class MokaGateway < Gateway + self.test_url = 'https://service.refmoka.com' + self.live_url = 'https://service.moka.com' + + self.supported_countries = %w[GB TR US] + self.default_currency = 'TRY' + self.money_format = :dollars + self.supported_cardtypes = %i[visa master american_express discover] + + self.homepage_url = 'http://developer.moka.com/' + self.display_name = 'Moka' + + ERROR_CODE_MAPPING = { + '000' => 'General error', + '001' => '3D Not authenticated', + '002' => 'Limit is insufficient', + '003' => 'Credit card number format is wrong', + '004' => 'General decline', + '005' => 'This process is invalid for the card owner', + '006' => 'Expiration date is wrong', + '007' => 'Invalid transaction', + '008' => 'Connection with the bank not established', + '009' => 'Undefined error code', + '010' => 'Bank SSL error', + '011' => 'Call the bank for the manual authentication', + '012' => 'Card info is wrong - Kart Number or CVV2', + '013' => '3D secure is not supported other than Visa MC cards', + '014' => 'Invalid account number', + '015' => 'CVV is wrong', + '016' => 'Authentication process is not present', + '017' => 'System error', + '018' => 'Stolen card', + '019' => 'Lost card', + '020' => 'Card with limited properties', + '021' => 'Timeout', + '022' => 'Invalid merchant', + '023' => 'False authentication', + '024' => '3D authorization is successful but the process cannot be completed', + '025' => '3D authorization failure', + '026' => 'Either the issuer bank or the card is not enrolled to the 3D process', + '027' => 'The bank did not allow the process', + '028' => 'Fraud suspect', + '029' => 'The card is closed to the e-commerce operations' + } + + def initialize(options = {}) + requires!(options, :dealer_code, :username, :password) + super + end + + def purchase(money, payment, options = {}) + post = {} + post[:PaymentDealerRequest] = {} + options[:pre_auth] = 0 + add_auth_purchase(post, money, payment, options) + add_3ds_data(post, options) if options[:execute_threed] + + action = options[:execute_threed] ? 'three_ds_purchase' : 'purchase' + commit(action, post) + end + + def authorize(money, payment, options = {}) + post = {} + post[:PaymentDealerRequest] = {} + options[:pre_auth] = 1 + add_auth_purchase(post, money, payment, options) + add_3ds_data(post, options) if options[:execute_threed] + + action = options[:execute_threed] ? 'three_ds_authorize' : 'authorize' + commit(action, post) + end + + def capture(money, authorization, options = {}) + post = {} + post[:PaymentDealerRequest] = {} + add_payment_dealer_authentication(post) + add_transaction_reference(post, authorization) + add_invoice(post, money, options) + + commit('capture', post) + end + + def refund(money, authorization, options = {}) + post = {} + post[:PaymentDealerRequest] = {} + add_payment_dealer_authentication(post) + add_transaction_reference(post, authorization) + add_void_refund_reason(post) + add_amount(post, money) + + commit('refund', post) + end + + def void(authorization, options = {}) + post = {} + post[:PaymentDealerRequest] = {} + add_payment_dealer_authentication(post) + add_transaction_reference(post, authorization) + add_void_refund_reason(post) + + 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(r.authorization, options) } + end + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r(("CardNumber\\?":\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("CvcNumber\\?":\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("DealerCode\\?":\\?"?)[^"?]*)i, '\1[FILTERED]'). + gsub(%r(("Username\\?":\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("Password\\?":\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("CheckKey\\?":\\?")[^"]*)i, '\1[FILTERED]') + end + + private + + def add_auth_purchase(post, money, payment, options) + add_payment_dealer_authentication(post) + add_invoice(post, money, options) + add_payment(post, payment) + add_additional_auth_purchase_data(post, options) + add_additional_transaction_data(post, options) + add_buyer_information(post, payment, options) + add_basket_product(post, options[:basket_product]) if options[:basket_product] + end + + def add_3ds_data(post, options) + post[:PaymentDealerRequest][:ReturnHash] = 1 + post[:PaymentDealerRequest][:RedirectUrl] = options[:redirect_url] || '' + post[:PaymentDealerRequest][:RedirectType] = options[:redirect_type] || 0 + end + + def add_payment_dealer_authentication(post) + post[:PaymentDealerAuthentication] = { + DealerCode: @options[:dealer_code], + Username: @options[:username], + Password: @options[:password], + CheckKey: check_key + } + end + + def check_key + str = "#{@options[:dealer_code]}MK#{@options[:username]}PD#{@options[:password]}" + Digest::SHA256.hexdigest(str) + end + + def add_invoice(post, money, options) + post[:PaymentDealerRequest][:Amount] = amount(money) || 0 + post[:PaymentDealerRequest][:Currency] = options[:currency] || 'TL' + end + + def add_payment(post, card) + post[:PaymentDealerRequest][:CardHolderFullName] = card.name + post[:PaymentDealerRequest][:CardNumber] = card.number + post[:PaymentDealerRequest][:ExpMonth] = card.month.to_s.rjust(2, '0') + post[:PaymentDealerRequest][:ExpYear] = card.year + post[:PaymentDealerRequest][:CvcNumber] = card.verification_value || '' + end + + def add_amount(post, money) + post[:PaymentDealerRequest][:Amount] = amount(money) || 0 + end + + def add_additional_auth_purchase_data(post, options) + post[:PaymentDealerRequest][:IsPreAuth] = options[:pre_auth] + post[:PaymentDealerRequest][:Description] = options[:description] if options[:description] + post[:PaymentDealerRequest][:InstallmentNumber] = options[:installment_number].to_i if options[:installment_number] + post[:SubMerchantName] = options[:sub_merchant_name] if options[:sub_merchant_name] + post[:IsPoolPayment] = options[:is_pool_payment] || 0 + end + + def add_buyer_information(post, card, options) + obj = {} + + obj[:BuyerFullName] = card.name || '' + obj[:BuyerEmail] = options[:email] if options[:email] + obj[:BuyerAddress] = options[:billing_address][:address1] if options[:billing_address] + obj[:BuyerGsmNumber] = options[:billing_address][:phone] if options[:billing_address] + + post[:PaymentDealerRequest][:BuyerInformation] = obj + end + + def add_basket_product(post, basket_options) + basket = [] + + basket_options.each do |product| + obj = {} + obj[:ProductId] = product[:product_id] if product[:product_id] + obj[:ProductCode] = product[:product_code] if product[:product_code] + obj[:UnitPrice] = amount(product[:unit_price]) if product[:unit_price] + obj[:Quantity] = product[:quantity] if product[:quantity] + basket << obj + end + + post[:PaymentDealerRequest][:BasketProduct] = basket + end + + def add_additional_transaction_data(post, options) + post[:PaymentDealerRequest][:ClientIP] = options[:ip] if options[:ip] + post[:PaymentDealerRequest][:OtherTrxCode] = options[:order_id] if options[:order_id] + end + + def add_transaction_reference(post, authorization) + post[:PaymentDealerRequest][:VirtualPosOrderId] = authorization + end + + def add_void_refund_reason(post) + post[:PaymentDealerRequest][:VoidRefundReason] = 2 + end + + def commit(action, parameters) + response = parse(ssl_post(url(action), post_data(parameters), request_headers)) + Response.new( + success_from(response), + message_from(response), + response, + authorization: authorization_from(response), + test: test?, + error_code: error_code_from(response) + ) + end + + def url(action) + host = (test? ? test_url : live_url) + endpoint = endpoint(action) + + "#{host}/PaymentDealer/#{endpoint}" + end + + def endpoint(action) + case action + when 'three_ds_authorize', 'three_ds_purchase' + 'DoDirectPaymentThreeD' + when 'purchase', 'authorize' + 'DoDirectPayment' + when 'capture' + 'DoCapture' + when 'void' + 'DoVoid' + when 'refund' + 'DoCreateRefundRequest' + end + end + + def request_headers + { 'Content-Type' => 'application/json' } + end + + def post_data(parameters = {}) + JSON.generate(parameters) + end + + def parse(body) + JSON.parse(body) + end + + def success_from(response) + return response.dig('Data', 'IsSuccessful') if response.dig('Data', 'IsSuccessful').to_s.present? + + response['ResultCode']&.casecmp('success') == 0 + end + + def message_from(response) + response.dig('Data', 'ResultMessage').presence || response['ResultCode'] + end + + def authorization_from(response) + response.dig('Data', 'VirtualPosOrderId') + end + + def error_code_from(response) + codes = [response['ResultCode'], response.dig('Data', 'ResultCode')].flatten + codes.reject! { |code| code.blank? || code.casecmp('success').zero? } + codes.map { |code| ERROR_CODE_MAPPING[code] || code }.join(', ') + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/monei.rb b/lib/active_merchant/billing/gateways/monei.rb index a5fce7eb0f0..52301e103e2 100755 --- a/lib/active_merchant/billing/gateways/monei.rb +++ b/lib/active_merchant/billing/gateways/monei.rb @@ -1,43 +1,40 @@ require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # # == Monei gateway # This class implements Monei gateway for Active Merchant. For more information about Monei - # gateway please go to http://www.monei.net + # gateway please go to http://www.monei.com # # === Setup - # In order to set-up the gateway you need four paramaters: sender_id, channel_id, login and pwd. + # In order to set-up the gateway you need only one paramater: the api_key # Request that data to Monei. class MoneiGateway < Gateway - self.test_url = 'https://test.monei-api.net/payment/ctpe' - self.live_url = 'https://monei-api.net/payment/ctpe' + self.live_url = self.test_url = 'https://api.monei.com/v1/payments' - self.supported_countries = ['AD', 'AT', 'BE', 'BG', 'CA', 'CH', 'CY', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FO', 'FR', 'GB', 'GI', 'GR', 'HU', 'IE', 'IL', 'IS', 'IT', 'LI', 'LT', 'LU', 'LV', 'MT', 'NL', 'NO', 'PL', 'PT', 'RO', 'SE', 'SI', 'SK', 'TR', 'US', 'VA'] + self.supported_countries = %w[AD AT BE BG CA CH CY CZ DE DK EE ES FI FO FR GB GI GR HU IE IL IS IT LI LT LU LV MT NL NO PL PT RO SE SI SK TR US VA] self.default_currency = 'EUR' - self.supported_cardtypes = [:visa, :master, :maestro, :jcb, :american_express] + self.money_format = :cents + self.supported_cardtypes = %i[visa master maestro jcb american_express] - self.homepage_url = 'http://www.monei.net/' - self.display_name = 'Monei' + self.homepage_url = 'https://monei.com/' + self.display_name = 'MONEI' # Constructor # # options - Hash containing the gateway credentials, ALL MANDATORY - # :sender_id Sender ID - # :channel_id Channel ID - # :login User login - # :pwd User password + # :api_key Account's API KEY # - def initialize(options={}) - requires!(options, :sender_id, :channel_id, :login, :pwd) + def initialize(options = {}) + requires!(options, :api_key) super end # Public: Performs purchase operation # # money - Amount of purchase - # credit_card - Credit card + # payment_method - Credit card # options - Hash containing purchase options # :order_id Merchant created id for the purchase # :billing_address Hash with billing address information @@ -45,14 +42,14 @@ def initialize(options={}) # :currency Sale currency to override money object or default (optional) # # Returns Active Merchant response object - def purchase(money, credit_card, options={}) - execute_new_order(:purchase, money, credit_card, options) + def purchase(money, payment_method, options = {}) + execute_new_order(:purchase, money, payment_method, options) end # Public: Performs authorization operation # # money - Amount to authorize - # credit_card - Credit card + # payment_method - Credit card # options - Hash containing authorization options # :order_id Merchant created id for the authorization # :billing_address Hash with billing address information @@ -60,8 +57,8 @@ def purchase(money, credit_card, options={}) # :currency Sale currency to override money object or default (optional) # # Returns Active Merchant response object - def authorize(money, credit_card, options={}) - execute_new_order(:authorize, money, credit_card, options) + def authorize(money, payment_method, options = {}) + execute_new_order(:authorize, money, payment_method, options) end # Public: Performs capture operation on previous authorization @@ -76,7 +73,7 @@ def authorize(money, credit_card, options={}) # Note: you should pass either order_id or description # # Returns Active Merchant response object - def capture(money, authorization, options={}) + def capture(money, authorization, options = {}) execute_dependant(:capture, money, authorization, options) end @@ -92,7 +89,7 @@ def capture(money, authorization, options={}) # Note: you should pass either order_id or description # # Returns Active Merchant response object - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) execute_dependant(:refund, money, authorization, options) end @@ -103,13 +100,13 @@ def refund(money, authorization, options={}) # :order_id Merchant created id for the authorization (optional) # # Returns Active Merchant response object - def void(authorization, options={}) + def void(authorization, options = {}) execute_dependant(:void, nil, authorization, options) end # Public: Verifies credit card. Does this by doing a authorization of 1.00 Euro and then voiding it. # - # credit_card - Credit card + # payment_method - Credit card # options - Hash containing authorization options # :order_id Merchant created id for the authorization # :billing_address Hash with billing address information @@ -117,189 +114,309 @@ def void(authorization, options={}) # :currency Sale currency to override money object or default (optional) # # Returns Active Merchant response object of Authorization operation - def verify(credit_card, options={}) + def verify(payment_method, options = {}) MultiResponse.run(:use_first_response) do |r| - r.process { authorize(100, credit_card, options) } + r.process { authorize(100, payment_method, options) } r.process(:ignore_result) { void(r.authorization, options) } end end + def store(payment_method, options = {}) + execute_new_order(:store, 0, payment_method, options) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r((Authorization: )\w+), '\1[FILTERED]'). + gsub(%r(("number\\?":\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("cvc\\?":\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("cavv\\?":\\?")[^"]*)i, '\1[FILTERED]') + end + private # Private: Execute purchase or authorize operation - def execute_new_order(action, money, credit_card, options) - request = build_request do |xml| - add_identification_new_order(xml, options) - add_payment(xml, action, money, options) - add_account(xml, credit_card) - add_customer(xml, credit_card, options) - end - - commit(request) + def execute_new_order(action, money, payment_method, options) + request = build_request + add_identification_new_order(request, options) + add_transaction(request, action, money, options) + add_payment(request, payment_method) + add_customer(request, payment_method, options) + add_3ds_authenticated_data(request, options) + add_browser_info(request, options) + commit(request, action, options) end # Private: Execute operation that depends on authorization code from previous purchase or authorize operation def execute_dependant(action, money, authorization, options) - request = build_request do |xml| - add_identification_authorization(xml, authorization, options) - add_payment(xml, action, money, options) - end + request = build_request + + add_identification_authorization(request, authorization, options) + add_transaction(request, action, money, options) - commit(request) + commit(request, action, options) end - # Private: Build XML wrapping code yielding to code to fill the transaction information + # Private: Build request object def build_request - builder = Nokogiri::XML::Builder.new(:encoding => 'UTF-8') do |xml| - xml.Request(:version => '1.0') do - xml.Header { xml.Security(:sender => @options[:sender_id]) } - xml.Transaction(:mode => test? ? 'CONNECTOR_TEST' : 'LIVE', :response => 'SYNC', :channel => @options[:channel_id]) do - xml.User(:login => @options[:login], :pwd => @options[:pwd]) - yield xml - end - end - end - builder.to_xml + request = {} + request[:livemode] = test? ? 'false' : 'true' + request end - # Private: Add identification part to XML for new orders - def add_identification_new_order(xml, options) + # Private: Add identification part to request for new orders + def add_identification_new_order(request, options) requires!(options, :order_id) - xml.Identification do - xml.TransactionID options[:order_id] + request[:orderId] = options[:order_id] + end + + # Private: Add identification part to request for orders that depend on authorization from previous operation + def add_identification_authorization(request, authorization, options) + options[:paymentId] = authorization + request[:orderId] = options[:order_id] if options[:order_id] + end + + # Private: Add payment part to request + def add_transaction(request, action, money, options) + request[:transactionType] = translate_payment_code(action) + request[:description] = options[:description] || options[:order_id] + unless money.nil? + request[:amount] = amount(money).to_i + request[:currency] = options[:currency] || currency(money) end end - # Private: Add identification part to XML for orders that depend on authorization from previous operation - def add_identification_authorization(xml, authorization, options) - xml.Identification do - xml.ReferenceID authorization - xml.TransactionID options[:order_id] + # Private: Add payment method to request + def add_payment(request, payment_method) + if payment_method.is_a? String + request[:paymentToken] = payment_method + else + request[:paymentMethod] = {} + request[:paymentMethod][:card] = {} + request[:paymentMethod][:card][:number] = payment_method.number + request[:paymentMethod][:card][:expMonth] = format(payment_method.month, :two_digits) + request[:paymentMethod][:card][:expYear] = format(payment_method.year, :two_digits) + request[:paymentMethod][:card][:cvc] = payment_method.verification_value.to_s + request[:paymentMethod][:card][:cardholderName] = payment_method.name end end - # Private: Add payment part to XML - def add_payment(xml, action, money, options) - code = tanslate_payment_code(action) + # Private: Add customer part to request + def add_customer(request, payment_method, options) + address = options[:billing_address] || options[:address] + + request[:customer] = {} + request[:customer][:email] = options[:email] || 'support@monei.net' + + if address + request[:customer][:name] = address[:name].to_s if address[:name] + + request[:billingDetails] = {} + request[:billingDetails][:email] = options[:email] if options[:email] + request[:billingDetails][:name] = address[:name] if address[:name] + request[:billingDetails][:company] = address[:company] if address[:company] + request[:billingDetails][:phone] = address[:phone] if address[:phone] + request[:billingDetails][:address] = {} + request[:billingDetails][:address][:line1] = address[:address1] if address[:address1] + request[:billingDetails][:address][:line2] = address[:address2] if address[:address2] + request[:billingDetails][:address][:city] = address[:city] if address[:city] + request[:billingDetails][:address][:state] = address[:state] if address[:state].present? + request[:billingDetails][:address][:zip] = address[:zip].to_s if address[:zip] + request[:billingDetails][:address][:country] = address[:country] if address[:country] + end + + request[:sessionDetails] = {} + request[:sessionDetails][:ip] = options[:ip] if options[:ip] + end - xml.Payment(:code => code) do - xml.Presentation do - xml.Amount amount(money) - xml.Currency options[:currency] || currency(money) - xml.Usage options[:description] || options[:order_id] - end unless money.nil? + # Private : Convert ECI to ResultIndicator + # Possible ECI values: + # 02 or 05 - Fully Authenticated Transaction + # 00 or 07 - Non 3D Secure Transaction + # Possible ResultIndicator values: + # 01 = MASTER_3D_ATTEMPT + # 02 = MASTER_3D_SUCCESS + # 05 = VISA_3D_SUCCESS + # 06 = VISA_3D_ATTEMPT + # 07 = DEFAULT_E_COMMERCE + def eci_to_result_indicator(eci) + case eci + when '02', '05' + return eci + else + return '07' end end - # Private: Add account part to XML - def add_account(xml, credit_card) - xml.Account do - xml.Holder credit_card.name - xml.Number credit_card.number - xml.Brand credit_card.brand.upcase - xml.Expiry(:month => credit_card.month, :year => credit_card.year) - xml.Verification credit_card.verification_value + # Private: add the already validated 3DSecure info to request + def add_3ds_authenticated_data(request, options) + if options[:three_d_secure] && options[:three_d_secure][:eci] && options[:three_d_secure][:xid] + add_3ds1_authenticated_data(request, options) + elsif options[:three_d_secure] + add_3ds2_authenticated_data(request, options) end end - # Private: Add customer part to XML - def add_customer(xml, credit_card, options) - requires!(options, :billing_address) - address = options[:billing_address] - xml.Customer do - xml.Name do - xml.Given credit_card.first_name - xml.Family credit_card.last_name - end - xml.Address do - xml.Street address[:address1].to_s - xml.Zip address[:zip].to_s - xml.City address[:city].to_s - xml.State address[:state].to_s if address.has_key? :state - xml.Country address[:country].to_s - end - xml.Contact do - xml.Email options[:email] || 'noemail@monei.net' - xml.Ip options[:ip] || '0.0.0.0' - end + def add_3ds1_authenticated_data(request, options) + three_d_secure_options = options[:three_d_secure] + request[:paymentMethod][:card][:auth] = { + cavv: three_d_secure_options[:cavv], + cavvAlgorithm: three_d_secure_options[:cavv_algorithm], + eci: three_d_secure_options[:eci], + xid: three_d_secure_options[:xid], + directoryResponse: three_d_secure_options[:enrolled], + authenticationResponse: three_d_secure_options[:authentication_response_status] + } + end + + def add_3ds2_authenticated_data(request, options) + three_d_secure_options = options[:three_d_secure] + # If the transaction was authenticated in a frictionless flow, send the transStatus from the ARes. + if three_d_secure_options[:authentication_response_status].nil? + authentication_response = three_d_secure_options[:directory_response_status] + else + authentication_response = three_d_secure_options[:authentication_response_status] end + request[:paymentMethod][:card][:auth] = { + threeDSVersion: three_d_secure_options[:version], + eci: three_d_secure_options[:eci], + cavv: three_d_secure_options[:cavv], + dsTransID: three_d_secure_options[:ds_transaction_id], + directoryResponse: three_d_secure_options[:directory_response_status], + authenticationResponse: authentication_response + } + end + + def add_browser_info(request, options) + request[:sessionDetails][:ip] = options[:ip] if options[:ip] + request[:sessionDetails][:userAgent] = options[:user_agent] if options[:user_agent] + request[:sessionDetails][:lang] = options[:lang] if options[:lang] end - # Private: Parse XML response from Monei servers + # Private: Parse JSON response from Monei servers def parse(body) - xml = Nokogiri::XML(body) + JSON.parse(body) + end + + def json_error(raw_response) + msg = 'Invalid response received from the MONEI API. Please contact support@monei.net if you continue to receive this message.' + msg += " (The raw response returned by the API was #{raw_response.inspect})" { - :unique_id => xml.xpath('//Response/Transaction/Identification/UniqueID').text, - :status => translate_status_code(xml.xpath('//Response/Transaction/Processing/Status/@code').text), - :reason => translate_status_code(xml.xpath('//Response/Transaction/Processing/Reason/@code').text), - :message => xml.xpath('//Response/Transaction/Processing/Return').text + 'status' => 'error', + 'message' => msg } end - # Private: Send XML transaction to Monei servers and create AM response - def commit(xml) + def response_error(raw_response) + parse(raw_response) + rescue JSON::ParserError + json_error(raw_response) + end + + def api_request(url, parameters, options = {}) + raw_response = response = nil + begin + raw_response = ssl_post(url, post_data(parameters), options) + response = parse(raw_response) + rescue ResponseError => e + raw_response = e.response.body + response = response_error(raw_response) + rescue JSON::ParserError + response = json_error(raw_response) + end + response + end + + # Private: Send transaction to Monei servers and create AM response + def commit(request, action, options) url = (test? ? test_url : live_url) + endpoint = translate_action_endpoint(action, options) + headers = { + 'Content-Type': 'application/json;charset=UTF-8', + Authorization: @options[:api_key], + 'User-Agent': 'MONEI/Shopify/0.1.0' + } - response = parse(ssl_post(url, post_data(xml), 'Content-Type' => 'application/x-www-form-urlencoded;charset=UTF-8')) + response = api_request(url + endpoint, params(request, action), headers) + success = success_from(response) Response.new( - success_from(response), - message_from(response), + success, + message_from(response, success), response, - authorization: authorization_from(response), + authorization: authorization_from(response, action), test: test?, - error_code: error_code_from(response) + error_code: error_code_from(response, success) ) end # Private: Decide success from servers response def success_from(response) - response[:status] == :success || response[:status] == :new + %w[ + SUCCEEDED + AUTHORIZED + REFUNDED + PARTIALLY_REFUNDED + CANCELED + ].include? response['status'] end # Private: Get message from servers response - def message_from(response) - response[:message] + def message_from(response, success) + success ? 'Transaction approved' : response.fetch('statusMessage', response.fetch('message', 'No error details')) end # Private: Get error code from servers response - def error_code_from(response) - success_from(response) ? nil : STANDARD_ERROR_CODE[:card_declined] + def error_code_from(response, success) + success ? nil : STANDARD_ERROR_CODE[:card_declined] end # Private: Get authorization code from servers response - def authorization_from(response) - response[:unique_id] + def authorization_from(response, action) + case action + when :store + return response['paymentToken'] + else + return response['id'] + end end # Private: Encode POST parameters - def post_data(xml) - "load=#{CGI.escape(xml)}" + def post_data(params) + params.clone.to_json end - # Private: Translate Monei status code to native ruby symbols - def translate_status_code(code) - { - '00' => :success, - '40' => :neutral, - '59' => :waiting_bank, - '60' => :rejected_bank, - '64' => :waiting_risk, - '65' => :rejected_risk, - '70' => :rejected_validation, - '80' => :waiting, - '90' => :new - }[code] + # Private: generate request params depending on action + def params(request, action) + request[:generatePaymentToken] = true if action == :store + request end # Private: Translate AM operations to Monei operations codes - def tanslate_payment_code(action) + def translate_payment_code(action) + { + purchase: 'SALE', + store: 'SALE', + authorize: 'AUTH', + capture: 'CAPTURE', + refund: 'REFUND', + void: 'CANCEL' + }[action] + end + + # Private: Translate AM operations to Monei endpoints + def translate_action_endpoint(action, options) { - :purchase => 'CC.DB', - :authorize => 'CC.PA', - :capture => 'CC.CP', - :refund => 'CC.RF', - :void => 'CC.RV' + purchase: '', + store: '', + authorize: '', + capture: "/#{options[:paymentId]}/capture", + refund: "/#{options[:paymentId]}/refund", + void: "/#{options[:paymentId]}/cancel" }[action] end end diff --git a/lib/active_merchant/billing/gateways/moneris.rb b/lib/active_merchant/billing/gateways/moneris.rb index 5ce35929532..1e830b00bad 100644 --- a/lib/active_merchant/billing/gateways/moneris.rb +++ b/lib/active_merchant/billing/gateways/moneris.rb @@ -1,8 +1,7 @@ require 'rexml/document' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: - +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # To learn more about the Moneris gateway, please contact # eselectplus@moneris.com for a copy of their integration guide. For # information on remote testing, please see "Test Environment Penny Value @@ -10,11 +9,13 @@ 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' self.supported_countries = ['CA'] - self.supported_cardtypes = [:visa, :master, :american_express, :diners_club, :discover] + self.supported_cardtypes = %i[visa master american_express diners_club discover] self.homepage_url = 'http://www.moneris.com/' self.display_name = 'Moneris' @@ -34,7 +35,7 @@ def initialize(options = {}) requires!(options, :login, :password) @cvv_enabled = options[:cvv_enabled] @avs_enabled = options[:avs_enabled] - options = { :crypt_type => 7 }.merge(options) + options[:crypt_type] = 7 unless options.has_key?(:crypt_type) super end @@ -47,18 +48,21 @@ def authorize(money, creditcard_or_datakey, options = {}) requires!(options, :order_id) post = {} add_payment_source(post, creditcard_or_datakey, options) - post[:amount] = amount(money) - post[:order_id] = options[:order_id] - post[:address] = options[:billing_address] || options[:address] + post[:amount] = amount(money) + 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] - action = if post[:cavv] - 'cavv_preauth' - elsif post[:data_key].blank? - 'preauth' - else - 'res_preauth_cc' - end - commit(action, post) + 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? + 'preauth' + else + 'res_preauth_cc' + end + commit(action, post, options) end # This action verifies funding on a customer's card and readies them for @@ -69,18 +73,21 @@ def purchase(money, creditcard_or_datakey, options = {}) requires!(options, :order_id) post = {} add_payment_source(post, creditcard_or_datakey, options) - post[:amount] = amount(money) - post[:order_id] = options[:order_id] - post[:address] = options[:billing_address] || options[:address] + post[:amount] = amount(money) + 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] - action = if post[:cavv] - 'cavv_purchase' - elsif post[:data_key].blank? - 'purchase' - else - 'res_purchase_cc' - end - commit(action, post) + 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? + 'purchase' + else + 'res_purchase_cc' + end + commit(action, post, options) end # This method retrieves locked funds from a customer's account (from a @@ -91,7 +98,7 @@ def purchase(money, creditcard_or_datakey, options = {}) # gateways the two numbers are concatenated together with a ; separator as # the authorization number returned by authorization def capture(money, authorization, options = {}) - commit 'completion', crediting_params(authorization, :comp_amount => amount(money)) + commit 'completion', crediting_params(authorization, comp_amount: amount(money)) end # Voiding requires the original transaction ID and order ID of some open @@ -129,22 +136,43 @@ def credit(money, authorization, options = {}) end def refund(money, authorization, options = {}) - commit 'refund', crediting_params(authorization, :amount => amount(money)) + commit 'refund', crediting_params(authorization, amount: amount(money)) end - def verify(credit_card, options={}) - MultiResponse.run(:use_first_response) do |r| - r.process { authorize(100, credit_card, options) } - r.process(:ignore_result) { void(r.authorization, options) } - end + def verify(credit_card, options = {}) + requires!(options, :order_id) + post = {} + add_payment_source(post, credit_card, options) + post[:order_id] = options[:order_id] + post[:address] = options[:billing_address] || options[:address] + post[:crypt_type] = options[:crypt_type] || @options[:crypt_type] + add_stored_credential(post, options) + action = if post[:data_key].blank? + 'card_verification' + else + 'res_card_verification_cc' + end + commit(action, post) end + # When passing a :duration option (time in seconds) you can create a + # temporary vault record to avoid incurring Moneris vault storage fees + # + # https://developer.moneris.com/Documentation/NA/E-Commerce%20Solutions/API/Vault#vaulttokenadd def store(credit_card, options = {}) post = {} post[:pan] = credit_card.number post[:expdate] = expdate(credit_card) + post[:address] = options[:billing_address] || options[:address] post[:crypt_type] = options[:crypt_type] || @options[:crypt_type] - commit('res_add_cc', post) + add_stored_credential(post, options) + + if options[:duration] + post[:duration] = options[:duration] + commit('res_temp_add', post) + else + commit('res_add_cc', post) + end end def unstore(data_key, options = {}) @@ -181,18 +209,33 @@ def expdate(creditcard) sprintf('%.4i', creditcard.year)[-2..-1] + sprintf('%.2i', creditcard.month) end + def add_external_mpi_fields(post, options) + # See these pages: + # https://developer.moneris.com/livedemo/3ds2/cavv_purchase/tool/php + # https://developer.moneris.com/livedemo/3ds2/cavv_preauth/guide/php + return unless options[:three_d_secure] + + three_d_secure_options = options[:three_d_secure] + + post[:threeds_version] = three_d_secure_options[:version] + post[:crypt_type] = three_d_secure_options.dig(:eci)&.to_s&.sub!(/^0/, '') + post[:cavv] = three_d_secure_options[:cavv] + post[:threeds_server_trans_id] = three_d_secure_options[:three_ds_server_trans_id] + post[:ds_trans_id] = three_d_secure_options[:ds_transaction_id] + end + def add_payment_source(post, payment_method, options) if payment_method.is_a?(String) - post[:data_key] = payment_method - post[:cust_id] = options[:customer] + post[:data_key] = payment_method + post[:cust_id] = options[:customer] else if payment_method.respond_to?(:track_data) && payment_method.track_data.present? - post[:pos_code] = '00' - post[:track2] = payment_method.track_data + post[:pos_code] = '00' + post[:track2] = payment_method.track_data else - post[:pan] = payment_method.number - post[:expdate] = expdate(payment_method) - post[:cvd_value] = payment_method.verification_value if payment_method.verification_value? + post[:pan] = payment_method.number + post[:expdate] = expdate(payment_method) + post[:cvd_value] = payment_method.verification_value if payment_method.verification_value? post[:cavv] = payment_method.payment_cryptogram if payment_method.is_a?(NetworkTokenizationCreditCard) post[:wallet_indicator] = wallet_indicator(payment_method.source.to_s) if payment_method.is_a?(NetworkTokenizationCreditCard) post[:crypt_type] = (payment_method.eci || 7) if payment_method.is_a?(NetworkTokenizationCreditCard) @@ -201,12 +244,65 @@ def add_payment_source(post, payment_method, options) end end + def add_cof(post, options) + post[:issuer_id] = options[:issuer_id] if options[:issuer_id] + post[:payment_indicator] = options[:payment_indicator] if options[:payment_indicator] + 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, + # then check for :stored credential options + return unless (stored_credential = options[:stored_credential]) && !cof_details_present?(options) + + if stored_credential[:initial_transaction] + add_stored_credential_initial(post, options) + else + add_stored_credential_used(post, options) + end + end + + def add_stored_credential_initial(post, options) + post[:payment_information] ||= '0' + post[:issuer_id] ||= '' + if options[:stored_credential][:initiator] == 'merchant' + case options[:stored_credential][:reason_type] + when 'recurring', 'installment' + post[:payment_indicator] ||= 'R' + when 'unscheduled' + post[:payment_indicator] ||= 'C' + end + else + post[:payment_indicator] ||= 'C' + end + end + + def add_stored_credential_used(post, options) + post[:payment_information] ||= '2' + post[:issuer_id] = options[:stored_credential][:network_transaction_id] if options[:issuer_id].blank? + if options[:stored_credential][:initiator] == 'merchant' + case options[:stored_credential][:reason_type] + when 'recurring', 'installment' + post[:payment_indicator] ||= 'R' + when '', 'unscheduled' + post[:payment_indicator] ||= 'U' + end + else + post[:payment_indicator] ||= 'Z' + end + end + # Common params used amongst the +credit+, +void+ and +capture+ methods def crediting_params(authorization, options = {}) { - :txn_number => split_authorization(authorization).first, - :order_id => split_authorization(authorization).last, - :crypt_type => options[:crypt_type] || @options[:crypt_type] + txn_number: split_authorization(authorization).first, + order_id: split_authorization(authorization).last, + crypt_type: options[:crypt_type] || @options[:crypt_type] }.merge(options) end @@ -220,36 +316,47 @@ def split_authorization(authorization) end end - def commit(action, parameters = {}) + def commit(action, parameters = {}, options = {}) + threed_ds_transaction = options[:three_d_secure].present? + data = post_data(action, parameters) url = test? ? self.test_url : self.live_url raw = ssl_post(url, data) response = parse(raw) - Response.new(successful?(response), message_from(response[:message]), response, - :test => test?, - :avs_result => { :code => response[:avs_result_code] }, - :cvv_result => response[:cvd_result_code] && response[:cvd_result_code][-1,1], - :authorization => authorization_from(response) + Response.new( + successful?(action, response, threed_ds_transaction), + message_from(response[:message]), + response, + test: test?, + avs_result: { code: response[:avs_result_code] }, + cvv_result: response[:cvd_result_code] && response[:cvd_result_code][-1, 1], + authorization: authorization_from(response) ) end # Generates a Moneris authorization string of the form 'trans_id;receipt_id'. def authorization_from(response = {}) - if response[:trans_id] && response[:receipt_id] - "#{response[:trans_id]};#{response[:receipt_id]}" - end + "#{response[:trans_id]};#{response[:receipt_id]}" if response[:trans_id] && response[:receipt_id] end # Tests for a successful response from Moneris' servers - def successful?(response) - response[:response_code] && - response[:complete] && - (0..49).include?(response[:response_code].to_i) + def successful?(action, response, threed_ds_transaction = false) + # See 9.4 CAVV Result Codes in https://developer.moneris.com/livedemo/3ds2/reference/guide/php + cavv_accepted = if threed_ds_transaction + response[:cavv_result_code] && response[:cavv_result_code] == '2' + else + true + end + + cavv_accepted && + response[:response_code] && + response[:complete] && + (0..49).cover?(response[:response_code].to_i) end def parse(xml) - response = { :message => 'Global Error Receipt', :complete => false } + response = { message: 'Global Error Receipt', complete: false } hashify_xml!(xml, response) response end @@ -257,15 +364,16 @@ def parse(xml) def hashify_xml!(xml, response) xml = REXML::Document.new(xml) return if xml.root.nil? + xml.elements.each('//receipt/*') do |node| response[node.name.underscore.to_sym] = normalize(node.text) end end def post_data(action, parameters = {}) - xml = REXML::Document.new - root = xml.add_element('request') - root.add_element('store_id').text = options[:login] + xml = REXML::Document.new + root = xml.add_element('request') + root.add_element('store_id').text = options[:login] root.add_element('api_token').text = options[:password] root.add_element(transaction_element(action, parameters)) @@ -282,6 +390,8 @@ def transaction_element(action, parameters) transaction.add_element(avs_element(parameters[:address])) if @avs_enabled && parameters[:address] when :cvd_info transaction.add_element(cvd_element(parameters[:cvd_value])) if @cvv_enabled + when :cof_info + transaction.add_element(credential_on_file(parameters)) if cof_details_present?(parameters) else transaction.add_element(key.to_s).text = parameters[key] unless parameters[key].blank? end @@ -295,8 +405,8 @@ def avs_element(address) tokens = full_address.split(/\s+/) element = REXML::Element.new('avs_info') - element.add_element('avs_street_number').text = tokens.select{|x| x =~ /\d/}.join(' ') - element.add_element('avs_street_name').text = tokens.reject{|x| x =~ /\d/}.join(' ') + element.add_element('avs_street_number').text = tokens.select { |x| x =~ /\d/ }.join(' ') + element.add_element('avs_street_name').text = tokens.reject { |x| x =~ /\d/ }.join(' ') element.add_element('avs_zipcode').text = address[:zip] element end @@ -312,37 +422,71 @@ def cvd_element(cvd_value) element end + def cof_details_present?(parameters) + parameters[:issuer_id] && parameters[:payment_indicator] && parameters[:payment_information] + end + + def credential_on_file(parameters) + issuer_id = parameters[:issuer_id] + payment_indicator = parameters[:payment_indicator] + payment_information = parameters[:payment_information] + + cof_info = REXML::Element.new('cof_info') + cof_info.add_element('issuer_id').text = issuer_id + cof_info.add_element('payment_indicator').text = payment_indicator + cof_info.add_element('payment_information').text = payment_information + cof_info + end + def wallet_indicator(token_source) - return 'APP' if token_source == 'apple_pay' - return 'ANP' if token_source == 'android_pay' - nil + return { + 'apple_pay' => 'APP', + 'google_pay' => 'GPP', + 'android_pay' => 'ANP' + }[token_source] + end + + def format_order_id(wallet_indicator_code, order_id = nil) + # Truncate (max 100 characters) order id for + # google pay and apple pay (specific wallets / token sources) + return truncate_order_id(order_id) if WALLETS.include?(wallet_indicator_code) + + order_id + end + + 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? + message.gsub(/[^\w]/, ' ').split.join(' ').capitalize end def actions { - 'purchase' => [:order_id, :cust_id, :amount, :pan, :expdate, :crypt_type, :avs_info, :cvd_info, :track2, :pos_code], - 'preauth' => [:order_id, :cust_id, :amount, :pan, :expdate, :crypt_type, :avs_info, :cvd_info, :track2, :pos_code], - 'command' => [:order_id], - 'refund' => [:order_id, :amount, :txn_number, :crypt_type], - 'indrefund' => [:order_id, :cust_id, :amount, :pan, :expdate, :crypt_type], - 'completion' => [:order_id, :comp_amount, :txn_number, :crypt_type], - 'purchasecorrection' => [:order_id, :txn_number, :crypt_type], - 'cavv_preauth' => [:order_id, :cust_id, :amount, :pan, :expdate, :cavv, :crypt_type, :wallet_indicator], - 'cavv_purchase' => [:order_id, :cust_id, :amount, :pan, :expdate, :cavv, :crypt_type, :wallet_indicator], - 'transact' => [:order_id, :cust_id, :amount, :pan, :expdate, :crypt_type], - 'Batchcloseall' => [], - 'opentotals' => [:ecr_number], - 'batchclose' => [:ecr_number], - 'res_add_cc' => [:pan, :expdate, :crypt_type], - 'res_delete' => [:data_key], - 'res_update_cc' => [:data_key, :pan, :expdate, :crypt_type], - 'res_purchase_cc' => [:data_key, :order_id, :cust_id, :amount, :crypt_type], - 'res_preauth_cc' => [:data_key, :order_id, :cust_id, :amount, :crypt_type] + 'purchase' => %i[order_id cust_id amount pan expdate crypt_type avs_info cvd_info track2 pos_code cof_info], + 'preauth' => %i[order_id cust_id amount pan expdate crypt_type avs_info cvd_info track2 pos_code cof_info], + 'command' => [:order_id], + 'refund' => %i[order_id amount txn_number crypt_type], + '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 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' => [], + 'opentotals' => [:ecr_number], + 'batchclose' => [:ecr_number], + 'res_add_cc' => %i[pan expdate crypt_type avs_info cof_info], + 'res_temp_add' => %i[pan expdate crypt_type duration], + 'res_delete' => [:data_key], + 'res_update_cc' => %i[data_key pan expdate crypt_type avs_info cof_info], + 'res_purchase_cc' => %i[data_key order_id cust_id amount crypt_type cof_info], + 'res_preauth_cc' => %i[data_key order_id cust_id amount crypt_type cof_info], + 'res_card_verification_cc' => %i[order_id data_key expdate crypt_type cof_info] } end end diff --git a/lib/active_merchant/billing/gateways/moneris_us.rb b/lib/active_merchant/billing/gateways/moneris_us.rb deleted file mode 100644 index e9e5ed3d476..00000000000 --- a/lib/active_merchant/billing/gateways/moneris_us.rb +++ /dev/null @@ -1,353 +0,0 @@ -require 'rexml/document' - -module ActiveMerchant #:nodoc: - module Billing #:nodoc: - - # To learn more about the Moneris (US) gateway, please contact - # ussales@moneris.com for a copy of their integration guide. For - # information on remote testing, please see "Test Environment Penny Value - # Response Table", and "Test Environment eFraud (AVS and CVD) Penny - # Response Values", available at Moneris' {eSelect Plus Documentation - # Centre}[https://www3.moneris.com/connect/en/documents/index.html]. - class MonerisUsGateway < Gateway - self.test_url = 'https://esplusqa.moneris.com/gateway_us/servlet/MpgRequest' - self.live_url = 'https://esplus.moneris.com/gateway_us/servlet/MpgRequest' - - self.supported_countries = ['US'] - self.supported_cardtypes = [:visa, :master, :american_express, :diners_club, :discover] - self.homepage_url = 'http://www.monerisusa.com/' - self.display_name = 'Moneris (US)' - - # Initialize the Gateway - # - # The gateway requires that a valid login and password be passed - # in the +options+ hash. - # - # ==== Options - # - # * :login -- Your Store ID - # * :password -- Your API Token - # * :cvv_enabled -- Specify that you would like the CVV passed to the gateway. - # Only particular account types at Moneris will allow this. - # Defaults to false. (optional) - def initialize(options = {}) - requires!(options, :login, :password) - @cvv_enabled = options[:cvv_enabled] - @avs_enabled = options[:avs_enabled] - options = { :crypt_type => 7 }.merge(options) - super - end - - def verify(creditcard_or_datakey, options = {}) - MultiResponse.run(:use_first_response) do |r| - r.process { authorize(100, creditcard_or_datakey, options) } - r.process(:ignore_result) { capture(0, r.authorization) } - end - end - - # Referred to as "PreAuth" in the Moneris integration guide, this action - # verifies and locks funds on a customer's card, which then must be - # captured at a later date. - # - # Pass in +order_id+ and optionally a +customer+ parameter. - def authorize(money, creditcard_or_datakey, options = {}) - requires!(options, :order_id) - post = {} - add_payment_source(post, creditcard_or_datakey, options) - post[:amount] = amount(money) - post[:order_id] = options[:order_id] - post[:address] = options[:billing_address] || options[:address] - post[:crypt_type] = options[:crypt_type] || @options[:crypt_type] - action = (post[:data_key].blank?) ? 'us_preauth' : 'us_res_preauth_cc' - commit(action, post) - end - - # This action verifies funding on a customer's card and readies them for - # deposit in a merchant's account. - # - # Pass in order_id and optionally a customer parameter - def purchase(money, creditcard_or_datakey, options = {}) - requires!(options, :order_id) - post = {} - add_payment_source(post, creditcard_or_datakey, options) - post[:amount] = amount(money) - post[:order_id] = options[:order_id] - add_address(post, creditcard_or_datakey, options) - post[:crypt_type] = options[:crypt_type] || @options[:crypt_type] - action = if creditcard_or_datakey.is_a?(String) - 'us_res_purchase_cc' - elsif card_brand(creditcard_or_datakey) == 'check' - 'us_ach_debit' - elsif post[:data_key].blank? - 'us_purchase' - end - commit(action, post) - end - - # This method retrieves locked funds from a customer's account (from a - # PreAuth) and prepares them for deposit in a merchant's account. - # - # Note: Moneris requires both the order_id and the transaction number of - # the original authorization. To maintain the same interface as the other - # gateways the two numbers are concatenated together with a ; separator as - # the authorization number returned by authorization - def capture(money, authorization, options = {}) - commit 'us_completion', crediting_params(authorization, :comp_amount => amount(money)) - end - - # Voiding requires the original transaction ID and order ID of some open - # transaction. Closed transactions must be refunded. Note that the only - # methods which may be voided are +capture+ and +purchase+. - # - # Concatenate your transaction number and order_id by using a semicolon - # (';'). This is to keep the Moneris interface consistent with other - # gateways. (See +capture+ for details.) - def void(authorization, options = {}) - commit 'us_purchasecorrection', crediting_params(authorization) - end - - # Performs a refund. This method requires that the original transaction - # number and order number be included. Concatenate your transaction - # number and order_id by using a semicolon (';'). This is to keep the - # Moneris interface consistent with other gateways. (See +capture+ for - # details.) - def credit(money, authorization, options = {}) - ActiveMerchant.deprecated CREDIT_DEPRECATION_MESSAGE - refund(money, authorization, options) - end - - def refund(money, authorization, options = {}) - commit 'us_refund', crediting_params(authorization, :amount => amount(money)) - end - - def store(payment_source, options = {}) - post = {} - add_payment_source(post, payment_source, options) - post[:crypt_type] = options[:crypt_type] || @options[:crypt_type] - card_brand(payment_source) == 'check' ? commit('us_res_add_ach', post) : commit('us_res_add_cc', post) - end - - def unstore(data_key, options = {}) - post = {} - post[:data_key] = data_key - commit('us_res_delete', post) - end - - def update(data_key, payment_source, options = {}) - post = {} - add_payment_source(post, payment_source, options) - post[:data_key] = data_key - card_brand(payment_source) == 'check' ? commit('us_res_update_ach', post) : commit('us_res_update_cc', post) - 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') - end - - private # :nodoc: all - - def expdate(creditcard) - sprintf('%.4i', creditcard.year)[-2..-1] + sprintf('%.2i', creditcard.month) - end - - def add_address(post, payment_method, options) - if !payment_method.is_a?(String) && card_brand(payment_method) == 'check' - post[:ach_info][:cust_first_name] = payment_method.first_name if payment_method.first_name - post[:ach_info][:cust_last_name] = payment_method.last_name if payment_method.last_name - if address = options[:billing_address] || options[:address] - post[:ach_info][:cust_address1] = address[:address1] if address[:address1] - post[:ach_info][:cust_address2] = address[:address2] if address[:address2] - post[:ach_info][:city] = address[:city] if address[:city] - post[:ach_info][:state] = address[:state] if address[:state] - post[:ach_info][:zip] = address[:zip] if address[:zip] - end - else - post[:address] = options[:billing_address] || options[:address] - end - end - - def add_payment_source(post, source, options) - if source.is_a?(String) - post[:data_key] = source - post[:cust_id] = options[:customer] - elsif card_brand(source) == 'check' - ach_info = {} - ach_info[:sec] = 'web' - ach_info[:routing_num] = source.routing_number - ach_info[:account_num] = source.account_number - ach_info[:account_type] = source.account_type - ach_info[:check_num] = source.number if source.number - post[:ach_info] = ach_info - else - post[:pan] = source.number - post[:expdate] = expdate(source) - post[:cvd_value] = source.verification_value if source.verification_value? - if crypt_type = options[:crypt_type] || @options[:crypt_type] - post[:crypt_type] = crypt_type - end - post[:cust_id] = options[:customer] || source.name - end - end - - # Common params used amongst the +credit+, +void+ and +capture+ methods - def crediting_params(authorization, options = {}) - { - :txn_number => split_authorization(authorization).first, - :order_id => split_authorization(authorization).last, - :crypt_type => options[:crypt_type] || @options[:crypt_type] - }.merge(options) - end - - # Splits an +authorization+ param and retrieves the order id and - # transaction number in that order. - def split_authorization(authorization) - if authorization.nil? || authorization.empty? || authorization !~ /;/ - raise ArgumentError, 'You must include a valid authorization code (e.g. "1234;567")' - else - authorization.split(';') - end - end - - def commit(action, parameters = {}) - data = post_data(action, parameters) - url = test? ? self.test_url : self.live_url - raw = ssl_post(url, data) - response = parse(raw) - - Response.new(successful?(response), message_from(response[:message]), response, - :test => test?, - :avs_result => { :code => response[:avs_result_code] }, - :cvv_result => response[:cvd_result_code] && response[:cvd_result_code][-1,1], - :authorization => authorization_from(response) - ) - end - - # Generates a Moneris authorization string of the form 'trans_id;receipt_id'. - def authorization_from(response = {}) - if response[:trans_id] && response[:receipt_id] - "#{response[:trans_id]};#{response[:receipt_id]}" - end - end - - # Tests for a successful response from Moneris' servers - def successful?(response) - response[:response_code] && - response[:complete] && - (0..49).include?(response[:response_code].to_i) - end - - def parse(xml) - response = { :message => 'Global Error Receipt', :complete => false } - hashify_xml!(xml, response) - response - end - - def hashify_xml!(xml, response) - xml = REXML::Document.new(xml) - return if xml.root.nil? - xml.elements.each('//receipt/*') do |node| - response[node.name.underscore.to_sym] = normalize(node.text) - end - end - - def post_data(action, parameters = {}) - xml = REXML::Document.new - root = xml.add_element('request') - root.add_element('store_id').text = options[:login] - root.add_element('api_token').text = options[:password] - root.add_element(transaction_element(action, parameters)) - - xml.to_s - end - - def transaction_element(action, parameters) - transaction = REXML::Element.new(action) - - # Must add the elements in the correct order - actions[action].each do |key| - case key - when :avs_info - transaction.add_element(avs_element(parameters[:address])) if @avs_enabled && parameters[:address] - when :cvd_info - transaction.add_element(cvd_element(parameters[:cvd_value])) if @cvv_enabled - when :ach_info - transaction.add_element(ach_element(parameters[:ach_info])) - else - transaction.add_element(key.to_s).text = parameters[key] unless parameters[key].blank? - end - end - - transaction - end - - def avs_element(address) - full_address = "#{address[:address1]} #{address[:address2]}" - tokens = full_address.split(/\s+/) - - element = REXML::Element.new('avs_info') - element.add_element('avs_street_number').text = tokens.select{|x| x =~ /\d/}.join(' ') - element.add_element('avs_street_name').text = tokens.reject{|x| x =~ /\d/}.join(' ') - element.add_element('avs_zipcode').text = address[:zip] - element - end - - def cvd_element(cvd_value) - element = REXML::Element.new('cvd_info') - if cvd_value - element.add_element('cvd_indicator').text = '1' - element.add_element('cvd_value').text = cvd_value - else - element.add_element('cvd_indicator').text = '0' - end - element - end - - def ach_element(ach_info) - element = REXML::Element.new('ach_info') - actions['ach_info'].each do |key| - element.add_element(key.to_s).text = ach_info[key] unless ach_info[key].blank? - end - element - end - - def message_from(message) - return 'Unspecified error' if message.blank? - message.gsub(/[^\w]/, ' ').split.join(' ').capitalize - end - - def actions - { - 'us_purchase' => [:order_id, :cust_id, :amount, :pan, :expdate, :crypt_type, :avs_info, :cvd_info], - 'us_preauth' => [:order_id, :cust_id, :amount, :pan, :expdate, :crypt_type, :avs_info, :cvd_info], - 'us_command' => [:order_id], - 'us_refund' => [:order_id, :amount, :txn_number, :crypt_type], - 'us_indrefund' => [:order_id, :cust_id, :amount, :pan, :expdate, :crypt_type], - 'us_completion' => [:order_id, :comp_amount, :txn_number, :crypt_type], - 'us_purchasecorrection' => [:order_id, :txn_number, :crypt_type], - 'us_cavvpurcha' => [:order_id, :cust_id, :amount, :pan, :expdate, :cav], - 'us_cavvpreaut' => [:order_id, :cust_id, :amount, :pan, :expdate, :cavv], - 'us_transact' => [:order_id, :cust_id, :amount, :pan, :expdate, :crypt_type], - 'us_Batchcloseall' => [], - 'us_opentotals' => [:ecr_number], - 'us_batchclose' => [:ecr_number], - 'us_res_add_cc' => [:pan, :expdate, :crypt_type], - 'us_res_delete' => [:data_key], - 'us_res_update_cc' => [:data_key, :pan, :expdate, :crypt_type], - 'us_res_purchase_cc' => [:data_key, :order_id, :cust_id, :amount, :crypt_type], - 'us_res_preauth_cc' => [:data_key, :order_id, :cust_id, :amount, :crypt_type], - 'us_ach_debit' => [:order_id, :cust_id, :amount, :ach_info], - 'us_res_add_ach' => [:order_id, :cust_id, :amount, :ach_info], - 'us_res_update_ach' => [:order_id, :data_key, :cust_id, :amount, :ach_info], - 'ach_info' => [:sec, :cust_first_name, :cust_last_name, :cust_address1, :cust_address2, :cust_city, :cust_state, :cust_zip, :routing_num, :account_num, :check_num, :account_type] - } - end - end - end -end diff --git a/lib/active_merchant/billing/gateways/money_movers.rb b/lib/active_merchant/billing/gateways/money_movers.rb index 95165a42479..59e1fe7c825 100644 --- a/lib/active_merchant/billing/gateways/money_movers.rb +++ b/lib/active_merchant/billing/gateways/money_movers.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class MoneyMoversGateway < Gateway self.live_url = self.test_url = 'https://secure.mmoagateway.com/api/transact.php' @@ -8,7 +8,7 @@ class MoneyMoversGateway < Gateway self.homepage_url = 'http://mmoa.us/' self.display_name = 'MoneyMovers' self.supported_countries = ['US'] - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] def initialize(options = {}) requires!(options, :login, :password) @@ -45,7 +45,7 @@ def void(authorization, options = {}) end def refund(money, authorization, options = {}) - commit('refund', money, options.merge(:transactionid => authorization)) + commit('refund', money, options.merge(transactionid: authorization)) end def credit(money, authorization, options = {}) @@ -113,11 +113,14 @@ def commit(action, money, parameters) response = parse(data) message = message_from(response) - Response.new(success?(response), message, response, - :test => test?, - :authorization => response['transactionid'], - :avs_result => {:code => response['avsresponse']}, - :cvv_result => response['cvvresponse'] + Response.new( + success?(response), + message, + response, + test: test?, + authorization: response['transactionid'], + avs_result: { code: response['avsresponse'] }, + cvv_result: response['cvvresponse'] ) end @@ -126,7 +129,7 @@ def success?(response) end def test? - (@options[:login].eql?('demo')) && (@options[:password].eql?('password')) + @options[:login].eql?('demo') && @options[:password].eql?('password') end def message_from(response) @@ -144,9 +147,8 @@ def post_data(action, parameters = {}) parameters[:type] = action parameters[:username] = @options[:login] parameters[:password] = @options[:password] - parameters.map{|k, v| "#{k}=#{CGI.escape(v.to_s)}"}.join('&') + parameters.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&') end end end end - diff --git a/lib/active_merchant/billing/gateways/mundipagg.rb b/lib/active_merchant/billing/gateways/mundipagg.rb index 7aefce004e4..87cf04ac6a0 100644 --- a/lib/active_merchant/billing/gateways/mundipagg.rb +++ b/lib/active_merchant/billing/gateways/mundipagg.rb @@ -1,11 +1,11 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class MundipaggGateway < Gateway self.live_url = 'https://api.mundipagg.com/core/v1' self.supported_countries = ['US'] self.default_currency = 'USD' - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover alelo] self.homepage_url = 'https://www.mundipagg.com/' self.display_name = 'Mundipagg' @@ -28,56 +28,62 @@ class MundipaggGateway < Gateway '500' => 'An internal error occurred;' } - def initialize(options={}) + def initialize(options = {}) requires!(options, :api_key) super end - def purchase(money, payment, options={}) + def purchase(money, payment, options = {}) post = {} add_invoice(post, money, options) add_customer_data(post, options) unless payment.is_a?(String) add_shipping_address(post, options) add_payment(post, payment, options) - + add_submerchant(post, options) + add_auth_key(post, options) commit('sale', post) end - def authorize(money, payment, options={}) + def authorize(money, payment, options = {}) post = {} add_invoice(post, money, options) add_customer_data(post, options) unless payment.is_a?(String) add_shipping_address(post, options) add_payment(post, payment, options) add_capture_flag(post, payment) + add_submerchant(post, options) + add_auth_key(post, options) commit('authonly', post) end - def capture(money, authorization, options={}) + def capture(money, authorization, options = {}) post = {} post[:code] = authorization add_invoice(post, money, options) + add_auth_key(post, options) commit('capture', post, authorization) end - def refund(money, authorization, options={}) - add_invoice(post={}, money, options) + def refund(money, authorization, options = {}) + add_invoice(post = {}, money, options) + add_auth_key(post, options) commit('refund', post, authorization) end - def void(authorization, options={}) + def void(authorization, options = {}) commit('void', nil, authorization) end - def store(payment, options={}) + def store(payment, options = {}) post = {} options.update(name: payment.name) options = add_customer(options) unless options[:customer_id] add_payment(post, payment, options) + add_auth_key(post, options) commit('store', post, options[:customer_id]) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -89,10 +95,10 @@ def supports_scrubbing? end def scrub(transcript) - transcript - .gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]') - .gsub(%r(("cvv\\":\\")\d*), '\1[FILTERED]') - .gsub(%r((card\\":{\\"number\\":\\")\d*), '\1[FILTERED]') + transcript. + gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). + gsub(%r(("cvv\\":\\")\d*), '\1[FILTERED]'). + gsub(%r((card\\":{\\"number\\":\\")\d*), '\1[FILTERED]') end private @@ -128,8 +134,8 @@ def add_billing_address(post, type, options) def add_shipping_address(post, options) if address = options[:shipping_address] post[:address] = {} - post[:address][:street] = address[:address1].match(/\D+/)[0].strip if address[:address1] - post[:address][:number] = address[:address1].match(/\d+/)[0] if address[:address1] + post[:address][:street] = address[:address1].match(/\D+/)[0].strip if address[:address1]&.match(/\D+/) + post[:address][:number] = address[:address1].match(/\d+/)[0] if address[:address1]&.match(/\d+/) post[:address][:compliment] = address[:address2] if address[:address2] post[:address][:city] = address[:city] if address[:city] post[:address][:state] = address[:state] if address[:state] @@ -155,7 +161,8 @@ def add_payment(post, payment, options) post[:customer][:name] = payment.name if post[:customer] post[:customer_id] = parse_auth(payment)[0] if payment.is_a?(String) post[:payment] = {} - post[:payment][:gateway_affiliation_id] = @options[:gateway_id] if @options[:gateway_id] + affiliation = options[:gateway_affiliation_id] || @options[:gateway_id] + post[:payment][:gateway_affiliation_id] = affiliation if affiliation post[:payment][:metadata] = { mundipagg_payment_method_code: '1' } if test? if voucher?(payment) add_voucher(post, payment, options) @@ -177,7 +184,7 @@ def add_credit_card(post, payment, options) post[:payment][:credit_card][:card][:exp_year] = payment.year post[:payment][:credit_card][:card][:cvv] = payment.verification_value post[:payment][:credit_card][:card][:holder_document] = options[:holder_document] if options[:holder_document] - add_billing_address(post,'credit_card', options) + add_billing_address(post, 'credit_card', options) end end @@ -197,12 +204,45 @@ def add_voucher(post, payment, options) def voucher?(payment) return false if payment.is_a?(String) + %w[sodexo vr].include? card_brand(payment) end - def headers + def add_submerchant(post, options) + if submerchant = options[:submerchant] + post[:SubMerchant] = {} + post[:SubMerchant][:Merchant_Category_Code] = submerchant[:merchant_category_code] if submerchant[:merchant_category_code] + post[:SubMerchant][:Payment_Facilitator_Code] = submerchant[:payment_facilitator_code] if submerchant[:payment_facilitator_code] + post[:SubMerchant][:Code] = submerchant[:code] if submerchant[:code] + post[:SubMerchant][:Name] = submerchant[:name] if submerchant[:name] + post[:SubMerchant][:Document] = submerchant[:document] if submerchant[:document] + post[:SubMerchant][:Type] = submerchant[:type] if submerchant[:type] + post[:SubMerchant][:Phone] = {} + post[:SubMerchant][:Phone][:Country_Code] = submerchant[:phone][:country_code] if submerchant.dig(:phone, :country_code) + post[:SubMerchant][:Phone][:Number] = submerchant[:phone][:number] if submerchant.dig(:phone, :number) + post[:SubMerchant][:Phone][:Area_Code] = submerchant[:phone][:area_code] if submerchant.dig(:phone, :area_code) + post[:SubMerchant][:Address] = {} + post[:SubMerchant][:Address][:Street] = submerchant[:address][:street] if submerchant.dig(:address, :street) + post[:SubMerchant][:Address][:Number] = submerchant[:address][:number] if submerchant.dig(:address, :number) + post[:SubMerchant][:Address][:Complement] = submerchant[:address][:complement] if submerchant.dig(:address, :complement) + post[:SubMerchant][:Address][:Neighborhood] = submerchant[:address][:neighborhood] if submerchant.dig(:address, :neighborhood) + post[:SubMerchant][:Address][:City] = submerchant[:address][:city] if submerchant.dig(:address, :city) + post[:SubMerchant][:Address][:State] = submerchant[:address][:state] if submerchant.dig(:address, :state) + post[:SubMerchant][:Address][:Country] = submerchant[:address][:country] if submerchant.dig(:address, :country) + post[:SubMerchant][:Address][:Zip_Code] = submerchant[:address][:zip_code] if submerchant.dig(:address, :zip_code) + end + end + + def add_auth_key(post, options) + if authorization_secret_key = options[:authorization_secret_key] + post[:authorization_secret_key] = authorization_secret_key + end + end + + def headers(authorization_secret_key = nil) + basic_token = authorization_secret_key || @options[:api_key] { - 'Authorization' => 'Basic ' + Base64.strict_encode64("#{@options[:api_key]}:"), + 'Authorization' => 'Basic ' + Base64.strict_encode64("#{basic_token}:"), 'Content-Type' => 'application/json', 'Accept' => 'application/json' } @@ -224,55 +264,86 @@ def url_for(action, auth = nil) when 'capture' "#{url}/charges/#{auth}/capture/" else - "#{url}/charges/" + "#{url}/charges/" end end def commit(action, parameters, auth = nil) url = url_for(action, auth) + authorization_secret_key = parameters[:authorization_secret_key] if parameters parameters.merge!(parameters[:payment][:credit_card].delete(:card)).delete(:payment) if action == 'store' response = if %w[refund void].include? action - parse(ssl_request(:delete, url, post_data(parameters), headers)) + parse(ssl_request(:delete, url, post_data(parameters), headers(authorization_secret_key))) else - parse(ssl_post(url, post_data(parameters), headers)) + parse(ssl_post(url, post_data(parameters), headers(authorization_secret_key))) end Response.new( - success_from(response), + success_from(response, action), message_from(response), response, authorization: authorization_from(response, action), avs_result: AVSResult.new(code: response['some_avs_response_key']), cvv_result: CVVResult.new(response['some_cvv_response_key']), test: test?, - error_code: error_code_from(response) + error_code: error_code_from(response, action) ) - rescue ResponseError => e - message = get_error_message(e) - return Response.new( - false, - "#{STANDARD_ERROR_MESSAGE_MAPPING[e.response.code]} #{message}", - parse(e.response.body), - test: test?, - error_code: STANDARD_ERROR_CODE_MAPPING[e.response.code], - ) - end + rescue ResponseError => e + message = get_error_messages(e) - def success_from(response) - %w[pending paid processing canceled active].include? response['status'] + return Response.new( + false, + "#{STANDARD_ERROR_MESSAGE_MAPPING[e.response.code]} #{message}", + parse(e.response.body), + test: test?, + error_code: STANDARD_ERROR_CODE_MAPPING[e.response.code] + ) end - def get_error_message(error) - JSON.parse(error.response.body)['message'] + def success_from(response, action) + success = response.try(:[], 'last_transaction').try(:[], 'success') unless action == 'store' + success = !response.try(:[], 'id').nil? if action == 'store' + success end def message_from(response) + return gateway_response_errors(response) if gateway_response_errors?(response) return response['message'] if response['message'] return response['last_transaction']['acquirer_message'] if response['last_transaction'] end + def get_error_messages(error) + parsed_response_body = parse(error.response.body) + message = parsed_response_body['message'] + + parsed_response_body['errors']&.each do |_type, descriptions| + message += ' | ' + message += descriptions.join(', ') + end + + message + end + + def gateway_response_errors?(response) + response.try(:[], 'last_transaction').try(:[], 'gateway_response').try(:[], 'errors').present? + end + + def gateway_response_errors(response) + error_string = '' + + response['last_transaction']['gateway_response']['errors']&.each do |error| + error.each do |_key, value| + error_string += ' | ' unless error_string.blank? + error_string += value + end + end + + error_string + end + def authorization_from(response, action) return "#{response['customer']['id']}|#{response['id']}" if action == 'store' + response['id'] end @@ -284,8 +355,11 @@ def post_data(parameters = {}) parameters.to_json end - def error_code_from(response) - STANDARD_ERROR_CODE[:processing_error] unless success_from(response) + def error_code_from(response, action) + return if success_from(response, action) + return response['last_transaction']['acquirer_return_code'] if response['last_transaction'] + + STANDARD_ERROR_CODE[:processing_error] end end end diff --git a/lib/active_merchant/billing/gateways/nab_transact.rb b/lib/active_merchant/billing/gateways/nab_transact.rb index 85123a13ebd..9081c4bbc6c 100644 --- a/lib/active_merchant/billing/gateways/nab_transact.rb +++ b/lib/active_merchant/billing/gateways/nab_transact.rb @@ -1,7 +1,7 @@ require 'rexml/document' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # The National Australia Bank provide a payment gateway that seems to # be a rebadged Securepay Australia service, though some differences exist. class NabTransactGateway < Gateway @@ -12,35 +12,34 @@ class NabTransactGateway < Gateway self.test_url = 'https://demo.transact.nab.com.au/xmlapi/payment' self.live_url = 'https://transact.nab.com.au/live/xmlapi/payment' - self.test_periodic_url = 'https://transact.nab.com.au/xmlapidemo/periodic' + self.test_periodic_url = 'https://demo.transact.nab.com.au/xmlapi/periodic' self.live_periodic_url = 'https://transact.nab.com.au/xmlapi/periodic' self.supported_countries = ['AU'] - self.supported_cardtypes = [:visa, :master, :american_express, :diners_club, :jcb] + self.supported_cardtypes = %i[visa master american_express diners_club jcb] self.homepage_url = 'http://transact.nab.com.au' self.display_name = 'NAB Transact' self.money_format = :cents self.default_currency = 'AUD' - #Transactions currently accepted by NAB Transact XML API + # Transactions currently accepted by NAB Transact XML API TRANSACTIONS = { - :purchase => 0, #Standard Payment - :refund => 4, #Refund - :void => 6, #Client Reversal (Void) - :unmatched_refund => 666, #Unmatched Refund - :authorization => 10, #Preauthorise - :capture => 11 #Preauthorise Complete (Advice) + purchase: 0, # Standard Payment + refund: 4, # Refund + void: 6, # Client Reversal (Void) + unmatched_refund: 666, # Unmatched Refund + authorization: 10, # Preauthorise + capture: 11 # Preauthorise Complete (Advice) } PERIODIC_TYPES = { - :addcrn => 5, - :deletecrn => 5, - :trigger => 8 + addcrn: 5, + deletecrn: 5, + trigger: 8 } - SUCCESS_CODES = [ '00', '08', '11', '16', '77' ] - + SUCCESS_CODES = %w[00 08 11 16 77] def initialize(options = {}) requires!(options, :login, :password) @@ -85,6 +84,7 @@ def supports_scrubbing? def scrub(transcript) return '' if transcript.blank? + transcript. gsub(%r(()[^<]+(<))i, '\1[FILTERED]\2'). gsub(%r(()[^<]+(<))i, '\1[FILTERED]\2'). @@ -96,8 +96,8 @@ def scrub(transcript) def add_metadata(xml, options) if options[:merchant_name] || options[:merchant_location] xml.tag! 'metadata' do - xml.tag! 'meta', :name => 'ca_name', :value => options[:merchant_name] if options[:merchant_name] - xml.tag! 'meta', :name => 'ca_location', :value => options[:merchant_location] if options[:merchant_location] + xml.tag! 'meta', name: 'ca_name', value: options[:merchant_name] if options[:merchant_name] + xml.tag! 'meta', name: 'ca_location', value: options[:merchant_location] if options[:merchant_location] end end end @@ -135,7 +135,7 @@ def build_reference_request(money, reference, options) xml.target! end - #Generate payment request XML + # Generate payment request XML # - API is set to allow multiple Txn's but currently only allows one # - txnSource = 23 - (XML) def build_request(action, body) @@ -233,17 +233,23 @@ 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, - :test => test?, - :authorization => authorization_from(action, response) + Response.new( + success?(response), + message_from(response), + response, + test: test?, + 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, - :test => test?, - :authorization => authorization_from(action, response) + Response.new( + success?(response), + message_from(response), + response, + test: test?, + authorization: authorization_from(action, response) ) end @@ -281,7 +287,7 @@ def parse(body) def parse_element(response, node) if node.has_elements? - node.elements.each{|element| parse_element(response, element) } + node.elements.each { |element| parse_element(response, element) } else response[node.name.underscore.to_sym] = node.text end @@ -296,7 +302,6 @@ def generate_timestamp def request_timeout @options[:request_timeout] || 60 end - end end end diff --git a/lib/active_merchant/billing/gateways/ncr_secure_pay.rb b/lib/active_merchant/billing/gateways/ncr_secure_pay.rb index 1a595dd196d..a7e1ca9f192 100644 --- a/lib/active_merchant/billing/gateways/ncr_secure_pay.rb +++ b/lib/active_merchant/billing/gateways/ncr_secure_pay.rb @@ -1,24 +1,24 @@ require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class NcrSecurePayGateway < Gateway self.test_url = 'https://testbox.monetra.com:8665/' self.live_url = 'https://portal.ncrsecurepay.com:8444/' self.supported_countries = ['US'] self.default_currency = 'USD' - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'http://www.ncrretailonline.com' self.display_name = 'NCR Secure Pay' - def initialize(options={}) + def initialize(options = {}) requires!(options, :username, :password) super end - def purchase(money, payment, options={}) + def purchase(money, payment, options = {}) post = {} add_invoice(post, money, options) add_payment(post, payment) @@ -27,7 +27,7 @@ def purchase(money, payment, options={}) commit('sale', post) end - def authorize(money, payment, options={}) + def authorize(money, payment, options = {}) post = {} add_invoice(post, money, options) add_payment(post, payment) @@ -36,7 +36,7 @@ def authorize(money, payment, options={}) commit('preauth', post) end - def capture(money, authorization, options={}) + def capture(money, authorization, options = {}) post = {} add_reference(post, authorization) add_invoice(post, money, options) @@ -44,7 +44,7 @@ def capture(money, authorization, options={}) commit('preauthcomplete', post) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) post = {} add_reference(post, authorization) add_invoice(post, money, options) @@ -52,13 +52,13 @@ def refund(money, authorization, options={}) commit('credit', post) end - def void(authorization, options={}) + def void(authorization, options = {}) post = {} add_reference(post, authorization) commit('void', post) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -141,7 +141,7 @@ def authorization_from(response) end def request_body(action, parameters = {}) - Nokogiri::XML::Builder.new(:encoding => 'utf-8') do |xml| + Nokogiri::XML::Builder.new(encoding: 'utf-8') do |xml| xml.MonetraTrans do xml.Trans(identifier: parameters.delete(:identifier) || '1') do xml.username(options[:username]) @@ -156,9 +156,7 @@ def request_body(action, parameters = {}) end def error_code_from(response) - unless success_from(response) - response[:msoft_code] || response[:phard_code] - end + response[:msoft_code] || response[:phard_code] unless success_from(response) end end end diff --git a/lib/active_merchant/billing/gateways/net_registry.rb b/lib/active_merchant/billing/gateways/net_registry.rb index f3767e1be66..7052581b7ba 100644 --- a/lib/active_merchant/billing/gateways/net_registry.rb +++ b/lib/active_merchant/billing/gateways/net_registry.rb @@ -24,7 +24,7 @@ module Billing class NetRegistryGateway < Gateway self.live_url = self.test_url = 'https://paygate.ssllock.net/external2.pl' - FILTERED_PARAMS = [ 'card_no', 'card_expiry', 'receipt_array' ] + FILTERED_PARAMS = %w[card_no card_expiry receipt_array] self.supported_countries = ['AU'] @@ -32,16 +32,16 @@ class NetRegistryGateway < Gateway # steps in setting up your account, as detailed in # "Programming for NetRegistry's E-commerce Gateway." # [http://rubyurl.com/hNG] - self.supported_cardtypes = [:visa, :master, :diners_club, :american_express, :jcb] + self.supported_cardtypes = %i[visa master diners_club american_express jcb] self.display_name = 'NetRegistry' self.homepage_url = 'http://www.netregistry.com.au' TRANSACTIONS = { - :authorization => 'preauth', - :purchase => 'purchase', - :capture => 'completion', - :status => 'status', - :refund => 'refund' + authorization: 'preauth', + purchase: 'purchase', + capture: 'completion', + status: 'status', + refund: 'refund' } # Create a new NetRegistry gateway. @@ -122,6 +122,7 @@ def status(identification) end private + def add_request_details(params, options) params['COMMENT'] = options[:description] unless options[:description].blank? end @@ -130,7 +131,7 @@ def add_request_details(params, options) # format for a command. def expiry(credit_card) month = format(credit_card.month, :two_digits) - year = format(credit_card.year , :two_digits) + year = format(credit_card.year, :two_digits) "#{month}/#{year}" end @@ -141,17 +142,20 @@ def expiry(credit_card) # omitted if nil. def commit(action, params) # get gateway response - response = parse( ssl_post(self.live_url, post_data(action, params)) ) + 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) params['COMMAND'] = TRANSACTIONS[action] params['LOGIN'] = "#{@options[:login]}/#{@options[:password]}" - escape_uri(params.map{|k,v| "#{k}=#{v}"}.join('&')) + escape_uri(params.map { |k, v| "#{k}=#{v}" }.join('&')) 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/netaxept.rb b/lib/active_merchant/billing/gateways/netaxept.rb index b3f1652c662..ff68be6fb03 100644 --- a/lib/active_merchant/billing/gateways/netaxept.rb +++ b/lib/active_merchant/billing/gateways/netaxept.rb @@ -1,16 +1,16 @@ require 'digest/md5' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class NetaxeptGateway < Gateway self.test_url = 'https://epayment-test.bbs.no/' self.live_url = 'https://epayment.bbs.no/' # The countries the gateway supports merchants from as 2 digit ISO country codes - self.supported_countries = ['NO', 'DK', 'SE', 'FI'] + self.supported_countries = %w[NO DK SE FI] # The card types supported by the payment gateway - self.supported_cardtypes = [:visa, :master, :american_express] + self.supported_cardtypes = %i[visa master american_express] # The homepage URL of the gateway self.homepage_url = 'http://www.betalingsterminal.no/Netthandel-forside/' @@ -31,8 +31,8 @@ def purchase(money, creditcard, options = {}) requires!(options, :order_id) MultiResponse.run do |r| - r.process{authorize(money, creditcard, options)} - r.process{capture(money, r.authorization, options)} + r.process { authorize(money, creditcard, options) } + r.process { capture(money, r.authorization, options) } end end @@ -40,9 +40,9 @@ def authorize(money, creditcard, options = {}) requires!(options, :order_id) MultiResponse.run do |r| - r.process{setup_transaction(money, options)} - r.process{add_and_auth_credit_card(r.authorization, creditcard, options)} - r.process{query_transaction(r.authorization, options)} + r.process { setup_transaction(money, options) } + r.process { add_and_auth_credit_card(r.authorization, creditcard, options) } + r.process { query_transaction(r.authorization, options) } end end @@ -94,12 +94,12 @@ def query_transaction(authorization, options) commit('Netaxept/query.aspx', post) end - def add_credentials(post, options, secure=true) + def add_credentials(post, options, secure = true) post[:merchantId] = @options[:login] post[:token] = @options[:password] if secure end - def add_authorization(post, authorization, money=nil) + def add_authorization(post, authorization, money = nil) post[:transactionId] = authorization post[:transactionAmount] = amount(money) if money end @@ -118,12 +118,12 @@ def add_creditcard(post, options) post[:securityCode] = options.verification_value end - def commit(path, parameters, xml=true) + def commit(path, parameters, xml = true) raw = parse(ssl_get(build_url(path, parameters)), xml) success = false authorization = (raw['TransactionId'] || parameters[:transactionId]) - if raw[:container] =~ /Exception|Error/ + if /Exception|Error/.match?(raw[:container]) message = (raw['Message'] || raw['Error']['Message']) elsif raw['Error'] && !raw['Error'].empty? message = (raw['Error']['ResponseText'] || raw['Error']['ResponseCode']) @@ -136,17 +136,17 @@ def commit(path, parameters, xml=true) success, message, raw, - :test => test?, - :authorization => authorization + test: test?, + authorization: ) end - def parse(result, expects_xml=true) + def parse(result, expects_xml = true) if expects_xml doc = REXML::Document.new(result) - extract_xml(doc.root).merge(:container => doc.root.name) + extract_xml(doc.root).merge(container: doc.root.name) else - {:result => result} + { result: } end end @@ -162,7 +162,7 @@ def extract_xml(element) end end - def build_url(base, parameters=nil) + def build_url(base, parameters = nil) url = (test? ? self.test_url : self.live_url).dup url << base if parameters @@ -173,9 +173,8 @@ def build_url(base, parameters=nil) end def encode(hash) - hash.collect{|(k,v)| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}"}.join('&') + hash.collect { |(k, v)| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join('&') end end end end - diff --git a/lib/active_merchant/billing/gateways/netbanx.rb b/lib/active_merchant/billing/gateways/netbanx.rb index 444723f66ff..4238d821cb8 100644 --- a/lib/active_merchant/billing/gateways/netbanx.rb +++ b/lib/active_merchant/billing/gateways/netbanx.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class NetbanxGateway < Gateway # Netbanx is the new REST based API for Optimal Payments / Paysafe self.test_url = 'https://api.test.netbanx.com/' @@ -7,14 +7,14 @@ class NetbanxGateway < Gateway self.supported_countries = %w(AF AX AL DZ AS AD AO AI AQ AG AR AM AW AU AT AZ BS BH BD BB BY BE BZ BJ BM BT BO BQ BA BW BV BR IO BN BG BF BI KH CM CA CV KY CF TD CL CN CX CC CO KM CG CD CK CR CI HR CU CW CY CZ DK DJ DM DO EC EG SV GQ ER EE ET FK FO FJ FI FR GF PF TF GA GM GE DE GH GI GR GL GD GP GU GT GG GN GW GY HT HM HN HK HU IS IN ID IR IQ IE IM IL IT JM JP JE JO KZ KE KI KP KR KW KG LA LV LB LS LR LY LI LT LU MO MK MG MW MY MV ML MT MH MQ MR MU YT MX FM MD MC MN ME MS MA MZ MM NA NR NP NC NZ NI NE NG NU NF MP NO OM PK PW PS PA PG PY PE PH PN PL PT PR QA RE RO RU RW BL SH KN LC MF VC WS SM ST SA SN RS SC SL SG SX SK SI SB SO ZA GS SS ES LK PM SD SR SJ SZ SE CH SY TW TJ TZ TH NL TL TG TK TO TT TN TR TM TC TV UG UA AE GB US UM UY UZ VU VA VE VN VG VI WF EH YE ZM ZW) self.default_currency = 'CAD' - self.supported_cardtypes = [ - :american_express, - :diners_club, - :discover, - :jcb, - :master, - :maestro, - :visa + self.supported_cardtypes = %i[ + american_express + diners_club + discover + jcb + master + maestro + visa ] self.money_format = :cents @@ -22,52 +22,98 @@ class NetbanxGateway < Gateway self.homepage_url = 'https://processing.paysafe.com/' self.display_name = 'Netbanx by PaySafe' - def initialize(options={}) + AVS_CODE_CONVERTER = { + 'MATCH' => 'X', + 'MATCH_ADDRESS_ONLY' => 'A', + 'MATCH_ZIP_ONLY' => 'Z', + 'NO_MATCH' => 'N', + 'NOT_PROCESSED' => 'U', + 'UNKNOWN' => 'Q' + } + + CVV_CODE_CONVERTER = { + 'MATCH' => 'M', + 'NO_MATCH' => 'N', + 'NOT_PROCESSED' => 'P', + 'UNKNOWN' => 'U' + } + + def initialize(options = {}) requires!(options, :account_number, :api_key) super end - def purchase(money, payment, options={}) + def purchase(money, payment, options = {}) + # Do a Verification with AVS prior to purchase + verification_response = verify(payment, options) + return verification_response if verification_response.message != 'OK' + post = {} add_invoice(post, money, options) add_settle_with_auth(post) add_payment(post, payment, options) + add_customer_detail_data(post, options) commit(:post, 'auths', post) end - def authorize(money, payment, options={}) + def authorize(money, payment, options = {}) + # Do a Verification with AVS prior to Auth + Settle + verification_response = verify(payment, options) + return verification_response if verification_response.message != 'OK' + post = {} add_invoice(post, money, options) add_payment(post, payment, options) + add_customer_detail_data(post, options) commit(:post, 'auths', post) end - def capture(money, authorization, options={}) + def capture(money, authorization, options = {}) post = {} add_invoice(post, money, options) commit(:post, "auths/#{authorization}/settlements", post) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) + # If the transactions that are pending, API call needs to be Cancellation + settlement_data = get_settlement(authorization) + return settlement_data if settlement_data.message != 'OK' + post = {} - add_invoice(post, money, options) + if settlement_data.params['status'] == 'PENDING' && money == settlement_data.params['amount'] + post[:status] = 'CANCELLED' + commit(:put, "settlements/#{authorization}", post) + elsif settlement_data.params['status'] == 'PENDING' && (money < settlement_data.params['amount'] || money > settlement_data.params['amount']) + return Response.new(false, 'Transaction not settled. Either do a full refund or try partial refund after settlement.') + else + add_invoice(post, money, options) + + # Setting merchantRefNumber to a unique id for each refund + # This is to support multiple partial refunds for the same order + post[:merchantRefNum] = SecureRandom.uuid - commit(:post, "settlements/#{authorization}/refunds", post) + commit(:post, "settlements/#{authorization}/refunds", post) + end end - def void(authorization, options={}) + def get_settlement(authorization) + post = {} + commit(:get, "settlements/#{authorization}", post) + end + + def void(authorization, options = {}) post = {} add_order_id(post, options) commit(:post, "auths/#{authorization}/voidauths", post) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) post = {} - add_payment(post, credit_card) + add_payment(post, credit_card, options) add_order_id(post, options) commit(:post, 'verifications', post) @@ -75,7 +121,7 @@ def verify(credit_card, options={}) # note: when passing options[:customer] we only attempt to add the # card to the profile_id passed as the options[:customer] - def store(credit_card, options={}) + def store(credit_card, options = {}) # locale can only be one of en_US, fr_CA, en_GB requires!(options, :locale) post = {} @@ -119,15 +165,24 @@ def add_customer_data(post, options) post[:locale] = options[:locale] end + def add_customer_detail_data(post, options) + post[:profile] ||= {} + post[:profile][:email] = options[:email] if options[:email] + post[:customerIp] = options[:ip] if options[:ip] + if (billing_address = options[:billing_address]) + post[:profile][:firstName], post[:profile][:lastName] = split_names(billing_address[:name]) + end + end + def add_credit_card(post, credit_card, options = {}) post[:card] ||= {} post[:card][:cardNum] = credit_card.number post[:card][:holderName] = credit_card.name post[:card][:cvv] = credit_card.verification_value post[:card][:cardExpiry] = expdate(credit_card) - if options[:billing_address] - post[:card][:billingAddress] = map_address(options[:billing_address]) - end + + post[:authentication] = map_3ds(options[:three_d_secure]) if options[:three_d_secure] + post[:card][:billingAddress] = map_address(options[:billing_address]) if options[:billing_address] end def add_invoice(post, money, options) @@ -135,18 +190,19 @@ def add_invoice(post, money, options) add_order_id(post, options) end - def add_payment(post, credit_card_or_reference, options = {}) + def add_payment(post, credit_card_reference, options = {}) post[:card] ||= {} - if credit_card_or_reference.is_a?(String) - post[:card][:paymentToken] = credit_card_or_reference + if credit_card_reference.is_a?(String) + post[:card][:paymentToken] = credit_card_reference else - post[:card][:cardNum] = credit_card_or_reference.number - post[:card][:cvv] = credit_card_or_reference.verification_value - post[:card][:cardExpiry] = expdate(credit_card_or_reference) + post[:card][:cardNum] = credit_card_reference.number + post[:card][:cvv] = credit_card_reference.verification_value + post[:card][:cardExpiry] = expdate(credit_card_reference) end post[:currencyCode] = options[:currency] if options[:currency] post[:billingDetails] = map_address(options[:billing_address]) if options[:billing_address] + post[:authentication] = map_3ds(options[:three_d_secure]) if options[:three_d_secure] end def expdate(credit_card) @@ -154,7 +210,7 @@ def expdate(credit_card) month = format(credit_card.month, :two_digits) # returns a hash (necessary in the card JSON object) - { :month => month, :year => year } + { month:, year: } end def add_order_id(post, options) @@ -163,44 +219,73 @@ def add_order_id(post, options) def map_address(address) return {} if address.nil? + country = Country.find(address[:country]) if address[:country] mapped = { - :street => address[:address1], - :city => address[:city], - :zip => address[:zip], + street: address[:address1], + city: address[:city], + zip: address[:zip], + state: address[:state] } - mapped.merge!({:country => country.code(:alpha2).value}) unless country.blank? + mapped[:country] = country.code(:alpha2).value unless country.blank? mapped end + def map_3ds(three_d_secure_options) + { + eci: three_d_secure_options[:eci], + cavv: three_d_secure_options[:cavv], + xid: three_d_secure_options[:xid], + threeDResult: three_d_secure_options[:directory_response_status], + threeDSecureVersion: three_d_secure_options[:version], + directoryServerTransactionId: three_d_secure_options[:ds_transaction_id] + } + end + def parse(body) body.blank? ? {} : JSON.parse(body) end def commit(method, uri, parameters) params = parameters.to_json unless parameters.nil? - response = begin - parse(ssl_request(method, get_url(uri), params, headers)) - rescue ResponseError => e - return Response.new(false, 'Invalid Login') if(e.response.code == '401') - parse(e.response.body) - end + response = + begin + if method == :get + parse(ssl_request(method, get_url(uri), nil, headers)) + else + parse(ssl_request(method, get_url(uri), params, headers)) + end + rescue ResponseError => e + return Response.new(false, 'Invalid Login') if e.response.code == '401' + + parse(e.response.body) + end success = success_from(response) Response.new( success, message_from(success, response), response, - :test => test?, - :error_code => error_code_from(response), - :authorization => authorization_from(success, get_url(uri), method, response) + test: test?, + error_code: error_code_from(response), + authorization: authorization_from(success, get_url(uri), method, response), + avs_result: avs_result(response), + cvv_result: cvv_result(response) ) end + def avs_result(response) + AVSResult.new(code: AVS_CODE_CONVERTER[response['avsResponse']]) + end + + def cvv_result(response) + CVVResult.new(CVV_CODE_CONVERTER[response['cvvVerification']]) + end + def get_url(uri) url = (test? ? test_url : live_url) - if uri =~ /^customervault/ + if /^customervault/.match?(uri) "#{url}#{uri}" else "#{url}cardpayments/v1/accounts/#{@options[:account_number]}/#{uri}" @@ -246,41 +331,41 @@ def headers def error_code_from(response) unless success_from(response) case response['errorCode'] - when '3002' then STANDARD_ERROR_CODE[:invalid_number] # You submitted an invalid card number or brand or combination of card number and brand with your request. - when '3004' then STANDARD_ERROR_CODE[:incorrect_zip] # The zip/postal code must be provided for an AVS check request. - when '3005' then STANDARD_ERROR_CODE[:incorrect_cvc] # You submitted an incorrect CVC value with your request. - when '3006' then STANDARD_ERROR_CODE[:expired_card] # You submitted an expired credit card number with your request. - when '3009' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined by the issuing bank. - when '3011' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined by the issuing bank because the card used is a restricted card. Contact the cardholder's credit card company for further investigation. - when '3012' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined by the issuing bank because the credit card expiry date submitted is invalid. - when '3013' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined by the issuing bank due to problems with the credit card account. - when '3014' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined - the issuing bank has returned an unknown response. Contact the card holder's credit card company for further investigation. - when '3015' then STANDARD_ERROR_CODE[:card_declined] # The bank has requested that you process the transaction manually by calling the cardholder's credit card company. - when '3016' then STANDARD_ERROR_CODE[:card_declined] # The bank has requested that you retrieve the card from the cardholder – it may be a lost or stolen card. - when '3017' then STANDARD_ERROR_CODE[:invalid_number] # You submitted an invalid credit card number with your request. - when '3022' then STANDARD_ERROR_CODE[:card_declined] # The card has been declined due to insufficient funds. - when '3023' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined by the issuing bank due to its proprietary card activity regulations. - when '3024' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined because the issuing bank does not permit the transaction for this card. - when '3032' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined by the issuing bank or external gateway because the card is probably in one of their negative databases. - when '3035' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined due to exceeded PIN attempts. - when '3036' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined due to an invalid issuer. - when '3037' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined because it is invalid. - when '3038' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined due to customer cancellation. - when '3039' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined due to an invalid authentication value. - when '3040' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined because the request type is not permitted on the card. - when '3041' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined due to a timeout. - when '3042' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined due to a cryptographic error. - when '3045' then STANDARD_ERROR_CODE[:invalid_expiry_date] # You submitted an invalid date format for this request. - when '3046' then STANDARD_ERROR_CODE[:card_declined] # The transaction was declined because the amount was set to zero. - when '3047' then STANDARD_ERROR_CODE[:card_declined] # The transaction was declined because the amount exceeds the floor limit. - when '3048' then STANDARD_ERROR_CODE[:card_declined] # The transaction was declined because the amount is less than the floor limit. - when '3049' then STANDARD_ERROR_CODE[:card_declined] # The bank has requested that you retrieve the card from the cardholder – the credit card has expired. - when '3050' then STANDARD_ERROR_CODE[:card_declined] # The bank has requested that you retrieve the card from the cardholder – fraudulent activity is suspected. - when '3051' then STANDARD_ERROR_CODE[:card_declined] # The bank has requested that you retrieve the card from the cardholder – contact the acquirer for more information. - when '3052' then STANDARD_ERROR_CODE[:card_declined] # The bank has requested that you retrieve the card from the cardholder – the credit card is restricted. - when '3053' then STANDARD_ERROR_CODE[:card_declined] # The bank has requested that you retrieve the card from the cardholder – please call the acquirer. - when '3054' then STANDARD_ERROR_CODE[:card_declined] # The transaction was declined due to suspected fraud. - else STANDARD_ERROR_CODE[:processing_error] + when '3002' then STANDARD_ERROR_CODE[:invalid_number] # You submitted an invalid card number or brand or combination of card number and brand with your request. + when '3004' then STANDARD_ERROR_CODE[:incorrect_zip] # The zip/postal code must be provided for an AVS check request. + when '3005' then STANDARD_ERROR_CODE[:incorrect_cvc] # You submitted an incorrect CVC value with your request. + when '3006' then STANDARD_ERROR_CODE[:expired_card] # You submitted an expired credit card number with your request. + when '3009' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined by the issuing bank. + when '3011' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined by the issuing bank because the card used is a restricted card. Contact the cardholder's credit card company for further investigation. + when '3012' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined by the issuing bank because the credit card expiry date submitted is invalid. + when '3013' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined by the issuing bank due to problems with the credit card account. + when '3014' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined - the issuing bank has returned an unknown response. Contact the card holder's credit card company for further investigation. + when '3015' then STANDARD_ERROR_CODE[:card_declined] # The bank has requested that you process the transaction manually by calling the cardholder's credit card company. + when '3016' then STANDARD_ERROR_CODE[:card_declined] # The bank has requested that you retrieve the card from the cardholder – it may be a lost or stolen card. + when '3017' then STANDARD_ERROR_CODE[:invalid_number] # You submitted an invalid credit card number with your request. + when '3022' then STANDARD_ERROR_CODE[:card_declined] # The card has been declined due to insufficient funds. + when '3023' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined by the issuing bank due to its proprietary card activity regulations. + when '3024' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined because the issuing bank does not permit the transaction for this card. + when '3032' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined by the issuing bank or external gateway because the card is probably in one of their negative databases. + when '3035' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined due to exceeded PIN attempts. + when '3036' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined due to an invalid issuer. + when '3037' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined because it is invalid. + when '3038' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined due to customer cancellation. + when '3039' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined due to an invalid authentication value. + when '3040' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined because the request type is not permitted on the card. + when '3041' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined due to a timeout. + when '3042' then STANDARD_ERROR_CODE[:card_declined] # Your request has been declined due to a cryptographic error. + when '3045' then STANDARD_ERROR_CODE[:invalid_expiry_date] # You submitted an invalid date format for this request. + when '3046' then STANDARD_ERROR_CODE[:card_declined] # The transaction was declined because the amount was set to zero. + when '3047' then STANDARD_ERROR_CODE[:card_declined] # The transaction was declined because the amount exceeds the floor limit. + when '3048' then STANDARD_ERROR_CODE[:card_declined] # The transaction was declined because the amount is less than the floor limit. + when '3049' then STANDARD_ERROR_CODE[:card_declined] # The bank has requested that you retrieve the card from the cardholder – the credit card has expired. + when '3050' then STANDARD_ERROR_CODE[:card_declined] # The bank has requested that you retrieve the card from the cardholder – fraudulent activity is suspected. + when '3051' then STANDARD_ERROR_CODE[:card_declined] # The bank has requested that you retrieve the card from the cardholder – contact the acquirer for more information. + when '3052' then STANDARD_ERROR_CODE[:card_declined] # The bank has requested that you retrieve the card from the cardholder – the credit card is restricted. + when '3053' then STANDARD_ERROR_CODE[:card_declined] # The bank has requested that you retrieve the card from the cardholder – please call the acquirer. + when '3054' then STANDARD_ERROR_CODE[:card_declined] # The transaction was declined due to suspected fraud. + else STANDARD_ERROR_CODE[:processing_error] end end end diff --git a/lib/active_merchant/billing/gateways/netbilling.rb b/lib/active_merchant/billing/gateways/netbilling.rb index 32635e806dd..9c1459df6d1 100644 --- a/lib/active_merchant/billing/gateways/netbilling.rb +++ b/lib/active_merchant/billing/gateways/netbilling.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # To perform PCI Compliant Repeat Billing # # Ensure that PCI Compliant Repeat Billing is enabled on your merchant account: @@ -14,16 +14,16 @@ class NetbillingGateway < Gateway self.live_url = self.test_url = 'https://secure.netbilling.com:1402/gw/sas/direct3.1' TRANSACTIONS = { - :authorization => 'A', - :purchase => 'S', - :refund => 'R', - :credit => 'C', - :capture => 'D', - :void => 'U', - :quasi => 'Q' + authorization: 'A', + purchase: 'S', + refund: 'R', + credit: 'C', + capture: 'D', + void: 'U', + quasi: 'Q' } - SUCCESS_CODES = [ '1', 'T' ] + SUCCESS_CODES = %w[1 T] SUCCESS_MESSAGE = 'The transaction was approved' FAILURE_MESSAGE = 'The transaction failed' TEST_LOGIN = '104901072025' @@ -31,7 +31,7 @@ class NetbillingGateway < Gateway self.display_name = 'NETbilling' self.homepage_url = 'http://www.netbilling.com' self.supported_countries = ['US'] - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :jcb, :diners_club] + self.supported_cardtypes = %i[visa master american_express discover jcb diners_club] def initialize(options = {}) requires!(options, :login) @@ -143,14 +143,14 @@ def add_address(post, credit_card, options) post[:bill_state] = billing_address[:state] end - if shipping_address = options[:shipping_address] - post[:ship_name1], post[:ship_name2] = split_names(shipping_address[:name]) - post[:ship_street] = shipping_address[:address1] - post[:ship_zip] = shipping_address[:zip] - post[:ship_city] = shipping_address[:city] - post[:ship_country] = shipping_address[:country] - post[:ship_state] = shipping_address[:state] - end + if shipping_address = options[:shipping_address] + post[:ship_name1], post[:ship_name2] = split_names(shipping_address[:name]) + post[:ship_street] = shipping_address[:address1] + post[:ship_zip] = shipping_address[:zip] + post[:ship_city] = shipping_address[:city] + post[:ship_country] = shipping_address[:country] + post[:ship_state] = shipping_address[:state] + end end def add_invoice(post, options) @@ -166,9 +166,7 @@ def add_payment_source(params, source) end def add_user_data(post, options) - if options[:order_id] - post[:user_data] = "order_id:#{options[:order_id]}" - end + post[:user_data] = "order_id:#{options[:order_id]}" if options[:order_id] end def add_transaction_id(post, transaction_id) @@ -186,7 +184,7 @@ def add_credit_card(post, credit_card) def parse(body) results = {} body.split(/&/).each do |pair| - key,val = pair.split(/\=/) + key, val = pair.split(/\=/) results[key.to_sym] = CGI.unescape(val) end results @@ -195,15 +193,19 @@ 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, - :test => test_response?(response), - :authorization => response[:trans_id], - :avs_result => { :code => response[:avs_code]}, - :cvv_result => response[:cvv2_code] + 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] ) rescue ActiveMerchant::ResponseError => e - raise unless(e.response.code =~ /^[67]\d\d$/) - return Response.new(false, e.response.message, {:status_code => e.response.code}, :test => test?) + raise unless e.response.code =~ /^[67]\d\d$/ + + return Response.new(false, e.response.message, { status_code: e.response.code }, test: test?) end def test_response?(response) @@ -224,9 +226,8 @@ def post_data(action, parameters = {}) parameters[:pay_type] = 'C' parameters[:tran_type] = TRANSACTIONS[action] - parameters.reject{|k,v| v.blank?}.collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&') + parameters.reject { |_k, v| v.blank? }.collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&') end - end end end diff --git a/lib/active_merchant/billing/gateways/netpay.rb b/lib/active_merchant/billing/gateways/netpay.rb index 5068fa894c6..34e46dda90c 100644 --- a/lib/active_merchant/billing/gateways/netpay.rb +++ b/lib/active_merchant/billing/gateways/netpay.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # # NETPAY Gateway # @@ -42,7 +42,7 @@ class NetpayGateway < Gateway self.default_currency = 'MXN' # The card types supported by the payment gateway - self.supported_cardtypes = [:visa, :master, :american_express, :diners_club] + self.supported_cardtypes = %i[visa master american_express diners_club] # The homepage URL of the gateway self.homepage_url = 'http://www.netpay.com.mx' @@ -55,7 +55,7 @@ class NetpayGateway < Gateway } # The header keys that we will provide in the response params hash - RESPONSE_KEYS = ['ResponseMsg', 'ResponseText', 'ResponseCode', 'TimeIn', 'TimeOut', 'AuthCode', 'OrderId', 'CardTypeName', 'MerchantId', 'IssuerAuthDate'] + RESPONSE_KEYS = %w[ResponseMsg ResponseText ResponseCode TimeIn TimeOut AuthCode OrderId CardTypeName MerchantId IssuerAuthDate] def initialize(options = {}) requires!(options, :store_id, :login, :password) @@ -110,7 +110,6 @@ def refund(money, authorization, options = {}) add_order_id(post, order_id_from(authorization)) add_amount(post, money, options) - #commit('Refund', post, options) commit('Credit', post, options) end @@ -181,8 +180,8 @@ def parse(response, request_params) success = (response_params['ResponseCode'] == '00') message = response_params['ResponseText'] || response_params['ResponseMsg'] - options = @options.merge(:test => test?, - :authorization => build_authorization(request_params, response_params)) + options = @options.merge(test: test?, + authorization: build_authorization(request_params, response_params)) Response.new(success, message, response_params, options) end @@ -191,7 +190,7 @@ def commit(action, parameters, options) add_login_data(parameters) add_action(parameters, action, options) - post = parameters.collect{|key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&') + post = parameters.collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&') parse(ssl_post(url, post), parameters) end @@ -216,6 +215,7 @@ def params_from_response(response) def currency_code(currency) return currency if currency =~ /^\d+$/ + CURRENCY_CODES[currency] end end diff --git a/lib/active_merchant/billing/gateways/network_merchants.rb b/lib/active_merchant/billing/gateways/network_merchants.rb index b684e92c5a8..e0ece0a04a9 100644 --- a/lib/active_merchant/billing/gateways/network_merchants.rb +++ b/lib/active_merchant/billing/gateways/network_merchants.rb @@ -1,10 +1,10 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class NetworkMerchantsGateway < Gateway self.live_url = self.test_url = 'https://secure.networkmerchants.com/api/transact.php' self.supported_countries = ['US'] - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'http://www.nmi.com/' self.display_name = 'Network Merchants (NMI)' @@ -190,7 +190,7 @@ def add_amount(post, money, options) end def commit_vault(action, parameters) - commit(nil, parameters.merge(:customer_vault => action)) + commit(nil, parameters.merge(customer_vault: action)) end def commit(action, parameters) @@ -200,11 +200,14 @@ def commit(action, parameters) authorization = authorization_from(success, parameters, raw) - Response.new(success, raw['responsetext'], raw, - :test => test?, - :authorization => authorization, - :avs_result => { :code => raw['avsresponse']}, - :cvv_result => raw['cvvresponse'] + Response.new( + success, + raw['responsetext'], + raw, + test: test?, + authorization:, + avs_result: { code: raw['avsresponse'] }, + cvv_result: raw['cvvresponse'] ) end @@ -218,9 +221,7 @@ def authorization_from(success, parameters, response) return nil unless success authorization = response['transactionid'] - if(parameters[:customer_vault] && (authorization.nil? || authorization.empty?)) - authorization = response['customer_vault_id'] - end + authorization = response['customer_vault_id'] if parameters[:customer_vault] && (authorization.nil? || authorization.empty?) authorization end @@ -239,4 +240,3 @@ def parse(raw_response) end end end - diff --git a/lib/active_merchant/billing/gateways/nmi.rb b/lib/active_merchant/billing/gateways/nmi.rb index 7ed61265c54..15cfd5fbfd7 100644 --- a/lib/active_merchant/billing/gateways/nmi.rb +++ b/lib/active_merchant/billing/gateways/nmi.rb @@ -1,15 +1,15 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class NmiGateway < Gateway include Empty DUP_WINDOW_DEPRECATION_MESSAGE = 'The class-level duplicate_window variable is deprecated. Please use the :dup_seconds transaction option instead.' - self.test_url = self.live_url = 'https://secure.nmi.com/api/transact.php' + 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_cardtypes = [:visa, :master, :american_express, :discover] + 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' @@ -23,31 +23,44 @@ def self.duplicate_window end def initialize(options = {}) - requires!(options, :login, :password) + if options.has_key?(:security_key) + requires!(options, :security_key) + else + requires!(options, :login, :password) + end super end - def purchase(amount, payment_method, options={}) + def purchase(amount, payment_method, options = {}) post = {} add_invoice(post, amount, options) + add_customer_vault_data(post, options) add_payment_method(post, payment_method, options) + add_stored_credential(post, options) add_customer_data(post, options) + add_vendor_data(post, options) add_merchant_defined_fields(post, options) + add_level3_fields(post, options) + add_three_d_secure(post, options) commit('sale', post) end - def authorize(amount, payment_method, options={}) + def authorize(amount, payment_method, options = {}) post = {} add_invoice(post, amount, options) + add_customer_vault_data(post, options) add_payment_method(post, payment_method, options) + add_stored_credential(post, options) add_customer_data(post, options) + add_vendor_data(post, options) add_merchant_defined_fields(post, options) - + add_level3_fields(post, options) + add_three_d_secure(post, options) commit('auth', post) end - def capture(amount, authorization, options={}) + def capture(amount, authorization, options = {}) post = {} add_invoice(post, amount, options) add_reference(post, authorization) @@ -56,7 +69,7 @@ def capture(amount, authorization, options={}) commit('capture', post) end - def void(authorization, options={}) + def void(authorization, options = {}) post = {} add_reference(post, authorization) add_payment_type(post, authorization) @@ -64,7 +77,7 @@ def void(authorization, options={}) commit('void', post) end - def refund(amount, authorization, options={}) + def refund(amount, authorization, options = {}) post = {} add_invoice(post, amount, options) add_reference(post, authorization) @@ -73,20 +86,25 @@ def refund(amount, authorization, options={}) commit('refund', post) end - def credit(amount, payment_method, options={}) + def credit(amount, payment_method, options = {}) post = {} add_invoice(post, amount, options) add_payment_method(post, payment_method, options) add_customer_data(post, options) + add_vendor_data(post, options) + add_level3_fields(post, options) commit('credit', post) end - def verify(payment_method, options={}) + def verify(payment_method, options = {}) post = {} + add_customer_vault_data(post, options) add_payment_method(post, payment_method, options) add_customer_data(post, options) + add_vendor_data(post, options) add_merchant_defined_fields(post, options) + add_level3_fields(post, options) commit('validate', post) end @@ -96,6 +114,7 @@ def store(payment_method, options = {}) add_invoice(post, nil, options) add_payment_method(post, payment_method, options) add_customer_data(post, options) + add_vendor_data(post, options) add_merchant_defined_fields(post, options) commit('add_customer', post) @@ -112,11 +131,13 @@ def supports_scrubbing? def scrub(transcript) transcript. - gsub(%r((password=)\w+), '\1[FILTERED]'). + gsub(%r((password=)[^&\n]*), '\1[FILTERED]'). + gsub(%r((security_key=)[^&\n]*), '\1[FILTERED]'). gsub(%r((ccnumber=)\d+), '\1[FILTERED]'). gsub(%r((cvv=)\d+), '\1[FILTERED]'). gsub(%r((checkaba=)\d+), '\1[FILTERED]'). gsub(%r((checkaccount=)\d+), '\1[FILTERED]'). + gsub(%r((cavv=)[^&\n]*), '\1[FILTERED]'). gsub(%r((cryptogram=)[^&]+(&?)), '\1[FILTERED]\2') end @@ -126,25 +147,32 @@ def supports_network_tokenization? private + def add_level3_fields(post, options) + add_fields_to_post_if_present(post, options, %i[tax shipping ponumber]) + end + 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) post[:billing_method] = 'recurring' if options[:recurring] + post[:industry] = options[:industry_indicator] if options[:industry_indicator] if (dup_seconds = (options[:dup_seconds] || self.class.duplicate_window)) post[:dup_seconds] = dup_seconds end end def add_payment_method(post, payment_method, options) - if(payment_method.is_a?(String)) - post[:customer_vault_id] = payment_method - elsif (payment_method.is_a?(NetworkTokenizationCreditCard)) + if payment_method.is_a?(String) + customer_vault_id, = split_authorization(payment_method) + post[:customer_vault_id] = customer_vault_id + elsif payment_method.is_a?(NetworkTokenizationCreditCard) post[:ccnumber] = payment_method.number post[:ccexp] = exp_date(payment_method) - post[:token_cryptogram] = payment_method.payment_cryptogram - elsif(card_brand(payment_method) == 'check') + add_network_token_fields(post, payment_method) + elsif card_brand(payment_method) == 'check' post[:payment] = 'check' post[:firstname] = payment_method.first_name post[:lastname] = payment_method.last_name @@ -164,34 +192,102 @@ def add_payment_method(post, payment_method, options) end end + def add_network_token_fields(post, payment_method) + if payment_method.source == :apple_pay || payment_method.source == :google_pay + post[:cavv] = payment_method.payment_cryptogram + post[:eci] = payment_method.eci + post[:decrypted_applepay_data] = 1 if payment_method.source == :apple_pay + post[:decrypted_googlepay_data] = 1 if payment_method.source == :google_pay + else + post[:token_cryptogram] = payment_method.payment_cryptogram + end + end + + def add_stored_credential(post, options) + return unless (stored_credential = options[:stored_credential]) + + if stored_credential[:initiator] == 'cardholder' + post[:initiated_by] = 'customer' + else + post[:initiated_by] = 'merchant' + end + + # :reason_type, when provided, overrides anything previously set in + # post[:billing_method] (see `add_invoice` and the :recurring) option + case stored_credential[:reason_type] + when 'recurring' + post[:billing_method] = 'recurring' + when 'installment' + post[:billing_method] = 'installment' + when 'unscheduled' + post.delete(:billing_method) + end + + if stored_credential[:initial_transaction] + post[:stored_credential_indicator] = 'stored' + else + post[:stored_credential_indicator] = 'used' + # should only send :initial_transaction_id if it is a MIT + ntid = options[:network_transaction_id] || stored_credential[:network_transaction_id] + post[:initial_transaction_id] = ntid if post[:initiated_by] == 'merchant' + end + end + def add_customer_data(post, options) post[:email] = options[:email] post[:ipaddress] = options[:ip] post[:customer_id] = options[:customer_id] || options[:customer] - if(billing_address = options[:billing_address] || options[:address]) + if (billing_address = options[:billing_address] || options[:address]) post[:company] = billing_address[:company] post[:address1] = billing_address[:address1] post[:address2] = billing_address[:address2] post[:city] = billing_address[:city] post[:state] = billing_address[:state] post[:country] = billing_address[:country] - post[:zip] = billing_address[:zip] + post[:zip] = billing_address[:zip] post[:phone] = billing_address[:phone] end - if(shipping_address = options[:shipping_address]) + 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] post[:shipping_city] = shipping_address[:city] post[:shipping_state] = shipping_address[:state] post[:shipping_country] = shipping_address[:country] - post[:shipping_zip] = shipping_address[:zip] + 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]) + post[:descriptor] = descriptor[:descriptor] + post[:descriptor_phone] = descriptor[:descriptor_phone] + post[:descriptor_address] = descriptor[:descriptor_address] + post[:descriptor_city] = descriptor[:descriptor_city] + post[:descriptor_state] = descriptor[:descriptor_state] + post[:descriptor_postal] = descriptor[:descriptor_postal] + post[:descriptor_country] = descriptor[:descriptor_country] + post[:descriptor_mcc] = descriptor[:descriptor_mcc] + post[:descriptor_merchant_id] = descriptor[:descriptor_merchant_id] + post[:descriptor_url] = descriptor[:descriptor_url] end end + def add_vendor_data(post, options) + post[:vendor_id] = options[:vendor_id] if options[:vendor_id] + post[:processor_id] = options[:processor_id] if options[:processor_id] + end + + def add_customer_vault_data(post, options) + post[:customer_vault] = options[:customer_vault] if options[:customer_vault] + post[:customer_vault_id] = options[:customer_vault_id] if options[:customer_vault_id] + end + def add_merchant_defined_fields(post, options) (1..20).each do |each| key = "merchant_defined_field_#{each}".to_sym @@ -199,8 +295,25 @@ def add_merchant_defined_fields(post, options) end end + def add_three_d_secure(post, options) + three_d_secure = options[:three_d_secure] + return unless three_d_secure + + post[:cardholder_auth] = cardholder_auth(three_d_secure[:authentication_response_status]) + post[:cavv] = three_d_secure[:cavv] + post[:xid] = three_d_secure[:xid] + post[:three_ds_version] = three_d_secure[:version] + post[:directory_server_id] = three_d_secure[:ds_transaction_id] + end + + def cardholder_auth(trans_status) + return nil if trans_status.nil? + + trans_status == 'Y' ? 'verified' : 'attempted' + end + def add_reference(post, authorization) - transaction_id, _ = split_authorization(authorization) + transaction_id, = split_authorization(authorization) post[:transactionid] = transaction_id end @@ -214,11 +327,10 @@ def exp_date(payment_method) end def commit(action, params) - params[action == 'add_customer' ? :customer_vault : :type] = action - params[:username] = @options[:login] - params[:password] = @options[:password] - + params[:username] = @options[:login] unless @options[:login].nil? + params[:password] = @options[:password] unless @options[:password].nil? + params[:security_key] = @options[:security_key] unless @options[:security_key].nil? raw_response = ssl_post(url, post_data(action, params), headers) response = parse(raw_response) succeeded = success_from(response) @@ -227,15 +339,16 @@ def commit(action, params) succeeded, message_from(succeeded, response), response, - authorization: authorization_from(response, params[:payment]), + authorization: authorization_from(response, params[:payment], action), avs_result: AVSResult.new(code: response[:avsresponse]), cvv_result: CVVResult.new(response[:cvvresponse]), test: test? ) end - def authorization_from(response, payment_type) - [ response[:transactionid], payment_type ].join('#') + def authorization_from(response, payment_type, action) + authorization = (action == 'add_customer' ? response[:customer_vault_id] : response[:transactionid]) + [authorization, payment_type].join('#') end def split_authorization(authorization) @@ -243,11 +356,11 @@ def split_authorization(authorization) end def headers - { 'Content-Type' => 'application/x-www-form-urlencoded;charset=UTF-8' } + { 'Content-Type' => 'application/x-www-form-urlencoded;charset=UTF-8' } end def post_data(action, params) - params.map {|k, v| "#{k}=#{CGI.escape(v.to_s)}"}.join('&') + params.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&') end def url @@ -255,7 +368,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) @@ -269,7 +382,6 @@ def message_from(succeeded, response) response[:responsetext] end end - end end end diff --git a/lib/active_merchant/billing/gateways/nuvei.rb b/lib/active_merchant/billing/gateways/nuvei.rb new file mode 100644 index 00000000000..44c55d3deb9 --- /dev/null +++ b/lib/active_merchant/billing/gateways/nuvei.rb @@ -0,0 +1,537 @@ +module ActiveMerchant + module Billing + class NuveiGateway < Gateway + self.test_url = 'https://ppp-test.nuvei.com/ppp/api/v1' + self.live_url = 'https://secure.safecharge.com/ppp/api/v1' + + self.supported_countries = %w[US CA IN NZ GB AU US] + self.default_currency = 'USD' + self.money_format = :dollars + self.supported_cardtypes = %i[visa master american_express discover union_pay] + self.currencies_without_fractions = %w[CLP KRW JPY ISK MMK PYG UGX VND XAF XOF] + self.homepage_url = 'https://www.nuvei.com/' + self.display_name = 'Nuvei' + + ENDPOINTS_MAPPING = { + authenticate: '/getSessionToken', + purchase: '/payment', # /authorize with transactionType: "Auth" + capture: '/settleTransaction', + refund: '/refundTransaction', + void: '/voidTransaction', + general_credit: '/payout', + init_payment: '/initPayment' + } + + NETWORK_TOKENIZATION_CARD_MAPPING = { + 'apple_pay' => 'ApplePay', + 'google_pay' => 'GooglePay' + } + + def initialize(options = {}) + requires!(options, :merchant_id, :merchant_site_id, :secret_key) + super + fetch_session_token unless session_token_valid? + end + + def authorize(money, payment, options = {}, transaction_type = 'Auth') + post = { transactionType: transaction_type } + post[:savePM] = options[:save_payment_method] ? options[:save_payment_method].to_s : 'false' + + build_post_data(post, options) + add_amount(post, money, options) + add_payment_method(post, payment, :paymentOption, options) + add_3ds_global(post, options) + add_address(post, payment, options) + add_customer_ip(post, options) + add_stored_credentials(post, payment, options) + add_account_funding_transaction(post, payment, options) + add_cardholder_name_verification(post, payment, transaction_type, options) + post[:userTokenId] = options[:user_token_id] if options[:user_token_id] + post[:isPartialApproval] = options[:is_partial_approval] ? 1 : 0 + post[:authenticationOnlyType] = options[:authentication_only_type] if options[:authentication_only_type] + + if options[:execute_threed] + execute_3ds_flow(post, money, payment, transaction_type, options) + else + commit(:purchase, post) + end + end + + def purchase(money, payment, options = {}) + fetch_session_token if payment.is_a?(String) + authorize(money, payment, options, 'Sale') + end + + def capture(money, authorization, options = {}) + post = { relatedTransactionId: authorization } + + build_post_data(post) + add_amount(post, money, options) + + commit(:capture, post) + end + + def refund(money, authorization, options = {}) + post = { relatedTransactionId: authorization } + + build_post_data(post) + add_amount(post, money, options) + + commit(:refund, post) + end + + def void(authorization, options = {}) + post = { relatedTransactionId: authorization } + build_post_data(post) + + commit(:void, post) + end + + def verify(credit_card, options = {}) + MultiResponse.run(:use_first_response) do |r| + r.process { authorize(0, credit_card, options) } + r.process(:ignore_result) { void(r.authorization, options) } + end + end + + def store(credit_card, options = {}) + options[:save_payment_method] = true + authorize(0, credit_card, options) + end + + def credit(money, payment, options = {}) + post = { userTokenId: options[:user_token_id] } + payment_key = payment.is_a?(NetworkTokenizationCreditCard) ? :userPaymentOption : :cardData + build_post_data(post) + add_amount(post, money, options) + options[:is_payout] ? send_payout_transaction(payment_key, post, payment, options) : send_unreferenced_refund_transaction(post, payment, options) + end + + def send_payout_transaction(payment_key, post, payment, options = {}) + add_payment_method(post, payment, payment_key, options) + add_customer_ip(post, options) + url_details(post, options) + commit(:general_credit, post.compact) + end + + def send_unreferenced_refund_transaction(post, payment, options = {}) + post[:paymentOption] = { userPaymentOptionId: options[:user_payment_option_id] } if options[:user_payment_option_id] + unless options[:user_payment_option_id] + add_payment_method(post, payment, :paymentOption, options) + post[:paymentOption][:card].slice!(:cardNumber, :cardHolderName, :expirationMonth, :expirationYear, :CVV) + end + commit(:refund, post.compact) + end + + def add_stored_credentials(post, payment, options = {}) + return unless options[:stored_credential] + + post[:savePM] = options[:save_payment_method] || true + set_initiator_type(post, payment, options) + set_reason_type(post, options) + end + + def set_initiator_type(post, payment, options) + stored_credential = options[:stored_credential] + return unless stored_credential + + is_initial_transaction = stored_credential[:initial_transaction] + stored_credentials_mode = is_initial_transaction ? '0' : '1' + + if payment.is_a?(CreditCard) + post[:paymentOption] ||= {} + post[:paymentOption][:card] ||= {} + post[:paymentOption][:card][:storedCredentials] ||= {} + post[:paymentOption][:card][:storedCredentials][:storedCredentialsMode] = stored_credentials_mode + end + + post[:isRebilling] = stored_credentials_mode + end + + def add_account_funding_transaction(post, payment, options = {}) + return unless options[:is_aft] + + recipient_details = { + firstName: options[:aft_recipient_first_name], + lastName: options[:aft_recipient_last_name] + }.compact + + address_details = { + firstName: payment.first_name, + lastName: payment.last_name, + country: options.dig(:billing_address, :country), + address: options.dig(:billing_address, :address1), + city: options.dig(:billing_address, :city), + state: options.dig(:billing_address, :state) + }.compact + + post[:billingAddress].merge!(address_details) + post[:recipientDetails] = recipient_details unless recipient_details.empty? + end + + def set_reason_type(post, options) + reason_type = options[:stored_credential][:reason_type] + + case reason_type + when 'recurring' + reason_type = 'RECURRING' + when 'installment' + reason_type = 'INSTALLMENTS' + when 'unscheduled' + reason_type = 'ADDCARD' + end + + unless reason_type == 'ADDCARD' + fetch_session_token + post[:relatedTransactionId] = options[:stored_credential][:network_transaction_id] if options[:stored_credential][:network_transaction_id] + end + + post[:authenticationOnlyType] = reason_type + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). + gsub(%r(("cardNumber\\?":\\?")[^"\\]*)i, '\1[FILTERED]'). + gsub(%r(("cardCvv\\?":\\?")\d+), '\1[FILTERED]'). + gsub(%r(("merchantId\\?":\\?")\d+), '\1[FILTERED]'). + gsub(%r(("merchantSiteId\\?":\\?")\d+), '\1[FILTERED]'). + gsub(%r(("merchantKey\\?":\\?")\d+), '\1[FILTERED]'). + gsub(%r(("accountNumber\\?":\\?")\d+), '\1[FILTERED]'). + gsub(%r(("cryptogram\\?":\\?")[^"\\]*)i, '\1[FILTERED]') + end + + private + + def network_transaction_id_from(response) + response.dig('transactionId') + end + + def add_customer_ip(post, options) + return unless options[:ip] + + post[:deviceDetails] = { ipAddress: options[:ip] } + end + + def url_details(post, options) + return unless options[:notification_url] + + post[:urlDetails] = { notificationUrl: options[:notification_url] } + end + + def add_amount(post, money, options) + post[:amount] = amount(money) + post[:currency] = (options[:currency] || currency(money)) + end + + def credit_card_hash(payment) + { + cardNumber: payment.number, + cardHolderName: payment.name, + expirationMonth: format(payment.month, :two_digits), + expirationYear: format(payment.year, :four_digits), + CVV: payment.verification_value, + last4Digits: get_last_four_digits(payment.number), + selectedBrand: payment.brand + } + end + + def get_last_four_digits(number) + number[-4..-1] + end + + def add_bank_account(post, payment, options) + post[:paymentOption] = { + alternativePaymentMethod: { + paymentMethod: 'apmgw_ACH', + AccountNumber: payment.account_number, + RoutingNumber: payment.routing_number, + SECCode: options[:account_type] || 'WEB' + } + } + end + + def add_payment_method(post, payment, key, options = {}) + return post[key] = { userPaymentOptionId: options[:user_payment_option_id] } if key == :userPaymentOption + + payment_data = extract_payment_data(payment) + + case payment + when NetworkTokenizationCreditCard + add_network_tokenization_data(post, payment, payment_data) + when CreditCard + post[key] = key == :paymentOption ? { card: payment_data } : payment_data + when Check + add_bank_account(post, payment, options) + url_details(post, options) + else + post[key] = { userPaymentOptionId: payment_data } + end + end + + def extract_payment_data(payment) + if payment.is_a?(CreditCard) || payment.is_a?(NetworkTokenizationCreditCard) + credit_card_hash(payment) + else + payment + end + end + + def add_network_tokenization_data(post, payment, payment_data) + payment_data[:brand] = payment.brand.upcase + external_token = { + externalTokenProvider: NETWORK_TOKENIZATION_CARD_MAPPING[payment.source.to_s], + cryptogram: payment.payment_cryptogram, + eciProvider: payment.eci + }.compact + + payment_data.slice!(:cardNumber, :expirationMonth, :expirationYear, :last4Digits, :brand, :CVV) + post[:paymentOption] = { card: payment_data.merge(externalToken: external_token) } + end + + def add_customer_names(full_name, payment_method) + split_names(full_name).tap do |names| + names[0] = payment_method&.first_name unless names[0].present? || payment_method.is_a?(String) + names[1] = payment_method&.last_name unless names[1].present? || payment_method.is_a?(String) + end + end + + def add_3ds_global(post, options) + return unless (three_d_secure_options = options[:three_d_secure]) + + card_options = post[:paymentOption][:card] ||= {} + card_options[:threeD] = build_three_d_secure_options(three_d_secure_options, options) + end + + def build_three_d_secure_options(three_d_secure_options, options) + three_d_secure_data = { + externalMpi: { + eci: three_d_secure_options[:eci], + cavv: three_d_secure_options[:cavv], + dsTransID: three_d_secure_options[:ds_transaction_id], + challengePreference: options[:challenge_preference] + } + }.compact + + three_d_secure_data[:externalMpi][:exemptionRequestReason] = options[:exemption_request_reason] if options[:challenge_preference] == 'ExemptionRequest' + + three_d_secure_data + end + + def add_address(post, payment, options) + return unless address = options[:billing_address] || options[:address] + + first_name, last_name = add_customer_names(address[:name], payment) + + post[:billingAddress] = { + email: options[:email], + country: address[:country], + phone: options[:phone] || address[:phone], + firstName: first_name, + lastName: last_name + }.compact + end + + def add_cardholder_name_verification(post, payment, transaction_type, options) + return unless transaction_type == 'Auth' + + post[:cardHolderNameVerification] = { performNameVerification: 'true' } if options[:perform_name_verification] + + cardholder_data = { + firstName: payment.first_name, + lastName: payment.last_name + }.compact + + post[:billingAddress] ||= {} + post[:billingAddress].merge!(cardholder_data) + end + + def execute_3ds_flow(post, money, payment, transaction_type, options = {}) + post_3ds = post.dup + + MultiResponse.run do |r| + r.process { commit(:init_payment, post) } + r.process do + three_d_params = r.params.dig('paymentOption', 'card', 'threeD') + three_d_supported = three_d_params['v2supported'] == 'true' + + [true, 'true'].include?(options[:force_3d_secure]) + + next r.process { Response.new(false, '3D Secure is required but not supported') } if !three_d_supported && [true, 'true'].include?(options[:force_3d_secure]) + + if three_d_supported + add_3ds_data(post_3ds, options.merge(version: three_d_params['version'])) + post_3ds[:relatedTransactionId] = r.authorization + end + + commit(:purchase, post_3ds) + end + end + end + + def add_3ds_data(post, options = {}) + return unless options[:three_ds_2] + + three_d_secure = options[:three_ds_2] + # 01 => Challenge requested, 02 => Exemption requested, 03 or not sending parameter => No preference + challenge_preference = if [true, 'true'].include?(options[:force_3d_secure]) + '01' + elsif [false, 'false'].include?(options[:force_3d_secure]) + '02' + end + browser_info_3ds = three_d_secure[:browser_info] + payment_options = post[:paymentOption] ||= {} + card = payment_options[:card] ||= {} + card[:threeD] = { + v2AdditionalParams: { + challengeWindowSize: options[:browser_size], + challengePreference: challenge_preference + }.compact, + browserDetails: { + acceptHeader: browser_info_3ds[:accept_header], + ip: options[:ip], + javaEnabled: browser_info_3ds[:java], + javaScriptEnabled: browser_info_3ds[:javascript] || false, + language: browser_info_3ds[:language], + colorDepth: browser_info_3ds[:depth], # Possible values: 1, 4, 8, 15, 16, 24, 32, 48 + screenHeight: browser_info_3ds[:height], + screenWidth: browser_info_3ds[:width], + timeZone: browser_info_3ds[:timezone], + userAgent: browser_info_3ds[:user_agent] + }.compact, + notificationURL: (options[:notification_url] || options[:callback_url]), + merchantURL: options[:merchant_url], # The URL of the merchant's fully qualified website. + version: options[:version], # returned from initPayment + methodCompletionInd: 'U', # to indicate "unavailable". + platformType: '02' # browser instead of app-based (app-based is only for SDK implementation) + }.compact + end + + def current_timestamp + Time.now.utc.strftime('%Y%m%d%H%M%S') + end + + def build_post_data(post, options = {}) + post[:merchantId] = @options[:merchant_id] + post[:merchantSiteId] = @options[:merchant_site_id] + post[:timeStamp] = current_timestamp.to_i + post[:clientRequestId] = SecureRandom.uuid + post[:clientUniqueId] = options[:order_id] || generate_unique_id + end + + def calculate_checksum(post, action) + common_keys = %i[merchantId merchantSiteId clientRequestId] + keys = case action + when :authenticate + [:timeStamp] + when :capture, :refund, :void + %i[clientUniqueId amount currency relatedTransactionId timeStamp] + else + %i[amount currency timeStamp] + end + + to_sha = post.values_at(*common_keys.concat(keys)).push(@options[:secret_key]).join + Digest::SHA256.hexdigest(to_sha) + end + + def send_session_request(post) + post[:checksum] = calculate_checksum(post, 'authenticate') + response = parse(ssl_post(url(:authenticate), post.to_json, headers)).with_indifferent_access + expiration_time = post[:timeStamp] + @options[:session_token] = response.dig('sessionToken') + @options[:token_expires] = expiration_time + + Response.new( + response[:sessionToken].present?, + message_from(response), + response, + test: test?, + error_code: error_code_from(response) + ) + end + + def fetch_session_token(post = {}) + build_post_data(post) + send_session_request(post) + end + + def session_token_valid? + return false unless @options[:session_token] && @options[:token_expires] + + (Time.now.utc.to_i - @options[:token_expires].to_i) < 900 # 15 minutes + end + + def commit(action, post, authorization = nil, method = :post) + post[:sessionToken] = @options[:session_token] unless %i(capture refund).include?(action) + post[:checksum] = calculate_checksum(post, action) + + response = parse(ssl_request(method, url(action, authorization), post.to_json, headers)) + + Response.new( + success_from(response), + message_from(response), + response, + authorization: authorization_from(action, response, post), + network_transaction_id: network_transaction_id_from(response), + test: test?, + error_code: error_code_from(response) + ) + rescue ResponseError => e + response = parse(e.response.body) + @options[:session_token] = '' if e.response.code == '401' + + Response.new(false, message_from(response), response, test: test?) + end + + def url(action, id = nil) + "#{test? ? test_url : live_url}#{ENDPOINTS_MAPPING[action] % id}" + end + + def error_code_from(response) + response[:errCode] == 0 ? response[:gwErrorCode] : response[:errCode] + end + + def headers + { 'Content-Type' => 'application/json' }.tap do |headers| + headers['Authorization'] = "Bearer #{@options[:session_token]}" if @options[:session_token] + end + end + + def parse(body) + body = '{}' if body.blank? + + JSON.parse(body).with_indifferent_access + rescue JSON::ParserError + { + errors: body, + status: 'Unable to parse JSON response' + }.with_indifferent_access + end + + def success_from(response) + response[:status] == 'SUCCESS' && %w[APPROVED REDIRECT PENDING].include?(response[:transactionStatus]) + end + + def authorization_from(action, response, post) + if zero_auth?(post) + response.dig(:paymentOption, :userPaymentOptionId) + else + response[:transactionId] + end + end + + def zero_auth?(post) + post[:userTokenId].present? && post[:transactionType] == 'Auth' && post[:amount].to_i == 0 + end + + def message_from(response) + reason = response[:reason]&.present? ? response[:reason] : nil + response[:gwErrorReason] || reason || response[:transactionStatus] + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/ogone.rb b/lib/active_merchant/billing/gateways/ogone.rb index ef81e22b14e..e8a5a750e16 100644 --- a/lib/active_merchant/billing/gateways/ogone.rb +++ b/lib/active_merchant/billing/gateways/ogone.rb @@ -1,8 +1,9 @@ # coding: utf-8 + require 'rexml/document' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # = Ogone DirectLink Gateway # # DirectLink is the API version of the Ogone Payment Platform. It allows server to server @@ -121,12 +122,10 @@ class OgoneGateway < Gateway SUCCESS_MESSAGE = 'The transaction was successful' - THREE_D_SECURE_DISPLAY_WAYS = { :main_window => 'MAINW', # display the identification page in the main window - # (default value). - :pop_up => 'POPUP', # display the identification page in a pop-up window - # and return to the main window at the end. - :pop_ix => 'POPIX' } # display the identification page in a pop-up window - # and remain in the pop-up window. + THREE_D_SECURE_DISPLAY_WAYS = { main_window: 'MAINW', # display the identification page in the main window (default value). + + pop_up: 'POPUP', # display the identification page in a pop-up window and return to the main window at the end. + pop_ix: 'POPIX' } # display the identification page in a pop-up window and remain in the pop-up window. OGONE_NO_SIGNATURE_DEPRECATION_MESSAGE = 'Signature usage will be the default for a future release of ActiveMerchant. You should either begin using it, or update your configuration to explicitly disable it (signature_encryptor: none)' OGONE_STORE_OPTION_DEPRECATION_MESSAGE = "The 'store' option has been renamed to 'billing_id', and its usage is deprecated." @@ -134,10 +133,10 @@ class OgoneGateway < Gateway self.test_url = 'https://secure.ogone.com/ncol/test/' self.live_url = 'https://secure.ogone.com/ncol/prod/' - self.supported_countries = ['BE', 'DE', 'FR', 'NL', 'AT', 'CH'] + self.supported_countries = %w[BE DE FR NL AT CH] # also supports Airplus and UATP - self.supported_cardtypes = [:visa, :master, :american_express, :diners_club, :discover, :jcb, :maestro] - self.homepage_url = 'http://www.ogone.com/' + self.supported_cardtypes = %i[visa master american_express diners_club discover jcb maestro] + self.homepage_url = 'https://www.ingenico.com/login/ogone/' self.display_name = 'Ogone' self.default_currency = 'EUR' self.money_format = :cents @@ -150,7 +149,7 @@ def initialize(options = {}) # Verify and reserve the specified amount on the account, without actually doing the transaction. def authorize(money, payment_source, options = {}) post = {} - action = (payment_source.brand == 'mastercard') ? 'PAU' : 'RES' + action = payment_source.brand == 'mastercard' ? 'PAU' : 'RES' add_invoice(post, options) add_payment_source(post, payment_source, options) add_address(post, payment_source, options) @@ -205,7 +204,7 @@ def refund(money, reference, options = {}) perform_reference_credit(money, reference, options) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -214,7 +213,7 @@ def verify(credit_card, options={}) # Store a credit card by creating an Ogone Alias def store(payment_source, options = {}) - options.merge!(:alias_operation => 'BYPSP') unless(options.has_key?(:billing_id) || options.has_key?(:store)) + options[:alias_operation] = 'BYPSP' unless options.has_key?(:billing_id) || options.has_key?(:store) response = authorize(@options[:store_amount] || 1, payment_source, options) void(response.authorization) if response.success? response @@ -226,10 +225,10 @@ def supports_scrubbing? def scrub(transcript) transcript. - gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). - gsub(%r((&?cardno=)[^&]*)i, '\1[FILTERED]'). - gsub(%r((&?cvc=)[^&]*)i, '\1[FILTERED]'). - gsub(%r((&?pswd=)[^&]*)i, '\1[FILTERED]') + gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). + gsub(%r((&?cardno=)[^&]*)i, '\1[FILTERED]'). + gsub(%r((&?cvc=)[^&]*)i, '\1[FILTERED]'). + gsub(%r((&?pswd=)[^&]*)i, '\1[FILTERED]') end private @@ -240,6 +239,7 @@ def reference_from(authorization) def reference_transaction?(identifier) return false unless identifier.is_a?(String) + _, action = identifier.split(';') !action.nil? end @@ -263,8 +263,7 @@ def perform_non_referenced_credit(money, payment_target, options = {}) end def add_payment_source(post, payment_source, options) - add_d3d(post, options) if options[:d3d] - + add_d3d(post, options) if options[:d3d] || three_d_secure(options) if payment_source.is_a?(String) add_alias(post, payment_source, options[:alias_operation]) add_eci(post, options[:eci] || '9') @@ -285,25 +284,54 @@ def add_d3d(post, options) THREE_D_SECURE_DISPLAY_WAYS[options[:win_3ds]] : THREE_D_SECURE_DISPLAY_WAYS[:main_window] add_pair post, 'WIN3DS', win_3ds - - add_pair post, 'HTTP_ACCEPT', options[:http_accept] || '*/*' add_pair post, 'HTTP_USER_AGENT', options[:http_user_agent] if options[:http_user_agent] add_pair post, 'ACCEPTURL', options[:accept_url] if options[:accept_url] add_pair post, 'DECLINEURL', options[:decline_url] if options[:decline_url] add_pair post, 'EXCEPTIONURL', options[:exception_url] if options[:exception_url] add_pair post, 'CANCELURL', options[:cancel_url] if options[:cancel_url] - add_pair post, 'PARAMVAR', options[:paramvar] if options[:paramvar] + add_pair post, 'PARAMVAR', options[:paramvar] if options[:paramvar] add_pair post, 'PARAMPLUS', options[:paramplus] if options[:paramplus] add_pair post, 'COMPLUS', options[:complus] if options[:complus] add_pair post, 'LANGUAGE', options[:language] if options[:language] + if options[:three_ds_2] + browser_info = options[:three_ds_2][:browser_info] + ecom_postal = options[:billing_address] + if browser_info + add_pair post, 'BROWSERACCEPTHEADER', browser_info[:accept_header] + add_pair post, 'BROWSERCOLORDEPTH', browser_info[:depth] + + # for 3ds v2.1 to v2.2 add BROWSERJAVASCRIPTENABLED: This boolean indicates whether your customers have enabled JavaScript in their browsers when making a purchase. + # the following BROWSER parameters will remain mandatory unless browser_info[:javascript] = false + # her documentation https://epayments-support.ingenico.com/en/integration-solutions/integrations/directlink#directlink_integration_guides_secure_payment_with_3_d_secure + add_pair post, 'BROWSERJAVASCRIPTENABLED', browser_info[:javascript] + add_pair post, 'BROWSERJAVAENABLED', browser_info[:java] + add_pair post, 'BROWSERLANGUAGE', browser_info[:language] + add_pair post, 'BROWSERSCREENHEIGHT', browser_info[:height] + add_pair post, 'BROWSERSCREENWIDTH', browser_info[:width] + add_pair post, 'BROWSERTIMEZONE', browser_info[:timezone] + add_pair post, 'BROWSERUSERAGENT', browser_info[:user_agent] + end + # recommended + if ecom_postal + add_pair post, 'ECOM_BILLTO_POSTAL_CITY', ecom_postal[:city] + add_pair post, 'ECOM_BILLTO_POSTAL_COUNTRYCODE', ecom_postal[:country] + add_pair post, 'ECOM_BILLTO_POSTAL_STREET_LINE1', ecom_postal[:address1] + add_pair post, 'ECOM_BILLTO_POSTAL_STREET_LINE2', ecom_postal[:address2] + add_pair post, 'ECOM_BILLTO_POSTAL_POSTALCODE', ecom_postal[:zip] + end + # optional + add_pair post, 'Mpi.threeDSRequestorChallengeIndicator', options[:three_ds_reqchallengeind] + else + add_pair post, 'HTTP_ACCEPT', options[:http_accept] || '*/*' + end end def add_eci(post, eci) add_pair post, 'ECI', eci.to_s end - def add_alias(post, _alias, alias_operation = nil) - add_pair post, 'ALIAS', _alias + def add_alias(post, alias_name, alias_operation = nil) + add_pair post, 'ALIAS', alias_name add_pair post, 'ALIASOPERATION', alias_operation unless alias_operation.nil? end @@ -323,6 +351,7 @@ def add_customer_data(post, options) def add_address(post, creditcard, options) return unless options[:billing_address] + add_pair post, 'Owneraddress', options[:billing_address][:address1] add_pair post, 'OwnerZip', options[:billing_address][:zip] add_pair post, 'ownertown', options[:billing_address][:city] @@ -365,10 +394,10 @@ def commit(action, parameters) response = parse(ssl_post(url(parameters['PAYID']), post_data(action, parameters))) options = { - :authorization => [response['PAYID'], action].join(';'), - :test => test?, - :avs_result => { :code => AVS_MAPPING[response['AAVCheck']] }, - :cvv_result => CVV_MAPPING[response['CVCCheck']] + authorization: [response['PAYID'], action].join(';'), + test: test?, + avs_result: { code: AVS_MAPPING[response['AAVCheck']] }, + cvv_result: CVV_MAPPING[response['CVCCheck']] } OgoneResponse.new(successful?(response), message_from(response), response, options) end @@ -409,30 +438,31 @@ def post_data(action, parameters = {}) def add_signature(parameters) if @options[:signature].blank? - ActiveMerchant.deprecated(OGONE_NO_SIGNATURE_DEPRECATION_MESSAGE) unless(@options[:signature_encryptor] == 'none') - return + ActiveMerchant.deprecated(OGONE_NO_SIGNATURE_DEPRECATION_MESSAGE) unless @options[:signature_encryptor] == 'none' + return end - add_pair parameters, 'SHASign', calculate_signature(parameters, @options[:signature_encryptor], @options[:signature]) + add_pair parameters, 'SHASIGN', calculate_signature(parameters, @options[:signature_encryptor], @options[:signature]) end def calculate_signature(signed_parameters, algorithm, secret) return legacy_calculate_signature(signed_parameters, secret) unless algorithm - sha_encryptor = case algorithm - when 'sha256' - Digest::SHA256 - when 'sha512' - Digest::SHA512 - when 'sha1' - Digest::SHA1 - else - raise "Unknown signature algorithm #{algorithm}" - end + sha_encryptor = + case algorithm + when 'sha256' + Digest::SHA256 + when 'sha512' + Digest::SHA512 + when 'sha1' + Digest::SHA1 + else + raise "Unknown signature algorithm #{algorithm}" + end - filtered_params = signed_parameters.select{|k,v| !v.blank?} + 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('') + filtered_params.sort_by { |k, _v| k.upcase }.map { |k, v| "#{k.upcase}=#{v}#{secret}" }.join('') ).upcase end @@ -447,14 +477,14 @@ def legacy_calculate_signature(parameters, secret) PSPID Operation ALIAS - ).map{|key| parameters[key]} + + ).map { |key| parameters[key] } + [secret] ).join('') ).upcase end def add_pair(post, key, value) - post[key] = value if !value.blank? + post[key] = value unless value.nil? end def convert_attributes_to_hash(rexml_attributes) @@ -464,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/omise.rb b/lib/active_merchant/billing/gateways/omise.rb index 2d37f1dd5de..191c9ab726d 100644 --- a/lib/active_merchant/billing/gateways/omise.rb +++ b/lib/active_merchant/billing/gateways/omise.rb @@ -1,7 +1,7 @@ require 'active_merchant/billing/rails' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class OmiseGateway < Gateway API_URL = 'https://api.omise.co/' VAULT_URL = 'https://vault.omise.co/' @@ -19,15 +19,15 @@ class OmiseGateway < Gateway self.default_currency = 'THB' self.money_format = :cents - #Country supported by Omise + # Country supported by Omise # * Thailand - self.supported_countries = %w( TH JP ) + self.supported_countries = %w(TH JP) # Credit cards supported by Omise # * VISA # * MasterCard # * JCB - self.supported_cardtypes = [:visa, :master, :jcb] + self.supported_cardtypes = %i[visa master jcb] # Omise main page self.homepage_url = 'https://www.omise.co/' @@ -46,7 +46,7 @@ class OmiseGateway < Gateway # * :api_version -- Omise's API Version (OPTIONAL), default version is '2014-07-27' # See version at page https://dashboard.omise.co/api-version/edit - def initialize(options={}) + def initialize(options = {}) requires!(options, :public_key, :secret_key) @public_key = options[:public_key] @secret_key = options[:secret_key] @@ -79,7 +79,7 @@ def initialize(options={}) # # purchase(money, nil, { :customer_id => customer_id }) - def purchase(money, payment_method, options={}) + def purchase(money, payment_method, options = {}) create_charge(money, payment_method, options) end @@ -91,7 +91,7 @@ def purchase(money, payment_method, options={}) # * payment_method -- The CreditCard object # * options -- An optional parameters, such as token or capture - def authorize(money, payment_method, options={}) + def authorize(money, payment_method, options = {}) options[:capture] = 'false' create_charge(money, payment_method, options) end @@ -104,7 +104,7 @@ def authorize(money, payment_method, options={}) # * charge_id -- The CreditCard object # * options -- An optional parameters, such as token or capture - def capture(money, charge_id, options={}) + def capture(money, charge_id, options = {}) post = {} add_amount(post, money, options) commit(:post, "charges/#{CGI.escape(charge_id)}/capture", post, options) @@ -118,7 +118,7 @@ def capture(money, charge_id, options={}) # * charge_id -- The CreditCard object # * options -- An optional parameters, such as token or capture - def refund(money, charge_id, options={}) + def refund(money, charge_id, options = {}) options[:amount] = money if money commit(:post, "charges/#{CGI.escape(charge_id)}/refunds", options) end @@ -132,7 +132,7 @@ def refund(money, charge_id, options={}) # 'email' (A customer email) # 'description' (A customer description) - def store(payment_method, options={}) + def store(payment_method, options = {}) post, card_params = {}, {} add_customer_data(post, options) add_token(card_params, payment_method, options) @@ -145,7 +145,7 @@ def store(payment_method, options={}) # # * customer_id -- The Customer identifier (REQUIRED). - def unstore(customer_id, options={}) + def unstore(customer_id, options = {}) commit(:delete, "customers/#{CGI.escape(customer_id)}") end @@ -164,7 +164,7 @@ def scrub(transcript) transcript. gsub(/(Authorization: Basic )\w+/i, '\1[FILTERED]'). gsub(/(\\"number\\":)\\"\d+\\"/, '\1[FILTERED]'). - gsub(/(\\"security_code\\":)\\"\d+\\"/,'\1[FILTERED]') + gsub(/(\\"security_code\\":)\\"\d+\\"/, '\1[FILTERED]') end private @@ -178,7 +178,7 @@ def create_charge(money, payment_method, options) commit(:post, 'charges', post, options) end - def headers(options={}) + def headers(options = {}) key = options[:key] || @secret_key { 'Content-Type' => 'application/json;utf-8', @@ -197,7 +197,7 @@ def post_data(parameters) parameters.present? ? parameters.to_json : nil end - def https_request(method, endpoint, parameters=nil, options={}) + def https_request(method, endpoint, parameters = nil, options = {}) raw_response = response = nil begin raw_response = ssl_request(method, url_for(endpoint), post_data(parameters), headers(options)) @@ -221,7 +221,7 @@ def json_error(raw_response) { message: msg } end - def commit(method, endpoint, params=nil, options={}) + def commit(method, endpoint, params = nil, options = {}) response = https_request(method, endpoint, params, options) Response.new( successful?(response), @@ -246,16 +246,16 @@ def error_code_from(response) def message_to_standard_error_code_from(response) message = response['message'] if response['code'] == 'invalid_card' case message - when /brand not supported/ - STANDARD_ERROR_CODE[:invalid_number] - when /number is invalid/ - STANDARD_ERROR_CODE[:incorrect_number] - when /expiration date cannot be in the past/ - STANDARD_ERROR_CODE[:expired_card] - when /expiration \w+ is invalid/ - STANDARD_ERROR_CODE[:invalid_expiry_date] - else - STANDARD_ERROR_CODE[:processing_error] + when /brand not supported/ + STANDARD_ERROR_CODE[:invalid_number] + when /number is invalid/ + STANDARD_ERROR_CODE[:incorrect_number] + when /expiration date cannot be in the past/ + STANDARD_ERROR_CODE[:expired_card] + when /expiration \w+ is invalid/ + STANDARD_ERROR_CODE[:invalid_expiry_date] + else + STANDARD_ERROR_CODE[:processing_error] end end @@ -284,7 +284,7 @@ def get_token(post, credit_card) commit(:post, 'tokens', post, { key: @public_key }) end - def add_token(post, credit_card, options={}) + def add_token(post, credit_card, options = {}) if options[:token_id].present? post[:card] = options[:token_id] else @@ -304,11 +304,11 @@ def add_creditcard(post, payment_method) post[:card] = card end - def add_customer(post, options={}) + def add_customer(post, options = {}) post[:customer] = options[:customer_id] if options[:customer_id] end - def add_customer_data(post, options={}) + def add_customer_data(post, options = {}) post[:description] = options[:description] if options[:description] post[:email] = options[:email] if options[:email] end @@ -318,7 +318,6 @@ def add_amount(post, money, options) post[:currency] = (options[:currency] || currency(money)) post[:description] = options[:description] if options.key?(:description) end - end end end diff --git a/lib/active_merchant/billing/gateways/openpay.rb b/lib/active_merchant/billing/gateways/openpay.rb index 42db1200b97..90891ddbccf 100644 --- a/lib/active_merchant/billing/gateways/openpay.rb +++ b/lib/active_merchant/billing/gateways/openpay.rb @@ -1,11 +1,18 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class OpenpayGateway < Gateway - self.live_url = 'https://api.openpay.mx/v1/' - self.test_url = 'https://sandbox-api.openpay.mx/v1/' - - self.supported_countries = ['MX'] - self.supported_cardtypes = [:visa, :master, :american_express] + class_attribute :mx_live_url, :mx_test_url + class_attribute :co_live_url, :co_test_url + + self.co_live_url = 'https://api.openpay.co/v1/' + self.co_test_url = 'https://sandbox-api.openpay.co/v1/' + self.mx_live_url = 'https://api.openpay.mx/v1/' + self.mx_test_url = 'https://sandbox-api.openpay.mx/v1/' + self.live_url = self.co_live_url + self.test_url = self.co_test_url + + self.supported_countries = %w(CO MX) + self.supported_cardtypes = %i[visa master american_express carnet] self.homepage_url = 'http://www.openpay.mx/' self.display_name = 'Openpay' self.default_currency = 'MXN' @@ -24,6 +31,16 @@ def initialize(options = {}) super end + def gateway_url(options = {}) + country = options[:merchant_country] || @options[:merchant_country] + + if country == 'MX' + test? ? mx_test_url : mx_live_url + else + test? ? co_test_url : co_live_url + end + end + def purchase(money, creditcard, options = {}) post = create_post_for_auth_or_purchase(money, creditcard, options) commit(:post, 'charges', post, options) @@ -75,7 +92,7 @@ def store(creditcard, options = {}) MultiResponse.run(:first) do |r| r.process { commit(:post, 'customers', post, options) } - if(r.success? && !r.params['id'].blank?) + if r.success? && !r.params['id'].blank? customer_id = r.params['id'] r.process { commit(:post, "customers/#{customer_id}/cards", card, options) } end @@ -104,6 +121,7 @@ def scrub(transcript) gsub(%r((cvv2\\?":\\?")\\?"), '\1[BLANK]"'). gsub(%r((cvv2\\?":\\?")\s+), '\1[BLANK]') end + private def create_post_for_auth_or_purchase(money, creditcard, options) @@ -115,7 +133,7 @@ def create_post_for_auth_or_purchase(money, creditcard, options) post[:device_session_id] = options[:device_session_id] post[:currency] = (options[:currency] || currency(money)).upcase post[:use_card_points] = options[:use_card_points] if options[:use_card_points] - post[:payment_plan] = {payments: options[:payments]} if options[:payments] + post[:payment_plan] = { payments: options[:payments] } if options[:payments] add_creditcard(post, creditcard, options) post end @@ -126,8 +144,8 @@ def add_creditcard(post, creditcard, options) elsif creditcard.respond_to?(:number) card = { card_number: creditcard.number, - expiration_month: "#{sprintf("%02d", creditcard.month)}", - expiration_year: "#{"#{creditcard.year}"[-2, 2]}", + expiration_month: sprintf('%02d', creditcard.month), + expiration_year: creditcard.year.to_s[-2, 2], cvv2: creditcard.verification_value, holder_name: creditcard.name } @@ -150,6 +168,7 @@ def add_customer_data(post, creditcard, options) def add_address(card, options) return unless card.kind_of?(Hash) + if address = (options[:billing_address] || options[:address]) card[:address] = { line1: address[:address1], @@ -174,6 +193,7 @@ def headers(options = {}) def parse(body) return {} unless body + JSON.parse(body) end @@ -181,16 +201,17 @@ def commit(method, resource, parameters, options = {}) response = http_request(method, resource, parameters, options) success = !error?(response) - Response.new(success, + Response.new( + success, (success ? response['error_code'] : response['description']), response, - :test => test?, - :authorization => response['id'] + test: test?, + authorization: response['id'] ) end - def http_request(method, resource, parameters={}, options={}) - url = (test? ? self.test_url : self.live_url) + @merchant_id + '/' + resource + def http_request(method, resource, parameters = {}, options = {}) + url = gateway_url(options) + @merchant_id + '/' + resource raw_response = nil begin raw_response = ssl_request(method, url, (parameters ? parameters.to_json : nil), headers(options)) @@ -204,24 +225,22 @@ def http_request(method, resource, parameters={}, options={}) end def error?(response) - response.key?('error_code') + response['error_code'] && !response['error_code'].blank? end def response_error(raw_response) - begin - parse(raw_response) - rescue JSON::ParserError - json_error(raw_response) - end + parse(raw_response) + rescue JSON::ParserError + json_error(raw_response) end def json_error(raw_response) msg = 'Invalid response received from the Openpay API. Please contact soporte@openpay.mx if you continue to receive this message.' msg += " (The raw response returned by the API was #{raw_response.inspect})" { - 'category' => 'request', - 'error_code' => '9999', - 'description' => msg + 'category' => 'request', + 'error_code' => '9999', + 'description' => msg } end end diff --git a/lib/active_merchant/billing/gateways/opp.rb b/lib/active_merchant/billing/gateways/opp.rb index f685d70ee9b..6a9371263d4 100644 --- a/lib/active_merchant/billing/gateways/opp.rb +++ b/lib/active_merchant/billing/gateways/opp.rb @@ -1,174 +1,186 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class OppGateway < Gateway - # = Open Payment Platform - # - # The Open Payment Platform includes a powerful omni-channel transaction processing API, - # enabling you to quickly and flexibly build new applications and services on the platform. - # - # This plugin enables connectivity to the Open Payment Platform for activemerchant. - # - # For any questions or comments please contact support@payon.com - # - # == Usage - # - # gateway = ActiveMerchant::Billing::OppGateway.new( - # user_id: 'merchant user id', - # password: 'password', - # entity_id: 'entity id', - # ) - # - # # set up credit card object as in main ActiveMerchant example - # creditcard = ActiveMerchant::Billing::CreditCard.new( - # :type => 'visa', - # :number => '4242424242424242', - # :month => 8, - # :year => 2009, - # :first_name => 'Bob', - # :last_name => 'Bobsen' - # :verification_value: '123') - # - # # Request: complete example, including address, billing address, shipping address - # complete_request_options = { - # order_id: "your merchant/shop order id", # alternative is to set merchantInvoiceId - # merchant_transaction_id: "your merchant/shop transaction id", - # address: address, - # description: 'Store Purchase - Books', - # risk_workflow: false, - # test_mode: 'EXTERNAL' # or 'INTERNAL', valid only for test system - # create_registration: false, # payment details will be stored on the server an latter can be referenced - # - # billing_address: { - # address1: '123 Test Street', - # city: 'Test', - # state: 'TE', - # zip: 'AB12CD', - # country: 'GB', - # }, - # shipping_address: { - # name: 'Muton DeMicelis', - # address1: 'My Street On Upiter, Apt 3.14/2.78', - # city: 'Munich', - # state: 'Bov', - # zip: '81675', - # country: 'DE', - # }, - # customer: { - # merchant_customer_id: "your merchant/customer id", - # givenname: 'Jane', - # surname: 'Jones', - # birth_date: '1965-05-01', - # phone: '(?!?)555-5555', - # mobile: '(?!?)234-23423', - # email: 'jane@jones.com', - # company_name: 'JJ Ltd.', - # identification_doctype: 'PASSPORT', - # identification_docid: 'FakeID2342431234123', - # ip: 101.102.103.104, - # }, - # } - # - # # Request: minimal example - # minimal_request_options = { - # order_id: "your merchant/shop order id", # alternative is to set merchantInvoiceId - # description: 'Store Purchase - Books', - # } - # - # options = - # # run request - # response = gateway.purchase(754, creditcard, options) # charge 7,54 EUR - # - # response.success? # Check whether the transaction was successful - # response.error_code # Retrieve the error message - it's mapped to Gateway::STANDARD_ERROR_CODE - # response.message # Retrieve the message returned by opp - # response.authorization # Retrieve the unique transaction ID returned by opp - # response.params['result']['code'] # Retrieve original return code returned by opp server - # - # == Errors - # If transaction is not successful, response.error_code contains mapped to Gateway::STANDARD_ERROR_CODE error message. - # Complete list of opp error codes can be viewed on https://docs.oppwa.com/ - # Because this list is much bigger than Gateway::STANDARD_ERROR_CODE, only fraction is mapped to Gateway::STANDARD_ERROR_CODE. - # All other codes are mapped as Gateway::STANDARD_ERROR_CODE[:processing_error], so if this is the case, - # you may check the original result code from OPP that can be found in response.params['result']['code'] - # - # == Special features - # For purchase method risk check can be forced when options[:risk_workflow] = true - # This will split (on OPP server side) the transaction into two separate transactions: authorize and capture, - # but capture will be executed only if risk checks are successful. - # - # For testing you may use the test account details listed fixtures.yml under opp. It is important to note that there are two test modes available: - # options[:test_mode]='EXTERNAL' causes test transactions to be forwarded to the processor's test system for 'end-to-end' testing - # options[:test_mode]='INTERNAL' causes transactions to be sent to opp simulators, which is useful when switching to the live endpoint for connectivity testing. - # If no test_mode parameter is sent, test_mode=INTERNAL is the default behaviour. - # - # Billing Address, Shipping Address, Custom Parameters are supported as described under https://docs.oppwa.com/parameters - # See complete example above for details. - # - # == Tokenization - # When create_registration is set to true, the payment details will be stored and a token will be returned in registrationId response field, - # which can subsequently be used to reference the stored payment. + # = Open Payment Platform + # + # The Open Payment Platform includes a powerful omni-channel transaction processing API, + # enabling you to quickly and flexibly build new applications and services on the platform. + # + # This plugin enables connectivity to the Open Payment Platform for activemerchant. + # + # For any questions or comments please contact support@payon.com + # + # == Usage + # + # gateway = ActiveMerchant::Billing::OppGateway.new( + # access_token: 'access_token', + # entity_id: 'entity id', + # ) + # + # # set up credit card object as in main ActiveMerchant example + # creditcard = ActiveMerchant::Billing::CreditCard.new( + # :type => 'visa', + # :number => '4242424242424242', + # :month => 8, + # :year => 2009, + # :first_name => 'Bob', + # :last_name => 'Bobsen' + # :verification_value: '123') + # + # # Request: complete example, including address, billing address, shipping address + # complete_request_options = { + # order_id: "your merchant/shop order id", # alternative is to set merchantInvoiceId + # merchant_transaction_id: "your merchant/shop transaction id", + # address: address, + # description: 'Store Purchase - Books', + # risk_workflow: false, + # test_mode: 'EXTERNAL' # or 'INTERNAL', valid only for test system + # create_registration: false, # payment details will be stored on the server an latter can be referenced + # + # billing_address: { + # address1: '123 Test Street', + # city: 'Test', + # state: 'TE', + # zip: 'AB12CD', + # country: 'GB', + # }, + # shipping_address: { + # name: 'Muton DeMicelis', + # address1: 'My Street On Upiter, Apt 3.14/2.78', + # city: 'Munich', + # state: 'Bov', + # zip: '81675', + # country: 'DE', + # }, + # customer: { + # merchant_customer_id: "your merchant/customer id", + # givenname: 'Jane', + # surname: 'Jones', + # birth_date: '1965-05-01', + # phone: '(?!?)555-5555', + # mobile: '(?!?)234-23423', + # email: 'jane@jones.com', + # company_name: 'JJ Ltd.', + # identification_doctype: 'PASSPORT', + # identification_docid: 'FakeID2342431234123', + # ip: 101.102.103.104, + # }, + # } + # + # # Request: minimal example + # minimal_request_options = { + # order_id: "your merchant/shop order id", # alternative is to set merchantInvoiceId + # description: 'Store Purchase - Books', + # } + # + # options = + # # run request + # response = gateway.purchase(754, creditcard, options) # charge 7,54 EUR + # + # response.success? # Check whether the transaction was successful + # response.error_code # Retrieve the error message - it's mapped to Gateway::STANDARD_ERROR_CODE + # response.message # Retrieve the message returned by opp + # response.authorization # Retrieve the unique transaction ID returned by opp + # response.params['result']['code'] # Retrieve original return code returned by opp server + # + # == Errors + # If transaction is not successful, response.error_code contains mapped to Gateway::STANDARD_ERROR_CODE error message. + # Complete list of opp error codes can be viewed on https://docs.oppwa.com/ + # Because this list is much bigger than Gateway::STANDARD_ERROR_CODE, only fraction is mapped to Gateway::STANDARD_ERROR_CODE. + # All other codes are mapped as Gateway::STANDARD_ERROR_CODE[:processing_error], so if this is the case, + # you may check the original result code from OPP that can be found in response.params['result']['code'] + # + # == Special features + # For purchase method risk check can be forced when options[:risk_workflow] = true + # This will split (on OPP server side) the transaction into two separate transactions: authorize and capture, + # but capture will be executed only if risk checks are successful. + # + # For testing you may use the test account details listed fixtures.yml under opp. It is important to note that there are two test modes available: + # options[:test_mode]='EXTERNAL' causes test transactions to be forwarded to the processor's test system for 'end-to-end' testing + # options[:test_mode]='INTERNAL' causes transactions to be sent to opp simulators, which is useful when switching to the live endpoint for connectivity testing. + # If no test_mode parameter is sent, test_mode=INTERNAL is the default behaviour. + # + # Billing Address, Shipping Address, Custom Parameters are supported as described under https://docs.oppwa.com/parameters + # See complete example above for details. + # + # == Tokenization + # When create_registration is set to true, the payment details will be stored and a token will be returned in registrationId response field, + # which can subsequently be used to reference the stored payment. self.test_url = 'https://test.oppwa.com/v1/payments' self.live_url = 'https://oppwa.com/v1/payments' self.supported_countries = %w(AD AI AG AR AU AT BS BB BE BZ BM BR BN BG CA HR CY CZ DK DM EE FI FR DE GR GD GY HK HU IS IN IL IT JP LV LI LT LU MY MT MX MC MS NL PA PL PT KN LC MF VC SM SG SK SI ZA ES SR SE CH TR GB US UY) self.default_currency = 'EUR' - self.supported_cardtypes = [:visa, :master, :american_express, :diners_club, :discover, :jcb, :maestro, :dankort] + self.supported_cardtypes = %i[visa master american_express diners_club discover jcb maestro dankort] self.homepage_url = 'https://docs.oppwa.com' self.display_name = 'Open Payment Platform' - def initialize(options={}) - requires!(options, :user_id, :password, :entity_id) + def initialize(options = {}) + requires!(options, :access_token, :entity_id) super end - def purchase(money, payment, options={}) + def purchase(money, payment, options = {}) # debit - execute_dbpa(options[:risk_workflow] ? 'PA.CP': 'DB', - money, payment, options) + options[:registrationId] = payment if payment.is_a?(String) + execute_dbpa(options[:risk_workflow] ? 'PA.CP' : 'DB', money, payment, options) end - def authorize(money, payment, options={}) + def authorize(money, payment, options = {}) # preauthorization PA execute_dbpa('PA', money, payment, options) end - def capture(money, authorization, options={}) + def capture(money, authorization, options = {}) # capture CP execute_referencing('CP', money, authorization, options) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) # refund RF execute_referencing('RF', money, authorization, options) end - def void(authorization, options={}) + def void(authorization, options = {}) # reversal RV execute_referencing('RV', nil, authorization, options) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } end end + def store(credit_card, options = {}) + execute_store(credit_card, options.merge(store: true)) + end + def supports_scrubbing? true end def scrub(transcript) transcript. - gsub(%r((authentication\.password=)\w+), '\1[FILTERED]'). + gsub(%r((Authorization: Bearer )\w+)i, '\1[FILTERED]'). gsub(%r((card\.number=)\d+), '\1[FILTERED]'). gsub(%r((card\.cvv=)\d+), '\1[FILTERED]') end private + def execute_store(payment, options) + post = {} + add_payment_method(post, payment, options) + add_address(post, options) + add_options(post, options) + add_3d_secure(post, options) + commit(post, nil, options) + end + def execute_dbpa(txtype, money, payment, options) post = {} post[:paymentType] = txtype @@ -189,7 +201,7 @@ def execute_referencing(txtype, money, authorization, options) end def add_authentication(post) - post[:authentication] = { entityId: @options[:entity_id], password: @options[:password], userId: @options[:user_id]} + post[:authentication] = { entityId: @options[:entity_id] } end def add_customer_data(post, payment, options) @@ -230,7 +242,7 @@ def address(post, address, prefix) city: address[:city], state: address[:state], postcode: address[:zip], - country: address[:country], + country: address[:country] } end @@ -243,10 +255,11 @@ def add_invoice(post, money, options) end def add_payment_method(post, payment, options) + return if payment.is_a?(String) + if options[:registrationId] - #post[:recurringType] = 'REPEATED' post[:card] = { - cvv: payment.verification_value, + cvv: payment.verification_value } else post[:paymentBrand] = payment.brand.upcase @@ -255,7 +268,7 @@ def add_payment_method(post, payment, options) number: payment.number, expiryMonth: '%02d' % payment.month, expiryYear: payment.year, - cvv: payment.verification_value, + cvv: payment.verification_value } end end @@ -273,13 +286,15 @@ def add_3d_secure(post, options) def add_options(post, options) post[:createRegistration] = options[:create_registration] if options[:create_registration] && !options[:registrationId] post[:testMode] = options[:test_mode] if test? && options[:test_mode] - options.each {|key, value| post[key] = value if key.to_s.match('customParameters\[[a-zA-Z0-9\._]{3,64}\]') } + options.each { |key, value| post[key] = value if key.to_s =~ /'customParameters\[[a-zA-Z0-9\._]{3,64}\]'/ } post['customParameters[SHOPPER_pluginId]'] = 'activemerchant' post['customParameters[custom_disable3DSecure]'] = options[:disable_3d_secure] if options[:disable_3d_secure] end def build_url(url, authorization, options) - if options[:registrationId] + if options[:store] + url.gsub(/payments/, 'registrations') + elsif options[:registrationId] "#{url.gsub(/payments/, 'registrations')}/#{options[:registrationId]}/payments" elsif authorization "#{url}/#{authorization}" @@ -293,17 +308,18 @@ def commit(post, authorization, options) add_authentication(post) post = flatten_hash(post) - response = begin - parse( - ssl_post( - url, - post.collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&'), - 'Content-Type' => 'application/x-www-form-urlencoded;charset=UTF-8' + response = + begin + parse( + ssl_post( + url, + post.collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&'), + headers + ) ) - ) - rescue ResponseError => e - parse(e.response.body) - end + rescue ResponseError => e + parse(e.response.body) + end success = success_from(response) @@ -313,21 +329,26 @@ def commit(post, authorization, options) response, authorization: authorization_from(response), test: test?, - error_code: success ? nil : error_code_from(response), + error_code: success ? nil : error_code_from(response) ) end + def headers + { + 'Content-Type' => 'application/x-www-form-urlencoded;charset=UTF-8', + 'Authorization' => "Bearer #{@options[:access_token]}" + } + end + def parse(body) - begin - JSON.parse(body) - rescue JSON::ParserError - json_error(body) - end + JSON.parse(body) + rescue JSON::ParserError + json_error(body) end def json_error(body) message = "Invalid response received #{body.inspect}" - { 'result' => {'description' => message, 'code' => 'unknown' } } + { 'result' => { 'description' => message, 'code' => 'unknown' } } end def success_from(response) @@ -335,7 +356,7 @@ def success_from(response) success_regex = /^(000\.000\.|000\.100\.1|000\.[36])/ - if success_regex =~ response['result']['code'] + if success_regex.match?(response['result']['code']) true else false diff --git a/lib/active_merchant/billing/gateways/optimal_payment.rb b/lib/active_merchant/billing/gateways/optimal_payment.rb index 2bf3b323f97..fa23c8183ae 100644 --- a/lib/active_merchant/billing/gateways/optimal_payment.rb +++ b/lib/active_merchant/billing/gateways/optimal_payment.rb @@ -1,14 +1,16 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class OptimalPaymentGateway < Gateway self.test_url = 'https://webservices.test.optimalpayments.com/creditcardWS/CreditCardServlet/v1' self.live_url = 'https://webservices.optimalpayments.com/creditcardWS/CreditCardServlet/v1' # The countries the gateway supports merchants from as 2 digit ISO country codes - self.supported_countries = ['CA', 'US', 'GB'] + self.supported_countries = %w[CA US GB AU AT BE BG HR CY CZ DK + EE FI DE GR HU IE IT LV LT LU MT + NL NO PL PT RO SK SI ES SE CH] # The card types supported by the payment gateway - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :solo] # :switch? + self.supported_cardtypes = %i[visa master american_express discover diners_club] # The homepage URL of the gateway self.homepage_url = 'http://www.optimalpayments.com/' @@ -17,13 +19,12 @@ class OptimalPaymentGateway < Gateway self.display_name = 'Optimal Payments' def initialize(options = {}) - - if(options[:login]) + if options[:login] ActiveMerchant.deprecated("The 'login' option is deprecated in favor of 'store_id' and will be removed in a future version.") options[:store_id] = options[:login] end - if(options[:account]) + if options[:account] ActiveMerchant.deprecated("The 'account' option is deprecated in favor of 'account_number' and will be removed in a future version.") options[:account_number] = options[:account] end @@ -59,6 +60,15 @@ def capture(money, authorization, options = {}) commit('ccSettlement', money, options) end + def verify(credit_card, options = {}) + parse_card_or_auth(credit_card, options) + commit('ccVerification', 0, options) + end + + def store(credit_card, options = {}) + verify(credit_card, options) + end + def supports_scrubbing? true end @@ -89,32 +99,36 @@ def parse(body) def commit(action, money, post) post[:order_id] ||= 'order_id' - xml = case action - when 'ccAuthorize', 'ccPurchase', 'ccVerification' - cc_auth_request(money, post) - when 'ccCredit', 'ccSettlement' - cc_post_auth_request(money, post) - when 'ccStoredDataAuthorize', 'ccStoredDataPurchase' - cc_stored_data_request(money, post) - when 'ccAuthorizeReversal' - cc_auth_reversal_request(post) - #when 'ccCancelSettle', 'ccCancelCredit', 'ccCancelPayment' - # cc_cancel_request(money, post) - #when 'ccPayment' - # cc_payment_request(money, post) - #when 'ccAuthenticate' - # cc_authenticate_request(money, post) - else - raise 'Unknown Action' - end + xml = + case action + when 'ccAuthorize', 'ccPurchase', 'ccVerification' + cc_auth_request(money, post) + when 'ccCredit', 'ccSettlement' + cc_post_auth_request(money, post) + when 'ccStoredDataAuthorize', 'ccStoredDataPurchase' + cc_stored_data_request(money, post) + when 'ccAuthorizeReversal' + cc_auth_reversal_request(post) + # when 'ccCancelSettle', 'ccCancelCredit', 'ccCancelPayment' + # cc_cancel_request(money, post) + # when 'ccPayment' + # cc_payment_request(money, post) + # when 'ccAuthenticate' + # cc_authenticate_request(money, post) + else + raise 'Unknown Action' + end 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), - :test => test?, - :authorization => authorization_from(response), - :avs_result => { :code => avs_result_from(response) }, - :cvv_result => cvv_result_from(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) ) end @@ -129,9 +143,7 @@ def successful?(response) def message_from(response) REXML::XPath.each(response, '//detail') do |detail| - if detail.is_a?(REXML::Element) && detail.elements['tag'].text == 'InternalResponseDescription' - return detail.elements['value'].text - end + return detail.elements['value'].text if detail.is_a?(REXML::Element) && detail.elements['tag'].text == 'InternalResponseDescription' end nil end @@ -153,13 +165,13 @@ def hash_from_xml(response) %w(confirmationNumber authCode decision code description actionCode avsResponse cvdResponse - txnTime duplicateFound - ).each do |tag| + txnTime duplicateFound).each do |tag| node = REXML::XPath.first(response, "//#{tag}") hsh[tag] = node.text if node end REXML::XPath.each(response, '//detail') do |detail| next unless detail.is_a?(REXML::Element) + tag = detail.elements['tag'].text value = detail.elements['value'].text hsh[tag] = value @@ -168,7 +180,7 @@ def hash_from_xml(response) end def xml_document(root_tag) - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 xml.tag!(root_tag, schema) do yield xml end @@ -177,14 +189,14 @@ def xml_document(root_tag) def get_text_from_document(document, node) node = REXML::XPath.first(document, node) - node && node.text + node&.text end def cc_auth_request(money, opts) xml_document('ccAuthRequestV1') do |xml| build_merchant_account(xml) xml.merchantRefNum opts[:order_id] - xml.amount(money/100.0) + xml.amount(money / 100.0) build_card(xml, opts) build_billing_details(xml, opts) build_shipping_details(xml, opts) @@ -205,7 +217,7 @@ def cc_post_auth_request(money, opts) build_merchant_account(xml) xml.confirmationNumber opts[:confirmationNumber] xml.merchantRefNum opts[:order_id] - xml.amount(money/100.0) + xml.amount(money / 100.0) end end @@ -214,7 +226,7 @@ def cc_stored_data_request(money, opts) build_merchant_account(xml) xml.merchantRefNum opts[:order_id] xml.confirmationNumber opts[:confirmationNumber] - xml.amount(money/100.0) + xml.amount(money / 100.0) end end @@ -248,33 +260,32 @@ def cc_stored_data_request(money, opts) def schema { 'xmlns' => 'http://www.optimalpayments.com/creditcard/xmlschema/v1', 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', - 'xsi:schemaLocation' => 'http://www.optimalpayments.com/creditcard/xmlschema/v1' - } + 'xsi:schemaLocation' => 'http://www.optimalpayments.com/creditcard/xmlschema/v1' } end def build_merchant_account(xml) xml.tag! 'merchantAccount' do - xml.tag! 'accountNum' , @options[:account_number] - xml.tag! 'storeID' , @options[:store_id] - xml.tag! 'storePwd' , @options[:password] + xml.tag! 'accountNum', @options[:account_number] + xml.tag! 'storeID', @options[:store_id] + xml.tag! 'storePwd', @options[:password] end end def build_card(xml, opts) xml.tag! 'card' do - xml.tag! 'cardNum' , @credit_card.number + xml.tag! 'cardNum', @credit_card.number xml.tag! 'cardExpiry' do - xml.tag! 'month' , @credit_card.month - xml.tag! 'year' , @credit_card.year + xml.tag! 'month', @credit_card.month + xml.tag! 'year', @credit_card.year end if brand = card_type(@credit_card.brand) - xml.tag! 'cardType' , brand + xml.tag! 'cardType', brand end if @credit_card.verification_value? - xml.tag! 'cvdIndicator' , '1' # Value Provided - xml.tag! 'cvd' , @credit_card.verification_value + xml.tag! 'cvdIndicator', '1' # Value Provided + xml.tag! 'cvd', @credit_card.verification_value else - xml.tag! 'cvdIndicator' , '0' + xml.tag! 'cvdIndicator', '0' end end end @@ -298,31 +309,27 @@ def build_address(xml, addr) if addr[:name] first_name, last_name = split_names(addr[:name]) xml.tag! 'firstName', first_name - xml.tag! 'lastName' , last_name + xml.tag! 'lastName', last_name end - xml.tag! 'street' , addr[:address1] if addr[:address1].present? + xml.tag! 'street', addr[:address1] if addr[:address1].present? xml.tag! 'street2', addr[:address2] if addr[:address2].present? - xml.tag! 'city' , addr[:city] if addr[:city].present? + xml.tag! 'city', addr[:city] if addr[:city].present? if addr[:state].present? state_tag = %w(US CA).include?(addr[:country]) ? 'state' : 'region' xml.tag! state_tag, addr[:state] end - xml.tag! 'country', addr[:country] if addr[:country].present? - xml.tag! 'zip' , addr[:zip] if addr[:zip].present? - xml.tag! 'phone' , addr[:phone] if addr[:phone].present? + xml.tag! 'country', addr[:country] if addr[:country].present? + xml.tag! 'zip', addr[:zip] if addr[:zip].present? + xml.tag! 'phone', addr[:phone] if addr[:phone].present? end def card_type(key) { 'visa' => 'VI', 'master' => 'MC', - 'american_express'=> 'AM', + 'american_express' => 'AM', 'discover' => 'DI', - 'diners_club' => 'DC', - #'switch' => '', - 'solo' => 'SO' - }[key] + 'diners_club' => 'DC' }[key] end - end end end diff --git a/lib/active_merchant/billing/gateways/orbital.rb b/lib/active_merchant/billing/gateways/orbital.rb index 27402de05be..78f20448131 100644 --- a/lib/active_merchant/billing/gateways/orbital.rb +++ b/lib/active_merchant/billing/gateways/orbital.rb @@ -1,8 +1,8 @@ require 'active_merchant/billing/gateways/orbital/orbital_soft_descriptors' require 'rexml/document' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # For more information on Orbital, visit the {integration center}[http://download.chasepaymentech.com] # # ==== Authentication Options @@ -30,11 +30,11 @@ module Billing #:nodoc: class OrbitalGateway < Gateway include Empty - API_VERSION = '7.1' + API_VERSION = '9.5' POST_HEADERS = { 'MIME-Version' => '1.1', - 'Content-Type' => "application/PTI#{API_VERSION.gsub(/\./, '')}", + 'Content-Type' => "application/PTI#{API_VERSION.delete('.')}", 'Content-transfer-encoding' => 'text', 'Request-number' => '1', 'Document-type' => 'Request', @@ -42,6 +42,7 @@ class OrbitalGateway < Gateway } SUCCESS = '0' + APPROVAL_SUCCESS = '1' APPROVED = [ '00', # Approved @@ -60,7 +61,8 @@ class OrbitalGateway < Gateway '93', # Approved high fraud '94', # Approved fraud service unavailable 'E7', # Stored - 'PA' # Partial approval + 'PA', # Partial approval + 'P1' # ECP - AVS - Account Status Verification and/or AOA data is in a positive status. ] class_attribute :secondary_test_url, :secondary_live_url @@ -71,16 +73,16 @@ class OrbitalGateway < Gateway self.live_url = 'https://orbital1.chasepaymentech.com/authorize' self.secondary_live_url = 'https://orbital2.chasepaymentech.com/authorize' - self.supported_countries = ['US', 'CA'] + self.supported_countries = %w[US CA] self.default_currency = 'CAD' - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb] + self.supported_cardtypes = %i[visa master american_express discover diners_club jcb] self.display_name = 'Orbital Paymentech' self.homepage_url = 'http://chasepaymentech.com/' self.money_format = :cents - AVS_SUPPORTED_COUNTRIES = ['US', 'CA', 'UK', 'GB'] + AVS_SUPPORTED_COUNTRIES = %w[US CA UK GB] CURRENCY_CODES = { 'AUD' => '036', @@ -91,11 +93,13 @@ class OrbitalGateway < Gateway 'DKK' => '208', 'HKD' => '344', 'ICK' => '352', + 'INR' => '356', 'JPY' => '392', 'MXN' => '484', 'NZD' => '554', 'NOK' => '578', 'SGD' => '702', + 'ZAR' => '710', 'SEK' => '752', 'CHF' => '756', 'GBP' => '826', @@ -112,11 +116,13 @@ class OrbitalGateway < Gateway 'DKK' => '2', 'HKD' => '2', 'ICK' => '2', + 'INR' => '2', 'JPY' => '0', 'MXN' => '2', 'NZD' => '2', 'NOK' => '2', 'SGD' => '2', + 'ZAR' => '2', 'SEK' => '2', 'CHF' => '2', 'GBP' => '2', @@ -181,76 +187,102 @@ class OrbitalGateway < Gateway USE_ORDER_ID = 'O' # Use OrderID field USE_COMMENTS = 'D' # Use Comments field - SENSITIVE_FIELDS = [:account_num, :cc_account_num] + SENSITIVE_FIELDS = %i[account_num cc_account_num] + + # Bank account types to be used for check processing + ACCOUNT_TYPE = { + 'savings' => 'S', + '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] super + @options[:merchant_id] = @options[:merchant_id].to_s + @use_secondary_url = false end # A – Authorization request - def authorize(money, creditcard, options = {}) - order = build_new_order_xml(AUTH_ONLY, money, creditcard, options) do |xml| - add_creditcard(xml, creditcard, options[:currency]) - add_address(xml, creditcard, options) - if @options[:customer_profiles] - add_customer_data(xml, creditcard, options) - add_managed_billing(xml, options) - end - end - commit(order, :authorize, options[:trace_number]) + def authorize(money, payment_source, options = {}) + # ECP for Orbital requires $0 prenotes so ensure + # if we are doing a force capture with a check, that + # we do a purchase here + return purchase(money, payment_source, options) if force_capture_with_echeck?(payment_source, options) + + order = build_new_auth_purchase_order(AUTH_ONLY, money, payment_source, options) + + commit(order, :authorize, options[:retry_logic], options[:trace_number]) end - def verify(creditcard, options = {}) + def verify(credit_card, options = {}) + amount = options[:verify_amount] ? options[:verify_amount].to_i : default_verify_amount(credit_card) MultiResponse.run(:use_first_response) do |r| - r.process { authorize(100, creditcard, options) } - r.process(:ignore_result) { void(r.authorization) } + r.process { authorize(amount, credit_card, options) } + r.process(:ignore_result) { void(r.authorization) } unless amount == 0 end end # AC – Authorization and Capture - def purchase(money, creditcard, options = {}) - order = build_new_order_xml(AUTH_AND_CAPTURE, money, creditcard, options) do |xml| - add_creditcard(xml, creditcard, options[:currency]) - add_address(xml, creditcard, options) - if @options[:customer_profiles] - add_customer_data(xml, creditcard, options) - add_managed_billing(xml, options) - end - end - commit(order, :purchase, options[:trace_number]) + def purchase(money, payment_source, options = {}) + action = options[:force_capture] ? FORCE_AUTH_AND_CAPTURE : AUTH_AND_CAPTURE + order = build_new_auth_purchase_order(action, money, payment_source, options) + + commit(order, :purchase, options[:retry_logic], options[:trace_number]) end # MFC - Mark For Capture def capture(money, authorization, options = {}) - commit(build_mark_for_capture_xml(money, authorization, options), :capture) + commit(build_mark_for_capture_xml(money, authorization, options), :capture, options[:retry_logic], options[:trace_number]) end # R – Refund request def refund(money, authorization, options = {}) - order = build_new_order_xml(REFUND, money, nil, options.merge(:authorization => authorization)) do |xml| - add_refund(xml, options[:currency]) + payment_method = options[:payment_method] + order = build_new_order_xml(REFUND, money, payment_method, options.merge(authorization:)) do |xml| + add_payment_source(xml, payment_method, options) xml.tag! :CustomerRefNum, options[:customer_ref_num] if @options[:customer_profiles] && options[:profile_txn] end - commit(order, :refund, options[:trace_number]) + + commit(order, :refund, options[:retry_logic], options[:trace_number]) + end + + def credit(money, payment_method, options = {}) + order = build_new_order_xml(REFUND, money, payment_method, options) do |xml| + add_payment_source(xml, payment_method, options) + end + + commit(order, :refund, options[:retry_logic], options[:trace_number]) end - def credit(money, authorization, options= {}) - ActiveMerchant.deprecated CREDIT_DEPRECATION_MESSAGE - refund(money, authorization, options) + # 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)) + 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.') - return void(options, deprecated.merge(:amount => authorization)) + return void(options, deprecated.merge(amount: authorization)) end order = build_void_request_xml(authorization, options) - commit(order, :void, options[:trace_number]) + + commit(order, :void, options[:retry_logic], options[:trace_number]) end + def default_verify_amount(credit_card) + allow_zero_auth?(credit_card) ? 0 : 100 + end + + def allow_zero_auth?(credit_card) + # Discover does not support a $0.00 authorization instead use $1.00 + %w(visa master american_express diners_club jcb).include?(credit_card.brand) + end # ==== Customer Profiles # :customer_ref_num should be set unless you're happy with Orbital providing one @@ -273,30 +305,34 @@ def void(authorization, options = {}, deprecated = {}) # 'I' - Inactive # 'MS' - Manual Suspend - def add_customer_profile(creditcard, options = {}) - options.merge!(:customer_profile_action => CREATE) - order = build_customer_request_xml(creditcard, options) + def add_customer_profile(credit_card, options = {}) + options[:customer_profile_action] = CREATE + order = build_customer_request_xml(credit_card, options) commit(order, :add_customer_profile) end - def update_customer_profile(creditcard, options = {}) - options.merge!(:customer_profile_action => UPDATE) - order = build_customer_request_xml(creditcard, options) + def update_customer_profile(credit_card, options = {}) + options[:customer_profile_action] = UPDATE + order = build_customer_request_xml(credit_card, options) commit(order, :update_customer_profile) end def retrieve_customer_profile(customer_ref_num) - options = {:customer_profile_action => RETRIEVE, :customer_ref_num => customer_ref_num} + options = { customer_profile_action: RETRIEVE, customer_ref_num: } order = build_customer_request_xml(nil, options) commit(order, :retrieve_customer_profile) end def delete_customer_profile(customer_ref_num) - options = {:customer_profile_action => DELETE, :customer_ref_num => customer_ref_num} + options = { customer_profile_action: DELETE, customer_ref_num: } order = build_customer_request_xml(nil, options) commit(order, :delete_customer_profile) end + def supports_network_tokenization? + true + end + def supports_scrubbing? true end @@ -306,14 +342,55 @@ def scrub(transcript) gsub(%r(().+()), '\1[FILTERED]\2'). gsub(%r(().+()), '\1[FILTERED]\2'). gsub(%r(().+()), '\1[FILTERED]\2'). - gsub(%r(().+()), '\1[FILTERED]\2'). + # the response sometimes contains a new line that cuts off the end of the closing tag + gsub(%r(().+().+()), '\1[FILTERED]\2'). gsub(%r(().+()), '\1[FILTERED]\2'). - gsub(%r(().+()), '\1[FILTERED]\2') + gsub(%r(().+()), '\1[FILTERED]\2'). + gsub(%r(().+()), '\1[FILTERED]\2'). + gsub(%r(().+()), '\1[FILTERED]\2'). + gsub(%r(().+()), '\1[FILTERED]\2'). + gsub(%r(().+()), '\1[FILTERED]\2') end private + def force_capture_with_echeck?(payment_source, options) + return false unless options[:force_capture] + return false unless payment_source.is_a?(Check) + + %w(W8 W9 ND).include?(options[:action_code]) + end + + #=====REFERENCE FIELDS===== + + def add_customer_data(xml, credit_card, options) + add_customer_ref_num(xml, options) + + return if options[:profile_txn] + + xml.tag! :CustomerProfileFromOrderInd, profile_number(options) if add_profile_number?(options, credit_card) + xml.tag! :CustomerProfileOrderOverrideInd, options[:customer_profile_order_override_ind] || NO_MAPPING_TO_ORDER_DATA + end + + def add_profile_number?(options, credit_card) + return true unless options[:customer_ref_num] && credit_card.nil? + end + + def profile_number(options) + options[:customer_ref_num] ? USE_CUSTOMER_REF_NUM : AUTO_GENERATE + end + + def add_customer_ref_num(xml, options) + xml.tag! :CustomerRefNum, options[:customer_ref_num] if options[:customer_ref_num] + end + + def add_tx_ref_num(xml, authorization) + return unless authorization + + xml.tag! :TxRefNum, split_authorization(authorization).first + end + def authorization_string(*args) args.compact.join(';') end @@ -322,23 +399,22 @@ def split_authorization(authorization) authorization.split(';') end - def add_customer_data(xml, creditcard, options) - if options[:profile_txn] - xml.tag! :CustomerRefNum, options[:customer_ref_num] - else - if options[:customer_ref_num] - if creditcard - xml.tag! :CustomerProfileFromOrderInd, USE_CUSTOMER_REF_NUM - end - xml.tag! :CustomerRefNum, options[:customer_ref_num] - else - xml.tag! :CustomerProfileFromOrderInd, AUTO_GENERATE - end - xml.tag! :CustomerProfileOrderOverrideInd, options[:customer_profile_order_override_ind] || NO_MAPPING_TO_ORDER_DATA - end + #=====DESCRIPTOR FIELDS===== + + def add_soft_descriptors(xml, descriptors) + return unless descriptors + + add_soft_descriptors_from_specialized_class(xml, descriptors) if descriptors.is_a?(OrbitalSoftDescriptors) + add_soft_descriptors_from_hash(xml, descriptors) if descriptors.is_a?(Hash) end - def add_soft_descriptors(xml, soft_desc) + def add_payment_action_ind(xml, payment_action_ind) + return unless payment_action_ind + + xml.tag! :PaymentActionInd, payment_action_ind + end + + def add_soft_descriptors_from_specialized_class(xml, soft_desc) xml.tag! :SDMerchantName, soft_desc.merchant_name if soft_desc.merchant_name xml.tag! :SDProductDescription, soft_desc.product_description if soft_desc.product_description xml.tag! :SDMerchantCity, soft_desc.merchant_city if soft_desc.merchant_city @@ -356,95 +432,205 @@ def add_soft_descriptors_from_hash(xml, soft_desc) xml.tag! :SDMerchantEmail, soft_desc[:merchant_email] || nil end - def add_level_2_tax(xml, options={}) - if (level_2 = options[:level_2_data]) - xml.tag! :TaxInd, level_2[:tax_indicator] if [TAX_NOT_PROVIDED, TAX_INCLUDED, NON_TAXABLE_TRANSACTION].include?(level_2[:tax_indicator].to_i) - xml.tag! :Tax, level_2[:tax].to_i if level_2[:tax] + def add_level2_tax(xml, options = {}) + if (level2 = options[:level_2_data]) + xml.tag! :TaxInd, level2[:tax_indicator] if [TAX_NOT_PROVIDED, TAX_INCLUDED, NON_TAXABLE_TRANSACTION].include?(level2[:tax_indicator].to_i) + xml.tag! :Tax, level2[:tax].to_i if level2[:tax] + end + end + + def add_level3_tax(xml, options = {}) + if (level3 = options[:level_3_data]) + xml.tag! :PC3VATtaxAmt, byte_limit(level3[:vat_tax], 12) if level3[:vat_tax] + xml.tag! :PC3VATtaxRate, byte_limit(level3[:vat_rate], 4) if level3[:vat_rate] + xml.tag! :PC3AltTaxInd, byte_limit(level3[:alt_ind], 15) if level3[:alt_ind] + xml.tag! :PC3AltTaxAmt, byte_limit(level3[:alt_tax], 9) if level3[:alt_tax] end end - def add_level_2_advice_addendum(xml, options={}) - if (level_2 = options[:level_2_data]) - xml.tag! :AMEXTranAdvAddn1, byte_limit(level_2[:advice_addendum_1], 40) if level_2[:advice_addendum_1] - xml.tag! :AMEXTranAdvAddn2, byte_limit(level_2[:advice_addendum_2], 40) if level_2[:advice_addendum_2] - xml.tag! :AMEXTranAdvAddn3, byte_limit(level_2[:advice_addendum_3], 40) if level_2[:advice_addendum_3] - xml.tag! :AMEXTranAdvAddn4, byte_limit(level_2[:advice_addendum_4], 40) if level_2[:advice_addendum_4] + def add_level2_advice_addendum(xml, options = {}) + if (level2 = options[:level_2_data]) + xml.tag! :AMEXTranAdvAddn1, byte_limit(level2[:advice_addendum_1], 40) if level2[:advice_addendum_1] + xml.tag! :AMEXTranAdvAddn2, byte_limit(level2[:advice_addendum_2], 40) if level2[:advice_addendum_2] + xml.tag! :AMEXTranAdvAddn3, byte_limit(level2[:advice_addendum_3], 40) if level2[:advice_addendum_3] + xml.tag! :AMEXTranAdvAddn4, byte_limit(level2[:advice_addendum_4], 40) if level2[:advice_addendum_4] end end - def add_level_2_purchase(xml, options={}) - if (level_2 = options[:level_2_data]) - xml.tag! :PCOrderNum, byte_limit(level_2[:purchase_order], 17) if level_2[:purchase_order] - xml.tag! :PCDestZip, byte_limit(format_address_field(level_2[:zip]), 10) if level_2[:zip] - xml.tag! :PCDestName, byte_limit(format_address_field(level_2[:name]), 30) if level_2[:name] - xml.tag! :PCDestAddress1, byte_limit(format_address_field(level_2[:address1]), 30) if level_2[:address1] - xml.tag! :PCDestAddress2, byte_limit(format_address_field(level_2[:address2]), 30) if level_2[:address2] - xml.tag! :PCDestCity, byte_limit(format_address_field(level_2[:city]), 20) if level_2[:city] - xml.tag! :PCDestState, byte_limit(format_address_field(level_2[:state]), 2) if level_2[:state] + def add_level2_purchase(xml, options = {}) + if (level2 = options[:level_2_data]) + xml.tag! :PCOrderNum, byte_limit(level2[:purchase_order], 17) if level2[:purchase_order] + xml.tag! :PCDestZip, byte_limit(format_address_field(level2[:zip]), 10) if level2[:zip] + xml.tag! :PCDestName, byte_limit(format_address_field(level2[:name]), 30) if level2[:name] + xml.tag! :PCDestAddress1, byte_limit(format_address_field(level2[:address1]), 30) if level2[:address1] + xml.tag! :PCDestAddress2, byte_limit(format_address_field(level2[:address2]), 30) if level2[:address2] + xml.tag! :PCDestCity, byte_limit(format_address_field(level2[:city]), 20) if level2[:city] + xml.tag! :PCDestState, byte_limit(format_address_field(level2[:state]), 2) if level2[:state] end end - def add_address(xml, creditcard, options) - if(address = (options[:billing_address] || options[:address])) - avs_supported = AVS_SUPPORTED_COUNTRIES.include?(address[:country].to_s) || empty?(address[:country]) + def add_level3_purchase(xml, options = {}) + if (level3 = options[:level_3_data]) + xml.tag! :PC3FreightAmt, byte_limit(level3[:freight_amount], 12) if level3[:freight_amount] + xml.tag! :PC3DutyAmt, byte_limit(level3[:duty_amount], 12) if level3[:duty_amount] + xml.tag! :PC3DestCountryCd, byte_limit(level3[:dest_country], 3) if level3[:dest_country] + xml.tag! :PC3ShipFromZip, byte_limit(level3[:ship_from_zip], 10) if level3[:ship_from_zip] + xml.tag! :PC3DiscAmt, byte_limit(level3[:discount_amount], 12) if level3[:discount_amount] + end + end - if avs_supported - xml.tag! :AVSzip, byte_limit(format_address_field(address[:zip]), 10) - xml.tag! :AVSaddress1, byte_limit(format_address_field(address[:address1]), 30) - xml.tag! :AVSaddress2, byte_limit(format_address_field(address[:address2]), 30) - xml.tag! :AVScity, byte_limit(format_address_field(address[:city]), 20) - xml.tag! :AVSstate, byte_limit(format_address_field(address[:state]), 2) - xml.tag! :AVSphoneNum, (address[:phone] ? address[:phone].scan(/\d/).join.to_s[0..13] : nil) + def add_line_items(xml, options = {}) + xml.tag! :PC3LineItemCount, byte_limit(options[:line_items].count, 2) + xml.tag! :PC3LineItemArray do + options[:line_items].each_with_index do |line_item, index| + xml.tag! :PC3LineItem do + xml.tag! :PC3DtlIndex, byte_limit(index + 1, 2) + line_item.each do |key, value| + if [:line_tot, 'line_tot'].include? key + formatted_key = :PC3Dtllinetot + else + formatted_key = "PC3Dtl#{key.to_s.camelize}".to_sym + end + xml.tag! formatted_key, value + end + end end + end + end - xml.tag! :AVSname, ((creditcard && creditcard.name) ? creditcard.name[0..29] : nil) - xml.tag! :AVScountryCode, (avs_supported ? (byte_limit(format_address_field(address[:country]), 2)) : '') + 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 - # Needs to come after AVScountryCode - add_destination_address(xml, address) if avs_supported + 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_destination_address(xml, address) - if address[:dest_zip] - avs_supported = AVS_SUPPORTED_COUNTRIES.include?(address[:dest_country].to_s) + 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===== - xml.tag! :AVSDestzip, byte_limit(format_address_field(address[:dest_zip]), 10) - xml.tag! :AVSDestaddress1, byte_limit(format_address_field(address[:dest_address1]), 30) - xml.tag! :AVSDestaddress2, byte_limit(format_address_field(address[:dest_address2]), 30) - xml.tag! :AVSDestcity, byte_limit(format_address_field(address[:dest_city]), 20) - xml.tag! :AVSDeststate, byte_limit(format_address_field(address[:dest_state]), 2) - xml.tag! :AVSDestphoneNum, (address[:dest_phone] ? address[:dest_phone].scan(/\d/).join.to_s[0..13] : nil) + def add_address(xml, payment_source, options) + return unless (address = get_address(options)) - xml.tag! :AVSDestname, byte_limit(address[:dest_name], 30) - xml.tag! :AVSDestcountryCode, (avs_supported ? address[:dest_country] : '') + if avs_supported?(address[:country]) || empty?(address[:country]) + xml.tag! :AVSzip, byte_limit(format_address_field(address[:zip]), 10) + xml.tag! :AVSaddress1, byte_limit(format_address_field(address[:address1]), 30) + xml.tag! :AVSaddress2, byte_limit(format_address_field(address[:address2]), 30) + xml.tag! :AVScity, byte_limit(format_address_field(address[:city]), 20) + xml.tag! :AVSstate, byte_limit(format_address_field(address[:state]), 2) + xml.tag! :AVSphoneNum, (address[:phone] ? address[:phone].scan(/\d/).join.to_s[0..13] : nil) end + + xml.tag! :AVSname, billing_name(payment_source, options) + xml.tag! :AVScountryCode, byte_limit(format_address_field(filter_unsupported_countries(address[:country])), 2) + + # Needs to come after AVScountryCode + add_destination_address(xml, address) if avs_supported?(address[:country]) || empty?(address[:country]) + end + + def add_destination_address(xml, address) + return unless address[:dest_zip] + + xml.tag! :AVSDestzip, byte_limit(format_address_field(address[:dest_zip]), 10) + xml.tag! :AVSDestaddress1, byte_limit(format_address_field(address[:dest_address1]), 30) + xml.tag! :AVSDestaddress2, byte_limit(format_address_field(address[:dest_address2]), 30) + xml.tag! :AVSDestcity, byte_limit(format_address_field(address[:dest_city]), 20) + xml.tag! :AVSDeststate, byte_limit(format_address_field(address[:dest_state]), 2) + xml.tag! :AVSDestphoneNum, (address[:dest_phone] ? address[:dest_phone].scan(/\d/).join.to_s[0..13] : nil) + xml.tag! :AVSDestname, byte_limit(address[:dest_name], 30) + xml.tag! :AVSDestcountryCode, filter_unsupported_countries(address[:dest_country]) end # For Profile requests def add_customer_address(xml, options) - if(address = (options[:billing_address] || options[:address])) - avs_supported = AVS_SUPPORTED_COUNTRIES.include?(address[:country].to_s) + return unless (address = get_address(options)) + + xml.tag! :CustomerAddress1, byte_limit(format_address_field(address[:address1]), 30) + xml.tag! :CustomerAddress2, byte_limit(format_address_field(address[:address2]), 30) + xml.tag! :CustomerCity, byte_limit(format_address_field(address[:city]), 20) + xml.tag! :CustomerState, byte_limit(format_address_field(address[:state]), 2) + xml.tag! :CustomerZIP, byte_limit(format_address_field(address[:zip]), 10) + xml.tag! :CustomerEmail, byte_limit(address[:email], 50) if address[:email] + xml.tag! :CustomerPhone, (address[:phone] ? address[:phone].scan(/\d/).join.to_s : nil) + xml.tag! :CustomerCountryCode, filter_unsupported_countries(address[:country]) + end - xml.tag! :CustomerAddress1, byte_limit(format_address_field(address[:address1]), 30) - xml.tag! :CustomerAddress2, byte_limit(format_address_field(address[:address2]), 30) - xml.tag! :CustomerCity, byte_limit(format_address_field(address[:city]), 20) - xml.tag! :CustomerState, byte_limit(format_address_field(address[:state]), 2) - xml.tag! :CustomerZIP, byte_limit(format_address_field(address[:zip]), 10) - xml.tag! :CustomerEmail, byte_limit(address[:email], 50) if address[:email] - xml.tag! :CustomerPhone, (address[:phone] ? address[:phone].scan(/\d/).join.to_s : nil) - xml.tag! :CustomerCountryCode, (avs_supported ? address[:country] : '') + def billing_name(payment_source, options) + if !payment_source.is_a?(String) && payment_source&.name.present? + payment_source.name[0..29] + elsif options[:billing_address] && options[:billing_address][:name].present? + options[:billing_address][:name][0..29] end end - def add_creditcard(xml, creditcard, currency=nil) - unless creditcard.nil? - xml.tag! :AccountNum, creditcard.number - xml.tag! :Exp, expiry_date(creditcard) - end + def avs_supported?(address) + AVS_SUPPORTED_COUNTRIES.include?(address.to_s) + end - xml.tag! :CurrencyCode, currency_code(currency) - xml.tag! :CurrencyExponent, currency_exponents(currency) + def filter_unsupported_countries(address) + avs_supported?(address) ? address.to_s : '' + end + + 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 + + def add_echeck(xml, check, options = {}) + return unless check + + xml.tag! :CardBrand, 'EC' + add_currency_fields(xml, options[:currency]) + xml.tag! :BCRtNum, check.routing_number + xml.tag! :CheckDDA, check.account_number if check.account_number + add_bank_account_type(xml, check) + xml.tag! :ECPAuthMethod, options[:auth_method] if options[:auth_method] + xml.tag! :BankPmtDelv, options[:payment_delivery] || 'B' + xml.tag! :AVSname, (check&.name ? check.name[0..29] : nil) if get_address(options).blank? + end + + def add_credit_card(xml, credit_card, options) + 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.is_a?(CreditCard) + end + + def add_verification_value(xml, credit_card) + return unless credit_card&.verification_value? # If you are trying to collect a Card Verification Number # (CardSecVal) for a Visa or Discover transaction, pass one of these values: @@ -455,103 +641,318 @@ def add_creditcard(xml, creditcard, currency=nil) # Null-fill this attribute OR # Do not submit the attribute at all. # - http://download.chasepaymentech.com/docs/orbital/orbital_gateway_xml_specification.pdf - unless creditcard.nil? - if creditcard.verification_value? - if %w( visa discover ).include?(creditcard.brand) - xml.tag! :CardSecValInd, '1' - end - xml.tag! :CardSecVal, creditcard.verification_value + xml.tag! :CardSecValInd, '1' if %w(visa discover diners_club).include?(credit_card.brand) + xml.tag! :CardSecVal, credit_card.verification_value + end + + def add_currency_fields(xml, currency) + xml.tag! :CurrencyCode, currency_code(currency) + xml.tag! :CurrencyExponent, currency_exponents(currency) + end + + def add_bank_account_type(xml, check) + bank_account_type = + if check.account_holder_type == 'business' + 'X' + else + ACCOUNT_TYPE[check.account_type] end - end + + xml.tag! :BankAccountType, bank_account_type if bank_account_type end - def add_cdpt_eci_and_xid(xml, creditcard) - xml.tag! :AuthenticationECIInd, creditcard.eci - xml.tag! :XID, creditcard.transaction_id if creditcard.transaction_id + def add_card_indicators(xml, options) + xml.tag! :CardIndicators, options[:card_indicators] if options[:card_indicators] end - def add_cdpt_payment_cryptogram(xml, creditcard) - xml.tag! :DPANInd, 'Y' - xml.tag! :DigitalTokenCryptogram, creditcard.payment_cryptogram + def currency_code(currency) + CURRENCY_CODES[(currency || self.default_currency)].to_s end - def add_refund(xml, currency=nil) - xml.tag! :AccountNum, nil + def currency_exponents(currency) + CURRENCY_EXPONENTS[(currency || self.default_currency)].to_s + end - xml.tag! :CurrencyCode, currency_code(currency) - xml.tag! :CurrencyExponent, currency_exponents(currency) + def expiry_date(credit_card) + "#{format(credit_card.month, :two_digits)}#{format(credit_card.year, :two_digits)}" end - def add_managed_billing(xml, options) - if mb = options[:managed_billing] - ActiveMerchant.deprecated RECURRING_DEPRECATION_MESSAGE - - # default to recurring (R). Other option is deferred (D). - xml.tag! :MBType, mb[:type] || RECURRING - # default to Customer Reference Number - xml.tag! :MBOrderIdGenerationMethod, mb[:order_id_generation_method] || 'IO' - # By default use MBRecurringEndDate, set to N. - # MMDDYYYY - xml.tag! :MBRecurringStartDate, mb[:start_date].scan(/\d/).join.to_s if mb[:start_date] - # MMDDYYYY - xml.tag! :MBRecurringEndDate, mb[:end_date].scan(/\d/).join.to_s if mb[:end_date] - # By default listen to any value set in MBRecurringEndDate. - xml.tag! :MBRecurringNoEndDateFlag, mb[:no_end_date_flag] || 'N' # 'Y' || 'N' (Yes or No). - xml.tag! :MBRecurringMaxBillings, mb[:max_billings] if mb[:max_billings] - xml.tag! :MBRecurringFrequency, mb[:frequency] if mb[:frequency] - xml.tag! :MBDeferredBillDate, mb[:deferred_bill_date] if mb[:deferred_bill_date] - xml.tag! :MBMicroPaymentMaxDollarValue, mb[:max_dollar_value] if mb[:max_dollar_value] - xml.tag! :MBMicroPaymentMaxBillingDays, mb[:max_billing_days] if mb[:max_billing_days] - xml.tag! :MBMicroPaymentMaxTransactions, mb[:max_transactions] if mb[:max_transactions] + def bin + @options[:bin] || (salem_mid? ? '000001' : '000002') + end + + def salem_mid? + @options[:merchant_id].length == 6 + end + + #=====BRAND-SPECIFIC FIELDS===== + + def add_cavv(xml, credit_card, three_d_secure) + return unless three_d_secure && credit_card.brand == 'visa' + + xml.tag!(:CAVV, three_d_secure[:cavv]) + end + + def add_aav(xml, credit_card, three_d_secure) + return unless three_d_secure && credit_card.brand == 'master' + + xml.tag!(:AAV, three_d_secure[:cavv]) + end + + def add_aevv(xml, credit_card, three_d_secure) + return unless three_d_secure && credit_card.brand == 'american_express' + + xml.tag!(:AEVV, three_d_secure[:cavv]) + end + + def add_xid(xml, credit_card, three_d_secure) + return unless three_d_secure && credit_card.brand == 'visa' + + xml.tag!(:XID, three_d_secure[:xid]) if three_d_secure[:xid] + end + + PYMT_PROGRAM_CODE_BY_BRAND = { + 'american_express' => 'ASK', + 'discover' => 'DPB' + }.freeze + def add_pymt_brand_program_code(xml, credit_card, three_d_secure) + return unless three_d_secure && (code = PYMT_PROGRAM_CODE_BY_BRAND[credit_card.brand]) + + xml.tag!(:PymtBrandProgramCode, code) + end + + def mastercard?(payment_source) + payment_source.is_a?(CreditCard) && payment_source.brand == 'master' + end + + def add_mastercard_fields(xml, credit_card, parameters, three_d_secure) + add_mc_sca_merchant_initiated(xml, credit_card, parameters, three_d_secure) + add_mc_sca_recurring(xml, credit_card, parameters, three_d_secure) + add_mc_program_protocol(xml, credit_card, three_d_secure) + add_mc_directory_trans_id(xml, credit_card, three_d_secure) + add_mc_ucafind(xml, credit_card, three_d_secure, parameters) + end + + def add_mc_sca_merchant_initiated(xml, credit_card, parameters, three_d_secure) + return unless parameters.try(:[], :sca_merchant_initiated) + return unless three_d_secure.try(:[], :eci) == '7' + + xml.tag!(:SCAMerchantInitiatedTransaction, parameters[:sca_merchant_initiated]) + end + + def add_mc_sca_recurring(xml, credit_card, parameters, three_d_secure) + return unless parameters.try(:[], :sca_recurring) + return unless three_d_secure.try(:[], :eci) == '7' + + xml.tag!(:SCARecurringPayment, parameters[:sca_recurring]) + end + + def add_mc_program_protocol(xml, credit_card, three_d_secure) + return unless version = three_d_secure.try(:[], :version) + + xml.tag!(:MCProgramProtocol, version.to_s[0]) + end + + def add_mc_directory_trans_id(xml, credit_card, three_d_secure) + return unless three_d_secure + + xml.tag!(:MCDirectoryTransID, three_d_secure[:ds_transaction_id]) if three_d_secure[:ds_transaction_id] + end + + def add_mc_ucafind(xml, credit_card, three_d_secure, options) + return unless three_d_secure && %w(4 6 7).include?(three_d_secure&.dig(:eci)) + + xml.tag! :UCAFInd, options[:ucaf_collection_indicator] if options[:ucaf_collection_indicator] + end + + #=====SCA (STORED CREDENTIAL) FIELDS===== + + def add_stored_credentials(xml, parameters) + return unless parameters[:mit_stored_credential_ind] == 'Y' || (parameters[:stored_credential] && !parameters[:stored_credential].values.all?(&:nil?)) + + if msg_type = get_msg_type(parameters) + xml.tag! :MITMsgType, msg_type + end + xml.tag! :MITStoredCredentialInd, 'Y' + if parameters[:mit_submitted_transaction_id] + xml.tag! :MITSubmittedTransactionID, parameters[:mit_submitted_transaction_id] + elsif parameters.dig(:stored_credential, :network_transaction_id) && parameters.dig(:stored_credential, :initiator) == 'merchant' + xml.tag! :MITSubmittedTransactionID, parameters[:stored_credential][:network_transaction_id] end end - def parse(body) - response = {} - xml = REXML::Document.new(body) - root = REXML::XPath.first(xml, '//Response') || - REXML::XPath.first(xml, '//ErrorResponse') - if root - root.elements.to_a.each do |node| - recurring_parse_element(response, node) + def get_msg_type(parameters) + return parameters[:mit_msg_type] if parameters[:mit_msg_type] + return 'CSTO' if parameters[:stored_credential][:initial_transaction] + return unless parameters[:stored_credential][:initiator] && parameters[:stored_credential][:reason_type] + + initiator = + case parameters[:stored_credential][:initiator] + when 'cardholder', 'customer' then 'C' + when 'merchant' then 'M' + end + reason = + case parameters[:stored_credential][:reason_type] + when 'recurring' then 'REC' + when 'installment' then 'INS' + when 'unscheduled' then 'USE' end + + "#{initiator}#{reason}" + end + + #=====NETWORK TOKENIZATION FIELDS===== + + def add_eci(xml, credit_card, three_d_secure) + eci = if three_d_secure + three_d_secure[:eci] + elsif credit_card.is_a?(NetworkTokenizationCreditCard) + credit_card.eci + end + + xml.tag!(:AuthenticationECIInd, eci) if eci + end + + def add_dpanind(xml, credit_card, industry_type = nil) + return unless credit_card.is_a?(NetworkTokenizationCreditCard) + + xml.tag! :DPANInd, 'Y' unless industry_type == 'RC' + end + + def add_digital_token_cryptogram(xml, credit_card, three_d_secure) + return unless credit_card.is_a?(NetworkTokenizationCreditCard) || (three_d_secure && credit_card.brand == 'discover') + + cryptogram = + if three_d_secure && credit_card.brand == 'discover' + three_d_secure[:cavv] + else + credit_card.payment_cryptogram + end + + xml.tag!(:DigitalTokenCryptogram, cryptogram) + end + + #=====OTHER FIELDS===== + + # For Canadian transactions on PNS Tampa on New Order + # RF - First Recurring Transaction + # RS - Subsequent Recurring Transactions + def set_recurring_ind(xml, parameters) + return unless parameters[:recurring_ind] + raise 'RecurringInd must be set to either "RF" or "RS"' unless %w(RF RS).include?(parameters[:recurring_ind]) + + xml.tag! :RecurringInd, parameters[:recurring_ind] + end + + def add_managed_billing(xml, options) + return unless mb = options[:managed_billing] + + ActiveMerchant.deprecated RECURRING_DEPRECATION_MESSAGE + + # default to recurring (R). Other option is deferred (D). + xml.tag! :MBType, mb[:type] || RECURRING + # default to Customer Reference Number + xml.tag! :MBOrderIdGenerationMethod, mb[:order_id_generation_method] || 'IO' + # By default use MBRecurringEndDate, set to N. + # MMDDYYYY + xml.tag! :MBRecurringStartDate, mb[:start_date].scan(/\d/).join.to_s if mb[:start_date] + # MMDDYYYY + xml.tag! :MBRecurringEndDate, mb[:end_date].scan(/\d/).join.to_s if mb[:end_date] + # By default listen to any value set in MBRecurringEndDate. + xml.tag! :MBRecurringNoEndDateFlag, mb[:no_end_date_flag] || 'N' # 'Y' || 'N' (Yes or No). + xml.tag! :MBRecurringMaxBillings, mb[:max_billings] if mb[:max_billings] + xml.tag! :MBRecurringFrequency, mb[:frequency] if mb[:frequency] + xml.tag! :MBDeferredBillDate, mb[:deferred_bill_date] if mb[:deferred_bill_date] + xml.tag! :MBMicroPaymentMaxDollarValue, mb[:max_dollar_value] if mb[:max_dollar_value] + xml.tag! :MBMicroPaymentMaxBillingDays, mb[:max_billing_days] if mb[:max_billing_days] + xml.tag! :MBMicroPaymentMaxTransactions, mb[:max_transactions] if mb[:max_transactions] + end + + def add_ews_details(xml, payment_source, parameters = {}) + split_name = payment_source.first_name.split if payment_source.first_name + xml.tag! :EWSFirstName, split_name[0] + xml.tag! :EWSMiddleName, split_name[1..-1].join(' ') + xml.tag! :EWSLastName, payment_source.last_name + xml.tag! :EWSBusinessName, parameters[:company] if payment_source.first_name.empty? && payment_source.last_name.empty? + + if (address = (parameters[:billing_address] || parameters[:address])) + xml.tag! :EWSAddressLine1, byte_limit(format_address_field(address[:address1]), 30) + xml.tag! :EWSAddressLine2, byte_limit(format_address_field(address[:address2]), 30) + xml.tag! :EWSCity, byte_limit(format_address_field(address[:city]), 20) + xml.tag! :EWSState, byte_limit(format_address_field(address[:state]), 2) + xml.tag! :EWSZip, byte_limit(format_address_field(address[:zip]), 10) end - response.delete_if { |k,_| SENSITIVE_FIELDS.include?(k) } + xml.tag! :EWSPhoneType, parameters[:phone_type] + xml.tag! :EWSPhoneNumber, parameters[:phone_number] + xml.tag! :EWSCheckSerialNumber, payment_source.account_number unless parameters[:auth_method].eql?('I') end - def recurring_parse_element(response, node) - if node.has_elements? - node.elements.each{|e| recurring_parse_element(response, e) } - else - response[node.name.underscore.to_sym] = node.text + # Adds ECP conditional attributes depending on other attribute values + def add_ecp_details(xml, payment_source, parameters = {}) + requires!(payment_source.account_number) if parameters[:auth_method]&.eql?('A') || parameters[:auth_method]&.eql?('P') + xml.tag! :ECPActionCode, parameters[:action_code] if parameters[:action_code] + xml.tag! :ECPCheckSerialNumber, payment_source.account_number if parameters[:auth_method]&.eql?('A') || parameters[:auth_method]&.eql?('P') + if parameters[:auth_method]&.eql?('P') + xml.tag! :ECPTerminalCity, parameters[:terminal_city] if parameters[:terminal_city] + xml.tag! :ECPTerminalState, parameters[:terminal_state] if parameters[:terminal_state] + xml.tag! :ECPImageReferenceNumber, parameters[:image_reference_number] if parameters[:image_reference_number] + end + if parameters[:action_code]&.eql?('W3') || parameters[:action_code]&.eql?('W5') || + parameters[:action_code]&.eql?('W7') || parameters[:action_code]&.eql?('W9') + add_ews_details(xml, payment_source, parameters) + end + end + + def add_xml_credentials(xml) + unless ip_authentication? + xml.tag! :OrbitalConnectionUsername, @options[:login] + xml.tag! :OrbitalConnectionPassword, @options[:password] end end - def commit(order, message_type, trace_number=nil) + def add_bin_merchant_and_terminal(xml, parameters) + xml.tag! :BIN, bin + xml.tag! :MerchantID, @options[:merchant_id] + xml.tag! :TerminalID, parameters[:terminal_id] || '001' + end + + #=====REQUEST/RESPONSE METHODS===== + + def commit(order, message_type, retry_logic = nil, trace_number = nil) headers = POST_HEADERS.merge('Content-length' => order.size.to_s) - headers.merge!( 'Trace-number' => trace_number.to_s, - 'Merchant-Id' => @options[:merchant_id] ) if @options[:retry_logic] && trace_number - request = lambda{|url| parse(ssl_post(url, order, headers))} + if (@options[:retry_logic] || retry_logic) && trace_number + headers['Trace-number'] = trace_number.to_s + headers['Merchant-Id'] = @options[:merchant_id] + end + request = ->(url) { parse(ssl_post(url, order, headers)) } # Failover URL will be attempted in the event of a connection error - response = begin - request.call(remote_url) - rescue ConnectionError - request.call(remote_url(:secondary)) - end + response = + begin + raise ConnectionError.new 'Should use secondary url', 500 if @use_secondary_url + + request.call(remote_url) + rescue ConnectionError + request.call(remote_url(:secondary)) + end - Response.new(success?(response, message_type), message_from(response), response, + 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]), - :test => self.test?, - :avs_result => OrbitalGateway::AVSResult.new(response[:avs_resp_code]), - :cvv_result => OrbitalGateway::CVVResult.new(response[:cvv2_resp_code]) + 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) + def remote_url(url = :primary) if url == :primary (self.test? ? self.test_url : self.live_url) else @@ -559,14 +960,38 @@ def remote_url(url=:primary) end end + def parse(body) + response = {} + xml = REXML::Document.new(strip_invalid_xml_chars(body)) + root = REXML::XPath.first(xml, '//Response') || + REXML::XPath.first(xml, '//ErrorResponse') + if root + root.elements.to_a.each do |node| + recurring_parse_element(response, node) + end + end + + response.delete_if { |k, _| SENSITIVE_FIELDS.include?(k) } + end + + def recurring_parse_element(response, node) + if node.has_elements? + node.elements.each { |e| recurring_parse_element(response, e) } + else + response[node.name.underscore.to_sym] = node.text + end + end + def success?(response, message_type) - if [:refund, :void].include?(message_type) + if %i[void].include?(message_type) response[:proc_status] == SUCCESS + elsif %i[refund].include?(message_type) + response[:proc_status] == SUCCESS && response[:approval_status] == APPROVAL_SUCCESS elsif response[:customer_profile_action] response[:profile_proc_status] == SUCCESS else response[:proc_status] == SUCCESS && - APPROVED.include?(response[:resp_code]) + APPROVED.include?(response[:resp_code]) end end @@ -578,76 +1003,74 @@ def ip_authentication? @options[:ip_authentication] == true end - def build_new_order_xml(action, money, creditcard, parameters = {}) + #=====BUILDER METHODS===== + + def build_new_auth_purchase_order(action, money, payment_source, options) + build_new_order_xml(action, money, payment_source, options) do |xml| + add_payment_source(xml, payment_source, options) + add_address(xml, payment_source, options) + if @options[:customer_profiles] + add_customer_data(xml, payment_source, options) + add_managed_billing(xml, options) + end + end + end + + def build_new_order_xml(action, money, payment_source, parameters = {}) requires!(parameters, :order_id) + @use_secondary_url = parameters[:use_secondary_url] if parameters[:use_secondary_url] xml = xml_envelope xml.tag! :Request do xml.tag! :NewOrder do add_xml_credentials(xml) - # EC - Ecommerce transaction - # RC - Recurring Payment transaction - # MO - Mail Order Telephone Order transaction - # IV - Interactive Voice Response - # IN - Interactive Voice Response xml.tag! :IndustryType, parameters[:industry_type] || ECOMMERCE_TRANSACTION - # A - Auth Only No Capture - # AC - Auth and Capture - # F - Force Auth No Capture and no online authorization - # FR - Force Auth No Capture and no online authorization - # FC - Force Auth and Capture no online authorization - # R - Refund and Capture no online authorization xml.tag! :MessageType, action add_bin_merchant_and_terminal(xml, parameters) yield xml if block_given? - if creditcard.is_a?(NetworkTokenizationCreditCard) - add_cdpt_eci_and_xid(xml, creditcard) - end + three_d_secure = parameters[:three_d_secure] + + add_eci(xml, payment_source, three_d_secure) + add_cavv(xml, payment_source, three_d_secure) + add_xid(xml, payment_source, three_d_secure) xml.tag! :OrderID, format_order_id(parameters[:order_id]) xml.tag! :Amount, amount(money) xml.tag! :Comments, parameters[:comments] if parameters[:comments] - - add_level_2_tax(xml, parameters) - add_level_2_advice_addendum(xml, parameters) - + 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. - - if creditcard.is_a?(NetworkTokenizationCreditCard) - add_cdpt_payment_cryptogram(xml, creditcard) - end - - if parameters[:soft_descriptors].is_a?(OrbitalSoftDescriptors) - add_soft_descriptors(xml, parameters[:soft_descriptors]) - elsif parameters[:soft_descriptors].is_a?(Hash) - add_soft_descriptors_from_hash(xml, parameters[:soft_descriptors]) - end - + add_soft_descriptors(xml, parameters[:soft_descriptors]) set_recurring_ind(xml, parameters) # Append Transaction Reference Number at the end for Refund transactions - if action == REFUND - tx_ref_num, _ = split_authorization(parameters[:authorization]) - xml.tag! :TxRefNum, tx_ref_num - end - - add_level_2_purchase(xml, parameters) + 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_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! end - # For Canadian transactions on PNS Tampa on New Order - # RF - First Recurring Transaction - # RS - Subsequent Recurring Transactions - def set_recurring_ind(xml, parameters) - if parameters[:recurring_ind] - raise 'RecurringInd must be set to either "RF" or "RS"' unless %w(RF RS).include?(parameters[:recurring_ind]) - xml.tag! :RecurringInd, parameters[:recurring_ind] - end - end - def build_mark_for_capture_xml(money, authorization, parameters = {}) tx_ref_num, order_id = split_authorization(authorization) xml = xml_envelope @@ -656,11 +1079,17 @@ def build_mark_for_capture_xml(money, authorization, parameters = {}) add_xml_credentials(xml) xml.tag! :OrderID, format_order_id(order_id) xml.tag! :Amount, amount(money) - add_level_2_tax(xml, parameters) + add_level2_tax(xml, parameters) add_bin_merchant_and_terminal(xml, parameters) xml.tag! :TxRefNum, tx_ref_num - add_level_2_purchase(xml, parameters) - add_level_2_advice_addendum(xml, parameters) + add_level2_purchase(xml, parameters) + add_level2_advice_addendum(xml, 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! @@ -684,43 +1113,16 @@ def build_void_request_xml(authorization, parameters = {}) xml.target! end - def currency_code(currency) - CURRENCY_CODES[(currency || self.default_currency)].to_s - end - - def currency_exponents(currency) - CURRENCY_EXPONENTS[(currency || self.default_currency)].to_s - end - - def expiry_date(credit_card) - "#{format(credit_card.month, :two_digits)}#{format(credit_card.year, :two_digits)}" - end - - def bin - @options[:bin] || (salem_mid? ? '000001' : '000002') - end - def xml_envelope - xml = Builder::XmlMarkup.new(:indent => 2) - xml.instruct!(:xml, :version => '1.0', :encoding => 'UTF-8') + xml = Builder::XmlMarkup.new(indent: 2) + xml.instruct!(:xml, version: '1.0', encoding: 'UTF-8') xml end - def add_xml_credentials(xml) - unless ip_authentication? - xml.tag! :OrbitalConnectionUsername, @options[:login] - xml.tag! :OrbitalConnectionPassword, @options[:password] - end - end - - def add_bin_merchant_and_terminal(xml, parameters) - xml.tag! :BIN, bin - xml.tag! :MerchantID, @options[:merchant_id] - xml.tag! :TerminalID, parameters[:terminal_id] || '001' - end - - def salem_mid? - @options[:merchant_id].length == 6 + # Null characters are possible in some responses (namely, the respMsg field), causing XML parsing errors + # Prevent by substituting these with a valid placeholder string + def strip_invalid_xml_chars(xml) + xml.gsub(/\u0000/, '[null]') end # The valid characters include: @@ -730,7 +1132,7 @@ def salem_mid? # 3. PINless Debit transactions can only use uppercase and lowercase alpha (A-Z, a-z) and numeric (0-9) def format_order_id(order_id) illegal_characters = /[^,$@&\- \w]/ - order_id = order_id.to_s.gsub(/\./, '-') + order_id = order_id.to_s.tr('.', '-') order_id.gsub!(illegal_characters, '') order_id.lstrip! order_id[0...22] @@ -748,14 +1150,15 @@ def byte_limit(value, byte_length) limited_value = '' value.to_s.each_char do |c| - break if((limited_value.bytesize + c.bytesize) > byte_length) + break if (limited_value.bytesize + c.bytesize) > byte_length + limited_value << c end limited_value end - def build_customer_request_xml(creditcard, options = {}) + def build_customer_request_xml(credit_card, options = {}) ActiveMerchant.deprecated 'Customer Profile support in Orbital is non-conformant to the ActiveMerchant API and will be removed in its current form in a future version. Please contact the ActiveMerchant maintainers if you have an interest in modifying it to conform to the store/unstore/update API.' xml = xml_envelope xml.tag! :Request do @@ -764,7 +1167,7 @@ def build_customer_request_xml(creditcard, options = {}) xml.tag! :OrbitalConnectionPassword, @options[:password] unless ip_authentication? xml.tag! :CustomerBin, bin xml.tag! :CustomerMerchantID, @options[:merchant_id] - xml.tag! :CustomerName, creditcard.name if creditcard + xml.tag! :CustomerName, credit_card.name if credit_card xml.tag! :CustomerRefNum, options[:customer_ref_num] if options[:customer_ref_num] add_customer_address(xml, options) @@ -792,8 +1195,8 @@ def build_customer_request_xml(creditcard, options = {}) xml.tag! :Status, options[:status] || ACTIVE # Active end - xml.tag! :CCAccountNum, creditcard.number if creditcard - xml.tag! :CCExpireDate, creditcard.expiry_date.expiration.strftime('%m%y') if creditcard + xml.tag! :CCAccountNum, credit_card.number if credit_card + xml.tag! :CCExpireDate, credit_card.expiry_date.expiration.strftime('%m%y') if credit_card # This has to come after CCExpireDate. add_managed_billing(xml, options) @@ -811,55 +1214,55 @@ def build_customer_request_xml(creditcard, options = {}) # class AVSResult < ActiveMerchant::Billing::AVSResult CODES = { - '1' => 'No address supplied', - '2' => 'Bill-to address did not pass Auth Host edit checks', - '3' => 'AVS not performed', - '4' => 'Issuer does not participate in AVS', - '5' => 'Edit-error - AVS data is invalid', - '6' => 'System unavailable or time-out', - '7' => 'Address information unavailable', - '8' => 'Transaction Ineligible for AVS', - '9' => 'Zip Match/Zip 4 Match/Locale match', - 'A' => 'Zip Match/Zip 4 Match/Locale no match', - 'B' => 'Zip Match/Zip 4 no Match/Locale match', - 'C' => 'Zip Match/Zip 4 no Match/Locale no match', - 'D' => 'Zip No Match/Zip 4 Match/Locale match', - 'E' => 'Zip No Match/Zip 4 Match/Locale no match', - 'F' => 'Zip No Match/Zip 4 No Match/Locale match', - 'G' => 'No match at all', - 'H' => 'Zip Match/Locale match', - 'J' => 'Issuer does not participate in Global AVS', - 'JA' => 'International street address and postal match', - 'JB' => 'International street address match. Postal code not verified', - 'JC' => 'International street address and postal code not verified', - 'JD' => 'International postal code match. Street address not verified', - 'M1' => 'Cardholder name matches', - 'M2' => 'Cardholder name, billing address, and postal code matches', - 'M3' => 'Cardholder name and billing code matches', - 'M4' => 'Cardholder name and billing address match', - 'M5' => 'Cardholder name incorrect, billing address and postal code match', - 'M6' => 'Cardholder name incorrect, billing postal code matches', - 'M7' => 'Cardholder name incorrect, billing address matches', - 'M8' => 'Cardholder name, billing address and postal code are all incorrect', - 'N3' => 'Address matches, ZIP not verified', - 'N4' => 'Address and ZIP code not verified due to incompatible formats', - 'N5' => 'Address and ZIP code match (International only)', - 'N6' => 'Address not verified (International only)', - 'N7' => 'ZIP matches, address not verified', - 'N8' => 'Address and ZIP code match (International only)', - 'N9' => 'Address and ZIP code match (UK only)', - 'R' => 'Issuer does not participate in AVS', - 'UK' => 'Unknown', - 'X' => 'Zip Match/Zip 4 Match/Address Match', - 'Z' => 'Zip Match/Locale no match', + '1' => 'No address supplied', + '2' => 'Bill-to address did not pass Auth Host edit checks', + '3' => 'AVS not performed', + '4' => 'Issuer does not participate in AVS', + '5' => 'Edit-error - AVS data is invalid', + '6' => 'System unavailable or time-out', + '7' => 'Address information unavailable', + '8' => 'Transaction Ineligible for AVS', + '9' => 'Zip Match/Zip 4 Match/Locale match', + 'A' => 'Zip Match/Zip 4 Match/Locale no match', + 'B' => 'Zip Match/Zip 4 no Match/Locale match', + 'C' => 'Zip Match/Zip 4 no Match/Locale no match', + 'D' => 'Zip No Match/Zip 4 Match/Locale match', + 'E' => 'Zip No Match/Zip 4 Match/Locale no match', + 'F' => 'Zip No Match/Zip 4 No Match/Locale match', + 'G' => 'No match at all', + 'H' => 'Zip Match/Locale match', + 'J' => 'Issuer does not participate in Global AVS', + 'JA' => 'International street address and postal match', + 'JB' => 'International street address match. Postal code not verified', + 'JC' => 'International street address and postal code not verified', + 'JD' => 'International postal code match. Street address not verified', + 'M1' => 'Cardholder name matches', + 'M2' => 'Cardholder name, billing address, and postal code matches', + 'M3' => 'Cardholder name and billing code matches', + 'M4' => 'Cardholder name and billing address match', + 'M5' => 'Cardholder name incorrect, billing address and postal code match', + 'M6' => 'Cardholder name incorrect, billing postal code matches', + 'M7' => 'Cardholder name incorrect, billing address matches', + 'M8' => 'Cardholder name, billing address and postal code are all incorrect', + 'N3' => 'Address matches, ZIP not verified', + 'N4' => 'Address and ZIP code not verified due to incompatible formats', + 'N5' => 'Address and ZIP code match (International only)', + 'N6' => 'Address not verified (International only)', + 'N7' => 'ZIP matches, address not verified', + 'N8' => 'Address and ZIP code match (International only)', + 'N9' => 'Address and ZIP code match (UK only)', + 'R' => 'Issuer does not participate in AVS', + 'UK' => 'Unknown', + 'X' => 'Zip Match/Zip 4 Match/Address Match', + 'Z' => 'Zip Match/Locale no match' } # Map vendor's AVS result code to a postal match code ORBITAL_POSTAL_MATCH_CODE = { - 'Y' => %w( 9 A B C H JA JD M2 M3 M5 N5 N8 N9 X Z ), - 'N' => %w( D E F G M8 ), - 'X' => %w( 4 J R ), - nil => %w( 1 2 3 5 6 7 8 JB JC M1 M4 M6 M7 N3 N4 N6 N7 UK ) + 'Y' => %w(9 A B C H JA JD M2 M3 M5 N5 N8 N9 X Z), + 'N' => %w(D E F G M8), + 'X' => %w(4 J R), + nil => %w(1 2 3 5 6 7 8 JB JC M1 M4 M6 M7 N3 N4 N6 N7 UK) }.inject({}) do |map, (type, codes)| codes.each { |code| map[code] = type } map @@ -867,10 +1270,10 @@ class AVSResult < ActiveMerchant::Billing::AVSResult # Map vendor's AVS result code to a street match code ORBITAL_STREET_MATCH_CODE = { - 'Y' => %w( 9 B D F H JA JB M2 M4 M5 M6 M7 N3 N5 N7 N8 N9 X ), - 'N' => %w( A C E G M8 Z ), - 'X' => %w( 4 J R ), - nil => %w( 1 2 3 5 6 7 8 JC JD M1 M3 N4 N6 UK ) + 'Y' => %w(9 B D F H JA JB M2 M4 M5 M6 M7 N3 N5 N7 N8 N9 X), + 'N' => %w(A C E G M8 Z), + 'X' => %w(4 J R), + nil => %w(1 2 3 5 6 7 8 JC JD M1 M3 N4 N6 UK) }.inject({}) do |map, (type, codes)| codes.each { |code| map[code] = type } map 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 da1d82d0ca0..3522b3f97e4 100644 --- a/lib/active_merchant/billing/gateways/orbital/orbital_soft_descriptors.rb +++ b/lib/active_merchant/billing/gateways/orbital/orbital_soft_descriptors.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class OrbitalSoftDescriptors < Model PHONE_FORMAT_1 = /\A\d{3}-\d{3}-\d{4}\z/ PHONE_FORMAT_2 = /\A\d{3}-\w{7}\z/ @@ -30,14 +30,10 @@ def validate errors << [:merchant_name, 'is required'] if self.merchant_name.blank? errors << [:merchant_name, 'is required to be 25 bytes or less'] if self.merchant_name.bytesize > 25 - if(!empty?(self.merchant_phone) && !self.merchant_phone.match(PHONE_FORMAT_1) && !self.merchant_phone.match(PHONE_FORMAT_2)) - errors << [:merchant_phone, 'is required to follow "NNN-NNN-NNNN" or "NNN-AAAAAAA" format'] - end + 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) - [: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 + %i[merchant_email merchant_url].each do |attr| + 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 1fd15b5c0ae..073a866ac24 100644 --- a/lib/active_merchant/billing/gateways/pac_net_raven.rb +++ b/lib/active_merchant/billing/gateways/pac_net_raven.rb @@ -1,7 +1,6 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class PacNetRavenGateway < Gateway - AVS_ADDRESS_CODES = { 'avs_address_unavailable' => 'X', 'avs_address_not_checked' => 'X', @@ -29,7 +28,7 @@ class PacNetRavenGateway < Gateway self.test_url = self.live_url self.supported_countries = ['US'] - self.supported_cardtypes = [:visa, :master] + self.supported_cardtypes = %i[visa master] self.money_format = :cents self.default_currency = 'USD' self.homepage_url = 'https://www.deepcovelabs.com/raven' @@ -107,7 +106,7 @@ def add_address(post, options) end def parse(body) - Hash[body.split('&').map{|x| x.split('=').map{|y| CGI.unescape(y)}}] + Hash[body.split('&').map { |x| x.split('=').map { |y| CGI.unescape(y) } }] end def commit(action, money, parameters) @@ -122,15 +121,18 @@ def commit(action, money, parameters) test_mode = test? || message =~ /TESTMODE/ - Response.new(success?(response), message, response, - :test => test_mode, - :authorization => response['TrackingNumber'], - :fraud_review => fraud_review?(response), - :avs_result => { - :postal_match => AVS_POSTAL_CODES[response['AVSPostalResponseCode']], - :street_match => AVS_ADDRESS_CODES[response['AVSAddressResponseCode']] - }, - :cvv_result => CVV2_CODES[response['CVV2ResponseCode']] + Response.new( + success?(response), + message, + response, + test: test_mode, + authorization: response['TrackingNumber'], + fraud_review: fraud_review?(response), + avs_result: { + postal_match: AVS_POSTAL_CODES[response['AVSPostalResponseCode']], + street_match: AVS_ADDRESS_CODES[response['AVSAddressResponseCode']] + }, + cvv_result: CVV2_CODES[response['CVV2ResponseCode']] ) end @@ -140,6 +142,7 @@ def url(action) def endpoint(action) return 'void' if action == 'void' + 'submit' end @@ -149,9 +152,9 @@ def fraud_review?(response) def success?(response) if %w(cc_settle cc_debit cc_preauth cc_refund).include?(response[:action]) - !response['ApprovalCode'].nil? and response['ErrorCode'].nil? and response['Status'] == 'Approved' + !response['ApprovalCode'].nil? && response['ErrorCode'].nil? && (response['Status'] == 'Approved') elsif response[:action] = 'void' - !response['ApprovalCode'].nil? and response['ErrorCode'].nil? and response['Status'] == 'Voided' + !response['ApprovalCode'].nil? && response['ErrorCode'].nil? && (response['Status'] == 'Voided') end end @@ -179,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 @@ -192,16 +194,16 @@ def request_id end def signature(action, post, parameters = {}) - string = if %w(cc_settle cc_debit cc_preauth cc_refund).include?(action) - post['UserName'] + post['Timestamp'] + post['RequestID'] + post['PymtType'] + parameters['Amount'].to_s + parameters['Currency'] - elsif action == 'void' - post['UserName'] + post['Timestamp'] + post['RequestID'] + parameters['TrackingNumber'] - else - post['UserName'] - end + string = + if %w(cc_settle cc_debit cc_preauth cc_refund).include?(action) + post['UserName'] + post['Timestamp'] + post['RequestID'] + post['PymtType'] + parameters['Amount'].to_s + parameters['Currency'] + elsif action == 'void' + post['UserName'] + post['Timestamp'] + post['RequestID'] + parameters['TrackingNumber'] + else + post['UserName'] + end OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new(@options[:secret]), @options[:secret], string) end end end end - diff --git a/lib/active_merchant/billing/gateways/pagarme.rb b/lib/active_merchant/billing/gateways/pagarme.rb index 77786486739..f4239f015d6 100644 --- a/lib/active_merchant/billing/gateways/pagarme.rb +++ b/lib/active_merchant/billing/gateways/pagarme.rb @@ -1,29 +1,29 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class PagarmeGateway < Gateway self.live_url = 'https://api.pagar.me/1/' self.supported_countries = ['BR'] self.default_currency = 'BRL' self.money_format = :cents - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club] + self.supported_cardtypes = %i[visa master american_express discover diners_club] self.homepage_url = 'https://pagar.me/' self.display_name = 'Pagar.me' STANDARD_ERROR_CODE_MAPPING = { 'refused' => STANDARD_ERROR_CODE[:card_declined], - 'processing_error' => STANDARD_ERROR_CODE[:processing_error], + 'processing_error' => STANDARD_ERROR_CODE[:processing_error] } - def initialize(options={}) + def initialize(options = {}) requires!(options, :api_key) @api_key = options[:api_key] super end - def purchase(money, payment_method, options={}) + def purchase(money, payment_method, options = {}) post = {} add_amount(post, money) add_payment_method(post, payment_method) @@ -32,7 +32,7 @@ def purchase(money, payment_method, options={}) commit(:post, 'transactions', post) end - def authorize(money, payment_method, options={}) + def authorize(money, payment_method, options = {}) post = {} add_amount(post, money) add_payment_method(post, payment_method) @@ -43,33 +43,27 @@ def authorize(money, payment_method, options={}) commit(:post, 'transactions', post) end - def capture(money, authorization, options={}) - if authorization.nil? - return Response.new(false, 'Não é possível capturar uma transação sem uma prévia autorização.') - end + def capture(money, authorization, options = {}) + return Response.new(false, 'Não é possível capturar uma transação sem uma prévia autorização.') if authorization.nil? post = {} commit(:post, "transactions/#{authorization}/capture", post) end - def refund(money, authorization, options={}) - if authorization.nil? - return Response.new(false, 'Não é possível estornar uma transação sem uma prévia captura.') - end + def refund(money, authorization, options = {}) + return Response.new(false, 'Não é possível estornar uma transação sem uma prévia captura.') if authorization.nil? void(authorization, options) end - def void(authorization, options={}) - if authorization.nil? - return Response.new(false, 'Não é possível estornar uma transação autorizada sem uma prévia autorização.') - end + def void(authorization, options = {}) + return Response.new(false, 'Não é possível estornar uma transação autorizada sem uma prévia autorização.') if authorization.nil? post = {} commit(:post, "transactions/#{authorization}/refund", post) end - def verify(payment_method, options={}) + def verify(payment_method, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(127, payment_method, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -105,7 +99,7 @@ def add_credit_card(post, credit_card) post[:card_cvv] = credit_card.verification_value end - def add_metadata(post, options={}) + def add_metadata(post, options = {}) post[:metadata] = {} post[:metadata][:order_id] = options[:order_id] post[:metadata][:ip] = options[:ip] @@ -125,6 +119,7 @@ def post_data(params) params.map do |key, value| next if value != false && value.blank? + if value.is_a?(Hash) h = {} value.each do |k, v| @@ -175,15 +170,13 @@ def commit(method, url, parameters, options = {}) end def response_error(raw_response) - begin - parse(raw_response) - rescue JSON::ParserError - json_error(raw_response) - end + parse(raw_response) + rescue JSON::ParserError + json_error(raw_response) end def json_error(raw_response) - msg = 'Resposta inválida retornada pela API do Pagar.me. Por favor entre em contato com suporte@pagar.me se você continuar recebendo essa mensagem.' + msg = 'Resposta inválida retornada pela API do Pagar.me. Por favor entre em contato com suporte@pagar.me se você continuar recebendo essa mensagem.' msg += " (A resposta retornada pela API foi #{raw_response.inspect})" { 'errors' => [{ @@ -214,7 +207,7 @@ def message_from(response) when 'refunded' 'Transação estornada' else - "Transação com status '#{response["status"]}'" + "Transação com status '#{response['status']}'" end elsif failure_from(response) 'Transação recusada' @@ -227,12 +220,10 @@ def message_from(response) end def authorization_from(response) - if success_from(response) - response['id'] - end + response['id'] if success_from(response) end - def test?() + def test? @api_key.start_with?('ak_test') end diff --git a/lib/active_merchant/billing/gateways/pago_facil.rb b/lib/active_merchant/billing/gateways/pago_facil.rb index 85793fbb369..e1c5be0d1dc 100644 --- a/lib/active_merchant/billing/gateways/pago_facil.rb +++ b/lib/active_merchant/billing/gateways/pago_facil.rb @@ -1,22 +1,22 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class PagoFacilGateway < Gateway self.test_url = 'https://www.pagofacil.net/st/public/Wsrtransaccion/index/format/json?' self.live_url = 'https://www.pagofacil.net/ws/public/Wsrtransaccion/index/format/json?' self.supported_countries = ['MX'] self.default_currency = 'MXN' - self.supported_cardtypes = [:visa, :master, :american_express, :jcb] + self.supported_cardtypes = %i[visa master american_express jcb] self.homepage_url = 'http://www.pagofacil.net/' self.display_name = 'PagoFacil' - def initialize(options={}) + def initialize(options = {}) requires!(options, :branch_id, :merchant_id, :service_id) super end - def purchase(money, credit_card, options={}) + def purchase(money, credit_card, options = {}) post = {} add_invoice(post, money, options) add_payment(post, credit_card) @@ -53,9 +53,7 @@ def add_invoice(post, money, options) def add_currency(post, money, options) currency = options.fetch(:currency, currency(money)) - unless currency == self.class.default_currency - post[:divisa] = currency - end + post[:divisa] = currency unless currency == self.class.default_currency end def add_payment(post, credit_card) diff --git a/lib/active_merchant/billing/gateways/pay_arc.rb b/lib/active_merchant/billing/gateways/pay_arc.rb new file mode 100644 index 00000000000..391ced8e925 --- /dev/null +++ b/lib/active_merchant/billing/gateways/pay_arc.rb @@ -0,0 +1,392 @@ +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class PayArcGateway < Gateway + self.test_url = 'https://testapi.payarc.net/v1' + self.live_url = 'https://api.payarc.net/v1' + + self.supported_countries = ['US'] + self.default_currency = 'usd' + self.supported_cardtypes = %i[visa master american_express discover jcb] + + self.homepage_url = 'https://www.payarc.net/' + self.display_name = 'PAYARC Gateway' + + STANDARD_ERROR_CODE_MAPPING = {} + STANDARD_ACTIONS = { + token: + { end_point: 'tokens', + allowed_fields: %i[card_source card_number exp_month exp_year cvv card_holder_name + address_line1 address_line2 city state zip country] }, + capture: + { end_point: 'charges', + allowed_fields: %i[amount statement_description card_id currency customer_id token_id card_source tip_amount + card_level sales_tax purchase_order supplier_reference_number customer_ref_id ship_to_zip + amex_descriptor customer_vat_number summary_commodity_code shipping_charges duty_charges + ship_from_zip destination_country_code vat_invoice order_date tax_category tax_type + tax_amount tax_rate address_line1 zip terminal_id surcharge description email receipt_phone statement_descriptor ] }, + void: + { end_point: 'charges/{{chargeID}}/void', + allowed_fields: %i[reason void_description] }, + refund: + { end_point: 'charges/{{charge_id}}/refunds', + allowed_fields: %i[amount reason description] }, + credit: + { end_point: 'refunds/wo_reference', + allowed_fields: %i[amount charge_description statement_description terminal_id card_source card_number + exp_month exp_year cvv card_holder_name address_line1 address_line2 city state zip + country currency reason receipt_phone receipt_email ] } + } + + SUCCESS_STATUS = %w[ + submitted_for_settlement authorized partially_submitted_for_settlement + credit partial_refund void refunded settled + ] + + FAILURE_STATUS = %w[not_processed failed_by_gateway invalid_track_data authorization_expired] + + # The gateway must be configured with Bearer token. + # + # :api_key PAYARC's Bearer token must be passsed to initialise the gateway. + + def initialize(options = {}) + requires!(options, :api_key) + super + end + + # + # Purchase API through PAYARC. + # + # :money A positive integer in cents representing how much to charge. The minimum amount is 50c USD. + # + # :creditcard CreditCard object with card details. + # + # :options Other information like address, card source etc can be passed in options + # + # ==== Options + # + # * :card_source -- Source of payment (REQUIRED) ( INTERNET, SWIPE, PHONE, MAIL, MANUAL ) + # * :currency -- Three-letter ISO currency code, in lowercase (REQUIRED) + # * :card_holder_name --Name of the Card Holder (OPTIONAL) + # * :address_line1 -- Set in payment method's billing address (OPTIONAL) + # * :address_line2 -- Set in payment method's billing address (OPTIONAL) + # * :state -- State (OPTIONAL) + # * :country -- Country (OPTIONAL) + # * :statement_description -- An arbitrary string to be displayed on your costomer's credit card statement. This may be up to 22 characters. (OPTIONAL) + # * :card_level -- Commercial card level - "LEVEL2" OR "LEVEL3" (OPTIONAL) + # * :sales_tax -- A positive integer in cents representing sales tax. (OPTIONAL) + # * :terminal_id -- Optional terminal id. (OPTIONAL) + # * :tip_amount -- A positive integer in cents representing tip amount. (OPTIONAL) + # * :sales_tax -- Applicable for LEVEL2 or LEVEL3 Charge. A positive integer in cents representing sales tax. (REQUIRED for LEVEL2 0r LEVEL3) + # * :purchase_order -- Applicable for Level2 or Level3 Charge. The value used by the customer to identify an order. Issued by the buyer to the seller. (REQUIRED for LEVEL2 0r LEVEL3) + # * :order_date -- Applicable for Level2 Charge for AMEX card only or Level3 Charge. The date the order was processed. Format: Alphanumeric and Special Character |Min Length=0 Max Length=10|Allowed format: MM/DD/YYYY For example: 12/01/2016 + # * :customer_ref_id -- Applicable for Level2 Charge for AMEX card only or Level3 Charge. The reference identifier supplied by the Commercial Card cardholder. Format: Alphanumeric and Special Character |Min Length=0 Max Length=17| a-z A-Z 0-9 Space <> + # * :ship_to_zip -- Applicable for Level2 Charge for AMEX card only or Level3 Charge. The postal code for the address to which the goods are being shipped. Format: Alphanumeric |Min Length=2 Max Length=10 + # * :amex_descriptor -- Applicable for Level2 Charge for AMEX card only. The value of the Transaction Advice Addendum field, displays descriptive information about a transactions on a customer's AMEX card statement. Format: Alphanumeric and Special Character |Min Length=0 Max Length=40|a-z A-Z 0-9 Space <> + # * :supplier_reference_number -- Applicable for Level2 Charge for AMEX card only or Level3 charge. The value used by the customer to identify an order. Issued by the buyer to the seller. + # * :tax_amount -- Applicable for Level3 Charge. The tax amount. Format: Numeric|Max Length=12|Allowed characters: 0-9 .(dot) Note: If a decimal point is included, the amount reflects a dollar value. If a decimal point is not included, the amount reflects a cent value. + # * :tax_category -- Applicable for Level3 Charge. The type of tax. Formerly established through TaxCategory messages. Allowed values: SERVICE, DUTY, VAT, ALTERNATE, NATIONAL, TAX_EXEMPT + # * :customer_vat_number -- Applicable for Level3 Charge. Indicates the customer's government assigned tax identification number or the identification number assigned to their purchasing company by the tax authorities. Format: Alphanumeric and Special Character|Min Length=0 Max Length=13| a-z A-Z 0-9 Space <> + # * :summary_commodity_code -- Applicable for Level3 Charge. The international description code of the overall goods or services being supplied. Format: Alphanumeric and Special Character |Min Length=0 Max Length=4|Allowed character: a-z A-Z 0-9 Space <> + # * :shipping_charges -- Applicable for Level3 Charge. The dollar amount for shipping or freight charges applied to a product or transaction. Format: Numeric |Max Length=12|Allowed characters: 0-9 .(dot) Note: If a decimal point is included, the amount reflects a dollar value. If a decimal point is not included, the amount reflects a cent value. + # * :duty_charges -- Applicable for Level3 Charge. Indicates the total charges for any import or export duties included in the order. Format: Numeric |Max Length=12|Allowed characters: 0-9 . (dot) Note: If a decimal point is included, the amount reflects a dollar value. If a decimal point is not included, the amount reflects a cent value. + # * :ship_from_zip -- Applicable for Level3 Charge. The postal code for the address to which the goods are being shipped. Format: Alphanumeric |Min Length=2 Max Length=10 + # * :destination_country_code -- Applicable for Level3 Charge. The destination country code indicator. Format: Alphanumeric. + # * :tax_type -- Applicable for Level3 Charge. The type of tax. For example, VAT, NATIONAL, Service Tax. Format: Alphanumeric and Special Character + # * :vat_invoice -- Applicable for Level3 Charge. The Value Added Tax (VAT) invoice number associated with the transaction. Format: Alphanumeric and Special Character |Min Length=0 Max Length=15|Allowed character: a-z A-Z 0-9 Space <> + # * :tax_rate -- Applicable for Level3 Charge. The type of tax rate. This field is used if taxCategory is not used. Default sale tax rate in percentage Must be between 0.1% - 22% ,Applicable only Level 2 AutoFill. Format: Decimal Number |Max Length=4|Allowed characters: 0-9 .(dot) Allowed range: 0.01 - 100 + # * :email -- Customer's email address sent with payment method. + + def purchase(money, creditcard, options = {}) + options[:capture] = 1 + MultiResponse.run do |r| + r.process { token(creditcard, options) } + r.process { charge(money, r.authorization, options) } + end + end + + # + # Authorize the payment API through PAYARC. + # + # :money A positive integer in cents representing how much to charge. The minimum amount is 50c USD. + # + # :creditcard CreditCard object with card details. + # + # :options Other information like address, card source etc can be passed in options + # + # ==== Options + # + # * :card_source -- Source of payment (REQUIRED) ( INTERNET, SWIPE, PHONE, MAIL, MANUAL ) + # * :currency -- Three-letter ISO currency code, in lowercase (REQUIRED) + # * :card_holder_name --Name of the Card Holder (OPTIONAL) + # * :address_line1 -- Set in payment method's billing address (OPTIONAL) + # * :address_line2 -- Set in payment method's billing address (OPTIONAL) + # * :state -- State (OPTIONAL) + # * :country -- Country (OPTIONAL) + # * :statement_description -- An arbitrary string to be displayed on your costomer's credit card statement. This may be up to 22 characters. (OPTIONAL) + # * :card_level -- Commercial card level - "LEVEL2" OR "LEVEL3" (OPTIONAL) + # * :sales_tax -- A positive integer in cents representing sales tax. (OPTIONAL) + # * :terminal_id -- Optional terminal id. (OPTIONAL) + # * :tip_amount -- A positive integer in cents representing tip amount. (OPTIONAL) + # * :sales_tax -- Applicable for LEVEL2 or LEVEL3 Charge. A positive integer in cents representing sales tax. (REQUIRED for LEVEL2 0r LEVEL3) + # * :purchase_order -- Applicable for Level2 or Level3 Charge. The value used by the customer to identify an order. Issued by the buyer to the seller. (REQUIRED for LEVEL2 0r LEVEL3) + # * :order_date -- Applicable for Level2 Charge for AMEX card only or Level3 Charge. The date the order was processed. Format: Alphanumeric and Special Character |Min Length=0 Max Length=10|Allowed format: MM/DD/YYYY For example: 12/01/2016 + # * :customer_ref_id -- Applicable for Level2 Charge for AMEX card only or Level3 Charge. The reference identifier supplied by the Commercial Card cardholder. Format: Alphanumeric and Special Character |Min Length=0 Max Length=17| a-z A-Z 0-9 Space <> + # * :ship_to_zip -- Applicable for Level2 Charge for AMEX card only or Level3 Charge. The postal code for the address to which the goods are being shipped. Format: Alphanumeric |Min Length=2 Max Length=10 + # * :amex_descriptor -- Applicable for Level2 Charge for AMEX card only. The value of the Transaction Advice Addendum field, displays descriptive information about a transactions on a customer's AMEX card statement. Format: Alphanumeric and Special Character |Min Length=0 Max Length=40|a-z A-Z 0-9 Space <> + # * :supplier_reference_number -- Applicable for Level2 Charge for AMEX card only or Level3 charge. The value used by the customer to identify an order. Issued by the buyer to the seller. + # * :tax_amount -- Applicable for Level3 Charge. The tax amount. Format: Numeric|Max Length=12|Allowed characters: 0-9 .(dot) Note: If a decimal point is included, the amount reflects a dollar value. If a decimal point is not included, the amount reflects a cent value. + # * :tax_category -- Applicable for Level3 Charge. The type of tax. Formerly established through TaxCategory messages. Allowed values: SERVICE, DUTY, VAT, ALTERNATE, NATIONAL, TAX_EXEMPT + # * :customer_vat_number -- Applicable for Level3 Charge. Indicates the customer's government assigned tax identification number or the identification number assigned to their purchasing company by the tax authorities. Format: Alphanumeric and Special Character|Min Length=0 Max Length=13| a-z A-Z 0-9 Space <> + # * :summary_commodity_code -- Applicable for Level3 Charge. The international description code of the overall goods or services being supplied. Format: Alphanumeric and Special Character |Min Length=0 Max Length=4|Allowed character: a-z A-Z 0-9 Space <> + # * :shipping_charges -- Applicable for Level3 Charge. The dollar amount for shipping or freight charges applied to a product or transaction. Format: Numeric |Max Length=12|Allowed characters: 0-9 .(dot) Note: If a decimal point is included, the amount reflects a dollar value. If a decimal point is not included, the amount reflects a cent value. + # * :duty_charges -- Applicable for Level3 Charge. Indicates the total charges for any import or export duties included in the order. Format: Numeric |Max Length=12|Allowed characters: 0-9 . (dot) Note: If a decimal point is included, the amount reflects a dollar value. If a decimal point is not included, the amount reflects a cent value. + # * :ship_from_zip -- Applicable for Level3 Charge. The postal code for the address to which the goods are being shipped. Format: Alphanumeric |Min Length=2 Max Length=10 + # * :destination_country_code -- Applicable for Level3 Charge. The destination country code indicator. Format: Alphanumeric. + # * :tax_type -- Applicable for Level3 Charge. The type of tax. For example, VAT, NATIONAL, Service Tax. Format: Alphanumeric and Special Character + # * :vat_invoice -- Applicable for Level3 Charge. The Value Added Tax (VAT) invoice number associated with the transaction. Format: Alphanumeric and Special Character |Min Length=0 Max Length=15|Allowed character: a-z A-Z 0-9 Space <> + # * :tax_rate -- Applicable for Level3 Charge. The type of tax rate. This field is used if taxCategory is not used. Default sale tax rate in percentage Must be between 0.1% - 22% ,Applicable only Level 2 AutoFill. Format: Decimal Number |Max Length=4|Allowed characters: 0-9 .(dot) Allowed range: 0.01 - 100 + # * :email -- Customer's email address. + + def authorize(money, creditcard, options = {}) + options[:capture] = '0' + MultiResponse.run do |r| + r.process { token(creditcard, options) } + r.process { charge(money, r.authorization, options) } + end + end + + # + # Capture the payment of an existing, uncaptured, charge. + # This is the second half of the two-step payment flow, where first you created / authorized a charge + # with the capture option set to false. + # + # :money A positive integer in cents representing how much to charge. The minimum amount is 50c USD. + # + # :tx_reference charge_id from previously created / authorized a charge + # + # :options Other information like address, card source etc can be passed in options + + def capture(money, tx_reference, options = {}) + post = {} + add_money(post, money, options) + action = "#{STANDARD_ACTIONS[:capture][:end_point]}/#{tx_reference}/capture" + post = filter_gateway_fields(post, options, STANDARD_ACTIONS[:capture][:allowed_fields]) + commit(action, post) + end + + # + # Voids the transaction / charge. + # + # :tx_reference charge_id from previously created charge + # + # :options Other information like address, card source etc can be passed in options + # + # ==== Options + # + # * :reason -- Reason for voiding transaction (REQUIRED) ( requested_by_customer, duplicate, fraudulent, other ) + + def void(tx_reference, options = {}) + post = {} + post['reason'] = options[:reason] + action = STANDARD_ACTIONS[:void][:end_point].gsub(/{{chargeID}}/, tx_reference) + post = filter_gateway_fields(post, options, STANDARD_ACTIONS[:void][:allowed_fields]) + commit(action, post) + end + + # + # Refund full / partial payment of an successful charge / capture / purchase. + # + # :money A positive integer in cents representing how much to charge. The minimum amount is 50c USD. + # + # :tx_reference charge_id from previously created / authorized a charge + # + # :options Other information like address, card source etc can be passed in options + + def refund(money, tx_reference, options = {}) + post = {} + add_money(post, money, options) + action = STANDARD_ACTIONS[:refund][:end_point].gsub(/{{charge_id}}/, tx_reference) + post = filter_gateway_fields(post, options, STANDARD_ACTIONS[:refund][:allowed_fields]) + commit(action, post) + end + + def credit(money, creditcard, options = {}) + post = {} + add_money(post, money, options) + add_creditcard(post, creditcard, options) + add_address(post, options) + add_phone(post, options) + post['receipt_email'] = options[:email] if options[:email] + action = STANDARD_ACTIONS[:credit][:end_point] + post = filter_gateway_fields(post, options, STANDARD_ACTIONS[:credit][:allowed_fields]) + commit(action, post) + end + + # + # Verify the creditcard API through PAYARC. + # + # :creditcard CreditCard object with card details. + # + # :options Other information like address, card source etc can be passed in options + # + # ==== Options + # + # * :card_source -- Source of payment (REQUIRED) ( INTERNET, SWIPE, PHONE, MAIL, MANUAL ) + # * :card_holder_name --Name of the Card Holder (OPTIONAL) + # * :address_line1 -- Set in payment method's billing address (OPTIONAL) + # * :address_line2 -- Set in payment method's billing address (OPTIONAL) + # * :state -- State (OPTIONAL) + # * :country -- Country (OPTIONAL) + + def verify(creditcard, options = {}) + token(creditcard, options) + end + + # :nodoc: + def token(creditcard, options = {}) + post = {} + post['authorize_card'] = 1 + post['card_source'] = options[:card_source] + add_creditcard(post, creditcard, options) + add_address(post, options) + post = filter_gateway_fields(post, options, STANDARD_ACTIONS[:token][:allowed_fields]) + commit(STANDARD_ACTIONS[:token][:end_point], post) + end + + def supports_scrubbing? # :nodoc: + true + end + + def scrub(transcript) + # :nodoc: + transcript. + gsub(%r((Authorization: Bearer )[^\s]+\s)i, '\1[FILTERED]\2'). + gsub(%r((&?card_number=)[^&]*)i, '\1[FILTERED]'). + gsub(%r((&?cvv=)[^&]*)i, '\1[BLANK]') + end + + private + + def charge(money, authorization, options = {}) + post = {} + post['token_id'] = authorization + post['capture'] = options[:capture] || 1 + add_money(post, money, options) + add_phone(post, options) + post = filter_gateway_fields(post, options, STANDARD_ACTIONS[:capture][:allowed_fields]) + commit(STANDARD_ACTIONS[:capture][:end_point], post) + end + + def add_creditcard(post, creditcard, options) + post['card_number'] = creditcard.number + post['exp_month'] = format(creditcard.month, :two_digits) + post['exp_year'] = creditcard.year + post['cvv'] = creditcard.verification_value unless creditcard.verification_value.nil? + post['card_holder_name'] = options[:card_holder_name] || "#{creditcard.first_name} #{creditcard.last_name}" + end + + def add_address(post, options) + return unless billing_address = options[:billing_address] + + post['address_line1'] = billing_address[:address1] + post['address_line2'] = billing_address[:address2] + post['city'] = billing_address[:city] + post['state'] = billing_address[:state] + post['zip'] = billing_address[:zip] + post['country'] = billing_address[:country] + end + + def add_phone(post, options) + post['phone_number'] = options[:billing_address][:phone] if options.dig(:billing_address, :phone) + end + + def add_money(post, money, options) + post['amount'] = money + post['currency'] = currency(money) unless options[:currency] + post['statement_description'] = options[:statement_description] + end + + def headers(api_key) + { + 'Authorization' => 'Bearer ' + api_key.strip, + 'Accept' => 'application/json', + 'User-Agent' => "PayArc ActiveMerchantBindings/#{ActiveMerchant::VERSION}" + } + end + + def parse(body) + JSON.parse(body) + rescue JSON::ParserError + body + end + + def filter_gateway_fields(post, options, gateway_fields) + filtered_options = options.slice(*gateway_fields).compact + post.update(filtered_options) + post + end + + def commit(action, parameters) + url = (test? ? test_url : live_url) + headers = headers(@options[:api_key]) + end_point = "#{url}/#{action}" + begin + response = ssl_post(end_point, post_data(parameters), headers) + parsed_response = parse(response) + + Response.new( + success_from(parsed_response, action), + message_from(parsed_response, action), + parsed_response, + test: test?, + authorization: parse_response_id(parsed_response), + error_code: error_code_from(parsed_response, action) + ) + rescue ResponseError => e + parsed_response = parse(e.response.body) + Response.new( + false, + message_from(parsed_response, action), + parsed_response, + test: test?, + authorization: nil, + error_code: error_code_from(parsed_response, action) + ) + end + end + + def success_from(response, action) + if action == STANDARD_ACTIONS[:token][:end_point] + token = parse_response_id(response) + (!token.nil? && !token.empty?) + elsif response + return SUCCESS_STATUS.include? response['data']['status'] if response['data'] + end + end + + def message_from(response, action) + if success_from(response, action) + if action == STANDARD_ACTIONS[:token][:end_point] + return response['data']['id'] + else + return response['data']['status'] + end + else + return response['message'] + end + end + + def parse_response_id(response) + response['data']['id'] if response && response['data'] + end + + def post_data(params) + params.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&') + end + + def error_code_from(response, action) + response['status_code'] unless success_from(response, action) + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/pay_conex.rb b/lib/active_merchant/billing/gateways/pay_conex.rb index bb97a65cde7..1a7f263bf82 100644 --- a/lib/active_merchant/billing/gateways/pay_conex.rb +++ b/lib/active_merchant/billing/gateways/pay_conex.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class PayConexGateway < Gateway include Empty @@ -8,36 +8,36 @@ class PayConexGateway < Gateway self.supported_countries = %w(US CA) self.default_currency = 'USD' - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :jcb, :diners_club] + self.supported_cardtypes = %i[visa master american_express discover jcb diners_club] self.homepage_url = 'http://www.bluefincommerce.com/' self.display_name = 'PayConex' - def initialize(options={}) + def initialize(options = {}) requires!(options, :account_id, :api_accesskey) super end - def purchase(money, payment_method, options={}) + def purchase(money, payment_method, options = {}) post = {} add_auth_purchase_params(post, money, payment_method, options) commit('SALE', post) end - def authorize(money, payment_method, options={}) + def authorize(money, payment_method, options = {}) post = {} add_auth_purchase_params(post, money, payment_method, options) commit('AUTHORIZATION', post) end - def capture(money, authorization, options={}) + def capture(money, authorization, options = {}) post = {} add_reference_params(post, authorization, options) add_amount(post, money, options) commit('CAPTURE', post) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) post = {} add_reference_params(post, authorization, options) add_amount(post, money, options) @@ -50,21 +50,19 @@ def void(authorization, options = {}) commit('REVERSAL', post) end - def credit(money, payment_method, options={}) - if payment_method.is_a?(String) - raise ArgumentError, 'Reference credits are not supported. Please supply the original credit card or use the #refund method.' - end + def credit(money, payment_method, options = {}) + raise ArgumentError, 'Reference credits are not supported. Please supply the original credit card or use the #refund method.' if payment_method.is_a?(String) post = {} add_auth_purchase_params(post, money, payment_method, options) commit('CREDIT', post) end - def verify(payment_method, options={}) + def verify(payment_method, options = {}) authorize(0, payment_method, options) end - def store(payment_method, options={}) + def store(payment_method, options = {}) post = {} add_credentials(post) add_payment_method(post, payment_method) @@ -81,14 +79,17 @@ def scrub(transcript) force_utf8(transcript). gsub(%r((api_accesskey=)\w+), '\1[FILTERED]'). gsub(%r((card_number=)\w+), '\1[FILTERED]'). - gsub(%r((card_verification=)\w+), '\1[FILTERED]') + gsub(%r((card_verification=)\w+), '\1[FILTERED]'). + gsub(%r((bank_account_number=)\w+), '\1[FILTERED]'). + gsub(%r((bank_routing_number=)\w+), '\1[FILTERED]') end private def force_utf8(string) return nil unless string - binary = string.encode('BINARY', invalid: :replace, undef: :replace, replace: '?') # Needed for Ruby 2.0 since #encode is a no-op if the string is already UTF-8. It's not needed for Ruby 2.1 and up since it's not a no-op there. + + binary = string.encode('BINARY', invalid: :replace, undef: :replace, replace: '?') # Needed for Ruby 2.0 since #encode is a no-op if the string is already UTF-8. It's not needed for Ruby 2.1 and up since it's not a no-op there. binary.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?') end @@ -209,11 +210,10 @@ def commit(action, params) message_from(response), response, authorization: response['transaction_id'], - :avs_result => AVSResult.new(code: response['avs_response']), - :cvv_result => CVVResult.new(response['cvv2_response']), + avs_result: AVSResult.new(code: response['avs_response']), + cvv_result: CVVResult.new(response['cvv2_response']), test: test? ) - rescue JSON::ParserError unparsable_response(raw_response) end @@ -232,7 +232,7 @@ def message_from(response) def post_data(action, params) params[:transaction_type] = action - params.map {|k, v| "#{k}=#{CGI.escape(v.to_s)}"}.join('&') + params.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&') end def unparsable_response(raw_response) @@ -240,7 +240,6 @@ def unparsable_response(raw_response) message += " (The raw response returned by the API was #{raw_response.inspect})" return Response.new(false, message) end - end end end diff --git a/lib/active_merchant/billing/gateways/pay_gate_xml.rb b/lib/active_merchant/billing/gateways/pay_gate_xml.rb index 55c9b7b67af..47374239b9e 100644 --- a/lib/active_merchant/billing/gateways/pay_gate_xml.rb +++ b/lib/active_merchant/billing/gateways/pay_gate_xml.rb @@ -1,7 +1,7 @@ require 'digest/md5' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # This gateway accepts the following arguments: # :login => your PayJunction username # :password => your PayJunction pass @@ -76,10 +76,10 @@ class PayGateXmlGateway < Gateway self.live_url = 'https://www.paygate.co.za/payxml/process.trans' # The countries the gateway supports merchants from as 2 digit ISO country codes - self.supported_countries = ['US', 'ZA'] + self.supported_countries = %w[US ZA] # The card types supported by the payment gateway - self.supported_cardtypes = [:visa, :master, :american_express, :diners_club] + self.supported_cardtypes = %i[visa master american_express diners_club] # The homepage URL of the gateway self.homepage_url = 'http://paygate.co.za/' @@ -106,7 +106,7 @@ class PayGateXmlGateway < Gateway 900002 => 'Card Expired', 900003 => 'Insufficient Funds', 900004 => 'Invalid Card Number', - 900005 => 'Bank Interface Timeout', # indicates a communications failure between the banks systems + 900005 => 'Bank Interface Timeout', # indicates a communications failure between the banks systems 900006 => 'Invalid Card', 900007 => 'Declined', 900009 => 'Lost Card', @@ -117,7 +117,7 @@ class PayGateXmlGateway < Gateway 900014 => 'Excessive Card Usage', 900015 => 'Card Blacklisted', - 900207 => 'Declined; authentication failed', # indicates the cardholder did not enter their MasterCard SecureCode / Verified by Visa password correctly + 900207 => 'Declined; authentication failed', # indicates the cardholder did not enter their MasterCard SecureCode / Verified by Visa password correctly 990020 => 'Auth Declined', @@ -135,15 +135,15 @@ class PayGateXmlGateway < Gateway 990053 => 'Error processing transaction', # Miscellaneous - Unless otherwise noted, the TRANSACTION_STATUS will be 0. - 900209 => 'Transaction verification failed (phase 2)', # Indicates the verification data returned from MasterCard SecureCode / Verified by Visa has been altered - 900210 => 'Authentication complete; transaction must be restarted', # Indicates that the MasterCard SecuerCode / Verified by Visa transaction has already been completed. Most likely caused by the customer clicking the refresh button + 900209 => 'Transaction verification failed (phase 2)', # Indicates the verification data returned from MasterCard SecureCode / Verified by Visa has been altered + 900210 => 'Authentication complete; transaction must be restarted', # Indicates that the MasterCard SecuerCode / Verified by Visa transaction has already been completed. Most likely caused by the customer clicking the refresh button 990024 => 'Duplicate Transaction Detected. Please check before submitting', - 990028 => 'Transaction cancelled' # Customer clicks the 'Cancel' button on the payment page + 990028 => 'Transaction cancelled' # Customer clicks the 'Cancel' button on the payment page } - SUCCESS_CODES = %w( 990004 990005 990017 990012 990018 990031 ) + SUCCESS_CODES = %w(990004 990005 990017 990012 990018 990031) TRANSACTION_CODES = { 0 => 'Not Done', @@ -162,29 +162,32 @@ def initialize(options = {}) def purchase(money, creditcard, options = {}) MultiResponse.run do |r| - r.process{authorize(money, creditcard, options)} - r.process{capture(money, r.authorization, options)} + r.process { authorize(money, creditcard, options) } + r.process { capture(money, r.authorization, options) } end end def authorize(money, creditcard, options = {}) action = 'authtx' - options.merge!(:money => money, :creditcard => creditcard) + options[:money] = money + options[:creditcard] = creditcard commit(action, build_request(action, options)) end def capture(money, authorization, options = {}) action = 'settletx' - options.merge!(:money => money, :authorization => authorization) + options[:money] = money + options[:authorization] = authorization commit(action, build_request(action, options), authorization) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) action = 'refundtx' - options.merge!(:money => money, :authorization => authorization) + options[:money] = money + options[:authorization] = authorization commit(action, build_request(action, options)) end @@ -194,11 +197,11 @@ def successful?(response) SUCCESS_CODES.include?(response[:res]) end - def build_request(action, options={}) + def build_request(action, options = {}) xml = Builder::XmlMarkup.new xml.instruct! - xml.tag! 'protocol', :ver => API_VERSION, :pgid => (test? ? TEST_ID : @options[:login]), :pwd => @options[:password] do |protocol| + xml.tag! 'protocol', ver: API_VERSION, pgid: (test? ? TEST_ID : @options[:login]), pwd: @options[:password] do |protocol| money = options.delete(:money) authorization = options.delete(:authorization) creditcard = options.delete(:creditcard) @@ -217,31 +220,31 @@ def build_request(action, options={}) xml.target! end - def build_authorization(xml, money, creditcard, options={}) + def build_authorization(xml, money, creditcard, options = {}) xml.tag! 'authtx', { - :cref => options[:order_id], - :cname => creditcard.name, - :cc => creditcard.number, - :exp => "#{format(creditcard.month, :two_digits)}#{format(creditcard.year, :four_digits)}", - :budp => 0, - :amt => amount(money), - :cur => (options[:currency] || currency(money)), - :cvv => creditcard.verification_value, - :email => options[:email], - :ip => options[:ip] + cref: options[:order_id], + cname: creditcard.name, + cc: creditcard.number, + exp: "#{format(creditcard.month, :two_digits)}#{format(creditcard.year, :four_digits)}", + budp: 0, + amt: amount(money), + cur: (options[:currency] || currency(money)), + cvv: creditcard.verification_value, + email: options[:email], + ip: options[:ip] } end - def build_capture(xml, money, authorization, options={}) + def build_capture(xml, money, authorization, options = {}) xml.tag! 'settletx', { - :tid => authorization + tid: authorization } end - def build_refund(xml, money, authorization, options={}) + def build_refund(xml, money, authorization, options = {}) xml.tag! 'refundtx', { - :tid => authorization, - :amt => amount(money) + tid: authorization, + amt: amount(money) } end @@ -252,9 +255,7 @@ def parse(action, body) response_action = action.gsub(/tx/, 'rx') root = REXML::XPath.first(xml.root, response_action) # we might have gotten an error - if root.nil? - root = REXML::XPath.first(xml.root, 'errorrx') - end + root = REXML::XPath.first(xml.root, 'errorrx') if root.nil? root.attributes.each do |name, value| hash[name.to_sym] = value end @@ -263,9 +264,12 @@ 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, - :test => test?, - :authorization => authorization || response[:tid] + Response.new( + successful?(response), + message_from(response), + response, + test: test?, + authorization: authorization || response[:tid] ) end @@ -274,4 +278,4 @@ def message_from(response) end end end -end \ No newline at end of file +end diff --git a/lib/active_merchant/billing/gateways/pay_hub.rb b/lib/active_merchant/billing/gateways/pay_hub.rb index d354f5c1db9..f14609d2545 100644 --- a/lib/active_merchant/billing/gateways/pay_hub.rb +++ b/lib/active_merchant/billing/gateways/pay_hub.rb @@ -1,11 +1,11 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class PayHubGateway < Gateway self.live_url = 'https://checkout.payhub.com/transaction/api' self.supported_countries = ['US'] self.default_currency = 'USD' - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'http://www.payhub.com/' self.display_name = 'PayHub' @@ -66,7 +66,7 @@ class PayHubGateway < Gateway '43' => STANDARD_ERROR_CODE[:pickup_card] } - def initialize(options={}) + def initialize(options = {}) requires!(options, :orgid, :username, :password, :tid) super @@ -82,7 +82,7 @@ def authorize(amount, creditcard, options = {}) commit(post) end - def purchase(amount, creditcard, options={}) + def purchase(amount, creditcard, options = {}) post = setup_post('sale') add_creditcard(post, creditcard) add_amount(post, amount) @@ -92,7 +92,7 @@ def purchase(amount, creditcard, options={}) commit(post) end - def refund(amount, trans_id, options={}) + def refund(amount, trans_id, options = {}) # Attempt a void in case the transaction is unsettled post = setup_post('void') add_reference(post, trans_id) @@ -115,7 +115,7 @@ def capture(amount, trans_id, options = {}) # No void, as PayHub's void does not work on authorizations - def verify(creditcard, options={}) + def verify(creditcard, options = {}) authorize(100, creditcard, options) end @@ -145,6 +145,7 @@ def add_customer_data(post, options = {}) def add_address(post, address) return unless address + post[:address1] = address[:address1] post[:address2] = address[:address2] post[:zip] = address[:zip] @@ -153,7 +154,7 @@ def add_address(post, address) end def add_amount(post, amount) - post[:amount] = amount(amount) + post[:amount] = amount(amount) end def add_creditcard(post, creditcard) @@ -171,7 +172,7 @@ def commit(post) success = false begin - raw_response = ssl_post(live_url, post.to_json, {'Content-Type' => 'application/json'} ) + raw_response = ssl_post(live_url, post.to_json, { 'Content-Type' => 'application/json' }) response = parse(raw_response) success = (response['RESPONSE_CODE'] == '00') rescue ResponseError => e @@ -181,11 +182,12 @@ 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']}, + 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'] @@ -200,8 +202,8 @@ def response_error(raw_response) def json_error(raw_response) { - error_message: 'Invalid response received from the Payhub API. Please contact wecare@payhub.com if you continue to receive this message.' + - " (The raw response returned by the API was #{raw_response.inspect})" + error_message: 'Invalid response received from the Payhub API. Please contact wecare@payhub.com if you continue to receive this message.' \ + " (The raw response returned by the API was #{raw_response.inspect})" } end diff --git a/lib/active_merchant/billing/gateways/pay_junction.rb b/lib/active_merchant/billing/gateways/pay_junction.rb index 5255ef2106d..86c7c825aec 100644 --- a/lib/active_merchant/billing/gateways/pay_junction.rb +++ b/lib/active_merchant/billing/gateways/pay_junction.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # PayJunction Gateway # # This gateway accepts the following arguments: @@ -97,7 +97,7 @@ module Billing #:nodoc: # See example use above for address AVS fields # See #recurring for periodic transaction fields class PayJunctionGateway < Gateway - API_VERSION = '1.2' + API_VERSION = '1.2' class_attribute :test_url, :live_url @@ -107,7 +107,7 @@ class PayJunctionGateway < Gateway TEST_LOGIN = 'pj-ql-01' TEST_PASSWORD = 'pj-ql-01p' - SUCCESS_CODES = ['00', '85'] + SUCCESS_CODES = %w[00 85] SUCCESS_MESSAGE = 'The transaction was approved.' FAILURE_MESSAGE = 'The transaction was declined.' @@ -150,7 +150,7 @@ class PayJunctionGateway < Gateway 'AB' => 'Aborted because of an upstream system error, please try again later.' } - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.supported_countries = ['US'] self.homepage_url = 'http://www.payjunction.com/' self.display_name = 'PayJunction' @@ -165,7 +165,7 @@ def initialize(options = {}) # transaction_id that can be used later to postauthorize (capture) the funds. def authorize(money, payment_source, options = {}) parameters = { - :transaction_amount => amount(money), + transaction_amount: amount(money) } add_payment_source(parameters, payment_source) @@ -178,7 +178,7 @@ def authorize(money, payment_source, options = {}) # Execute authorization and capture in a single step. def purchase(money, payment_source, options = {}) parameters = { - :transaction_amount => amount(money), + transaction_amount: amount(money) } add_payment_source(parameters, payment_source) @@ -191,8 +191,8 @@ def purchase(money, payment_source, options = {}) # Retrieve funds that have been previously authorized with _authorization_ def capture(money, authorization, options = {}) parameters = { - :transaction_id => authorization, - :posture => 'capture' + transaction_id: authorization, + posture: 'capture' } add_optional_fields(parameters, options) @@ -203,8 +203,8 @@ def capture(money, authorization, options = {}) # _authorization_ should be the transaction id of the transaction we are returning. def refund(money, authorization, options = {}) parameters = { - :transaction_amount => amount(money), - :transaction_id => authorization + transaction_amount: amount(money), + transaction_id: authorization } commit('CREDIT', parameters) @@ -219,8 +219,8 @@ def credit(money, authorization, options = {}) # through the batch process. def void(authorization, options = {}) parameters = { - :transaction_id => authorization, - :posture => 'void' + transaction_id: authorization, + posture: 'void' } add_optional_fields(parameters, options) @@ -241,16 +241,17 @@ def void(authorization, options = {}) def recurring(money, payment_source, options = {}) ActiveMerchant.deprecated RECURRING_DEPRECATION_MESSAGE - requires!(options, [:periodicity, :monthly, :weekly, :daily], :payments) + requires!(options, %i[periodicity monthly weekly daily], :payments) - periodic_type = case options[:periodicity] - when :monthly - 'month' - when :weekly - 'week' - when :daily - 'day' - end + periodic_type = + case options[:periodicity] + when :monthly + 'month' + when :weekly + 'week' + when :daily + 'day' + end if options[:starting_at].nil? start_date = Time.now.strftime('%Y-%m-%d') @@ -262,12 +263,12 @@ def recurring(money, payment_source, options = {}) end parameters = { - :transaction_amount => amount(money), - :schedule_periodic_type => periodic_type, - :schedule_create => 'true', - :schedule_limit => options[:payments].to_i > 1 ? options[:payments] : 1, - :schedule_periodic_number => 1, - :schedule_start => start_date + transaction_amount: amount(money), + schedule_periodic_type: periodic_type, + schedule_create: 'true', + schedule_limit: options[:payments].to_i > 1 ? options[:payments] : 1, + schedule_periodic_number: 1, + schedule_start: start_date } add_payment_source(parameters, payment_source) @@ -318,7 +319,7 @@ def add_address(params, options) address = options[:billing_address] || options[:address] if address - params[:address] = address[:address1] unless address[:address1].blank? + params[:address] = address[:address1] unless address[:address1].blank? params[:city] = address[:city] unless address[:city].blank? params[:state] = address[:state] unless address[:state].blank? params[:zipcode] = address[:zip] unless address[:zip].blank? @@ -334,11 +335,14 @@ def add_optional_fields(params, options) def commit(action, parameters) url = test? ? self.test_url : self.live_url - response = parse( ssl_post(url, post_data(action, parameters)) ) + response = parse(ssl_post(url, post_data(action, parameters))) - Response.new(successful?(response), message_from(response), response, - :test => test?, - :authorization => response[:transaction_id] || parameters[:transaction_id] + Response.new( + successful?(response), + message_from(response), + response, + test: test?, + authorization: response[:transaction_id] || parameters[:transaction_id] ) end @@ -366,7 +370,7 @@ def post_data(action, params) params[:version] = API_VERSION params[:transaction_type] = action - params.reject{|k,v| v.blank?}.collect{ |k, v| "dc_#{k.to_s}=#{CGI.escape(v.to_s)}" }.join('&') + params.reject { |_k, v| v.blank? }.collect { |k, v| "dc_#{k}=#{CGI.escape(v.to_s)}" }.join('&') end def parse(body) diff --git a/lib/active_merchant/billing/gateways/pay_junction_v2.rb b/lib/active_merchant/billing/gateways/pay_junction_v2.rb index 9cac25e238a..e0d7d2151f5 100644 --- a/lib/active_merchant/billing/gateways/pay_junction_v2.rb +++ b/lib/active_merchant/billing/gateways/pay_junction_v2.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class PayJunctionV2Gateway < Gateway self.display_name = 'PayJunction' self.homepage_url = 'https://www.payjunction.com/' @@ -10,31 +10,33 @@ class PayJunctionV2Gateway < Gateway self.supported_countries = ['US'] self.default_currency = 'USD' self.money_format = :dollars - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] - def initialize(options={}) + def initialize(options = {}) requires!(options, :api_login, :api_password, :api_key) super end - def purchase(amount, payment_method, options={}) + def purchase(amount, payment_method, options = {}) post = {} add_invoice(post, amount, options) add_payment_method(post, payment_method) + add_address(post, options) commit('purchase', post) end - def authorize(amount, payment_method, options={}) + def authorize(amount, payment_method, options = {}) post = {} post[:status] = 'HOLD' add_invoice(post, amount, options) add_payment_method(post, payment_method) + add_address(post, options) commit('authorize', post) end - def capture(amount, authorization, options={}) + def capture(amount, authorization, options = {}) post = {} post[:status] = 'CAPTURE' post[:transactionId] = authorization @@ -43,7 +45,7 @@ def capture(amount, authorization, options={}) commit('capture', post) end - def void(authorization, options={}) + def void(authorization, options = {}) post = {} post[:status] = 'VOID' post[:transactionId] = authorization @@ -51,7 +53,7 @@ def void(authorization, options={}) commit('void', post) end - def refund(amount, authorization, options={}) + def refund(amount, authorization, options = {}) post = {} post[:action] = 'REFUND' post[:transactionId] = authorization @@ -60,7 +62,7 @@ def refund(amount, authorization, options={}) commit('refund', post) end - def credit(amount, payment_method, options={}) + def credit(amount, payment_method, options = {}) post = {} post[:action] = 'REFUND' add_invoice(post, amount, options) @@ -69,7 +71,7 @@ def credit(amount, payment_method, options={}) commit('credit', post) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -110,12 +112,27 @@ def add_payment_method(post, payment_method) end end - def commit(action, params) - response = begin - parse(ssl_invoke(action, params)) - rescue ResponseError => e - parse(e.response.body) + def add_address(post, options) + if address = options[:billing_address] + post[:billingFirstName] = address[:first_name] if address[:first_name] + post[:billingLastName] = address[:last_name] if address[:last_name] + post[:billingCompanyName] = address[:company] if address[:company] + post[:billingPhone] = address[:phone_number] if address[:phone_number] + post[:billingAddress] = address[:address1] if address[:address1] + post[:billingCity] = address[:city] if address[:city] + post[:billingState] = address[:state] if address[:state] + post[:billingCountry] = address[:country] if address[:country] + post[:billingZip] = address[:zip] if address[:zip] end + end + + def commit(action, params) + response = + begin + parse(ssl_invoke(action, params)) + rescue ResponseError => e + parse(e.response.body) + end success = success_from(response) Response.new( @@ -129,7 +146,7 @@ def commit(action, params) end def ssl_invoke(action, params) - if ['purchase', 'authorize', 'refund', 'credit'].include?(action) + if %w[purchase authorize refund credit].include?(action) ssl_post(url(), post_data(params), headers) else ssl_request(:put, url(params), post_data(params), headers) @@ -140,42 +157,41 @@ def headers { 'Authorization' => 'Basic ' + Base64.encode64("#{@options[:api_login]}:#{@options[:api_password]}").strip, 'Content-Type' => 'application/x-www-form-urlencoded;charset=UTF-8', - 'Accept' => 'application/json', - 'X-PJ-Application-Key' => "#{@options[:api_key]}" + 'Accept' => 'application/json', + 'X-PJ-Application-Key' => @options[:api_key].to_s } end def post_data(params) - params.map {|k, v| "#{k}=#{CGI.escape(v.to_s)}"}.join('&') + params.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&') end - def url(params={}) + def url(params = {}) test? ? "#{test_url}/#{params[:transactionId]}" : "#{live_url}/#{params[:transactionId]}" end def parse(body) - begin - JSON.parse(body) - rescue JSON::ParserError - message = 'Invalid JSON response received from PayJunctionV2Gateway. Please contact PayJunctionV2Gateway if you continue to receive this message.' - message += " (The raw response returned by the API was #{body.inspect})" - { - 'errors' => [{ - 'message' => message - }] - } - end + JSON.parse(body) + rescue JSON::ParserError + message = 'Invalid JSON response received from PayJunctionV2Gateway. Please contact PayJunctionV2Gateway if you continue to receive this message.' + message += " (The raw response returned by the API was #{body.inspect})" + { + 'errors' => [{ + 'message' => message + }] + } end def success_from(response) return response['response']['approved'] if response['response'] + false end def message_from(response) return response['response']['message'] if response['response'] - response['errors'].inject(''){ |message,error| error['message'] + '|' + message } if response['errors'] + response['errors']&.inject('') { |message, error| error['message'] + '|' + message } end def authorization_from(response) diff --git a/lib/active_merchant/billing/gateways/pay_secure.rb b/lib/active_merchant/billing/gateways/pay_secure.rb index 10dcbfbed57..1b5b5b48ba2 100644 --- a/lib/active_merchant/billing/gateways/pay_secure.rb +++ b/lib/active_merchant/billing/gateways/pay_secure.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class PaySecureGateway < Gateway self.live_url = self.test_url = 'https://clearance.commsecure.com.au/cgi-bin/PSDirect' @@ -8,10 +8,10 @@ class PaySecureGateway < Gateway # Currently Authorization and Capture is not implemented because # capturing requires the original credit card information TRANSACTIONS = { - :purchase => 'PURCHASE', - :authorization => 'AUTHORISE', - :capture => 'ADVICE', - :credit => 'REFUND' + purchase: 'PURCHASE', + authorization: 'AUTHORISE', + capture: 'ADVICE', + credit: 'REFUND' } SUCCESS = 'Accepted' @@ -20,7 +20,7 @@ class PaySecureGateway < Gateway self.supported_countries = ['AU'] self.homepage_url = 'http://www.commsecure.com.au/paysecure.shtml' self.display_name = 'PaySecure' - self.supported_cardtypes = [:visa, :master, :american_express, :diners_club] + self.supported_cardtypes = %i[visa master american_express diners_club] def initialize(options = {}) requires!(options, :login, :password) @@ -39,10 +39,11 @@ def purchase(money, credit_card, options = {}) end private + # Used for capturing, which is currently not supported. def add_reference(post, identification) auth, trans_id = identification.split(';') - post[:authnum] = auth + post[:authnum] = auth post[:transid] = trans_id end @@ -51,7 +52,7 @@ def add_amount(post, money) end def add_invoice(post, options) - post[:merchant_transid] = options[:order_id].to_s.slice(0,21) + post[:merchant_transid] = options[:order_id].to_s.slice(0, 21) post[:memnum] = options[:invoice] post[:custnum] = options[:customer] post[:clientdata] = options[:description] @@ -65,13 +66,15 @@ def add_credit_card(post, credit_card) end def commit(action, money, parameters) - response = parse( ssl_post(self.live_url, post_data(action, parameters)) ) - - Response.new(successful?(response), message_from(response), response, - :test => test_response?(response), - :authorization => authorization_from(response) + response = parse(ssl_post(self.live_url, post_data(action, parameters))) + + Response.new( + successful?(response), + message_from(response), + response, + test: test_response?(response), + authorization: authorization_from(response) ) - end def successful?(response) @@ -79,7 +82,7 @@ def successful?(response) end def authorization_from(response) - [ response[:authnum], response[:transid] ].compact.join(';') + [response[:authnum], response[:transid]].compact.join(';') end def test_response?(response) @@ -104,9 +107,8 @@ def post_data(action, parameters = {}) parameters[:merchant_id] = @options[:login] parameters[:password] = @options[:password] - parameters.reject{|k,v| v.blank?}.collect { |key, value| "#{key.to_s.upcase}=#{CGI.escape(value.to_s)}" }.join('&') + parameters.reject { |_k, v| v.blank? }.collect { |key, value| "#{key.to_s.upcase}=#{CGI.escape(value.to_s)}" }.join('&') end end end end - diff --git a/lib/active_merchant/billing/gateways/pay_trace.rb b/lib/active_merchant/billing/gateways/pay_trace.rb new file mode 100644 index 00000000000..3346d816158 --- /dev/null +++ b/lib/active_merchant/billing/gateways/pay_trace.rb @@ -0,0 +1,464 @@ +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class PayTraceGateway < Gateway + self.test_url = 'https://api.sandbox.paytrace.com' + self.live_url = 'https://api.paytrace.com' + + self.supported_countries = ['US'] + self.default_currency = 'USD' + self.supported_cardtypes = %i[visa master american_express discover] + + self.homepage_url = 'https://paytrace.com/' + self.display_name = 'PayTrace' + + # Response codes based on API Response Codes found here: https://developers.paytrace.com/support/home#14000041297 + STANDARD_ERROR_CODE_MAPPING = { + '1' => STANDARD_ERROR_CODE[:error_occurred], + '102' => STANDARD_ERROR_CODE[:declined], + '103' => STANDARD_ERROR_CODE[:auto_voided], + '107' => STANDARD_ERROR_CODE[:unsuccessful_refund], + '108' => STANDARD_ERROR_CODE[:test_refund], + '110' => STANDARD_ERROR_CODE[:unsuccessful_void], + '113' => STANDARD_ERROR_CODE[:unsuccessful_capture] + } + + ENDPOINTS = { + customer_id_sale: 'transactions/sale/by_customer', + keyed_sale: 'transactions/sale/keyed', + customer_id_auth: 'transactions/authorization/by_customer', + keyed_auth: 'transactions/authorization/keyed', + capture: 'transactions/authorization/capture', + transaction_refund: 'transactions/refund/for_transaction', + transaction_void: 'transactions/void', + store: 'customer/create', + redact: 'customer/delete', + level_3_visa: 'level_three/visa', + level_3_mastercard: 'level_three/mastercard', + ach_sale: 'checks/sale/by_account', + ach_customer_sale: 'checks/sale/by_customer', + ach_authorize: 'checks/hold/by_account', + ach_customer_authorize: 'checks/hold/by_customer', + ach_refund: 'checks/refund/by_transaction', + ach_capture: 'checks/manage/fund', + ach_void: 'checks/manage/void' + } + + def initialize(options = {}) + requires!(options, :username, :password, :integrator_id) + super + acquire_access_token unless options[:access_token] + end + + def purchase(money, payment_or_customer_id, options = {}) + if visa_or_mastercard?(options) + MultiResponse.run(:use_first_response) do |r| + endpoint = customer_id?(payment_or_customer_id) ? ENDPOINTS[:customer_id_sale] : ENDPOINTS[:keyed_sale] + + r.process { commit(endpoint, build_purchase_request(money, payment_or_customer_id, options)) } + r.process { commit(ENDPOINTS[:"level_3_#{options[:visa_or_mastercard]}"], send_level_3_data(r, options)) } + end + else + post = build_purchase_request(money, payment_or_customer_id, options) + endpoint = if payment_or_customer_id.kind_of?(Check) + ENDPOINTS[:ach_sale] + elsif options[:check_transaction] + ENDPOINTS[:ach_customer_sale] + elsif post[:customer_id] + ENDPOINTS[:customer_id_sale] + else + ENDPOINTS[:keyed_sale] + end + response = commit(endpoint, post) + check_token_response(response, endpoint, post, options) + end + end + + def authorize(money, payment_or_customer_id, options = {}) + post = {} + add_amount(post, money, options) + if customer_id?(payment_or_customer_id) + post[:customer_id] = payment_or_customer_id + endpoint = if options[:check_transaction] + ENDPOINTS[:ach_customer_authorize] + else + ENDPOINTS[:customer_id_auth] + end + else + add_payment(post, payment_or_customer_id) + add_address(post, payment_or_customer_id, options) + add_customer_data(post, options) + endpoint = payment_or_customer_id.kind_of?(Check) ? ENDPOINTS[:ach_authorize] : ENDPOINTS[:keyed_auth] + end + response = commit(endpoint, post) + check_token_response(response, endpoint, post, options) + end + + def capture(money, authorization, options = {}) + if visa_or_mastercard?(options) + MultiResponse.run(:use_first_response) do |r| + r.process { commit(ENDPOINTS[:capture], build_capture_request(money, authorization, options)) } + r.process { commit(ENDPOINTS[:"level_3_#{options[:visa_or_mastercard]}"], send_level_3_data(r, options)) } + end + else + post = build_capture_request(money, authorization, options) + endpoint = if options[:check_transaction] + ENDPOINTS[:ach_capture] + else + ENDPOINTS[:capture] + end + response = commit(endpoint, post) + check_token_response(response, endpoint, post, options) + end + end + + def refund(money, authorization, options = {}) + # currently only support full and partial refunds of settled transactions via a transaction ID + post = {} + add_amount(post, money, options) + if options[:check_transaction] + post[:check_transaction_id] = authorization + endpoint = ENDPOINTS[:ach_refund] + else + post[:transaction_id] = authorization + endpoint = ENDPOINTS[:transaction_refund] + end + response = commit(endpoint, post) + check_token_response(response, endpoint, post, options) + end + + def void(authorization, options = {}) + post = {} + if options[:check_transaction] + post[:check_transaction_id] = authorization + endpoint = ENDPOINTS[:ach_void] + else + post[:transaction_id] = authorization + endpoint = ENDPOINTS[:transaction_void] + end + + response = commit(endpoint, post) + check_token_response(response, endpoint, post, options) + end + + def verify(credit_card, options = {}) + authorize(0, credit_card, options) + end + + # The customer_IDs that come from storing cards can be used for auth and purchase transaction types + def store(credit_card, options = {}) + post = {} + post[:customer_id] = options[:customer_id] || SecureRandom.hex(12) + add_payment(post, credit_card) + add_address(post, credit_card, options) + response = commit(ENDPOINTS[:store], post) + check_token_response(response, ENDPOINTS[:store], post, options) + end + + def unstore(customer_id) + post = {} + post[:customer_id] = customer_id + response = commit(ENDPOINTS[:redact], post) + check_token_response(response, ENDPOINTS[:redact], post, options) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r((Authorization: Bearer )[a-zA-Z0-9:_]+), '\1[FILTERED]'). + gsub(%r(("credit_card\\?":{\\?"number\\?":\\?")\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 = base_url + '/oauth/token' + oauth_headers = { + 'Accept' => '*/*', + 'Content-Type' => 'application/x-www-form-urlencoded' + } + response = ssl_post(url, data, oauth_headers) + json_response = parse(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 + + def build_purchase_request(money, payment_or_customer_id, options) + post = {} + add_amount(post, money, options) + if customer_id?(payment_or_customer_id) + post[:customer_id] = payment_or_customer_id + else + add_payment(post, payment_or_customer_id) + add_address(post, payment_or_customer_id, options) + add_customer_data(post, options) + end + + post + end + + def build_capture_request(money, authorization, options) + post = {} + if options[:check_transaction] + post[:check_transaction_id] = authorization + else + post[:transaction_id] = authorization + end + add_amount(post, money, options) + + post + end + + # method can only be used to add level 3 data to any approved and unsettled sale transaction so it is built into the standard purchase workflow above + def send_level_3_data(response, options) + post = {} + post[:transaction_id] = response.authorization + add_level_3_data(post, options) + + post + end + + def visa_or_mastercard?(options) + return false unless options[:visa_or_mastercard] + + options[:visa_or_mastercard] == 'visa' || options[:visa_or_mastercard] == 'mastercard' + end + + def customer_id?(payment_or_customer_id) + payment_or_customer_id.instance_of?(String) + end + + def string_literal_to_boolean(value) + return value unless value.instance_of?(String) + + if value.casecmp('true').zero? + true + elsif value.casecmp('false').zero? + false + else + return nil + end + end + + def add_customer_data(post, options) + return unless options[:email] + + post[:email] = options[:email] + end + + def add_address(post, creditcard, options) + 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 + end + + def add_amount(post, money, options) + post[:amount] = amount(money) + end + + def add_payment(post, payment) + if payment.kind_of?(Check) + post[:check] = {} + post[:check][:account_number] = payment.account_number + post[:check][:routing_number] = payment.routing_number + else + post[:credit_card] = {} + post[:credit_card][:number] = payment.number + post[:credit_card][:expiration_month] = payment.month + post[:credit_card][:expiration_year] = payment.year + post[:csc] = payment.verification_value + end + end + + def add_level_3_data(post, options) + post[:invoice_id] = options[:invoice_id] if options[:invoice_id] + post[:customer_reference_id] = options[:customer_reference_id] if options[:customer_reference_id] + post[:tax_amount] = options[:tax_amount].to_i if options[:tax_amount] + post[:national_tax_amount] = options[:national_tax_amount].to_i if options[:national_tax_amount] + post[:merchant_tax_id] = options[:merchant_tax_id] if options[:merchant_tax_id] + post[:customer_tax_id] = options[:customer_tax_id] if options[:customer_tax_id] + post[:commodity_code] = options[:commodity_code] if options[:commodity_code] + post[:discount_amount] = options[:discount_amount].to_i if options[:discount_amount] + post[:freight_amount] = options[:freight_amount].to_i if options[:freight_amount] + post[:duty_amount] = options[:duty_amount].to_i if options[:duty_amount] + post[:additional_tax_amount] = options[:additional_tax_amount].to_i if options[:additional_tax_amount] + post[:additional_tax_rate] = options[:additional_tax_rate].to_i if options[:additional_tax_rate] + + add_source_address(post, options) + add_shipping_address(post, options) + add_line_items(post, options) + end + + def add_source_address(post, options) + return unless source_address = options[:source_address] || + options[:billing_address] || + options[:address] + + post[:source_address] = {} + post[:source_address][:zip] = source_address[:zip] if source_address[:zip] + end + + def add_shipping_address(post, options) + return unless shipping_address = options[:shipping_address] + + post[:shipping_address] = {} + post[:shipping_address][:name] = shipping_address[:name] if shipping_address[:name] + post[:shipping_address][:street_address] = shipping_address[:address1] if shipping_address[:address1] + post[:shipping_address][:street_address2] = shipping_address[:address2] if shipping_address[:address2] + post[:shipping_address][:city] = shipping_address[:city] if shipping_address[:city] + post[:shipping_address][:state] = shipping_address[:state] if shipping_address[:state] + post[:shipping_address][:zip] = shipping_address[:zip] if shipping_address[:zip] + post[:shipping_address][:country] = shipping_address[:country] if shipping_address[:country] + end + + def add_line_items(post, options) + return unless options[:line_items] + + line_items = [] + options[:line_items].each do |li| + obj = {} + + obj[:additional_tax_amount] = li[:additional_tax_amount].to_i if li[:additional_tax_amount] + obj[:additional_tax_included] = string_literal_to_boolean(li[:additional_tax_included]) if li[:additional_tax_included] + obj[:additional_tax_rate] = li[:additional_tax_rate].to_i if li[:additional_tax_rate] + obj[:amount] = li[:amount].to_i if li[:amount] + obj[:commodity_code] = li[:commodity_code] if li[:commodity_code] + obj[:debit_or_credit] = li[:debit_or_credit] if li[:debit_or_credit] + obj[:description] = li[:description] if li[:description] + obj[:discount_amount] = li[:discount_amount].to_i if li[:discount_amount] + obj[:discount_rate] = li[:discount_rate].to_i if li[:discount_rate] + obj[:discount_included] = string_literal_to_boolean(li[:discount_included]) if li[:discount_included] + obj[:merchant_tax_id] = li[:merchant_tax_id] if li[:merchant_tax_id] + obj[:product_id] = li[:product_id] if li[:product_id] + obj[:quantity] = li[:quantity] if li[:quantity] + obj[:transaction_id] = li[:transaction_id] if li[:transaction_id] + obj[:tax_included] = string_literal_to_boolean(li[:tax_included]) if li[:tax_included] + obj[:unit_of_measure] = li[:unit_of_measure] if li[:unit_of_measure] + obj[:unit_cost] = li[:unit_cost].to_i if li[:unit_cost] + + line_items << obj + end + post[:line_items] = line_items + end + + def check_token_response(response, endpoint, body = {}, options = {}) + return response unless response.params['error'] == 'invalid_token' + + acquire_access_token + commit(endpoint, body) + end + + def parse(body) + JSON.parse(body) + end + + def commit(action, parameters) + base_url = (test? ? test_url : live_url) + 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( + success, + message_from(success, response), + response, + authorization: authorization_from(action, response), + avs_result: AVSResult.new(code: response['avs_response']), + cvv_result: response['csc_response'], + test: test?, + error_code: success ? nil : error_code_from(response) + ) + end + + def unparsable_response(raw_response) + message = 'Unparsable response received from PayTrace. Please contact PayTrace if you continue to receive this message.' + message += " (The raw response returned by the API was #{raw_response.inspect})" + return Response.new(false, message) + end + + def headers + { + 'Content-type' => 'application/json', + 'Authorization' => 'Bearer ' + @options[:access_token] + } + end + + def success_from(response) + response['success'] + end + + def message_from(success, response) + return response['status_message'] if success + + if error = response['errors'] + message = 'Errors-' + error.each do |k, v| + message.concat(" code:#{k}, message:#{v}") + end + else + message = response['status_message'].to_s + " #{response['approval_message']}" + end + + message + end + + # store transactions do not return a transaction_id, but they return a customer_id that will then be used as the third_party_token for the stored payment method + def authorization_from(action, response) + if action == ENDPOINTS[:store] + response['customer_id'] + else + response['transaction_id'] || response['check_transaction_id'] + end + end + + def post_data(parameters = {}) + parameters[:password] = @options[:password] + parameters[:username] = @options[:username] + parameters[:integrator_id] = @options[:integrator_id] + + parameters.to_json + end + + def error_code_from(response) + STANDARD_ERROR_CODE_MAPPING[response['response_code']] + end + + def handle_response(response) + response.body + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/paybox_direct.rb b/lib/active_merchant/billing/gateways/paybox_direct.rb index 4e51c84ba0c..041695542f5 100644 --- a/lib/active_merchant/billing/gateways/paybox_direct.rb +++ b/lib/active_merchant/billing/gateways/paybox_direct.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class PayboxDirectGateway < Gateway class_attribute :live_url_backup @@ -12,33 +12,34 @@ class PayboxDirectGateway < Gateway # Transactions hash TRANSACTIONS = { - :authorization => '00001', - :capture => '00002', - :purchase => '00003', - :unreferenced_credit => '00004', - :void => '00005', - :refund => '00014' + authorization: '00001', + capture: '00002', + purchase: '00003', + unreferenced_credit: '00004', + void: '00005', + refund: '00014' } CURRENCY_CODES = { - 'AUD'=> '036', - 'CAD'=> '124', - 'CZK'=> '203', - 'DKK'=> '208', - 'HKD'=> '344', - 'ICK'=> '352', - 'JPY'=> '392', - 'NOK'=> '578', - 'SGD'=> '702', - 'SEK'=> '752', - 'CHF'=> '756', - 'GBP'=> '826', - 'USD'=> '840', - 'EUR'=> '978' + 'AUD' => '036', + 'CAD' => '124', + 'CZK' => '203', + 'DKK' => '208', + 'HKD' => '344', + 'ICK' => '352', + 'JPY' => '392', + 'NOK' => '578', + 'SGD' => '702', + 'SEK' => '752', + 'CHF' => '756', + 'GBP' => '826', + 'USD' => '840', + 'EUR' => '978', + 'XPF' => '953' } SUCCESS_CODES = ['00000'] - UNAVAILABILITY_CODES = ['00001', '00097', '00098'] + UNAVAILABILITY_CODES = %w[00001 00097 00098] SUCCESS_MESSAGE = 'The transaction was approved' FAILURE_MESSAGE = 'The transaction failed' @@ -50,7 +51,7 @@ class PayboxDirectGateway < Gateway self.supported_countries = ['FR'] # The card types supported by the payment gateway - self.supported_cardtypes = [:visa, :master, :american_express, :diners_club, :jcb] + self.supported_cardtypes = %i[visa master american_express diners_club jcb] # The homepage URL of the gateway self.homepage_url = 'http://www.paybox.com/' @@ -63,10 +64,31 @@ def initialize(options = {}) super end + def add_3dsecure(post, options) + # ECI=02 => MasterCard success + # ECI=05 => Visa, Amex or JCB success + if options[:eci] == '02' || options[:eci] == '05' + post[:'3DSTATUS'] = 'Y' + post[:'3DENROLLED'] = 'Y' + post[:'3DSIGNVAL'] = 'Y' + post[:'3DERROR'] = '0' + else + post[:'3DSTATUS'] = 'N' + post[:'3DENROLLED'] = 'N' + post[:'3DSIGNVAL'] = 'N' + post[:'3DERROR'] = '10000' + end + post[:'3DECI'] = options[:eci] + post[:'3DXID'] = options[:xid] + post[:'3DCAVV'] = options[:cavv] + post[:'3DCAVVALGO'] = options[:cavv_algorithm] + end + def authorize(money, creditcard, options = {}) post = {} add_invoice(post, options) add_creditcard(post, creditcard) + add_3dsecure(post, options[:three_d_secure]) if options[:three_d_secure] add_amount(post, money, options) commit('authorization', money, post) @@ -76,6 +98,7 @@ def purchase(money, creditcard, options = {}) post = {} add_invoice(post, options) add_creditcard(post, creditcard) + add_3dsecure(post, options[:three_d_secure]) if options[:three_d_secure] add_amount(post, money, options) commit('purchase', money, post) @@ -86,15 +109,15 @@ def capture(money, authorization, options = {}) post = {} add_invoice(post, options) add_amount(post, money, options) - post[:numappel] = authorization[0,10] - post[:numtrans] = authorization[10,10] + post[:numappel] = authorization[0, 10] + post[:numtrans] = authorization[10, 10] commit('capture', money, post) end def void(identification, options = {}) requires!(options, :order_id, :amount) - post ={} + post = {} add_invoice(post, options) add_reference(post, identification) add_amount(post, options[:amount], options) @@ -130,8 +153,8 @@ def add_creditcard(post, creditcard) end def add_reference(post, identification) - post[:numappel] = identification[0,10] - post[:numtrans] = identification[10,10] + post[:numappel] = identification[0, 10] + post[:numtrans] = identification[10, 10] end def add_amount(post, money, options) @@ -142,22 +165,24 @@ def add_amount(post, money, options) def parse(body) results = {} body.split(/&/).each do |pair| - key,val = pair.split(/\=/) + key, val = pair.split(/\=/) results[key.downcase.to_sym] = CGI.unescape(val) if val end results end def commit(action, money = nil, parameters = nil) - request_data = post_data(action,parameters) + request_data = post_data(action, parameters) response = parse(ssl_post(test? ? self.test_url : self.live_url, request_data)) response = parse(ssl_post(self.live_url_backup, request_data)) if service_unavailable?(response) && !test? - Response.new(success?(response), message_from(response), response.merge( - :timestamp => parameters[:dateq]), - :test => test?, - :authorization => response[:numappel].to_s + response[:numtrans].to_s, - :fraud_review => false, - :sent_params => parameters.delete_if{|key,value| ['porteur','dateval','cvv'].include?(key.to_s)} + Response.new( + success?(response), + message_from(response), + response.merge(timestamp: parameters[:dateq]), + test: test?, + authorization: response[:numappel].to_s + response[:numtrans].to_s, + fraud_review: false, + sent_params: parameters.delete_if { |key, _value| %w[porteur dateval cvv].include?(key.to_s) } ) end @@ -170,21 +195,20 @@ def service_unavailable?(response) end def message_from(response) - success?(response) ? SUCCESS_MESSAGE : (response[:commentaire] || FAILURE_MESSAGE) + success?(response) ? SUCCESS_MESSAGE : (response[:commentaire] || FAILURE_MESSAGE) end def post_data(action, parameters = {}) - parameters.update( - :version => API_VERSION, - :type => TRANSACTIONS[action.to_sym], - :dateq => Time.now.strftime('%d%m%Y%H%M%S'), - :numquestion => unique_id(parameters[:order_id]), - :site => @options[:login].to_s[0,7], - :rang => @options[:rang] || @options[:login].to_s[7..-1], - :cle => @options[:password], - :pays => '', - :archivage => parameters[:order_id] + version: API_VERSION, + type: TRANSACTIONS[action.to_sym], + dateq: Time.now.strftime('%d%m%Y%H%M%S'), + numquestion: unique_id(parameters[:order_id]), + site: @options[:login].to_s[0, 7], + rang: @options[:rang] || @options[:login].to_s[7..-1], + cle: @options[:password], + pays: '', + archivage: parameters[:order_id] ) parameters.collect { |key, value| "#{key.to_s.upcase}=#{CGI.escape(value.to_s)}" }.join('&') diff --git a/lib/active_merchant/billing/gateways/payeezy.rb b/lib/active_merchant/billing/gateways/payeezy.rb index fb8609b526a..50248894423 100644 --- a/lib/active_merchant/billing/gateways/payeezy.rb +++ b/lib/active_merchant/billing/gateways/payeezy.rb @@ -11,7 +11,7 @@ class PayeezyGateway < Gateway self.money_format = :cents self.supported_countries = %w(US CA) - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :jcb, :diners_club] + self.supported_cardtypes = %i[visa master american_express discover jcb diners_club] self.homepage_url = 'https://developer.payeezy.com/' self.display_name = 'Payeezy' @@ -34,28 +34,40 @@ def purchase(amount, payment_method, options = {}) params = payment_method.is_a?(String) ? { transaction_type: 'recurring' } : { transaction_type: 'purchase' } 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 def authorize(amount, payment_method, options = {}) - params = {transaction_type: 'authorize'} + params = { transaction_type: 'authorize' } 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 def capture(amount, authorization, options = {}) - params = {transaction_type: 'capture'} + params = { transaction_type: 'capture' } add_authorization_info(params, authorization) add_amount(params, amount, options) @@ -65,16 +77,28 @@ def capture(amount, authorization, options = {}) end def refund(amount, authorization, options = {}) - params = {transaction_type: 'refund'} + params = { transaction_type: 'refund' } add_authorization_info(params, authorization) add_amount(params, (amount || amount_from_authorization(authorization)), options) + add_soft_descriptors(params, options) + add_invoice(params, options) commit(params, options) end + def credit(amount, payment_method, options = {}) + params = { transaction_type: 'refund' } + + add_amount(params, amount, options) + add_payment_method(params, payment_method, options) + add_soft_descriptors(params, options) + add_invoice(params, options) + commit(params, options) + end + def store(payment_method, options = {}) - params = {transaction_type: 'store'} + params = { transaction_type: 'store' } add_creditcard_for_tokenization(params, payment_method, options) @@ -82,17 +106,17 @@ def store(payment_method, options = {}) end def void(authorization, options = {}) - params = {transaction_type: 'void'} + params = { transaction_type: 'void' } - add_authorization_info(params, authorization) + add_authorization_info(params, authorization, options) add_amount(params, amount_from_authorization(authorization), options) commit(params, options) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| - r.process { authorize(100, credit_card, options) } + r.process { authorize(0, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } end end @@ -107,6 +131,7 @@ def scrub(transcript) gsub(%r((Apikey: )(\w|-)+), '\1[FILTERED]'). gsub(%r((\\?"card_number\\?":\\?")\d+), '\1[FILTERED]'). gsub(%r((\\?"cvv\\?":\\?")\d+), '\1[FILTERED]'). + gsub(%r((\\?"cvv\\?":\\?)\d+), '\1[FILTERED]'). gsub(%r((\\?"account_number\\?":\\?")\d+), '\1[FILTERED]'). gsub(%r((\\?"routing_number\\?":\\?")\d+), '\1[FILTERED]'). gsub(%r((\\?card_number=)\d+(&?)), '\1[FILTERED]'). @@ -114,35 +139,76 @@ def scrub(transcript) gsub(%r((\\?apikey=)\w+(&?)), '\1[FILTERED]'). gsub(%r{(\\?"credit_card\.card_number\\?":)(\\?"[^"]+\\?")}, '\1[FILTERED]'). gsub(%r{(\\?"credit_card\.cvv\\?":)(\\?"[^"]+\\?")}, '\1[FILTERED]'). - gsub(%r{(\\?"apikey\\?":)(\\?"[^"]+\\?")}, '\1[FILTERED]') + gsub(%r{(\\?"apikey\\?":)(\\?"[^"]+\\?")}, '\1[FILTERED]'). + gsub(%r{(\\?"cavv\\?":)(\\?"[^"]+\\?")}, '\1[FILTERED]'). + gsub(%r{(\\?"xid\\?":)(\\?"[^"]+\\?")}, '\1[FILTERED]') end private + def add_external_three_ds(params, payment_method, options) + return unless three_ds = options[:three_d_secure] + + params[:'3DS'] = { + program_protocol: three_ds[:version][0], + directory_server_transaction_id: three_ds[:ds_transaction_id], + cardholder_name: payment_method.name, + card_number: payment_method.number, + exp_date: format_exp_date(payment_method.month, payment_method.year), + cvv: payment_method.verification_value, + xid: three_ds[:acs_transaction_id], + cavv: three_ds[:cavv], + wallet_provider_id: 'NO_WALLET', + type: 'D' + }.compact + + params[:eci_indicator] = options[:three_d_secure][:eci] + params[:method] = '3DS' + end + def add_invoice(params, options) params[:merchant_ref] = options[:order_id] end + 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 - def add_authorization_info(params, authorization) - transaction_id, transaction_tag, method, _ = authorization.split('|') - params[:transaction_id] = transaction_id - params[:transaction_tag] = transaction_tag - params[:method] = (method == 'token') ? 'credit_card' : method + 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] + else + params[:transaction_id] = transaction_id + params[:transaction_tag] = transaction_tag + end end 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 - def is_store_action?(params) + def store_action?(params) params[:transaction_type] == 'store' end @@ -151,8 +217,10 @@ def add_payment_method(params, payment_method, options) add_echeck(params, payment_method, options) elsif payment_method.is_a? String add_token(params, payment_method, options) + elsif payment_method.is_a? NetworkTokenizationCreditCard + add_network_tokenization(params, payment_method, options) else - add_creditcard(params, payment_method) + add_creditcard(params, payment_method, options) end end @@ -163,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] @@ -189,27 +257,54 @@ 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? card end + def add_network_tokenization(params, payment_method, options) + nt_card = {} + nt_card[:type] = 'D' + nt_card[:cardholder_name] = name_from_payment_method(payment_method) || name_from_address(options) + nt_card[:card_number] = payment_method.number + nt_card[:exp_date] = format_exp_date(payment_method.month, payment_method.year) + nt_card[:cvv] = payment_method.verification_value + nt_card[:xid] = payment_method.payment_cryptogram unless payment_method.payment_cryptogram.empty? || payment_method.brand.include?('american_express') + nt_card[:cavv] = payment_method.payment_cryptogram unless payment_method.payment_cryptogram.empty? + nt_card[:wallet_provider_id] = 'APPLE_PAY' + + params['3DS'] = nt_card + params[:method] = '3DS' + params[:eci_indicator] = payment_method.eci.nil? ? '5' : payment_method.eci + end + def format_exp_date(month, year) "#{format(month, :two_digits)}#{format(year, :two_digits)}" end + def name_from_address(options) + return unless address = options[:billing_address] + return address[:name] if address[:name] + end + + def name_from_payment_method(payment_method) + return unless payment_method.first_name && payment_method.last_name + + return "#{payment_method.first_name} #{payment_method.last_name}" + end + def add_address(params, options) address = options[:billing_address] return unless address @@ -233,6 +328,41 @@ def add_soft_descriptors(params, options) params[:soft_descriptors] = options[:soft_descriptors] if options[:soft_descriptors] end + def add_level2_data(params, options) + return unless level2_data = options[:level2] + + params[:level2] = {} + params[:level2][:customer_ref] = level2_data[:customer_ref] + end + + def add_stored_credentials(params, options) + if options[:sequence] || options[:stored_credential] + params[:stored_credentials] = {} + params[:stored_credentials][:cardbrand_original_transaction_id] = original_transaction_id(options) if original_transaction_id(options) + params[:stored_credentials][:initiator] = initiator(options) if initiator(options) + params[:stored_credentials][:sequence] = options[:sequence] || sequence(options[:stored_credential][:initial_transaction]) + params[:stored_credentials][:is_scheduled] = options[:is_scheduled] || is_scheduled(options[:stored_credential][:reason_type]) + params[:stored_credentials][:auth_type_override] = options[:auth_type_override] if options[:auth_type_override] + end + end + + def original_transaction_id(options) + return options[:cardbrand_original_transaction_id] || options.dig(:stored_credential, :network_transaction_id) + end + + def initiator(options) + return options[:initiator] if options[:initiator] + return options[:stored_credential][:initiator].upcase if options.dig(:stored_credential, :initiator) + end + + def sequence(initial_transaction) + initial_transaction ? 'FIRST' : 'SUBSEQUENT' + end + + def is_scheduled(reason_type) + reason_type == 'recurring' ? 'true' : 'false' + end + def commit(params, options) url = base_url(options) + endpoint(params) @@ -248,15 +378,16 @@ def commit(params, options) response = json_error(e.response.body) end + success = success_from(response) Response.new( - success_from(response), - handle_message(response, success_from(response)), + success, + handle_message(response, success), response, test: test?, authorization: authorization_from(params, response), - avs_result: {code: response['avs']}, + avs_result: { code: response['avs'] }, cvv_result: response['cvv2'], - error_code: error_code(response, success_from(response)) + error_code: success ? nil : error_code_from(response) ) end @@ -271,7 +402,7 @@ def base_url(options) end def endpoint(params) - is_store_action?(params) ? '/transactions/tokens' : '/transactions' + store_action?(params) ? '/transactions/tokens' : '/transactions' end def api_request(url, params) @@ -281,7 +412,8 @@ def api_request(url, params) def post_data(params) return nil unless params - params.reject { |k, v| v.blank? }.collect { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&') + + params.reject { |_k, v| v.blank? }.collect { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&') end def generate_hmac(nonce, current_timestamp, payload) @@ -292,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) @@ -309,9 +440,16 @@ def headers(payload) } end - def error_code(response, success) - return if success - response['Error'].to_h['messages'].to_a.map { |e| e['code'] }.join(', ') + def error_code_from(response) + error_code = nil + if response['bank_resp_code'] == '100' + return + elsif response['bank_resp_code'] + error_code = response['bank_resp_code'] + elsif error_code = response['Error'].to_h['messages'].to_a.map { |e| e['code'] }.join(', ') + end + + error_code end def success_from(response) @@ -347,7 +485,7 @@ def handle_message(response, success) end def authorization_from(params, response) - if is_store_action?(params) + if store_action?(params) if success_from(response) [ response['token']['type'], @@ -363,7 +501,7 @@ def authorization_from(params, response) response['transaction_id'], response['transaction_tag'], params[:method], - (response['amount'] && response['amount'].to_i) + response['amount']&.to_i ].join('|') end end @@ -379,7 +517,7 @@ def response_error(raw_response) end def json_error(raw_response) - {'error' => "Unable to parse response: #{raw_response.inspect}"} + { 'error' => "Unable to parse response: #{raw_response.inspect}" } end end end diff --git a/lib/active_merchant/billing/gateways/payex.rb b/lib/active_merchant/billing/gateways/payex.rb index 3a63f379872..2abf8b21e35 100644 --- a/lib/active_merchant/billing/gateways/payex.rb +++ b/lib/active_merchant/billing/gateways/payex.rb @@ -1,7 +1,7 @@ require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class PayexGateway < Gateway class_attribute :live_external_url, :test_external_url, :live_confined_url, :test_confined_url @@ -12,8 +12,8 @@ class PayexGateway < Gateway self.test_confined_url = 'https://test-confined.payex.com/' self.money_format = :cents - self.supported_countries = ['DK', 'FI', 'NO', 'SE'] - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_countries = %w[DK FI NO SE] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'http://payex.com/' self.display_name = 'Payex' self.default_currency = 'EUR' @@ -25,18 +25,18 @@ class PayexGateway < Gateway authorize: '3', cancel: '4', failure: '5', - capture: '6', + capture: '6' } SOAP_ACTIONS = { initialize: { name: 'Initialize8', url: 'pxorder/pxorder.asmx', xmlns: 'http://external.payex.com/PxOrder/' }, - purchasecc: { name: 'PurchaseCC', url: 'pxconfined/pxorder.asmx', xmlns: 'http://confined.payex.com/PxOrder/', confined: true}, + purchasecc: { name: 'PurchaseCC', url: 'pxconfined/pxorder.asmx', xmlns: 'http://confined.payex.com/PxOrder/', confined: true }, cancel: { name: 'Cancel2', url: 'pxorder/pxorder.asmx', xmlns: 'http://external.payex.com/PxOrder/' }, capture: { name: 'Capture5', url: 'pxorder/pxorder.asmx', xmlns: 'http://external.payex.com/PxOrder/' }, credit: { name: 'Credit5', url: 'pxorder/pxorder.asmx', xmlns: 'http://external.payex.com/PxOrder/' }, create_agreement: { name: 'CreateAgreement3', url: 'pxagreement/pxagreement.asmx', xmlns: 'http://external.payex.com/PxAgreement/' }, delete_agreement: { name: 'DeleteAgreement', url: 'pxagreement/pxagreement.asmx', xmlns: 'http://external.payex.com/PxAgreement/' }, - autopay: { name: 'AutoPay3', url: 'pxagreement/pxagreement.asmx', xmlns: 'http://external.payex.com/PxAgreement/' }, + autopay: { name: 'AutoPay3', url: 'pxagreement/pxagreement.asmx', xmlns: 'http://external.payex.com/PxAgreement/' } } def initialize(options = {}) @@ -63,14 +63,13 @@ def authorize(amount, payment_method, options = {}) if payment_method.respond_to?(:number) # credit card authorization MultiResponse.new.tap do |r| - r.process {send_initialize(amount, true, options)} - r.process {send_purchasecc(payment_method, r.params['orderref'])} + r.process { send_initialize(amount, true, options) } + r.process { send_purchasecc(payment_method, r.params['orderref']) } end else # stored authorization send_autopay(amount, payment_method, true, options) end - end # Public: Send a purchase Payex request @@ -92,8 +91,8 @@ def purchase(amount, payment_method, options = {}) if payment_method.respond_to?(:number) # credit card purchase MultiResponse.new.tap do |r| - r.process {send_initialize(amount, false, options)} - r.process {send_purchasecc(payment_method, r.params['orderref'])} + r.process { send_initialize(amount, false, options) } + r.process { send_purchasecc(payment_method, r.params['orderref']) } end else # stored purchase @@ -118,7 +117,7 @@ def capture(money, authorization, options = {}) # options - A standard ActiveMerchant options hash # # Returns an ActiveMerchant::Billing::Response object - def void(authorization, options={}) + def void(authorization, options = {}) send_cancel(authorization) end @@ -155,10 +154,10 @@ def store(creditcard, options = {}) requires!(options, :order_id) amount = amount(1) # 1 cent for authorization MultiResponse.run(:first) do |r| - r.process {send_create_agreement(options)} - r.process {send_initialize(amount, true, options.merge({agreement_ref: r.authorization}))} + r.process { send_create_agreement(options) } + r.process { send_initialize(amount, true, options.merge({ agreement_ref: r.authorization })) } order_ref = r.params['orderref'] - r.process {send_purchasecc(creditcard, order_ref)} + r.process { send_purchasecc(creditcard, order_ref) } end end @@ -194,9 +193,9 @@ def send_initialize(amount, is_auth, options = {}) cancelUrl: nil, clientLanguage: nil } - hash_fields = [:accountNumber, :purchaseOperation, :price, :priceArgList, :currency, :vat, :orderID, - :productNumber, :description, :clientIPAddress, :clientIdentifier, :additionalValues, - :externalID, :returnUrl, :view, :agreementRef, :cancelUrl, :clientLanguage] + hash_fields = %i[accountNumber purchaseOperation price priceArgList currency vat orderID + productNumber description clientIPAddress clientIdentifier additionalValues + externalID returnUrl view agreementRef cancelUrl clientLanguage] add_request_hash(properties, hash_fields) soap_action = SOAP_ACTIONS[:initialize] request = build_xml_request(soap_action, properties) @@ -214,8 +213,8 @@ def send_purchasecc(payment_method, order_ref) cardHolderName: payment_method.name, cardNumberCVC: payment_method.verification_value } - hash_fields = [:accountNumber, :orderRef, :transactionType, :cardNumber, :cardNumberExpireMonth, - :cardNumberExpireYear, :cardNumberCVC, :cardHolderName] + hash_fields = %i[accountNumber orderRef transactionType cardNumber cardNumberExpireMonth + cardNumberExpireYear cardNumberCVC cardHolderName] add_request_hash(properties, hash_fields) soap_action = SOAP_ACTIONS[:purchasecc] @@ -232,9 +231,9 @@ def send_autopay(amount, authorization, is_auth, options = {}) description: options[:description] || options[:order_id], orderId: options[:order_id], purchaseOperation: is_auth ? 'AUTHORIZATION' : 'SALE', - currency: (options[:currency] || default_currency), + currency: (options[:currency] || default_currency) } - hash_fields = [:accountNumber, :agreementRef, :price, :productNumber, :description, :orderId, :purchaseOperation, :currency] + hash_fields = %i[accountNumber agreementRef price productNumber description orderId purchaseOperation currency] add_request_hash(properties, hash_fields) soap_action = SOAP_ACTIONS[:autopay] @@ -246,12 +245,12 @@ def send_capture(amount, transaction_number, options = {}) properties = { accountNumber: @options[:account], transactionNumber: transaction_number, - amount: amount, + amount:, orderId: options[:order_id] || '', vatAmount: options[:vat_amount] || 0, additionalValues: '' } - hash_fields = [:accountNumber, :transactionNumber, :amount, :orderId, :vatAmount, :additionalValues] + hash_fields = %i[accountNumber transactionNumber amount orderId vatAmount additionalValues] add_request_hash(properties, hash_fields) soap_action = SOAP_ACTIONS[:capture] @@ -263,12 +262,12 @@ def send_credit(transaction_number, amount, options = {}) properties = { accountNumber: @options[:account], transactionNumber: transaction_number, - amount: amount, + amount:, orderId: options[:order_id], vatAmount: options[:vat_amount] || 0, additionalValues: '' } - hash_fields = [:accountNumber, :transactionNumber, :amount, :orderId, :vatAmount, :additionalValues] + hash_fields = %i[accountNumber transactionNumber amount orderId vatAmount additionalValues] add_request_hash(properties, hash_fields) soap_action = SOAP_ACTIONS[:credit] @@ -279,9 +278,9 @@ def send_credit(transaction_number, amount, options = {}) def send_cancel(transaction_number) properties = { accountNumber: @options[:account], - transactionNumber: transaction_number, + transactionNumber: transaction_number } - hash_fields = [:accountNumber, :transactionNumber] + hash_fields = %i[accountNumber transactionNumber] add_request_hash(properties, hash_fields) soap_action = SOAP_ACTIONS[:cancel] @@ -300,7 +299,7 @@ def send_create_agreement(options) startDate: options[:startDate] || '', stopDate: options[:stopDate] || '' } - hash_fields = [:accountNumber, :merchantRef, :description, :purchaseOperation, :maxAmount, :notifyUrl, :startDate, :stopDate] + hash_fields = %i[accountNumber merchantRef description purchaseOperation maxAmount notifyUrl startDate stopDate] add_request_hash(properties, hash_fields) soap_action = SOAP_ACTIONS[:create_agreement] @@ -311,9 +310,9 @@ def send_create_agreement(options) def send_delete_agreement(authorization) properties = { accountNumber: @options[:account], - agreementRef: authorization, + agreementRef: authorization } - hash_fields = [:accountNumber, :agreementRef] + hash_fields = %i[accountNumber agreementRef] add_request_hash(properties, hash_fields) soap_action = SOAP_ACTIONS[:delete_agreement] @@ -342,9 +341,9 @@ def add_request_hash(properties, fields) def build_xml_request(soap_action, properties) builder = Nokogiri::XML::Builder.new - builder.__send__('soap12:Envelope', {'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', + builder.__send__('soap12:Envelope', { 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema', - 'xmlns:soap12' => 'http://www.w3.org/2003/05/soap-envelope'}) do |root| + 'xmlns:soap12' => 'http://www.w3.org/2003/05/soap-envelope' }) do |root| root.__send__('soap12:Body') do |body| body.__send__(soap_action[:name], xmlns: soap_action[:xmlns]) do |doc| properties.each do |key, val| @@ -364,8 +363,8 @@ def parse(xml) doc = Nokogiri::XML(body) - doc.root.xpath('*').each do |node| - if (node.elements.size == 0) + doc.root&.xpath('*')&.each do |node| + if node.elements.size == 0 response[node.name.downcase.to_sym] = node.text else node.elements.each do |childnode| @@ -373,7 +372,7 @@ def parse(xml) response[name.to_sym] = childnode.text end end - end unless doc.root.nil? + end response end @@ -386,12 +385,13 @@ def commit(soap_action, request) 'Content-Length' => request.size.to_s } response = parse(ssl_post(url, request, headers)) - Response.new(success?(response), - message_from(response), - response, - test: test?, - authorization: build_authorization(response) - ) + Response.new( + success?(response), + message_from(response), + response, + test: test?, + authorization: build_authorization(response) + ) end def build_authorization(response) @@ -409,4 +409,3 @@ def message_from(response) end end end - diff --git a/lib/active_merchant/billing/gateways/payflow.rb b/lib/active_merchant/billing/gateways/payflow.rb index da13e3a1d40..6a0f0d51e2c 100644 --- a/lib/active_merchant/billing/gateways/payflow.rb +++ b/lib/active_merchant/billing/gateways/payflow.rb @@ -1,15 +1,16 @@ +require 'nokogiri' require 'active_merchant/billing/gateways/payflow/payflow_common_api' require 'active_merchant/billing/gateways/payflow/payflow_response' require 'active_merchant/billing/gateways/payflow_express' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class PayflowGateway < Gateway include PayflowCommonAPI - RECURRING_ACTIONS = Set.new([:add, :modify, :cancel, :inquiry, :reactivate, :payment]) + RECURRING_ACTIONS = Set.new(%i[add modify cancel inquiry reactivate payment]) - self.supported_cardtypes = [:visa, :master, :american_express, :jcb, :discover, :diners_club] + self.supported_cardtypes = %i[visa master american_express jcb discover diners_club] self.homepage_url = 'https://www.paypal.com/cgi-bin/webscr?cmd=_payflow-pro-overview-outside' self.display_name = 'PayPal Payflow Pro' @@ -44,7 +45,7 @@ def refund(money, reference, options = {}) commit(build_reference_request(:credit, money, reference, options), options) end - def verify(payment, options={}) + def verify(payment, options = {}) if credit_card_type(payment) == 'Amex' MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, payment, options) } @@ -55,6 +56,10 @@ def verify(payment, options={}) end end + def store(payment, options = {}) + raise ArgumentError, 'Store is not supported on Payflow gateways' + end + def verify_credentials response = void('0') response.params['result'] != '26' @@ -78,22 +83,23 @@ def recurring(money, credit_card, options = {}) options[:name] = credit_card.name if options[:name].blank? && credit_card request = build_recurring_request(options[:profile_id] ? :modify : :add, money, options) do |xml| add_credit_card(xml, credit_card, options) if credit_card + add_stored_credential(xml, options[:stored_credential]) end - commit(request, options.merge(:request_type => :recurring)) + commit(request, options.merge(request_type: :recurring)) end def cancel_recurring(profile_id) ActiveMerchant.deprecated RECURRING_DEPRECATION_MESSAGE - request = build_recurring_request(:cancel, 0, :profile_id => profile_id) - commit(request, options.merge(:request_type => :recurring)) + request = build_recurring_request(:cancel, 0, profile_id:) + commit(request, options.merge(request_type: :recurring)) end def recurring_inquiry(profile_id, options = {}) ActiveMerchant.deprecated RECURRING_DEPRECATION_MESSAGE - request = build_recurring_request(:inquiry, nil, options.update( :profile_id => profile_id )) - commit(request, options.merge(:request_type => :recurring)) + request = build_recurring_request(:inquiry, nil, options.update(profile_id:)) + commit(request, options.merge(request_type: :recurring)) end def express @@ -135,24 +141,27 @@ def build_reference_sale_or_authorization_request(action, money, reference, opti xml.tag! 'Description', options[:description] unless options[:description].blank? xml.tag! 'OrderDesc', options[:order_desc] unless options[:order_desc].blank? xml.tag! 'Comment', options[:comment] unless options[:comment].blank? - xml.tag!('ExtData', 'Name'=> 'COMMENT2', 'Value'=> options[:comment2]) unless options[:comment2].blank? + xml.tag!('ExtData', 'Name' => 'COMMENT2', 'Value' => options[:comment2]) unless options[:comment2].blank? xml.tag! 'TaxAmt', options[:taxamt] unless options[:taxamt].blank? xml.tag! 'FreightAmt', options[:freightamt] unless options[:freightamt].blank? xml.tag! 'DutyAmt', options[:dutyamt] unless options[:dutyamt].blank? xml.tag! 'DiscountAmt', options[:discountamt] unless options[:discountamt].blank? + xml.tag! 'MerchDescr', options[:merch_descr] unless options[:merch_descr].blank? billing_address = options[:billing_address] || options[:address] add_address(xml, 'BillTo', billing_address, options) if billing_address - add_address(xml, 'ShipTo', options[:shipping_address],options) if options[:shipping_address] + add_address(xml, 'ShipTo', options[:shipping_address], options) if options[:shipping_address] xml.tag! 'TotalAmt', amount(money), 'Currency' => options[:currency] || currency(money) end xml.tag! 'Tender' do xml.tag! 'Card' do - xml.tag! 'ExtData', 'Name' => 'ORIGID', 'Value' => reference + xml.tag! 'ExtData', 'Name' => 'ORIGID', 'Value' => reference end end + add_stored_credential(xml, options[:stored_credential]) end + xml.tag! 'ExtData', 'Name' => 'BUTTONSOURCE', 'Value' => application_id unless application_id.blank? end xml.target! end @@ -168,12 +177,13 @@ def build_credit_card_request(action, money, credit_card, options) xml.tag! 'OrderDesc', options[:order_desc] unless options[:order_desc].blank? # Comment and Comment2 will show up in manager.paypal.com as Comment1 and Comment2 xml.tag! 'Comment', options[:comment] unless options[:comment].blank? - xml.tag!('ExtData', 'Name'=> 'COMMENT2', 'Value'=> options[:comment2]) unless options[:comment2].blank? + xml.tag!('ExtData', 'Name' => 'COMMENT2', 'Value' => options[:comment2]) unless options[:comment2].blank? xml.tag! 'TaxAmt', options[:taxamt] unless options[:taxamt].blank? xml.tag! 'FreightAmt', options[:freightamt] unless options[:freightamt].blank? xml.tag! 'DutyAmt', options[:dutyamt] unless options[:dutyamt].blank? xml.tag! 'DiscountAmt', options[:discountamt] unless options[:discountamt].blank? xml.tag! 'EMail', options[:email] unless options[:email].nil? + xml.tag! 'MerchDescr', options[:merch_descr] unless options[:merch_descr].blank? billing_address = options[:billing_address] || options[:address] add_address(xml, 'BillTo', billing_address, options) if billing_address @@ -182,12 +192,76 @@ def build_credit_card_request(action, money, credit_card, options) xml.tag! 'TotalAmt', amount(money), 'Currency' => options[:currency] || currency(money) 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) end + add_stored_credential(xml, options[:stored_credential]) end + xml.tag! 'ExtData', 'Name' => 'BUTTONSOURCE', 'Value' => application_id unless application_id.blank? end - xml.target! + add_level_two_three_fields(xml.target!, options) + end + + def add_mpi_3ds(xml, three_d_secure_options) + # structure as per https://developer.paypal.com/api/nvp-soap/payflow/3d-secure-mpi/ + authentication_id = three_d_secure_options[:authentication_id] + authentication_status = three_d_secure_options[:authentication_response_status] + + eci = three_d_secure_options[:eci] + cavv = three_d_secure_options[:cavv] + xid = three_d_secure_options[:xid] + version = three_d_secure_options[:version] + + # 3DS2 only + ds_transaction_id = three_d_secure_options[:ds_transaction_id] if version_2_or_newer?(three_d_secure_options) + + xml.tag!('ExtData', 'Name' => 'AUTHENTICATION_ID', 'Value' => authentication_id) unless authentication_id.blank? + xml.tag!('ExtData', 'Name' => 'AUTHENTICATION_STATUS', 'Value' => authentication_status) unless authentication_status.blank? + + xml.tag!('ExtData', 'Name' => 'CAVV', 'Value' => cavv) unless cavv.blank? + xml.tag!('ExtData', 'Name' => 'ECI', 'Value' => eci) unless eci.blank? + xml.tag!('ExtData', 'Name' => 'XID', 'Value' => xid) unless xid.blank? + xml.tag!('ExtData', 'Name' => 'THREEDSVERSION', 'Value' => version) unless version.blank? + xml.tag!('ExtData', 'Name' => 'DSTRANSACTIONID', 'Value' => ds_transaction_id) unless ds_transaction_id.blank? + end + + def add_level_two_three_fields(xml_string, options) + if options[:level_two_fields] || options[:level_three_fields] + xml_doc = Nokogiri::XML.parse(xml_string) + %i[level_two_fields level_three_fields].each do |fields| + xml_string = add_fields(xml_doc, options[fields]) if options[fields] + end + end + xml_string + end + + def check_fields(parent, fields, xml_doc) + fields.each do |k, v| + if v.is_a? String + new_node = Nokogiri::XML::Node.new(k, xml_doc) + new_node.add_child(v) + xml_doc.at_css(parent).add_child(new_node) + else + check_subparent_before_continuing(parent, k, xml_doc) + check_fields(k, v, xml_doc) + end + end + xml_doc + end + + def check_subparent_before_continuing(parent, subparent, xml_doc) + unless xml_doc.at_css(subparent) + subparent_node = Nokogiri::XML::Node.new(subparent, xml_doc) + xml_doc.at_css(parent).add_child(subparent_node) + end + end + + def add_fields(xml_doc, options_fields) + fields_to_add = JSON.parse(options_fields) + check_fields('Invoice', fields_to_add, xml_doc) + xml_doc.root.to_s end def build_check_request(action, money, check, options) @@ -199,6 +273,7 @@ def build_check_request(action, money, check, options) xml.tag! 'InvNum', options[:order_id].to_s.gsub(/[^\w.]/, '') unless options[:order_id].blank? xml.tag! 'Description', options[:description] unless options[:description].blank? xml.tag! 'OrderDesc', options[:order_desc] unless options[:order_desc].blank? + xml.tag! 'MerchDescr', options[:merch_descr] unless options[:merch_descr].blank? xml.tag! 'BillTo' do xml.tag! 'Name', check.name end @@ -211,9 +286,11 @@ def build_check_request(action, money, check, options) xml.tag! 'ABA', check.routing_number end end + add_stored_credential(xml, options[:stored_credential]) end + xml.tag! 'ExtData', 'Name' => 'BUTTONSOURCE', 'Value' => application_id unless application_id.blank? end - xml.target! + add_level_two_three_fields(xml.target!, options) end def add_credit_card(xml, credit_card, options = {}) @@ -224,27 +301,76 @@ def add_credit_card(xml, credit_card, options = {}) xml.tag! 'NameOnCard', credit_card.first_name xml.tag! 'CVNum', credit_card.verification_value if credit_card.verification_value? - if options[:three_d_secure] - three_d_secure = options[:three_d_secure] - xml.tag! 'BuyerAuthResult' do - xml.tag! 'Status', three_d_secure[:status] unless three_d_secure[:status].blank? - xml.tag! 'AuthenticationId', three_d_secure[:authentication_id] unless three_d_secure[:authentication_id].blank? - xml.tag! 'PAReq', three_d_secure[:pareq] unless three_d_secure[:pareq].blank? - xml.tag! 'ACSUrl', three_d_secure[:acs_url] unless three_d_secure[:acs_url].blank? - xml.tag! 'ECI', three_d_secure[:eci] unless three_d_secure[:eci].blank? - xml.tag! 'CAVV', three_d_secure[:cavv] unless three_d_secure[:cavv].blank? - xml.tag! 'XID', three_d_secure[:xid] unless three_d_secure[:xid].blank? - end - end + add_three_d_secure(options, xml) + + xml.tag! 'ExtData', 'Name' => 'LASTNAME', 'Value' => credit_card.last_name + end + end + + def add_stored_credential(xml, stored_credential) + return unless stored_credential + + xml.tag! 'CardOnFile', add_card_on_file_type(stored_credential) + xml.tag! 'TxnId', stored_credential[:network_transaction_id] if stored_credential[:network_transaction_id] + end + + def card_on_file_initiator(initator) + case initator + when 'merchant' + 'MIT' + when 'cardholder' + 'CIT' + end + end + + def card_on_file_reason(stored_credential) + return 'I' if stored_credential[:initial_transaction] && stored_credential[:reason_type] == 'unscheduled' - if requires_start_date_or_issue_number?(credit_card) - xml.tag!('ExtData', 'Name' => 'CardStart', 'Value' => startdate(credit_card)) unless credit_card.start_month.blank? || credit_card.start_year.blank? - xml.tag!('ExtData', 'Name' => 'CardIssue', 'Value' => format(credit_card.issue_number, :two_digits)) unless credit_card.issue_number.blank? + case stored_credential[:reason_type] + when 'recurring', 'installment' + 'R' + when 'unscheduled' + 'U' + end + end + + def add_card_on_file_type(stored_credential) + card_on_file_initiator(stored_credential[:initiator]).to_s + card_on_file_reason(stored_credential).to_s + end + + def add_three_d_secure(options, xml) + if options[:three_d_secure] + three_d_secure = options[:three_d_secure] + xml.tag! 'BuyerAuthResult' do + authentication_status(three_d_secure, xml) + xml.tag! 'AuthenticationId', three_d_secure[:authentication_id] unless three_d_secure[:authentication_id].blank? + xml.tag! 'PAReq', three_d_secure[:pareq] unless three_d_secure[:pareq].blank? + xml.tag! 'ACSUrl', three_d_secure[:acs_url] unless three_d_secure[:acs_url].blank? + xml.tag! 'ECI', three_d_secure[:eci] unless three_d_secure[:eci].blank? + xml.tag! 'CAVV', three_d_secure[:cavv] unless three_d_secure[:cavv].blank? + xml.tag! 'XID', three_d_secure[:xid] unless three_d_secure[:xid].blank? + xml.tag! 'ThreeDSVersion', three_d_secure[:version] unless three_d_secure[:version].blank? + xml.tag! 'DSTransactionID', three_d_secure[:ds_transaction_id] unless three_d_secure[:ds_transaction_id].blank? end - xml.tag! 'ExtData', 'Name' => 'LASTNAME', 'Value' => credit_card.last_name end end + def authentication_status(three_d_secure, xml) + status = if three_d_secure[:authentication_response_status].present? + three_d_secure[:authentication_response_status] + elsif three_d_secure[:directory_response_status].present? + three_d_secure[:directory_response_status] + end + if status.present? + xml.tag! 'Status', status + xml.tag! 'AuthenticationStatus', status if version_2_or_newer?(three_d_secure) + end + end + + def version_2_or_newer?(three_d_secure) + three_d_secure[:version]&.start_with?('2') + end + def credit_card_type(credit_card) return '' if card_brand(credit_card).blank? @@ -266,15 +392,13 @@ def startdate(creditcard) end def build_recurring_request(action, money, options) - unless RECURRING_ACTIONS.include?(action) - raise StandardError, "Invalid Recurring Profile Action: #{action}" - end + raise StandardError, "Invalid Recurring Profile Action: #{action}" unless RECURRING_ACTIONS.include?(action) xml = Builder::XmlMarkup.new xml.tag! 'RecurringProfiles' do xml.tag! 'RecurringProfile' do xml.tag! action.to_s.capitalize do - unless [:cancel, :inquiry].include?(action) + unless %i[cancel inquiry].include?(action) xml.tag! 'RPData' do xml.tag! 'Name', options[:name] unless options[:name].nil? xml.tag! 'TotalAmt', amount(money), 'Currency' => options[:currency] || currency(money) @@ -285,7 +409,7 @@ def build_recurring_request(action, money, options) xml.tag! 'MaxFailPayments', options[:max_fail_payments] unless options[:max_fail_payments].nil? if initial_tx = options[:initial_transaction] - requires!(initial_tx, [:type, :authorization, :purchase]) + requires!(initial_tx, %i[type authorization purchase]) requires!(initial_tx, :amount) if initial_tx[:type] == :purchase xml.tag! 'OptionalTrans', TRANSACTIONS[initial_tx[:type]] @@ -293,7 +417,7 @@ def build_recurring_request(action, money, options) end if action == :add - xml.tag! 'Start', format_rp_date(options[:starting_at] || Date.today + 1 ) + xml.tag! 'Start', format_rp_date(options[:starting_at] || (Date.today + 1)) else xml.tag! 'Start', format_rp_date(options[:starting_at]) unless options[:starting_at].nil? end @@ -308,11 +432,9 @@ def build_recurring_request(action, money, options) yield xml end end - if action != :add - xml.tag! 'ProfileID', options[:profile_id] - end + xml.tag! 'ProfileID', options[:profile_id] if action != :add if action == :inquiry - xml.tag! 'PaymentHistory', ( options[:history] ? 'Y' : 'N' ) + xml.tag! 'PaymentHistory', (options[:history] ? 'Y' : 'N') end end end @@ -320,22 +442,22 @@ def build_recurring_request(action, money, options) end def get_pay_period(options) - requires!(options, [:periodicity, :bimonthly, :monthly, :biweekly, :weekly, :yearly, :daily, :semimonthly, :quadweekly, :quarterly, :semiyearly]) + requires!(options, %i[periodicity bimonthly monthly biweekly weekly yearly daily semimonthly quadweekly quarterly semiyearly]) case options[:periodicity] - when :weekly then 'Weekly' - when :biweekly then 'Bi-weekly' - when :semimonthly then 'Semi-monthly' - when :quadweekly then 'Every four weeks' - when :monthly then 'Monthly' - when :quarterly then 'Quarterly' - when :semiyearly then 'Semi-yearly' - when :yearly then 'Yearly' + when :weekly then 'Weekly' + when :biweekly then 'Bi-weekly' + when :semimonthly then 'Semi-monthly' + when :quadweekly then 'Every four weeks' + when :monthly then 'Monthly' + when :quarterly then 'Quarterly' + when :semiyearly then 'Semi-yearly' + when :yearly then 'Yearly' end end def format_rp_date(time) case time - when Time, Date then time.strftime('%m%d%Y') + when Time, Date then time.strftime('%m%d%Y') else time.to_s end 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 fc0b78a5013..481ecde38d0 100644 --- a/lib/active_merchant/billing/gateways/payflow/payflow_common_api.rb +++ b/lib/active_merchant/billing/gateways/payflow/payflow_common_api.rb @@ -1,6 +1,6 @@ require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: module PayflowCommonAPI def self.included(base) base.default_currency = 'USD' @@ -10,7 +10,7 @@ def self.included(base) # Set the default partner to PayPal base.partner = 'PayPal' - base.supported_countries = ['US', 'CA', 'NZ', 'AU'] + base.supported_countries = %w[US CA NZ AU] base.class_attribute :timeout base.timeout = 60 @@ -37,22 +37,20 @@ def self.included(base) XMLNS = 'http://www.paypal.com/XMLPay' CARD_MAPPING = { - :visa => 'Visa', - :master => 'MasterCard', - :discover => 'Discover', - :american_express => 'Amex', - :jcb => 'JCB', - :diners_club => 'DinersClub', - :switch => 'Switch', - :solo => 'Solo' + visa: 'Visa', + master: 'MasterCard', + discover: 'Discover', + american_express: 'Amex', + jcb: 'JCB', + diners_club: 'DinersClub' } TRANSACTIONS = { - :purchase => 'Sale', - :authorization => 'Authorization', - :capture => 'Capture', - :void => 'Void', - :credit => 'Credit' + purchase: 'Sale', + authorization: 'Authorization', + capture: 'Capture', + void: 'Void', + credit: 'Credit' } CVV_CODE = { @@ -80,6 +78,7 @@ def void(authorization, options = {}) end private + def build_request(body, options = {}) xml = Builder::XmlMarkup.new xml.instruct! @@ -100,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 @@ -118,7 +117,13 @@ def build_reference_request(action, money, authorization, options) xml.tag!('TotalAmt', amount(money), 'Currency' => options[:currency] || currency(money)) xml.tag!('Description', options[:description]) unless options[:description].blank? xml.tag!('Comment', options[:comment]) unless options[:comment].blank? - xml.tag!('ExtData', 'Name'=> 'COMMENT2', 'Value'=> options[:comment2]) unless options[:comment2].blank? + xml.tag!('ExtData', 'Name' => 'COMMENT2', 'Value' => options[:comment2]) unless options[:comment2].blank? + xml.tag!('MerchDescr', options[:merch_descr]) unless options[:merch_descr].blank? + xml.tag!( + 'ExtData', + 'Name' => 'CAPTURECOMPLETE', + 'Value' => options[:capture_complete] + ) unless options[:capture_complete].blank? end end end @@ -128,6 +133,7 @@ def build_reference_request(action, money, authorization, options) def add_address(xml, tag, address, options) return if address.nil? + xml.tag! tag do xml.tag! 'Name', address[:name] unless address[:name].blank? xml.tag! 'EMail', options[:email] unless options[:email].blank? @@ -155,9 +161,7 @@ def parse(data) # REXML::XPath in Ruby 1.8.6 is now unable to match nodes based on their attributes tx_result = root.xpath('.//TransactionResult').first - if tx_result && tx_result.attributes['Duplicate'].to_s == 'true' - response[:duplicate] = true - end + response[:duplicate] = true if tx_result && tx_result.attributes['Duplicate'].to_s == 'true' root.xpath('.//*').each do |node| parse_element(response, node) @@ -174,11 +178,11 @@ def parse_element(response, node) # down as we do everywhere else. RPPaymentResult elements are not contained # in an RPPaymentResults element so we'll come here multiple times response[node_name] ||= [] - response[node_name] << ( payment_result_response = {} ) - node.xpath('.//*').each{ |e| parse_element(payment_result_response, e) } + response[node_name] << (payment_result_response = {}) + node.xpath('.//*').each { |e| parse_element(payment_result_response, e) } when node.xpath('.//*').to_a.any? - node.xpath('.//*').each{|e| parse_element(response, e) } - when node_name.to_s =~ /amt$/ + node.xpath('.//*').each { |e| parse_element(response, e) } + when /amt$/.match?(node_name.to_s) # *Amt elements don't put the value in the #text - instead they use a Currency attribute response[node_name] = node.attributes['Currency'].to_s when node_name == :ext_data @@ -198,7 +202,7 @@ def build_headers(content_length) 'X-VPS-Request-ID' => SecureRandom.hex(16) } - headers.merge!('PAYPAL-NVP' => 'Y') if self.use_paypal_nvp + headers['PAYPAL-NVP'] = 'Y' if self.use_paypal_nvp headers end diff --git a/lib/active_merchant/billing/gateways/payflow/payflow_express_response.rb b/lib/active_merchant/billing/gateways/payflow/payflow_express_response.rb index 7b4068dea05..d73ccb1a929 100644 --- a/lib/active_merchant/billing/gateways/payflow/payflow_express_response.rb +++ b/lib/active_merchant/billing/gateways/payflow/payflow_express_response.rb @@ -1,29 +1,33 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class PayflowExpressResponse < Response def email @params['e_mail'] end - + def full_name "#{@params['name']} #{@params['lastname']}" end - + def token @params['token'] end - + def payer_id @params['payer_id'] end - + # Really the shipping country, but it is all the information provided def payer_country address['country'] end - + + def phone + @params['phone'] + end + def address - { 'name' => full_name, + { 'name' => @params['shiptoname'] || full_name, 'company' => nil, 'address1' => @params['street'], 'address2' => @params['shiptostreet2'] || @params['street2'], @@ -31,8 +35,7 @@ def address 'state' => @params['state'], 'country' => @params['country'], 'zip' => @params['zip'], - 'phone' => nil - } + 'phone' => phone } end end end diff --git a/lib/active_merchant/billing/gateways/payflow/payflow_response.rb b/lib/active_merchant/billing/gateways/payflow/payflow_response.rb index d2b6e670009..648ef3ba9fd 100644 --- a/lib/active_merchant/billing/gateways/payflow/payflow_response.rb +++ b/lib/active_merchant/billing/gateways/payflow/payflow_response.rb @@ -1,13 +1,13 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class PayflowResponse < Response def profile_id @params['profile_id'] end - + def payment_history - @payment_history ||= @params['rp_payment_result'].collect{ |result| result.stringify_keys } rescue [] + @payment_history ||= @params['rp_payment_result'].collect(&:stringify_keys) rescue [] end end end -end \ No newline at end of file +end diff --git a/lib/active_merchant/billing/gateways/payflow_express.rb b/lib/active_merchant/billing/gateways/payflow_express.rb index d2e0b27ede5..cc08facd0d8 100644 --- a/lib/active_merchant/billing/gateways/payflow_express.rb +++ b/lib/active_merchant/billing/gateways/payflow_express.rb @@ -2,64 +2,61 @@ require 'active_merchant/billing/gateways/payflow/payflow_express_response' require 'active_merchant/billing/gateways/paypal_express_common' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: - # ==General Parameters - # The following parameters are supported for #setup_authorization, #setup_purchase, #authorize and #purchase transactions. I've read - # in the docs that they recommend you pass the exact same parameters to both setup and authorize/purchase. - # - # This information was gleaned from a mix of: - # * PayFlow documentation - # * for key value pairs: {Express Checkout for Payflow Pro (PDF)}[https://cms.paypal.com/cms_content/US/en_US/files/developer/PFP_ExpressCheckout_PP.pdf] - # * XMLPay: {Payflow Pro XMLPay Developer's Guide (PDF)}[https://cms.paypal.com/cms_content/US/en_US/files/developer/PP_PayflowPro_XMLPay_Guide.pdf] - # * previous ActiveMerchant code - # * trial & error - # - # The following parameters are currently supported. - # [:ip] (opt) Customer IP Address - # [:order_id] (opt) An order or invoice number. This will be passed through to the Payflow backend at manager.paypal.com, and show up as "Supplier Reference #" - # [:description] (opt) Order description, shown to buyer (after redirected to PayPal). If Order Line Items are used (see below), then the description is suppressed. This will not be passed through to the Payflow backend. - # [:billing_address] (opt) See ActiveMerchant::Billing::Gateway for details - # [:shipping_address] (opt) See ActiveMerchant::Billing::Gateway for details - # [:currency] (req) Currency of transaction, will be set to USD by default for PayFlow Express if not specified - # [:email] (opt) Email of buyer; used to pre-fill PayPal login screen - # [:payer_id] (opt) Unique PayPal buyer account identification number, as returned by details_for request - # [:token] (req for #authorize & #purchase) Token returned by setup transaction - # [:no_shipping] (opt) Boolean for whether or not to display shipping address to buyer - # [:address_override] (opt) Boolean. If true, display shipping address passed by parameters, rather than shipping address on file with PayPal - # [:allow_note] (opt) Boolean for permitting buyer to add note during checkout. Note contents can be retrieved with details_for transaction - # [:return_url] (req) URL to which the buyer’s browser is returned after choosing to pay. - # [:cancel_return_url] (req) URL to which the buyer is returned if the buyer cancels the order. - # [:notify_url] (opt) Your URL for receiving Instant Payment Notification (IPN) about this transaction. - # [:comment] (opt) Comment field which will be reported to Payflow backend (at manager.paypal.com) as Comment1 - # [:comment2] (opt) Comment field which will be reported to Payflow backend (at manager.paypal.com) as Comment2 - # [:discount] (opt) Total discounts in cents - # - # ==Line Items - # Support for order line items is available, but has to be enabled on the PayFlow backend. This is what I was told by Todd Sieber at Technical Support: - # - # You will need to call Payflow Support at 1-888-883-9770, choose option #2. Request that they update your account in "Pandora" under Product Settings >> PayPal Mark and update the Features Bitmap to 1111111111111112. This is 15 ones and a two. - # - # See here[https://www.x.com/message/206214#206214] for the forum discussion (requires login to {x.com}[https://x.com] - # - # [:items] (opt) Array of Order Line Items hashes. These are shown to the buyer after redirect to PayPal. - # - # - # - # The following keys are supported for line items: - # [:name] Name of line item - # [:description] Description of line item - # [:amount] Line Item Amount in Cents (as Integer) - # [:quantity] Line Item Quantity (default to 1 if left blank) - # - # ====Customization of Payment Page - # [:page_style] (opt) Your URL for receiving Instant Payment Notification (IPN) about this transaction. - # [:header_image] (opt) Your URL for receiving Instant Payment Notification (IPN) about this transaction. - # [:background_color] (opt) Your URL for receiving Instant Payment Notification (IPN) about this transaction. - # ====Additional options for old Checkout Experience, being phased out in 2010 and 2011 - # [:header_background_color] (opt) Your URL for receiving Instant Payment Notification (IPN) about this transaction. - # [:header_border_color] (opt) Your URL for receiving Instant Payment Notification (IPN) about this transaction. - +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + # ==General Parameters + # The following parameters are supported for #setup_authorization, #setup_purchase, #authorize and #purchase transactions. I've read + # in the docs that they recommend you pass the exact same parameters to both setup and authorize/purchase. + # + # This information was gleaned from a mix of: + # * {PayFlow documentation}[https://developer.paypal.com/docs/classic/payflow/integration-guide/] + # * previous ActiveMerchant code + # * trial & error + # + # The following parameters are currently supported. + # [:ip] (opt) Customer IP Address + # [:order_id] (opt) An order or invoice number. This will be passed through to the Payflow backend at manager.paypal.com, and show up as "Supplier Reference #" + # [:description] (opt) Order description, shown to buyer (after redirected to PayPal). If Order Line Items are used (see below), then the description is suppressed. This will not be passed through to the Payflow backend. + # [:billing_address] (opt) See ActiveMerchant::Billing::Gateway for details + # [:shipping_address] (opt) See ActiveMerchant::Billing::Gateway for details + # [:currency] (req) Currency of transaction, will be set to USD by default for PayFlow Express if not specified + # [:email] (opt) Email of buyer; used to pre-fill PayPal login screen + # [:payer_id] (opt) Unique PayPal buyer account identification number, as returned by details_for request + # [:token] (req for #authorize & #purchase) Token returned by setup transaction + # [:no_shipping] (opt) Boolean for whether or not to display shipping address to buyer + # [:address_override] (opt) Boolean. If true, display shipping address passed by parameters, rather than shipping address on file with PayPal + # [:allow_note] (opt) Boolean for permitting buyer to add note during checkout. Note contents can be retrieved with details_for transaction + # [:return_url] (req) URL to which the buyer’s browser is returned after choosing to pay. + # [:cancel_return_url] (req) URL to which the buyer is returned if the buyer cancels the order. + # [:notify_url] (opt) Your URL for receiving Instant Payment Notification (IPN) about this transaction. + # [:comment] (opt) Comment field which will be reported to Payflow backend (at manager.paypal.com) as Comment1 + # [:comment2] (opt) Comment field which will be reported to Payflow backend (at manager.paypal.com) as Comment2 + # [:discount] (opt) Total discounts in cents + # + # ==Line Items + # Support for order line items is available, but has to be enabled on the PayFlow backend. This is what I was told by Todd Sieber at Technical Support: + # + # You will need to call Payflow Support at 1-888-883-9770, choose option #2. Request that they update your account in "Pandora" under Product Settings >> PayPal Mark and update the Features Bitmap to 1111111111111112. This is 15 ones and a two. + # + # See here[https://www.x.com/message/206214#206214] for the forum discussion (requires login to {x.com}[https://x.com] + # + # [:items] (opt) Array of Order Line Items hashes. These are shown to the buyer after redirect to PayPal. + # + # + # + # The following keys are supported for line items: + # [:name] Name of line item + # [:description] Description of line item + # [:amount] Line Item Amount in Cents (as Integer) + # [:quantity] Line Item Quantity (default to 1 if left blank) + # + # ====Customization of Payment Page + # [:page_style] (opt) Your URL for receiving Instant Payment Notification (IPN) about this transaction. + # [:header_image] (opt) Your URL for receiving Instant Payment Notification (IPN) about this transaction. + # [:background_color] (opt) Your URL for receiving Instant Payment Notification (IPN) about this transaction. + # ====Additional options for old Checkout Experience, being phased out in 2010 and 2011 + # [:header_background_color] (opt) Your URL for receiving Instant Payment Notification (IPN) about this transaction. + # [:header_border_color] (opt) Your URL for receiving Instant Payment Notification (IPN) about this transaction. class PayflowExpressGateway < Gateway include PayflowCommonAPI @@ -111,8 +108,9 @@ def details_for(token) end private + def build_get_express_details_request(token) - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 xml.tag! 'GetExpressCheckout' do xml.tag! 'Authorization' do xml.tag! 'PayData' do @@ -128,7 +126,7 @@ def build_get_express_details_request(token) end def build_setup_express_sale_or_authorization_request(action, money, options = {}) - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 xml.tag! 'SetExpressCheckout' do xml.tag! action do add_pay_data xml, money, options @@ -138,7 +136,7 @@ def build_setup_express_sale_or_authorization_request(action, money, options = { end def build_sale_or_authorization_request(action, money, options) - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 xml.tag! 'DoExpressCheckout' do xml.tag! action do add_pay_data xml, money, options @@ -157,7 +155,7 @@ def add_pay_data(xml, money, options) # Comment, Comment2 should make it to the backend at manager.paypal.com, as with Payflow credit card transactions # but that doesn't seem to work (yet?). See: https://www.x.com/thread/51908?tstart=0 xml.tag! 'Comment', options[:comment] unless options[:comment].nil? - xml.tag!('ExtData', 'Name'=> 'COMMENT2', 'Value'=> options[:comment2]) unless options[:comment2].nil? + xml.tag!('ExtData', 'Name' => 'COMMENT2', 'Value' => options[:comment2]) unless options[:comment2].nil? billing_address = options[:billing_address] || options[:address] add_address(xml, 'BillTo', billing_address, options) if billing_address @@ -182,7 +180,6 @@ def add_pay_data(xml, money, options) end xml.tag! 'DiscountAmt', amount(options[:discount]) if options[:discount] xml.tag! 'TotalAmt', amount(money), 'Currency' => options[:currency] || currency(money) - end xml.tag! 'Tender' do @@ -201,7 +198,7 @@ def add_paypal_details(xml, options) xml.tag! 'Token', options[:token] unless options[:token].blank? xml.tag! 'NoShipping', options[:no_shipping] ? '1' : '0' xml.tag! 'AddressOverride', options[:address_override] ? '1' : '0' - xml.tag! 'ButtonSource', application_id.to_s.slice(0,32) unless application_id.blank? + xml.tag! 'ButtonSource', application_id.to_s.slice(0, 32) unless application_id.blank? # Customization of the payment page xml.tag! 'PageStyle', options[:page_style] unless options[:page_style].blank? @@ -221,4 +218,3 @@ def build_response(success, message, response, options = {}) end end end - diff --git a/lib/active_merchant/billing/gateways/payflow_express_uk.rb b/lib/active_merchant/billing/gateways/payflow_express_uk.rb index 79bf204de91..2907d4b79c0 100644 --- a/lib/active_merchant/billing/gateways/payflow_express_uk.rb +++ b/lib/active_merchant/billing/gateways/payflow_express_uk.rb @@ -1,7 +1,7 @@ require 'active_merchant/billing/gateways/payflow_express' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class PayflowExpressUkGateway < PayflowExpressGateway self.default_currency = 'GBP' self.partner = 'PayPalUk' @@ -12,4 +12,3 @@ class PayflowExpressUkGateway < PayflowExpressGateway end end end - diff --git a/lib/active_merchant/billing/gateways/payflow_uk.rb b/lib/active_merchant/billing/gateways/payflow_uk.rb index b8c3a711a44..c05737ae6db 100644 --- a/lib/active_merchant/billing/gateways/payflow_uk.rb +++ b/lib/active_merchant/billing/gateways/payflow_uk.rb @@ -1,8 +1,8 @@ require 'active_merchant/billing/gateways/payflow' require 'active_merchant/billing/gateways/payflow_express_uk' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class PayflowUkGateway < PayflowGateway self.default_currency = 'GBP' self.partner = 'PayPalUk' @@ -11,11 +11,10 @@ def express @express ||= PayflowExpressUkGateway.new(@options) end - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :solo, :switch] + self.supported_cardtypes = %i[visa master american_express discover] self.supported_countries = ['GB'] self.homepage_url = 'https://www.paypal.com/uk/webapps/mpp/pro' self.display_name = 'PayPal Payments Pro (UK)' end end end - diff --git a/lib/active_merchant/billing/gateways/payment_express.rb b/lib/active_merchant/billing/gateways/payment_express.rb index b54e5fb1e2f..3807611746a 100644 --- a/lib/active_merchant/billing/gateways/payment_express.rb +++ b/lib/active_merchant/billing/gateways/payment_express.rb @@ -1,8 +1,7 @@ require 'rexml/document' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: - +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # In NZ DPS supports ANZ, Westpac, National Bank, ASB and BNZ. # In Australia DPS supports ANZ, NAB, Westpac, CBA, St George and Bank of South Australia. # The Maybank in Malaysia is supported and the Citibank for Singapore. @@ -14,24 +13,24 @@ class PaymentExpressGateway < Gateway # VISA, Mastercard, Diners Club and Farmers cards are supported # # However, regular accounts with DPS only support VISA and Mastercard - self.supported_cardtypes = [ :visa, :master, :american_express, :diners_club, :jcb ] + self.supported_cardtypes = %i[visa master american_express diners_club jcb] - self.supported_countries = %w[ AU FJ GB HK IE MY NZ PG SG US ] + self.supported_countries = %w[AU FJ GB HK IE MY NZ PG SG US] - self.homepage_url = 'http://www.paymentexpress.com/' - self.display_name = 'PaymentExpress' + self.homepage_url = 'https://windcave.com/' + self.display_name = 'Windcave (formerly PaymentExpress)' - self.live_url = 'https://sec.paymentexpress.com/pxaccess/pxpay.aspx' - self.test_url = 'https://uat.paymentexpress.com/pxaccess/pxpay.aspx' + self.live_url = 'https://sec.windcave.com/pxaccess/pxpay.aspx' + self.test_url = 'https://uat.windcave.com/pxaccess/pxpay.aspx' APPROVED = '1' TRANSACTIONS = { - :purchase => 'Purchase', - :credit => 'Refund', - :authorization => 'Auth', - :capture => 'Complete', - :validate => 'Validate' + purchase: 'Purchase', + credit: 'Refund', + authorization: 'Auth', + capture: 'Complete', + validate: 'Validate' } # We require the DPS gateway username and password when the object is created. @@ -95,6 +94,11 @@ def get_transaction_result(result_code) Hash.from_xml(response) end + def verify(payment_source, options = {}) + request = build_purchase_or_authorization_request(100, payment_source, options) + commit(:validate, request) + end + # Token Based Billing # # Instead of storing the credit card details locally, you can store them inside the @@ -127,7 +131,7 @@ def get_transaction_result(result_code) # # NOTE: Once stored, PaymentExpress does not support unstoring a stored card. def store(credit_card, options = {}) - request = build_token_request(credit_card, options) + request = build_token_request(credit_card, options) commit(:validate, request) end @@ -164,10 +168,11 @@ def build_purchase_or_authorization_request(money, payment_source, options) end end - add_amount(result, money, options) + add_amount(result, money, options) if money add_invoice(result, options) add_address_verification_data(result, options) add_optional_elements(result, options) + add_ip(result, options) result end @@ -178,15 +183,17 @@ def build_capture_or_credit_request(money, identification, options) add_invoice(result, options) add_reference(result, identification) add_optional_elements(result, options) + add_ip(result, options) result end def build_token_request(credit_card, options) result = new_transaction add_credit_card(result, credit_card) - add_amount(result, 100, options) #need to make an auth request for $1 + add_amount(result, 100, options) # need to make an auth request for $1 add_token_request(result, options) add_optional_elements(result, options) + add_ip(result, options) result end @@ -215,11 +222,6 @@ def add_credit_card(xml, credit_card) xml.add_element('Cvc2').text = credit_card.verification_value xml.add_element('Cvc2Presence').text = '1' end - - if requires_start_date_or_issue_number?(credit_card) - xml.add_element('DateStart').text = format_date(credit_card.start_month, credit_card.start_year) unless credit_card.start_month.blank? || credit_card.start_year.blank? - xml.add_element('IssueNumber').text = credit_card.issue_number unless credit_card.issue_number.blank? - end end def add_billing_token(xml, token) @@ -253,13 +255,17 @@ def add_address_verification_data(xml, options) address = options[:billing_address] || options[:address] return if address.nil? - xml.add_element('EnableAvsData').text = 1 - xml.add_element('AvsAction').text = 1 + xml.add_element('EnableAvsData').text = options[:enable_avs_data] || 1 + xml.add_element('AvsAction').text = options[:avs_action] || 1 xml.add_element('AvsStreetAddress').text = address[:address1] xml.add_element('AvsPostCode').text = address[:zip] end + def add_ip(xml, options) + xml.add_element('ClientInfo').text = options[:ip] if options[:ip] + end + # The options hash may contain optional data which will be passed # through the specialized optional fields at PaymentExpress # as follows: @@ -322,7 +328,13 @@ def commit(action, request) response = parse(ssl_post(url, request.to_s)) # Return a response - PaymentExpressResponse.new(response[:valid], message_from(response), response) + PaymentExpressResponse.new( + response[:success] == APPROVED, + message_from(response), + response, + test: response[:test_mode] == '1', + authorization: authorization_from(action, response) + ) end # Response XML documentation: @@ -355,7 +367,7 @@ def message_from(response) def authorization_from(action, response) case action when :validate - (response[:billing_id] || response[:dps_billing_id]) + (response[:billing_id] || response[:dps_billing_id] || response[:dps_txn_ref]) else response[:dps_txn_ref] end @@ -367,13 +379,13 @@ def format_date(month, year) def normalized_client_type(client_type_from_options) case client_type_from_options.to_s.downcase - when 'web' then 'Web' - when 'ivr' then 'IVR' - when 'moto' then 'MOTO' - when 'unattended' then 'Unattended' - when 'internet' then 'Internet' - when 'recurring' then 'Recurring' - else nil + when 'web' then 'Web' + when 'ivr' then 'IVR' + when 'moto' then 'MOTO' + when 'unattended' then 'Unattended' + when 'internet' then 'Internet' + when 'recurring' then 'Recurring' + else nil end end end @@ -382,7 +394,7 @@ class PaymentExpressResponse < Response # add a method to response so we can easily get the token # for Validate transactions def token - @params['billing_id'] || @params['dps_billing_id'] + @params['billing_id'] || @params['dps_billing_id'] || @params['dps_txn_ref'] end end end diff --git a/lib/active_merchant/billing/gateways/paymentez.rb b/lib/active_merchant/billing/gateways/paymentez.rb index 49b628834d7..0807f5e6257 100644 --- a/lib/active_merchant/billing/gateways/paymentez.rb +++ b/lib/active_merchant/billing/gateways/paymentez.rb @@ -1,15 +1,15 @@ require 'base64' require 'digest' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: - class PaymentezGateway < Gateway #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class PaymentezGateway < Gateway # :nodoc: self.test_url = 'https://ccapi-stg.paymentez.com/v2/' self.live_url = 'https://ccapi.paymentez.com/v2/' - self.supported_countries = %w[MX EC VE CO BR CL] + self.supported_countries = %w[MX EC CO BR CL PE] self.default_currency = 'USD' - self.supported_cardtypes = %i[visa master american_express diners_club] + self.supported_cardtypes = %i[visa master american_express diners_club elo alia olimpica discover maestro sodexo carnet unionpay jcb] self.homepage_url = 'https://secure.paymentez.com/' self.display_name = 'Paymentez' @@ -34,11 +34,21 @@ class PaymentezGateway < Gateway #:nodoc: 28 => :card_declined }.freeze + SUCCESS_STATUS = ['APPROVED', 'PENDING', 'pending', 'success', 1, 0] + CARD_MAPPING = { 'visa' => 'vi', 'master' => 'mc', 'american_express' => 'ax', - 'diners_club' => 'di' + 'diners_club' => 'di', + 'elo' => 'el', + 'discover' => 'dc', + 'maestro' => 'ms', + 'sodexo' => 'sx', + 'olimpica' => 'ol', + 'carnet' => 'ct', + 'unionpay' => 'up', + 'jcb' => 'jc' }.freeze def initialize(options = {}) @@ -52,32 +62,48 @@ def purchase(money, payment, options = {}) add_invoice(post, money, options) add_payment(post, payment) add_customer_data(post, options) + add_extra_params(post, options) action = payment.is_a?(String) ? 'debit' : 'debit_cc' commit_transaction(action, post) end def authorize(money, payment, options = {}) + return purchase(money, payment, options) if options[:otp_flow] + post = {} add_invoice(post, money, options) add_payment(post, payment) add_customer_data(post, options) + add_extra_params(post, options) commit_transaction('authorize', post) end - def capture(money, authorization, _options = {}) + def capture(money, authorization, options = {}) post = { - transaction: { id: authorization } + transaction: { id: authorization } } - post[:order] = {amount: amount(money).to_f} if money + verify_flow = options[:type] && options[:value] - commit_transaction('capture', post) + if verify_flow + add_customer_data(post, options) + add_verify_value(post, options) + elsif money + post[:order] = { amount: amount(money).to_f } + end + + action = verify_flow ? 'verify' : 'capture' + commit_transaction(action, post) end - def refund(_money, authorization, options = {}) - void(authorization, options) + def refund(money, authorization, options = {}) + post = { transaction: { id: authorization } } + post[:order] = { amount: amount(money).to_f } if money + add_more_info(post, options) + + commit_transaction('refund', post) end def void(authorization, _options = {}) @@ -107,30 +133,37 @@ def store(credit_card, options = {}) end def unstore(identification, options = {}) - post = { card: { token: identification }, user: { id: options[:user_id] }} + post = { card: { token: identification }, user: { id: options[:user_id] } } commit_card('delete', post) end + def inquire(authorization, options = {}) + commit_transaction('inquire', authorization) + end + def supports_scrubbing? true end def scrub(transcript) - transcript - .gsub(%r{(\\?"number\\?":)(\\?"[^"]+\\?")}, '\1[FILTERED]') - .gsub(%r{(\\?"cvc\\?":)(\\?"[^"]+\\?")}, '\1[FILTERED]') - .gsub(%r{(Auth-Token: )([A-Za-z0-9=]+)}, '\1[FILTERED]') + transcript. + gsub(%r{(\\?"number\\?":)(\\?"[^"]+\\?")}, '\1[FILTERED]'). + gsub(%r{(\\?"cvc\\?":)(\\?"[^"]+\\?")}, '\1[FILTERED]'). + gsub(%r{(Auth-Token: )([A-Za-z0-9=]+)}, '\1[FILTERED]') end private def add_customer_data(post, options) - requires!(options, :user_id, :email) + requires!(options, :user_id) post[:user] ||= {} post[:user][:id] = options[:user_id] - post[:user][:email] = options[:email] + post[:user][:email] = options[:email] if options[:email] post[:user][:ip_address] = options[:ip] if options[:ip] post[:user][:fiscal_number] = options[:fiscal_number] if options[:fiscal_number] + if phone = options[:phone] || options.dig(:billing_address, :phone) + post[:user][:phone] = phone + end end def add_invoice(post, money, options) @@ -162,30 +195,74 @@ def add_payment(post, payment) end end + def add_verify_value(post, options) + post[:type] = options[:type] if options[:type] + post[:value] = options[:value] if options[:value] + end + + def add_extra_params(post, options) + extra_params = {} + extra_params.merge!(options[:extra_params]) if options[:extra_params] + + add_external_mpi_fields(extra_params, options) + + post['extra_params'] = extra_params unless extra_params.empty? + end + + def add_external_mpi_fields(extra_params, options) + three_d_secure_options = options[:three_d_secure] + return unless three_d_secure_options + + auth_data = { + cavv: three_d_secure_options[:cavv], + xid: three_d_secure_options[:xid], + eci: three_d_secure_options[:eci], + version: three_d_secure_options[:version], + 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 + + return if auth_data.empty? + + extra_params[:auth_data] = auth_data + end + + def add_more_info(post, options) + post[:more_info] = options[:more_info] if options[:more_info] + end + def parse(body) JSON.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 parse(raw_response) rescue JSON::ParserError - {'status' => 'Internal server error'} + { 'status' => 'Internal server error' } end end 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), @@ -213,29 +290,46 @@ def headers } end - def success_from(response) - !response.include?('error') && (response['status'] || response['transaction']['status']) == 'success' + 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) + + 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) return false if response.include?('error') return true if response['message'] == 'card deleted' + response['card']['status'] == 'valid' end def message_from(response) + return response['detail'] if response['detail'].present? + if !success_from(response) && response['error'] response['error'] && response['error']['type'] else - response['transaction'] && response['transaction']['message'] + (response['transaction'] && response['transaction']['message']) || (response['message']) end 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 @@ -258,11 +352,10 @@ def post_data(parameters = {}) def error_code_from(response) return if success_from(response) + if response['transaction'] detail = response['transaction']['status_detail'] - if STANDARD_ERROR_CODE_MAPPING.include?(detail) - return STANDARD_ERROR_CODE[STANDARD_ERROR_CODE_MAPPING[detail]] - end + return STANDARD_ERROR_CODE[STANDARD_ERROR_CODE_MAPPING[detail]] if STANDARD_ERROR_CODE_MAPPING.include?(detail) elsif response['error'] return STANDARD_ERROR_CODE[:config_error] end diff --git a/lib/active_merchant/billing/gateways/paymill.rb b/lib/active_merchant/billing/gateways/paymill.rb index 95b4028b670..d1b992ceaea 100644 --- a/lib/active_merchant/billing/gateways/paymill.rb +++ b/lib/active_merchant/billing/gateways/paymill.rb @@ -1,11 +1,11 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class PaymillGateway < Gateway self.supported_countries = %w(AD AT BE BG CH CY CZ DE DK EE ES FI FO FR GB GI GR HR HU IE IL IM IS IT LI LT LU LV MC MT NL NO PL PT RO SE SI SK TR VA) - self.supported_cardtypes = [:visa, :master, :american_express, :diners_club, :discover, :union_pay, :jcb] + self.supported_cardtypes = %i[visa master american_express diners_club discover union_pay jcb] self.homepage_url = 'https://paymill.com' self.display_name = 'PAYMILL' self.money_format = :cents @@ -17,7 +17,7 @@ def initialize(options = {}) super end - def purchase(money, payment_method, options={}) + def purchase(money, payment_method, options = {}) action_with_token(:purchase, money, payment_method, options) end @@ -35,7 +35,7 @@ def capture(money, authorization, options = {}) commit(:post, 'transactions', post) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) post = {} post[:amount] = amount(money) @@ -43,11 +43,16 @@ def refund(money, authorization, options={}) commit(:post, "refunds/#{transaction_id(authorization)}", post) end - def void(authorization, options={}) + def void(authorization, options = {}) commit(:delete, "preauthorizations/#{preauth(authorization)}") end - def store(credit_card, options={}) + def store(credit_card, options = {}) + # The store request requires a currency and amount of at least $1 USD. + # This is used for an authorization that is handled internally by Paymill. + options[:currency] = 'USD' + options[:money] = 100 + save_card(credit_card, options) end @@ -81,15 +86,15 @@ def add_credit_card(post, credit_card, options) post['account.expiry.year'] = sprintf('%.4i', credit_card.year) post['account.verification'] = credit_card.verification_value post['account.email'] = (options[:email] || nil) - post['presentation.amount3D'] = (options[:money] || nil) - post['presentation.currency3D'] = (options[:currency] || currency( options[:money])) + post['presentation.amount3D'] = (options[:money] || nil) + post['presentation.currency3D'] = (options[:currency] || currency(options[:money])) end def headers { 'Authorization' => ('Basic ' + Base64.strict_encode64("#{@options[:private_key]}:X").chomp) } end - def commit(method, action, parameters=nil) + def commit(method, action, parameters = nil) begin raw_response = ssl_request(method, live_url + action, post_data(parameters), headers) rescue ResponseError => e @@ -107,8 +112,8 @@ def commit(method, action, parameters=nil) def response_from(raw_response) parsed = JSON.parse(raw_response) options = { - :authorization => authorization_from(parsed), - :test => (parsed['mode'] == 'test'), + authorization: authorization_from(parsed), + test: (parsed['mode'] == 'test') } succeeded = (parsed['data'] == []) || (parsed['data']['response_code'].to_i == 20000) @@ -128,13 +133,13 @@ def authorization_from(parsed_response) def action_with_token(action, money, payment_method, options) options[:money] = money case payment_method - when String - self.send("#{action}_with_token", money, payment_method, options) - else - MultiResponse.run do |r| - r.process { save_card(payment_method, options) } - r.process { self.send("#{action}_with_token", money, r.authorization, options) } - end + when String + self.send("#{action}_with_token", money, payment_method, options) + else + MultiResponse.run do |r| + r.process { save_card(payment_method, options) } + r.process { self.send("#{action}_with_token", money, r.authorization, options) } + end end end @@ -176,7 +181,7 @@ def save_card(credit_card, options) end def response_for_save_from(raw_response) - options = { :test => test? } + options = { test: test? } parser = ResponseParser.new(raw_response, options) parser.generate_response @@ -193,7 +198,7 @@ def save_card_url def post_data(params) return nil unless params - no_blanks = params.reject { |key, value| value.blank? } + no_blanks = params.reject { |_key, value| value.blank? } no_blanks.map { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&') end @@ -240,7 +245,6 @@ def transaction_id(authorization) 31203 => 'Pending due to currency conflict (accept manually)', 31204 => 'Pending due to fraud filters (accept manually)', - 40000 => 'Problem with transaction data', 40001 => 'Problem with payment data', 40002 => 'Invalid checksum', @@ -315,17 +319,16 @@ def transaction_id(authorization) def response_message(parsed_response) return parsed_response['error'] if parsed_response['error'] - return 'Transaction approved.' if (parsed_response['data'] == []) + return 'Transaction approved.' if parsed_response['data'] == [] code = parsed_response['data']['response_code'].to_i RESPONSE_CODES[code] || code.to_s end - class ResponseParser attr_reader :raw_response, :parsed, :succeeded, :message, :options - def initialize(raw_response='', options={}) + def initialize(raw_response = '', options = {}) @raw_response = raw_response @options = options end @@ -354,12 +357,10 @@ def handle_response_parse_error def handle_response_correct_parsing @message = parsed['transaction']['processing']['return']['message'] - if @succeeded = is_ack? - @options[:authorization] = parsed['transaction']['identification']['uniqueId'] - end + @options[:authorization] = parsed['transaction']['identification']['uniqueId'] if @succeeded = ack? end - def is_ack? + def ack? parsed['transaction']['processing']['result'] == 'ACK' end end diff --git a/lib/active_merchant/billing/gateways/paypal.rb b/lib/active_merchant/billing/gateways/paypal.rb index 4ca95d13f48..aafb33472db 100644 --- a/lib/active_merchant/billing/gateways/paypal.rb +++ b/lib/active_merchant/billing/gateways/paypal.rb @@ -2,14 +2,14 @@ require 'active_merchant/billing/gateways/paypal/paypal_recurring_api' require 'active_merchant/billing/gateways/paypal_express' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class PaypalGateway < Gateway include PaypalCommonAPI include PaypalRecurringApi - self.supported_cardtypes = [:visa, :master, :american_express, :discover] - self.supported_countries = ['US'] + self.supported_countries = %w[CA NZ GB US] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'https://www.paypal.com/us/webapps/mpp/paypal-payments-pro' self.display_name = 'PayPal Payments Pro (US)' @@ -55,10 +55,10 @@ def build_sale_or_authorization_request(action, money, credit_card_or_referenced billing_address = options[:billing_address] || options[:address] currency_code = options[:currency] || currency(money) - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 xml.tag! transaction_type + 'Req', 'xmlns' => PAYPAL_NAMESPACE do xml.tag! transaction_type + 'Request', 'xmlns:n2' => EBAY_NAMESPACE do - xml.tag! 'n2:Version', API_VERSION + xml.tag! 'n2:Version', api_version(options) xml.tag! 'n2:' + transaction_type + 'RequestDetails' do xml.tag! 'n2:ReferenceID', reference_id if transaction_type == 'DoReferenceTransaction' xml.tag! 'n2:PaymentAction', action @@ -73,6 +73,12 @@ def build_sale_or_authorization_request(action, money, credit_card_or_referenced xml.target! end + def api_version(options) + return API_VERSION_3DS2 if options.dig(:three_d_secure, :version) =~ /^2/ + + API_VERSION + end + def add_credit_card(xml, credit_card, address, options) xml.tag! 'n2:CreditCard' do xml.tag! 'n2:CreditCardType', credit_card_type(card_brand(credit_card)) @@ -81,12 +87,6 @@ def add_credit_card(xml, credit_card, address, options) xml.tag! 'n2:ExpYear', format(credit_card.year, :four_digits) xml.tag! 'n2:CVV2', credit_card.verification_value unless credit_card.verification_value.blank? - if [ 'switch', 'solo' ].include?(card_brand(credit_card).to_s) - xml.tag! 'n2:StartMonth', format(credit_card.start_month, :two_digits) unless credit_card.start_month.blank? - xml.tag! 'n2:StartYear', format(credit_card.start_year, :four_digits) unless credit_card.start_year.blank? - xml.tag! 'n2:IssueNumber', format(credit_card.issue_number, :two_digits) unless credit_card.issue_number.blank? - end - xml.tag! 'n2:CardOwner' do xml.tag! 'n2:PayerName' do xml.tag! 'n2:FirstName', credit_card.first_name @@ -96,6 +96,8 @@ def add_credit_card(xml, credit_card, address, options) xml.tag! 'n2:Payer', options[:email] add_address(xml, 'n2:Address', address) end + + add_three_d_secure(xml, options) if options[:three_d_secure] end end @@ -104,19 +106,30 @@ def add_descriptors(xml, options) xml.tag! 'n2:SoftDescriptorCity', options[:soft_descriptor_city] unless options[:soft_descriptor_city].blank? end + def add_three_d_secure(xml, options) + three_d_secure = options[:three_d_secure] + xml.tag! 'ThreeDSecureRequest' do + xml.tag! 'MpiVendor3ds', 'Y' + xml.tag! 'AuthStatus3ds', three_d_secure[:authentication_response_status] || three_d_secure[:trans_status] if three_d_secure[:authentication_response_status] || three_d_secure[:trans_status] + xml.tag! 'Cavv', three_d_secure[:cavv] unless three_d_secure[:cavv].blank? + xml.tag! 'Eci3ds', three_d_secure[:eci] unless three_d_secure[:eci].blank? + xml.tag! 'Xid', three_d_secure[:xid] unless three_d_secure[:xid].blank? + xml.tag! 'ThreeDSVersion', three_d_secure[:version] unless three_d_secure[:version].blank? + xml.tag! 'DSTransactionId', three_d_secure[:ds_transaction_id] unless three_d_secure[:ds_transaction_id].blank? + end + end + def credit_card_type(type) case type when 'visa' then 'Visa' when 'master' then 'MasterCard' when 'discover' then 'Discover' when 'american_express' then 'Amex' - when 'switch' then 'Switch' - when 'solo' then 'Solo' end end def build_response(success, message, response, options = {}) - Response.new(success, message, response, options) + Response.new(success, message, response, options) end end end diff --git a/lib/active_merchant/billing/gateways/paypal/paypal_common_api.rb b/lib/active_merchant/billing/gateways/paypal/paypal_common_api.rb index fe3146eac55..15315cf6d24 100644 --- a/lib/active_merchant/billing/gateways/paypal/paypal_common_api.rb +++ b/lib/active_merchant/billing/gateways/paypal/paypal_common_api.rb @@ -1,10 +1,11 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # This module is included in both PaypalGateway and PaypalExpressGateway module PaypalCommonAPI include Empty API_VERSION = '124' + API_VERSION_3DS2 = '214.0' URLS = { :test => { :certificate => 'https://api.sandbox.paypal.com/2.0/', @@ -178,6 +179,11 @@ def credit(money, identification, options = {}) # . (period) # {space} # + + def inquire(authorization, options = {}) + transaction_details(authorization) + end + def reference_transaction(money, options = {}) requires!(options, :reference_id) commit 'DoReferenceTransaction', build_reference_transaction_request(money, options) @@ -586,7 +592,7 @@ def add_payment_details(xml, money, currency_code, options = {}) xml.tag! 'n2:OrderTotal', localized_amount(money, currency_code), 'currencyID' => currency_code # All of the values must be included together and add up to the order total - if [:subtotal, :shipping, :handling, :tax].all?{ |o| options.has_key?(o) } + if [:subtotal, :shipping, :handling, :tax].all?{ |o| options[o].present? } xml.tag! 'n2:ItemTotal', localized_amount(options[:subtotal], currency_code), 'currencyID' => currency_code xml.tag! 'n2:ShippingTotal', localized_amount(options[:shipping], currency_code),'currencyID' => currency_code xml.tag! 'n2:HandlingTotal', localized_amount(options[:handling], currency_code),'currencyID' => currency_code diff --git a/lib/active_merchant/billing/gateways/paypal/paypal_express_response.rb b/lib/active_merchant/billing/gateways/paypal/paypal_express_response.rb index eeb38da4117..7909b55e775 100644 --- a/lib/active_merchant/billing/gateways/paypal/paypal_express_response.rb +++ b/lib/active_merchant/billing/gateways/paypal/paypal_express_response.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class PaypalExpressResponse < Response def email info['Payer'] @@ -13,6 +13,10 @@ def details (@params['PaymentDetails']||{}) end + def checkout_status + (@params['CheckoutStatus']||{}) + end + def name payer = (info['PayerName']||{}) [payer['FirstName'], payer['MiddleName'], payer['LastName']].compact.join(' ') diff --git a/lib/active_merchant/billing/gateways/paypal/paypal_recurring_api.rb b/lib/active_merchant/billing/gateways/paypal/paypal_recurring_api.rb index 764e7b0a2eb..789d70ab3db 100644 --- a/lib/active_merchant/billing/gateways/paypal/paypal_recurring_api.rb +++ b/lib/active_merchant/billing/gateways/paypal/paypal_recurring_api.rb @@ -1,7 +1,7 @@ require 'active_merchant/billing/gateways/paypal/paypal_common_api' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # This module is included in both PaypalGateway and PaypalExpressGateway module PaypalRecurringApi PAYPAL_NAMESPACE = ActiveMerchant::Billing::PaypalCommonAPI::PAYPAL_NAMESPACE diff --git a/lib/active_merchant/billing/gateways/paypal_ca.rb b/lib/active_merchant/billing/gateways/paypal_ca.rb index 91fb551d342..5ba98b4c3b7 100644 --- a/lib/active_merchant/billing/gateways/paypal_ca.rb +++ b/lib/active_merchant/billing/gateways/paypal_ca.rb @@ -1,10 +1,10 @@ require 'active_merchant/billing/gateways/paypal' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # The PayPal gateway for PayPal Website Payments Pro Canada only supports Visa and MasterCard class PaypalCaGateway < PaypalGateway - self.supported_cardtypes = [:visa, :master] + self.supported_cardtypes = %i[visa master] self.supported_countries = ['CA'] self.homepage_url = 'https://www.paypal.com/cgi-bin/webscr?cmd=_wp-pro-overview-outside' self.display_name = 'PayPal Website Payments Pro (CA)' diff --git a/lib/active_merchant/billing/gateways/paypal_digital_goods.rb b/lib/active_merchant/billing/gateways/paypal_digital_goods.rb index 653a8c59015..7f5417c31c9 100644 --- a/lib/active_merchant/billing/gateways/paypal_digital_goods.rb +++ b/lib/active_merchant/billing/gateways/paypal_digital_goods.rb @@ -2,8 +2,8 @@ require 'active_merchant/billing/gateways/paypal/paypal_express_response' require 'active_merchant/billing/gateways/paypal_express_common' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class PaypalDigitalGoodsGateway < PaypalExpressGateway self.test_redirect_url = 'https://www.sandbox.paypal.com/incontext' self.live_redirect_url = 'https://www.paypal.com/incontext' @@ -31,6 +31,7 @@ def redirect_url_for(token, options = {}) def build_setup_request(action, money, options) requires!(options, :items) raise ArgumentError, 'Must include at least 1 Item' unless options[:items].length > 0 + options[:items].each do |item| requires!(item, :name, :number, :quantity, :amount, :description, :category) raise ArgumentError, "Each of the items must have the category 'Digital'" unless item[:category] == 'Digital' @@ -38,7 +39,6 @@ def build_setup_request(action, money, options) super end - end end end diff --git a/lib/active_merchant/billing/gateways/paypal_express.rb b/lib/active_merchant/billing/gateways/paypal_express.rb index cc5dde222d4..b5b2df3110b 100644 --- a/lib/active_merchant/billing/gateways/paypal_express.rb +++ b/lib/active_merchant/billing/gateways/paypal_express.rb @@ -3,8 +3,8 @@ require 'active_merchant/billing/gateways/paypal/paypal_recurring_api' require 'active_merchant/billing/gateways/paypal_express_common' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class PaypalExpressGateway < Gateway include PaypalCommonAPI include PaypalExpressCommon @@ -73,7 +73,7 @@ def agreement_details(reference_id, options = {}) end def authorize_reference_transaction(money, options = {}) - requires!(options, :reference_id, :payment_type, :invoice_id, :description, :ip) + requires!(options, :reference_id) commit 'DoReferenceTransaction', build_reference_transaction_request('Authorization', money, options) end @@ -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 @@ -146,7 +147,9 @@ def build_setup_request(action, money, options) xml.tag! 'n2:cpp-payflow-color', options[:background_color] unless options[:background_color].blank? if options[:allow_guest_checkout] xml.tag! 'n2:SolutionType', 'Sole' - xml.tag! 'n2:LandingPage', options[:landing_page] || 'Billing' + unless options[:paypal_chooses_landing_page] + xml.tag! 'n2:LandingPage', options[:landing_page] || 'Billing' + end end xml.tag! 'n2:BuyerEmail', options[:email] unless options[:email].blank? @@ -248,6 +251,8 @@ def build_reference_transaction_request(action, money, options) xml.tag! 'n2:PaymentType', options[:payment_type] || 'Any' 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/paypal_express_common.rb b/lib/active_merchant/billing/gateways/paypal_express_common.rb index 7cb72b82745..4da80ed71a2 100644 --- a/lib/active_merchant/billing/gateways/paypal_express_common.rb +++ b/lib/active_merchant/billing/gateways/paypal_express_common.rb @@ -17,7 +17,7 @@ def redirect_url end def redirect_url_for(token, options = {}) - options = {:review => true, :mobile => false}.update(options) + options = { review: true, mobile: false }.update(options) cmd = options[:mobile] ? '_express-checkout-mobile' : '_express-checkout' url = "#{redirect_url}?cmd=#{cmd}&token=#{token}" diff --git a/lib/active_merchant/billing/gateways/paysafe.rb b/lib/active_merchant/billing/gateways/paysafe.rb new file mode 100644 index 00000000000..2a9fe77005f --- /dev/null +++ b/lib/active_merchant/billing/gateways/paysafe.rb @@ -0,0 +1,426 @@ +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class PaysafeGateway < Gateway + self.test_url = 'https://api.test.paysafe.com' + self.live_url = 'https://api.paysafe.com' + + self.supported_countries = %w(AL AT BE BA BG CA HR CY CZ DK EE FI FR DE GR HU IS IE IT LV LI LT LU MT ME NL MK NO PL PT RO RS SK SI ES SE CH TR GB US) + self.supported_cardtypes = %i[visa master american_express discover] + + self.homepage_url = 'https://www.paysafe.com/' + self.display_name = 'Paysafe' + + def initialize(options = {}) + requires!(options, :username, :password, :account_id) + super + end + + def purchase(money, payment, options = {}) + post = {} + add_auth_purchase_params(post, money, payment, options) + add_airline_travel_details(post, options) + add_split_pay_details(post, options) + post[:settleWithAuth] = true + + commit(:post, 'auths', post, options) + end + + def authorize(money, payment, options = {}) + post = {} + add_auth_purchase_params(post, money, payment, options) + + commit(:post, 'auths', post, options) + end + + def capture(money, authorization, options = {}) + post = {} + add_invoice(post, money, options) + + commit(:post, "auths/#{authorization}/settlements", post, options) + end + + def refund(money, authorization, options = {}) + post = {} + add_invoice(post, money, options) + + commit(:post, "settlements/#{authorization}/refunds", post, options) + end + + def void(authorization, options = {}) + post = {} + money = options[:amount] + add_invoice(post, money, options) + + commit(:post, "auths/#{authorization}/voidauths", post, options) + end + + def credit(money, payment, options = {}) + post = {} + add_invoice(post, money, options) + add_payment(post, payment) + add_customer_data(post, payment, options) unless payment.is_a?(String) + add_merchant_details(post, options) + add_billing_address(post, options) + + commit(:post, 'standalonecredits', post, options) + end + + # This is a '$0 auth' done at a specific verification endpoint at the gateway + def verify(payment, options = {}) + post = {} + add_payment(post, payment) + add_billing_address(post, options) + add_customer_data(post, payment, options) unless payment.is_a?(String) + + commit(:post, 'verifications', post, options) + end + + def store(payment, options = {}) + post = {} + add_payment(post, payment) + add_address_for_vaulting(post, options) + add_profile_data(post, payment, options) + add_store_data(post, payment, options) + + commit(:post, 'profiles', post, options) + end + + def unstore(pm_profile_id) + commit(:delete, "profiles/#{get_id_from_store_auth(pm_profile_id)}", nil, nil) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r((Authorization: Basic )[a-zA-Z0-9:_]+), '\1[FILTERED]'). + gsub(%r(("cardNum\\?":\\?")\d+), '\1[FILTERED]'). + gsub(%r(("cvv\\?":\\?")\d+), '\1[FILTERED]') + end + + private + + def add_auth_purchase_params(post, money, payment, options) + add_invoice(post, money, options) + add_payment(post, payment) + add_billing_address(post, options) + add_merchant_details(post, options) + add_customer_data(post, payment, options) unless payment.is_a?(String) + add_three_d_secure(post, payment, options) if options[:three_d_secure] + add_stored_credential(post, options) if options[:stored_credential] + add_funding_transaction(post, options) + end + + # Customer data can be included in transactions where the payment method is a credit card + # but should not be sent when the payment method is a token + def add_customer_data(post, creditcard, options) + post[:profile] = {} + post[:profile][:firstName] = creditcard.first_name + post[:profile][:lastName] = creditcard.last_name + post[:profile][:email] = options[:email] if options[:email] + post[:profile][:merchantCustomerId] = options[:customer_id] if options[:customer_id] + post[:customerIp] = options[:ip] if options[:ip] + end + + def add_billing_address(post, options) + return unless address = options[:billing_address] || options[:address] + + post[:billingDetails] = {} + 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] = 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 + # object is formatted differently from the standard transaction billing address + def add_address_for_vaulting(post, options) + return unless address = options[:billing_address] || options[:address] + + post[:card][:billingAddress] = {} + 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] = truncate(address[:state], 40) if address[:state] + end + + # This data is specific to creating a profile at the gateway's vault level + def add_profile_data(post, payment, options) + post[:firstName] = payment.first_name + post[:lastName] = payment.last_name + post[:dateOfBirth] = {} + post[:dateOfBirth][:year] = options.dig(:date_of_birth, :year) + post[:dateOfBirth][:month] = options.dig(:date_of_birth, :month) + post[:dateOfBirth][:day] = options.dig(:date_of_birth, :day) + post[:email] = options[:email] if options[:email] + post[:ip] = options[:ip] if options[:ip] + + if options[:phone] + post[:phone] = options[:phone] + elsif address = options[:billing_address] || options[:address] + post[:phone] = address[:phone] if address[:phone] + end + end + + def add_store_data(post, payment, options) + post[:merchantCustomerId] = options[:customer_id] || SecureRandom.hex(12) + post[:locale] = options[:locale] || 'en_US' + post[:card][:holderName] = payment.name + end + + # Paysafe expects minor units so we are not calling amount method on money parameter + def add_invoice(post, money, options) + post[:amount] = money + end + + def add_payment(post, payment) + if payment.is_a?(String) + post[:card] = {} + post[:card][:paymentToken] = get_pm_from_store_auth(payment) + else + post[:card] = { cardExpiry: {} } + post[:card][:cardNum] = payment.number + post[:card][:cardExpiry][:month] = payment.month + post[:card][:cardExpiry][:year] = payment.year + post[:card][:cvv] = payment.verification_value + end + end + + def add_merchant_details(post, options) + return unless options[:merchant_descriptor] + + post[:merchantDescriptor] = {} + post[:merchantDescriptor][:dynamicDescriptor] = options[:merchant_descriptor][:dynamic_descriptor] if options[:merchant_descriptor][:dynamic_descriptor] + post[:merchantDescriptor][:phone] = options[:merchant_descriptor][:phone] if options[:merchant_descriptor][:phone] + end + + def add_three_d_secure(post, payment, options) + three_d_secure = options[:three_d_secure] + + post[:authentication] = {} + post[:authentication][:eci] = three_d_secure[:eci] + post[:authentication][:cavv] = three_d_secure[:cavv] + post[:authentication][:xid] = three_d_secure[:xid] if three_d_secure[:xid] + post[:authentication][:threeDSecureVersion] = three_d_secure[:version] + post[:authentication][:directoryServerTransactionId] = three_d_secure[:ds_transaction_id] unless payment.is_a?(String) || !mastercard?(payment) + end + + def add_airline_travel_details(post, options) + return unless options[:airline_travel_details] + + post[:airlineTravelDetails] = {} + post[:airlineTravelDetails][:passengerName] = options[:airline_travel_details][:passenger_name] if options[:airline_travel_details][:passenger_name] + post[:airlineTravelDetails][:departureDate] = options[:airline_travel_details][:departure_date] if options[:airline_travel_details][:departure_date] + post[:airlineTravelDetails][:origin] = options[:airline_travel_details][:origin] if options[:airline_travel_details][:origin] + post[:airlineTravelDetails][:computerizedReservationSystem] = options[:airline_travel_details][:computerized_reservation_system] if options[:airline_travel_details][:computerized_reservation_system] + post[:airlineTravelDetails][:customerReferenceNumber] = options[:airline_travel_details][:customer_reference_number] if options[:airline_travel_details][:customer_reference_number] + + add_ticket_details(post, options) + add_travel_agency_details(post, options) + add_trip_legs(post, options) + end + + def add_ticket_details(post, options) + return unless ticket = options[:airline_travel_details][:ticket] + + post[:airlineTravelDetails][:ticket] = {} + post[:airlineTravelDetails][:ticket][:ticketNumber] = ticket[:ticket_number] if ticket[:ticket_number] + post[:airlineTravelDetails][:ticket][:isRestrictedTicket] = ticket[:is_restricted_ticket] if ticket[:is_restricted_ticket] + end + + def add_travel_agency_details(post, options) + return unless agency = options[:airline_travel_details][:travel_agency] + + post[:airlineTravelDetails][:travelAgency] = {} + post[:airlineTravelDetails][:travelAgency][:name] = agency[:name] if agency[:name] + post[:airlineTravelDetails][:travelAgency][:code] = agency[:code] if agency[:code] + end + + def add_trip_legs(post, options) + return unless trip_legs = options[:airline_travel_details][:trip_legs] + + trip_legs_hash = {} + trip_legs.each.with_index(1) do |leg, i| + my_leg = "leg#{i}".to_sym + details = add_leg_details(my_leg, leg[1]) + + trip_legs_hash[my_leg] = details + end + post[:airlineTravelDetails][:tripLegs] = trip_legs_hash + end + + def add_leg_details(obj, leg) + details = {} + add_flight_details(details, obj, leg) + details[:serviceClass] = leg[:service_class] if leg[:service_class] + details[:isStopOverAllowed] = leg[:is_stop_over_allowed] if leg[:is_stop_over_allowed] + details[:destination] = leg[:destination] if leg[:destination] + details[:fareBasis] = leg[:fare_basis] if leg[:fare_basis] + details[:departureDate] = leg[:departure_date] if leg[:departure_date] + + details + end + + def add_flight_details(details, obj, leg) + details[:flight] = {} + details[:flight][:carrierCode] = leg[:flight][:carrier_code] if leg[:flight][:carrier_code] + details[:flight][:flightNumber] = leg[:flight][:flight_number] if leg[:flight][:flight_number] + end + + def add_split_pay_details(post, options) + return unless options[:split_pay] + + split_pay = [] + options[:split_pay].each do |pmnt| + split = {} + + split[:linkedAccount] = pmnt[:linked_account] + split[:amount] = pmnt[:amount].to_i if pmnt[:amount] + split[:percent] = pmnt[:percent].to_i if pmnt[:percent] + + split_pay << split + end + post[:splitpay] = split_pay + end + + def add_funding_transaction(post, options) + return unless options[:funding_transaction] + + post[:fundingTransaction] = {} + post[:fundingTransaction][:type] = options[:funding_transaction] + post[:profile] ||= {} + post[:profile][:merchantCustomerId] = options[:customer_id] || SecureRandom.hex(12) + end + + def add_stored_credential(post, options) + return unless options[:stored_credential] + + post[:storedCredential] = {} + + case options[:stored_credential][:initial_transaction] + when true + post[:storedCredential][:occurrence] = 'INITIAL' + when false + post[:storedCredential][:occurrence] = 'SUBSEQUENT' + end + + case options[:stored_credential][:reason_type] + when 'recurring', 'installment' + post[:storedCredential][:type] = 'RECURRING' + when 'unscheduled' + if options[:stored_credential][:initiator] == 'merchant' + post[:storedCredential][:type] = 'TOPUP' + elsif options[:stored_credential][:initiator] == 'cardholder' + post[:storedCredential][:type] = 'ADHOC' + else + return + end + end + + post[:storedCredential][:initialTransactionId] = options[:stored_credential][:network_transaction_id] if options[:stored_credential][:network_transaction_id] + post[:storedCredential][:externalInitialTransactionId] = options[:external_initial_transaction_id] if options[:external_initial_transaction_id] + end + + def mastercard?(payment) + return false unless payment.respond_to?(:brand) + + payment.brand == 'master' + end + + def parse(body) + return {} if body.empty? + + JSON.parse(body) + end + + def commit(method, action, parameters, options) + url = url(action) + raw_response = ssl_request(method, url, post_data(parameters, options), headers) + response = parse(raw_response) + success = success_from(response) + + Response.new( + success, + message_from(success, response), + response, + authorization: authorization_from(action, response), + avs_result: AVSResult.new(code: response['avsResponse']), + cvv_result: CVVResult.new(response['cvvVerification']), + test: test?, + error_code: success ? nil : error_code_from(response) + ) + end + + def headers + { + 'Content-Type' => 'application/json', + 'Authorization' => 'Basic ' + Base64.strict_encode64("#{@options[:username]}:#{@options[:password]}") + } + end + + def url(action, options = {}) + base_url = (test? ? test_url : live_url) + + if action.include? 'profiles' + "#{base_url}/customervault/v1/#{action}" + else + "#{base_url}/cardpayments/v1/accounts/#{@options[:account_id]}/#{action}" + end + end + + def success_from(response) + return false if response['status'] == 'FAILED' || response['error'] + + true + end + + def message_from(success, response) + return response['status'] unless response['error'] + + "Error(s)- code:#{response['error']['code']}, message:#{response['error']['message']}" + end + + def authorization_from(action, response) + if action == 'profiles' + pm = response['cards'].first['paymentToken'] + "#{pm}|#{response['id']}" + else + response['id'] + end + end + + def get_pm_from_store_auth(authorization) + authorization.split('|')[0] + end + + def get_id_from_store_auth(authorization) + authorization.split('|')[1] + end + + def post_data(parameters = {}, options = {}) + return unless parameters.present? + + parameters[:merchantRefNum] = options[:merchant_ref_num] || options[:order_id] || SecureRandom.hex(16).to_s + + parameters.to_json + end + + def error_code_from(response) + return unless response['error'] + + response['error']['code'] + end + + def handle_response(response) + response.body + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/payscout.rb b/lib/active_merchant/billing/gateways/payscout.rb index f0f9602e4fc..22cc670c5d4 100644 --- a/lib/active_merchant/billing/gateways/payscout.rb +++ b/lib/active_merchant/billing/gateways/payscout.rb @@ -1,10 +1,10 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class PayscoutGateway < Gateway self.live_url = self.test_url = 'https://secure.payscout.com/api/transact.php' self.supported_countries = ['US'] - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.default_currency = 'USD' self.homepage_url = 'http://www.payscout.com/' self.display_name = 'Payscout' @@ -41,7 +41,6 @@ def capture(money, authorization, options = {}) commit('capture', money, post) end - def refund(money, authorization, options = {}) post = {} post[:transactionid] = authorization @@ -63,7 +62,7 @@ def add_address(post, options) post[:address1] = address[:address1].to_s post[:address2] = address[:address2].to_s post[:city] = address[:city].to_s - post[:state] = (address[:state].blank? ? 'n/a' : address[:state]) + post[:state] = (address[:state].blank? ? 'n/a' : address[:state]) post[:zip] = address[:zip].to_s post[:country] = address[:country].to_s post[:phone] = address[:phone].to_s @@ -79,7 +78,7 @@ def add_address(post, options) post[:shipping_address2] = address[:address2].to_s post[:shipping_city] = address[:city].to_s post[:shipping_country] = address[:country].to_s - post[:shipping_state] = (address[:state].blank? ? 'n/a' : address[:state]) + post[:shipping_state] = (address[:state].blank? ? 'n/a' : address[:state]) post[:shipping_zip] = address[:zip].to_s post[:shipping_email] = address[:email].to_s end @@ -103,7 +102,7 @@ def add_creditcard(post, creditcard) end def parse(body) - Hash[body.split('&').map{|x|x.split('=')}] + Hash[body.split('&').map { |x| x.split('=') }] end def commit(action, money, parameters) @@ -116,12 +115,15 @@ def commit(action, money, parameters) message = message_from(response) test_mode = (test? || message =~ /TESTMODE/) - 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'] + 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'] ) end @@ -153,10 +155,8 @@ 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 end - diff --git a/lib/active_merchant/billing/gateways/paystation.rb b/lib/active_merchant/billing/gateways/paystation.rb index da9ff63c15d..1550b8231e2 100644 --- a/lib/active_merchant/billing/gateways/paystation.rb +++ b/lib/active_merchant/billing/gateways/paystation.rb @@ -1,7 +1,6 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class PaystationGateway < Gateway - self.live_url = self.test_url = 'https://www.paystation.co.nz/direct/paystation.dll' # an "error code" of "0" means "No error - transaction successful" @@ -14,7 +13,7 @@ class PaystationGateway < Gateway self.supported_countries = ['NZ'] # TODO: check this with paystation (amex and diners need to be enabled) - self.supported_cardtypes = [:visa, :master, :american_express, :diners_club ] + self.supported_cardtypes = %i[visa master american_express diners_club] self.homepage_url = 'http://paystation.co.nz' self.display_name = 'Paystation' @@ -75,7 +74,7 @@ def store(credit_card, options = {}) commit(post) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) post = new_request add_amount(post, money, options) add_invoice(post, options) @@ -84,7 +83,7 @@ def refund(money, authorization, options={}) commit(post) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) authorize(0, credit_card, options) end @@ -100,102 +99,104 @@ def scrub(transcript) private - def new_request - { - :pi => @options[:paystation_id], # paystation account id - :gi => @options[:gateway_id], # paystation gateway id - '2p' => 't', # two-party transaction type - :nr => 't', # -- redirect?? - :df => 'yymm' # date format: optional sometimes, required others - } - end - - def add_customer_data(post, options) - post[:mc] = options[:customer] - end + def new_request + { + :pi => @options[:paystation_id], # paystation account id + :gi => @options[:gateway_id], # paystation gateway id + '2p' => 't', # two-party transaction type + :nr => 't', # -- redirect?? + :df => 'yymm' # date format: optional sometimes, required others + } + end - def add_invoice(post, options) - post[:ms] = generate_unique_id - post[:mo] = options[:description] - post[:mr] = options[:order_id] - end + def add_customer_data(post, options) + post[:mc] = options[:customer] + end - def add_credit_card(post, credit_card) - post[:cn] = credit_card.number - post[:ct] = credit_card.brand - post[:ex] = format_date(credit_card.month, credit_card.year) - post[:cc] = credit_card.verification_value if credit_card.verification_value? - end + def add_invoice(post, options) + post[:ms] = generate_unique_id + post[:mo] = options[:description] + post[:mr] = options[:order_id] + end - def add_token(post, token) - post[:fp] = 't' # turn on "future payments" - what paystation calls Token Billing - post[:ft] = token - end + def add_credit_card(post, credit_card) + post[:cn] = credit_card.number + post[:ct] = credit_card.brand + post[:ex] = format_date(credit_card.month, credit_card.year) + post[:cc] = credit_card.verification_value if credit_card.verification_value? + end - def store_credit_card(post, options) - post[:fp] = 't' # turn on "future payments" - what paystation calls Token Billing - post[:fs] = 't' # tells paystation to store right now, not bill - post[:ft] = options[:token] if options[:token] # specify a token to use that, or let Paystation generate one - end + def add_token(post, token) + post[:fp] = 't' # turn on "future payments" - what paystation calls Token Billing + post[:ft] = token + end - def add_authorize_flag(post, options) - post[:pa] = 't' # tells Paystation that this is a pre-auth authorisation payment (account must be in pre-auth mode) - end + def store_credit_card(post, options) + post[:fp] = 't' # turn on "future payments" - what paystation calls Token Billing + post[:fs] = 't' # tells paystation to store right now, not bill + post[:ft] = options[:token] if options[:token] # specify a token to use that, or let Paystation generate one + end - def add_refund_specific_fields(post, authorization) - post[:rc] = 't' - post[:rt] = authorization - end + def add_authorize_flag(post, options) + post[:pa] = 't' # tells Paystation that this is a pre-auth authorisation payment (account must be in pre-auth mode) + end - def add_authorization_token(post, auth_token, verification_value = nil) - post[:cp] = 't' # Capture Payment flag – tells Paystation this transaction should be treated as a capture payment - post[:cx] = auth_token - post[:cc] = verification_value - end + def add_refund_specific_fields(post, authorization) + post[:rc] = 't' + post[:rt] = authorization + end - def add_amount(post, money, options) - post[:am] = amount(money) - post[:cu] = options[:currency] || currency(money) - end + def add_authorization_token(post, auth_token, verification_value = nil) + post[:cp] = 't' # Capture Payment flag – tells Paystation this transaction should be treated as a capture payment + post[:cx] = auth_token + post[:cc] = verification_value + end - def parse(xml_response) - response = {} + def add_amount(post, money, options) + post[:am] = amount(money) + post[:cu] = options[:currency] || currency(money) + end - xml = REXML::Document.new(xml_response) + def parse(xml_response) + response = {} - xml.elements.each("#{xml.root.name}/*") do |element| - response[element.name.underscore.to_sym] = element.text - end + xml = REXML::Document.new(xml_response) - response + xml.elements.each("#{xml.root.name}/*") do |element| + response[element.name.underscore.to_sym] = element.text end - def commit(post) - post[:tm] = 'T' if test? - pstn_prefix_params = post.collect { |key, value| "pstn_#{key}=#{CGI.escape(value.to_s)}" }.join('&') - - data = ssl_post(self.live_url, "#{pstn_prefix_params}&paystation=_empty") - response = parse(data) - message = message_from(response) - - PaystationResponse.new(success?(response), message, response, - :test => (response[:tm] && response[:tm].downcase == 't'), - :authorization => response[:paystation_transaction_id] - ) - end + response + end - def success?(response) - (response[:ec] == SUCCESSFUL_RESPONSE_CODE) || (response[:ec] == SUCCESSFUL_FUTURE_PAYMENT) - end + def commit(post) + post[:tm] = 'T' if test? + pstn_prefix_params = post.collect { |key, value| "pstn_#{key}=#{CGI.escape(value.to_s)}" }.join('&') + + data = ssl_post(self.live_url, "#{pstn_prefix_params}&paystation=_empty") + response = parse(data) + message = message_from(response) + + PaystationResponse.new( + success?(response), + message, + response, + test: response[:tm]&.casecmp('t')&.zero?, + authorization: response[:paystation_transaction_id] + ) + end - def message_from(response) - response[:em] - end + def success?(response) + (response[:ec] == SUCCESSFUL_RESPONSE_CODE) || (response[:ec] == SUCCESSFUL_FUTURE_PAYMENT) + end - def format_date(month, year) - "#{format(year, :two_digits)}#{format(month, :two_digits)}" - end + def message_from(response) + response[:em] + end + def format_date(month, year) + "#{format(year, :two_digits)}#{format(month, :two_digits)}" + end end class PaystationResponse < Response diff --git a/lib/active_merchant/billing/gateways/payu_in.rb b/lib/active_merchant/billing/gateways/payu_in.rb index f75cbbd7df7..79673bf48fb 100644 --- a/lib/active_merchant/billing/gateways/payu_in.rb +++ b/lib/active_merchant/billing/gateways/payu_in.rb @@ -1,7 +1,7 @@ # encoding: utf-8 -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class PayuInGateway < Gateway self.test_url = 'https://test.payu.in/_payment' self.live_url = 'https://secure.payu.in/_payment' @@ -11,17 +11,17 @@ class PayuInGateway < Gateway self.supported_countries = ['IN'] self.default_currency = 'INR' - self.supported_cardtypes = [:visa, :master, :american_express, :diners_club, :maestro] + self.supported_cardtypes = %i[visa master american_express diners_club maestro] self.homepage_url = 'https://www.payu.in/' self.display_name = 'PayU India' - def initialize(options={}) + def initialize(options = {}) requires!(options, :key, :salt) super end - def purchase(money, payment, options={}) + def purchase(money, payment, options = {}) requires!(options, :order_id) post = {} @@ -32,16 +32,16 @@ def purchase(money, payment, options={}) add_auth(post) MultiResponse.run do |r| - r.process{commit(url('purchase'), post)} - if(r.params['enrolled'].to_s == '0') - r.process{commit(r.params['post_uri'], r.params['form_post_vars'])} + r.process { commit(url('purchase'), post) } + if r.params['enrolled'].to_s == '0' + r.process { commit(r.params['post_uri'], r.params['form_post_vars']) } else - r.process{handle_3dsecure(r)} + r.process { handle_3dsecure(r) } end end end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) raise ArgumentError, 'Amount is required' unless money post = {} @@ -154,20 +154,21 @@ def add_payment(post, payment) def clean(value, format, maxlength) value ||= '' - value = case format - when :alphanumeric - value.gsub(/[^A-Za-z0-9]/, '') - when :name - value.gsub(/[^A-Za-z ]/, '') - when :numeric - value.gsub(/[^0-9]/, '') - when :text - value.gsub(/[^A-Za-z0-9@\-_\/\. ]/, '') - when nil - value - else - raise "Unknown format #{format} for #{value}" - end + value = + case format + when :alphanumeric + value.gsub(/[^A-Za-z0-9]/, '') + when :name + value.gsub(/[^A-Za-z ]/, '') + when :numeric + value.gsub(/[^0-9]/, '') + when :text + value.gsub(/[^A-Za-z0-9@\-_\/\. ]/, '') + when nil + value + else + raise "Unknown format #{format} for #{value}" + end value[0...maxlength] end diff --git a/lib/active_merchant/billing/gateways/payu_latam.rb b/lib/active_merchant/billing/gateways/payu_latam.rb index a3192fe79e7..abb53b14b88 100644 --- a/lib/active_merchant/billing/gateways/payu_latam.rb +++ b/lib/active_merchant/billing/gateways/payu_latam.rb @@ -1,7 +1,7 @@ require 'digest/md5' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class PayuLatamGateway < Gateway self.display_name = 'PayU Latam' self.homepage_url = 'http://www.payulatam.com' @@ -9,16 +9,19 @@ class PayuLatamGateway < Gateway self.test_url = 'https://sandbox.api.payulatam.com/payments-api/4.0/service.cgi' self.live_url = 'https://api.payulatam.com/payments-api/4.0/service.cgi' - self.supported_countries = ['AR', 'BR', 'CL', 'CO', 'MX', 'PA', 'PE'] + self.supported_countries = %w[AR BR CL CO MX PA PE] self.default_currency = 'USD' self.money_format = :dollars - self.supported_cardtypes = [:visa, :master, :american_express, :diners_club] + self.supported_cardtypes = %i[visa master american_express diners_club naranja cabal] BRAND_MAP = { 'visa' => 'VISA', 'master' => 'MASTERCARD', + 'maestro' => 'MASTERCARD', 'american_express' => 'AMEX', - 'diners_club' => 'DINERS' + 'diners_club' => 'DINERS', + 'naranja' => 'NARANJA', + 'cabal' => 'CABAL' } MINIMUMS = { @@ -28,34 +31,39 @@ class PayuLatamGateway < Gateway 'PEN' => 500 } - def initialize(options={}) + def initialize(options = {}) requires!(options, :merchant_id, :account_id, :api_login, :api_key, :payment_country) super end - def purchase(amount, payment_method, options={}) + def purchase(amount, payment_method, options = {}) post = {} auth_or_sale(post, 'AUTHORIZATION_AND_CAPTURE', amount, payment_method, options) commit('purchase', post) end - def authorize(amount, payment_method, options={}) + def authorize(amount, payment_method, options = {}) post = {} auth_or_sale(post, 'AUTHORIZATION', amount, payment_method, options) commit('auth', post) end - def capture(amount, authorization, options={}) + def capture(amount, authorization, options = {}) post = {} add_credentials(post, 'SUBMIT_TRANSACTION', options) add_transaction_elements(post, 'CAPTURE', options) add_reference(post, authorization) + if !amount.nil? && amount.to_f != 0.0 + post[:transaction][:additionalValues] ||= {} + post[:transaction][:additionalValues][:TX_VALUE] = invoice_for(amount, options)[:TX_VALUE] + end + commit('capture', post) end - def void(authorization, options={}) + def void(authorization, options = {}) post = {} add_credentials(post, 'SUBMIT_TRANSACTION', options) @@ -65,17 +73,24 @@ def void(authorization, options={}) commit('void', post) end - def refund(amount, authorization, options={}) + def refund(amount, authorization, options = {}) post = {} add_credentials(post, 'SUBMIT_TRANSACTION', options) - add_transaction_elements(post, 'REFUND', options) - add_reference(post, authorization) + if options[:partial_refund] + add_transaction_elements(post, 'PARTIAL_REFUND', options) + post[:transaction][:additionalValues] ||= {} + post[:transaction][:additionalValues][:TX_VALUE] = invoice_for(amount, options)[:TX_VALUE] + else + add_transaction_elements(post, 'REFUND', options) + end + + add_reference(post, authorization) commit('refund', post) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) minimum = MINIMUMS[options[:currency].upcase] if options[:currency] amount = options[:verify_amount] || minimum || 100 @@ -89,7 +104,7 @@ def store(payment_method, options = {}) post = {} add_credentials(post, 'CREATE_TOKEN') - add_payment_method_to_be_tokenized(post, payment_method) + add_payment_method_to_be_tokenized(post, payment_method, options) commit('store', post) end @@ -126,7 +141,7 @@ def auth_or_sale(post, transaction_type, amount, payment_method, options) add_extra_parameters(post, options) end - def add_credentials(post, command, options={}) + def add_credentials(post, command, options = {}) post[:test] = test? unless command == 'CREATE_TOKEN' post[:language] = options[:language] || 'en' post[:command] = command @@ -152,7 +167,7 @@ def add_order(post, options) order[:accountId] = @options[:account_id] order[:partnerId] = options[:partner_id] if options[:partner_id] order[:referenceCode] = options[:order_id] || generate_unique_id - order[:description] = options[:description] || 'Compra en ' + @options[:merchant_id] + order[:description] = options[:description] || ('Compra en ' + @options[:merchant_id]) order[:language] = options[:language] || 'en' order[:shippingAddress] = shipping_address_fields(options) if options[:shipping_address] post[:transaction][:order] = order @@ -162,7 +177,7 @@ def add_payer(post, payment_method, options) address = options[:billing_address] payer = {} payer[:fullName] = payment_method.name.strip - payer[:contactPhone] = address[:phone] if (address && address[:phone]) + payer[:contactPhone] = address[:phone] if address && address[:phone] payer[:dniNumber] = options[:dni_number] if options[:dni_number] payer[:dniType] = options[:dni_type] if options[:dni_type] payer[:emailAddress] = options[:email] if options[:email] @@ -173,12 +188,13 @@ def add_payer(post, payment_method, options) def billing_address_fields(options) return unless address = options[:billing_address] + billing_address = {} billing_address[:street1] = address[:address1] billing_address[:street2] = address[:address2] billing_address[:city] = address[:city] billing_address[:state] = address[:state] - billing_address[:country] = address[:country] + billing_address[:country] = address[:country] unless address[:country].blank? billing_address[:postalCode] = address[:zip] if @options[:payment_country] == 'MX' billing_address[:phone] = address[:phone] billing_address @@ -190,17 +206,19 @@ def add_buyer(post, payment_method, options) buyer[:fullName] = buyer_hash[:name] buyer[:dniNumber] = buyer_hash[:dni_number] buyer[:dniType] = buyer_hash[:dni_type] + buyer[:merchantBuyerId] = buyer_hash[:merchant_buyer_id] buyer[:cnpj] = buyer_hash[:cnpj] if @options[:payment_country] == 'BR' buyer[:emailAddress] = buyer_hash[:email] - buyer[:contactPhone] = (options[:billing_address][:phone] if options[:billing_address]) || (options[:shipping_address][:phone] if options[:shipping_address]) || '' + buyer[:contactPhone] = (options[:billing_address][:phone] if options[:billing_address]) || (options[:shipping_address][:phone_number] if options[:shipping_address]) || '' buyer[:shippingAddress] = shipping_address_fields(options) if options[:shipping_address] else buyer[:fullName] = payment_method.name.strip buyer[:dniNumber] = options[:dni_number] buyer[:dniType] = options[:dni_type] + buyer[:merchantBuyerId] = options[:merchant_buyer_id] buyer[:cnpj] = options[:cnpj] if @options[:payment_country] == 'BR' buyer[:emailAddress] = options[:email] - buyer[:contactPhone] = (options[:billing_address][:phone] if options[:billing_address]) || (options[:shipping_address][:phone] if options[:shipping_address]) || '' + buyer[:contactPhone] = (options[:billing_address][:phone] if options[:billing_address]) || (options[:shipping_address][:phone_number] if options[:shipping_address]) || '' buyer[:shippingAddress] = shipping_address_fields(options) if options[:shipping_address] end post[:transaction][:order][:buyer] = buyer @@ -208,6 +226,7 @@ def add_buyer(post, payment_method, options) def shipping_address_fields(options) return unless address = options[:shipping_address] + shipping_address = {} shipping_address[:street1] = address[:address1] shipping_address[:street2] = address[:address2] @@ -215,11 +234,15 @@ def shipping_address_fields(options) shipping_address[:state] = address[:state] shipping_address[:country] = address[:country] shipping_address[:postalCode] = address[:zip] - shipping_address[:phone] = address[:phone] + shipping_address[:phone] = address[:phone_number] shipping_address end def add_invoice(post, money, options) + post[:transaction][:order][:additionalValues] = invoice_for(money, options) + end + + def invoice_for(money, options) tx_value = {} tx_value[:value] = amount(money) tx_value[:currency] = options[:currency] || currency(money) @@ -237,7 +260,7 @@ def add_invoice(post, money, options) additional_values[:TX_TAX] = tx_tax if @options[:payment_country] == 'CO' additional_values[:TX_TAX_RETURN_BASE] = tx_tax_return_base if @options[:payment_country] == 'CO' - post[:transaction][:order][:additionalValues] = additional_values + additional_values end def add_signature(post) @@ -256,6 +279,10 @@ def signature_from(post) Digest::MD5.hexdigest(signature_string) end + def codensa_bin?(number) + number.start_with?('590712') + end + def add_payment_method(post, payment_method, options) if payment_method.is_a?(String) brand, token = split_authorization(payment_method) @@ -273,18 +300,22 @@ def add_payment_method(post, payment_method, options) credit_card[:name] = payment_method.name.strip credit_card[:processWithoutCvv2] = true if add_process_without_cvv2(payment_method, options) post[:transaction][:creditCard] = credit_card - post[:transaction][:paymentMethod] = BRAND_MAP[payment_method.brand.to_s] + post[:transaction][:paymentMethod] = codensa_bin?(payment_method.number) ? 'CODENSA' : BRAND_MAP[payment_method.brand.to_s] end end def add_process_without_cvv2(payment_method, options) return true if payment_method.verification_value.blank? && options[:cvv].blank? + false end def add_extra_parameters(post, options) extra_parameters = {} extra_parameters[:INSTALLMENTS_NUMBER] = options[:installments_number] || 1 + extra_parameters[:EXTRA1] = options[:extra_1] if options[:extra_1] + extra_parameters[:EXTRA2] = options[:extra_2] if options[:extra_2] + extra_parameters[:EXTRA3] = options[:extra_3] if options[:extra_3] post[:transaction][:extraParameters] = extra_parameters end @@ -297,44 +328,41 @@ def add_reference(post, authorization) post[:transaction][:reason] = 'n/a' end - def add_payment_method_to_be_tokenized(post, payment_method) + def add_payment_method_to_be_tokenized(post, payment_method, options) credit_card_token = {} - credit_card_token[:payerId] = generate_unique_id + credit_card_token[:payerId] = options[:payer_id] || generate_unique_id credit_card_token[:name] = payment_method.name.strip - credit_card_token[:identificationNumber] = generate_unique_id + credit_card_token[:identificationNumber] = options[:dni_number] credit_card_token[:paymentMethod] = BRAND_MAP[payment_method.brand.to_s] credit_card_token[:number] = payment_method.number credit_card_token[:expirationDate] = format(payment_method.year, :four_digits).to_s + '/' + format(payment_method.month, :two_digits).to_s - credit_card_token[:securityCode] = payment_method.verification_value post[:creditCardToken] = credit_card_token end def commit(action, params) - begin - raw_response = ssl_post(url, post_data(params), headers) - response = parse(raw_response) - rescue ResponseError => e - raw_response = e.response.body - response_error(raw_response) - rescue JSON::ParserError - unparsable_response(raw_response) - else - success = success_from(action, response) - Response.new( - success, - message_from(action, success, response), - response, - authorization: success ? authorization_from(action, response) : nil, - error_code: success ? nil : error_from(action, response), - test: test? - ) - end + raw_response = ssl_post(url, post_data(params), headers) + response = parse(raw_response) + rescue ResponseError => e + raw_response = e.response.body + response_error(raw_response) + rescue JSON::ParserError + unparsable_response(raw_response) + else + success = success_from(action, response) + Response.new( + success, + message_from(action, success, response), + response, + authorization: success ? authorization_from(action, response) : nil, + error_code: success ? nil : error_from(action, response), + test: test? + ) end def headers { - 'Content-Type' => 'application/json', - 'Accept' => 'application/json' + 'Content-Type' => 'application/json', + 'Accept' => 'application/json' } end @@ -358,7 +386,7 @@ def success_from(action, response) when 'verify_credentials' response['code'] == 'SUCCESS' when 'refund', 'void' - response['code'] == 'SUCCESS' && response['transactionResponse'] && (response['transactionResponse']['state'] == 'PENDING' || response['transactionResponse']['state'] == 'APPROVED') + response['code'] == 'SUCCESS' && response['transactionResponse'] && (response['transactionResponse']['state'] == 'PENDING' || response['transactionResponse']['state'] == 'APPROVED') else response['code'] == 'SUCCESS' && response['transactionResponse'] && (response['transactionResponse']['state'] == 'APPROVED') end @@ -367,22 +395,38 @@ def success_from(action, response) def message_from(action, success, response) case action when 'store' - return response['code'] if success - error_description = response['creditCardToken']['errorDescription'] if response['creditCardToken'] - response['error'] || error_description || 'FAILED' + message_from_store(success, response) when 'verify_credentials' - return 'VERIFIED' if success - 'FAILED' + message_from_verify_credentials(success) else - if response['transactionResponse'] - response_message = response['transactionResponse']['responseMessage'] - response_code = response['transactionResponse']['responseCode'] || response['transactionResponse']['pendingReason'] - end - return response_code if success - response['error'] || response_message || response_code || 'FAILED' + message_from_transaction_response(success, response) end end + def message_from_store(success, response) + return response['code'] if success + + error_description = response['creditCardToken']['errorDescription'] if response['creditCardToken'] + response['error'] || error_description || 'FAILED' + end + + def message_from_verify_credentials(success) + return 'VERIFIED' if success + + 'FAILED' + end + + def message_from_transaction_response(success, response) + response_code = response.dig('transactionResponse', 'responseCode') || response.dig('transactionResponse', 'pendingReason') + return response_code if success + return response_code + ' | ' + response.dig('transactionResponse', 'paymentNetworkResponseErrorMessage') if response.dig('transactionResponse', 'paymentNetworkResponseErrorMessage') + return response.dig('transactionResponse', 'responseMessage') if response.dig('transactionResponse', 'responseMessage') + return response['error'] if response['error'] + return response_code if response_code + + 'FAILED' + end + def authorization_from(action, response) case action when 'store' @@ -411,23 +455,21 @@ 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 def response_error(raw_response) - begin - response = parse(raw_response) - rescue JSON::ParserError - unparsable_response(raw_response) - else - return Response.new( - false, - message_from('', false, response), - response, - :test => test? - ) - end + response = parse(raw_response) + rescue JSON::ParserError + unparsable_response(raw_response) + else + return Response.new( + false, + message_from('', false, response), + response, + test: test? + ) end def unparsable_response(raw_response) diff --git a/lib/active_merchant/billing/gateways/payway.rb b/lib/active_merchant/billing/gateways/payway.rb index b7b40b6c8b0..c48373ea608 100644 --- a/lib/active_merchant/billing/gateways/payway.rb +++ b/lib/active_merchant/billing/gateways/payway.rb @@ -3,8 +3,8 @@ module Billing class PaywayGateway < Gateway self.live_url = self.test_url = 'https://ccapi.client.qvalent.com/payway/ccapi' - self.supported_countries = [ 'AU' ] - self.supported_cardtypes = [ :visa, :master, :diners_club, :american_express, :bankcard ] + self.supported_countries = ['AU'] + self.supported_cardtypes = %i[visa master diners_club american_express bankcard] self.display_name = 'Pay Way' self.homepage_url = 'http://www.payway.com.au' self.default_currency = 'AUD' @@ -17,7 +17,7 @@ class PaywayGateway < Gateway '3' => 'Rejected' } - RESPONSE_CODES= { + RESPONSE_CODES = { '00' => 'Completed Successfully', '01' => 'Refer to card issuer', '03' => 'Invalid merchant', @@ -74,16 +74,16 @@ class PaywayGateway < Gateway 'QZ' => 'Zero value transaction' } - TRANSACTIONS = { - :authorize => 'preauth', - :purchase => 'capture', - :capture => 'captureWithoutAuth', - :status => 'query', - :refund => 'refund', - :store => 'registerAccount' + TRANSACTIONS = { + authorize: 'preauth', + purchase: 'capture', + capture: 'captureWithoutAuth', + status: 'query', + refund: 'refund', + store: 'registerAccount' } - def initialize(options={}) + def initialize(options = {}) @options = options @options[:merchant] ||= 'TEST' if test? @@ -92,7 +92,7 @@ def initialize(options={}) @options[:eci] ||= 'SSL' end - def authorize(amount, payment_method, options={}) + def authorize(amount, payment_method, options = {}) requires!(options, :order_id) post = {} @@ -101,7 +101,7 @@ def authorize(amount, payment_method, options={}) commit(:authorize, post) end - def capture(amount, authorization, options={}) + def capture(amount, authorization, options = {}) requires!(options, :order_id) post = {} @@ -110,7 +110,7 @@ def capture(amount, authorization, options={}) commit(:capture, post) end - def purchase(amount, payment_method, options={}) + def purchase(amount, payment_method, options = {}) requires!(options, :order_id) post = {} @@ -119,7 +119,7 @@ def purchase(amount, payment_method, options={}) commit(:purchase, post) end - def refund(amount, authorization, options={}) + def refund(amount, authorization, options = {}) requires!(options, :order_id) post = {} @@ -128,7 +128,7 @@ def refund(amount, authorization, options={}) commit(:refund, post) end - def store(credit_card, options={}) + def store(credit_card, options = {}) requires!(options, :billing_id) post = {} @@ -137,7 +137,7 @@ def store(credit_card, options={}) commit(:store, post) end - def status(options={}) + def status(options = {}) requires!(options, :order_id) commit(:status, 'customer.orderNumber' => options[:order_id]) @@ -150,7 +150,7 @@ def add_payment_method(post, payment_method) post['card.cardHolderName'] = "#{payment_method.first_name} #{payment_method.last_name}" post['card.PAN'] = payment_method.number post['card.CVN'] = payment_method.verification_value - post['card.expiryYear'] = payment_method.year.to_s[-2,2] + post['card.expiryYear'] = payment_method.year.to_s[-2, 2] post['card.expiryMonth'] = sprintf('%02d', payment_method.month) else post['customer.customerReferenceNumber'] = payment_method @@ -177,7 +177,7 @@ def add_auth(post) # Creates the request and returns the summarized result def commit(action, post) add_auth(post) - post.merge!('order.type' => TRANSACTIONS[action]) + post['order.type'] = TRANSACTIONS[action] request = post.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&') response = ssl_post(self.live_url, request) @@ -192,15 +192,19 @@ def commit(action, post) success = (params[:summary_code] ? (params[:summary_code] == '0') : (params[:response_code] == '00')) - Response.new(success, message, params, - :test => (@options[:merchant].to_s == 'TEST'), - :authorization => post[:order_number] + Response.new( + success, + message, + params, + test: (@options[:merchant].to_s == 'TEST'), + authorization: post[:order_number] ) rescue ActiveMerchant::ResponseError => e raise unless e.response.code == '403' - return Response.new(false, 'Invalid credentials', {}, :test => test?) + + return Response.new(false, 'Invalid credentials', {}, test: test?) rescue ActiveMerchant::ClientCertificateError - return Response.new(false, 'Invalid certificate', {}, :test => test?) + return Response.new(false, 'Invalid certificate', {}, test: test?) end end end diff --git a/lib/active_merchant/billing/gateways/payway_dot_com.rb b/lib/active_merchant/billing/gateways/payway_dot_com.rb new file mode 100644 index 00000000000..a0f3f6b1e6b --- /dev/null +++ b/lib/active_merchant/billing/gateways/payway_dot_com.rb @@ -0,0 +1,253 @@ +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class PaywayDotComGateway < Gateway + self.test_url = 'https://paywaywsdev.com/PaywayWS/Payment/CreditCard' + self.live_url = 'https://paywayws.net/PaywayWS/Payment/CreditCard' + + self.supported_countries = %w[US CA] + self.default_currency = 'USD' + self.supported_cardtypes = %i[visa master american_express discover] + + self.money_format = :cents + + self.homepage_url = 'http://www.payway.com' + self.display_name = 'Payway Gateway' + + STANDARD_ERROR_CODE_MAPPING = { + '5012' => STANDARD_ERROR_CODE[:card_declined], + '5035' => STANDARD_ERROR_CODE[:invalid_number], + '5037' => STANDARD_ERROR_CODE[:invalid_expiry_date], + '5045' => STANDARD_ERROR_CODE[:incorrect_zip] + } + + # Payway to standard AVSResult codes. + AVS_MAPPING = { + 'N1' => 'I', # No address given with order + 'N2' => 'I', # Bill-to address did not pass + '““' => 'R', # AVS not performed (Blanks returned) + 'IU' => 'G', # AVS not performed by Issuer + 'ID' => 'S', # Issuer does not participate in AVS + 'IE' => 'E', # Edit Error - AVS data is invalid + 'IS' => 'R', # System unavailable or time-out + 'IB' => 'B', # Street address match. Postal code not verified due to incompatible formats (both were sent). + 'IC' => 'C', # Street address and postal code not verified due to incompatible format (both were sent). + 'IP' => 'P', # Postal code match. Street address not verified due to incompatible formats (both were sent). + 'A1' => 'K', # Accountholder name matches + 'A3' => 'V', # Accountholder name, billing address and postal code. + 'A4' => 'L', # Accountholder name and billing postal code match + 'A7' => 'O', # Accountholder name and billing address match + 'B3' => 'H', # Accountholder name incorrect, billing address and postal code match + 'B4' => 'F', # Accountholder name incorrect, billing postal code matches + 'B7' => 'T', # Accountholder name incorrect, billing address matches + 'B8' => 'N', # Accountholder name, billing address and postal code are all incorrect + '??' => 'R', # A double question mark symbol indicates an unrecognized response from association + 'I1' => 'X', # Zip code +4 and Address Match + 'I2' => 'W', # Zip code +4 Match + 'I3' => 'Y', # Zip code and Address Match + 'I4' => 'Z', # Zip code Match + 'I5' => 'M', # +4 and Address Match + 'I6' => 'W', # +4 Match + 'I7' => 'A', # Address Match + 'I8' => 'C' # No Match + } + + PAYWAY_WS_SUCCESS = '5000' + + SCRUB_PATTERNS = [ + %r(("password\\?":\\?")[^\\]+), + %r(("fsv\\?":\\?")\d+), + %r(("fsv\\?": \\?")\d+), + %r(("accountNumber\\?":\\?")\d+), + %r(("accountNumber\\?": \\?")[^\\]+), + %r(("Invalid account number: )\d+) + ].freeze + + SCRUB_REPLACEMENT = '\1[FILTERED]' + + def initialize(options = {}) + requires!(options, :login, :password, :company_id, :source_id) + super + end + + def purchase(money, payment, options = {}) + post = {} + add_common(post, options) + add_card_payment(post, payment, options) + add_card_transaction_details(post, money, options) + add_address(post, payment, options) + + commit('sale', post) + end + + def authorize(money, payment, options = {}) + post = {} + add_common(post, options) + add_card_payment(post, payment, options) + add_card_transaction_details(post, money, options) + add_address(post, payment, options) + + commit('authorize', post) + end + + def capture(money, authorization, options = {}) + post = {} + add_common(post, options) + add_card_transaction_name(post, authorization, options) + + commit('capture', post) + end + + def credit(money, payment, options = {}) + post = {} + add_common(post, options) + add_card_payment(post, payment, options) + add_card_transaction_details(post, money, options) + add_address(post, payment, options) + + commit('credit', post) + end + + def void(authorization, options = {}) + post = {} + add_common(post, options) + add_card_transaction_name(post, authorization, options) + + commit('void', post) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + SCRUB_PATTERNS.inject(transcript) do |text, pattern| + text.gsub(pattern, SCRUB_REPLACEMENT) + end + end + + private + + def add_common(post, options) + post[:userName] = @options[:login] + post[:password] = @options[:password] + post[:companyId] = @options[:company_id] + post[:cardTransaction] = {} + post[:cardTransaction][:sourceId] = @options[:source_id] + end + + def add_address(post, payment, options) + post[:cardAccount] ||= {} + address = options[:billing_address] || options[:address] || {} + first_name, last_name = split_names(address[:name]) + full_address = "#{address[:address1]} #{address[:address2]}".strip + phone = address[:phone] || address[:phone_number] + + post[:cardAccount][:firstName] = first_name if first_name + post[:cardAccount][:lastName] = last_name if last_name + post[:cardAccount][:address] = full_address if full_address + post[:cardAccount][:city] = address[:city] if address[:city] + post[:cardAccount][:state] = address[:state] if address[:state] + post[:cardAccount][:zip] = address[:zip] if address[:zip] + post[:cardAccount][:phone] = phone if phone + end + + def add_card_transaction_details(post, money, options) + post[:cardTransaction][:amount] = amount(money) + eci_type = options[:eci_type].nil? ? '1' : options[:eci_type] + post[:cardTransaction][:eciType] = eci_type + post[:cardTransaction][:processorSoftDescriptor] = options[:processor_soft_descriptor] if options[:processor_soft_descriptor] + post[:cardTransaction][:tax] = options[:tax] if options[:tax] + end + + def add_card_transaction_name(post, identifier, options) + post[:cardTransaction][:name] = identifier + end + + def add_card_payment(post, payment, options) + # credit_card + post[:accountInputMode] = 'primaryAccountNumber' + + post[:cardAccount] ||= {} + post[:cardAccount][:accountNumber] = payment.number + post[:cardAccount][:fsv] = payment.verification_value + post[:cardAccount][:expirationDate] = expdate(payment) + post[:cardAccount][:email] = options[:email] if options[:email] + end + + def expdate(credit_card) + year = format(credit_card.year, :four_digits) + month = format(credit_card.month, :two_digits) + + month + year + end + + def parse(body) + body.blank? ? {} : JSON.parse(body) + end + + def commit(action, parameters) + parameters[:request] = action + + url = (test? ? test_url : live_url) + payload = parameters.to_json unless parameters.nil? + + response = + begin + parse(ssl_request(:post, url, payload, headers)) + rescue ResponseError => e + return Response.new(false, 'Invalid Login') if e.response.code == '401' + + parse(e.response.body) + end + + success = success_from(response) + avs_result_code = response['cardTransaction'].nil? || response['cardTransaction']['addressVerificationResults'].nil? ? '' : response['cardTransaction']['addressVerificationResults'] + avs_result = AVSResult.new(code: AVS_MAPPING[avs_result_code]) + cvv_result = CVVResult.new(response['cardTransaction']['fraudSecurityResults']) if response['cardTransaction'] && response['cardTransaction']['fraudSecurityResults'] + + Response.new( + success, + message_from(success, response), + response, + test: test?, + error_code: error_code_from(response), + authorization: authorization_from(response), + avs_result:, + cvv_result: + ) + end + + def success_from(response) + response['paywayCode'] == PAYWAY_WS_SUCCESS + end + + def error_code_from(response) + return '' if success_from(response) + + error = STANDARD_ERROR_CODE_MAPPING[response['paywayCode']].nil? ? STANDARD_ERROR_CODE[:processing_error] : STANDARD_ERROR_CODE_MAPPING[response['paywayCode']] + return error + end + + def message_from(success, response) + return '' if response['paywayCode'].nil? + + return response['paywayCode'] + '-' + 'success' if success + + response['paywayCode'] + end + + def authorization_from(response) + return '' if !success_from(response) || response['cardTransaction'].nil? + + response['cardTransaction']['name'] + end + + def headers + { + 'Accept' => 'application/json', + 'Content-type' => 'application/json' + } + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/pin.rb b/lib/active_merchant/billing/gateways/pin.rb index 7b364e2eaea..54cb150799a 100644 --- a/lib/active_merchant/billing/gateways/pin.rb +++ b/lib/active_merchant/billing/gateways/pin.rb @@ -1,14 +1,14 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class PinGateway < Gateway - self.test_url = 'https://test-api.pin.net.au/1' - self.live_url = 'https://api.pin.net.au/1' + self.test_url = 'https://test-api.pinpayments.com/1' + self.live_url = 'https://api.pinpayments.com/1' self.default_currency = 'AUD' self.money_format = :cents - self.supported_countries = ['AU'] - self.supported_cardtypes = [:visa, :master, :american_express] - self.homepage_url = 'http://www.pin.net.au/' + self.supported_countries = %w(AU NZ) + self.supported_cardtypes = %i[visa master american_express diners_club discover jcb] + self.homepage_url = 'http://www.pinpayments.com/' self.display_name = 'Pin Payments' def initialize(options = {}) @@ -30,6 +30,8 @@ def purchase(money, creditcard, options = {}) add_address(post, 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 @@ -45,9 +47,20 @@ def store(creditcard, options = {}) commit(:post, 'customers', post, options) end + # Unstore a customer and associated credit card. + def unstore(token) + customer_token = + if /cus_/.match?(token) + get_customer_token(token) + else + token + end + commit(:delete, "customers/#{CGI.escape(customer_token)}", {}, {}) + end + # Refund a transaction def refund(money, token, options = {}) - commit(:post, "charges/#{CGI.escape(token)}/refunds", { :amount => amount(money) }, options) + commit(:post, "charges/#{CGI.escape(token)}/refunds", { amount: amount(money) }, options) end # Authorize an amount on a credit card. Once authorized, you can later @@ -61,12 +74,23 @@ def authorize(money, creditcard, options = {}) # Captures a previously authorized charge. Capturing only part of the original # authorization is currently not supported. def capture(money, token, options = {}) - commit(:put, "charges/#{CGI.escape(token)}/capture", { :amount => amount(money) }, options) + commit(:put, "charges/#{CGI.escape(token)}/capture", { amount: amount(money) }, options) + end + + # Voids a previously authorized charge. + def void(token, options = {}) + commit(:put, "charges/#{CGI.escape(token)}/void", {}, options) + end + + # Verify a previously authorized charge. + def verify_3ds(session_token, options = {}) + commit(:get, "/charges/verify?session_token=#{session_token}", nil, options) end # Updates the credit card for the customer. def update(token, creditcard, options = {}) post = {} + token = get_customer_token(token) add_creditcard(post, creditcard) add_customer_data(post, options) @@ -84,6 +108,7 @@ def scrub(transcript) gsub(/(number\\?":\\?")(\d*)/, '\1[FILTERED]'). gsub(/(cvc\\?":\\?")(\d*)/, '\1[FILTERED]') end + private def add_amount(post, money, options) @@ -99,27 +124,29 @@ def add_customer_data(post, options) def add_address(post, creditcard, options) return if creditcard.kind_of?(String) + address = (options[:billing_address] || options[:address]) return unless address post[:card] ||= {} post[:card].merge!( - :address_line1 => address[:address1], - :address_city => address[:city], - :address_postcode => address[:zip], - :address_state => address[:state], - :address_country => address[:country] + address_line1: address[:address1], + address_city: address[:city], + address_postcode: address[:zip], + address_state: address[:state], + address_country: address[:country] ) end def add_invoice(post, options) post[:description] = options[:description] || 'Active Merchant Purchase' + post[:reference] = options[:reference] if options[:reference] end def add_capture(post, options) capture = options[:capture] - post[:capture] = capture == false ? false : true + post[:capture] = capture != false end def add_creditcard(post, creditcard) @@ -127,25 +154,53 @@ def add_creditcard(post, creditcard) post[:card] ||= {} post[:card].merge!( - :number => creditcard.number, - :expiry_month => creditcard.month, - :expiry_year => creditcard.year, - :cvc => creditcard.verification_value, - :name => creditcard.name + number: creditcard.number, + expiry_month: creditcard.month, + expiry_year: creditcard.year, + cvc: creditcard.verification_value, + name: creditcard.name ) elsif creditcard.kind_of?(String) - if creditcard =~ /^card_/ - post[:card_token] = creditcard + if /^card_/.match?(creditcard) + post[:card_token] = get_card_token(creditcard) else post[:customer_token] = creditcard end end end + def get_customer_token(token) + token.split(/;(?=cus)/).last + end + + def get_card_token(token) + token.split(/;(?=cus)/).first + end + 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] = {} + if options[:three_d_secure][:enabled] + post[:three_d_secure][:enabled] = true + post[:three_d_secure][:fallback_ok] = options[:three_d_secure][:fallback_ok] unless options[:three_d_secure][:fallback_ok].nil? + post[:three_d_secure][:callback_url] = options[:three_d_secure][:callback_url] if options[:three_d_secure][:callback_url] + else + post[:three_d_secure][:version] = options[:three_d_secure][:version] if options[:three_d_secure][:version] + post[:three_d_secure][:eci] = options[:three_d_secure][:eci] if options[:three_d_secure][:eci] + post[:three_d_secure][:cavv] = options[:three_d_secure][:cavv] if options[:three_d_secure][:cavv] + post[:three_d_secure][:transaction_id] = options[:three_d_secure][:ds_transaction_id] || options[:three_d_secure][:xid] + end + end + end + def headers(params = {}) result = { 'Content-Type' => 'application/json', @@ -167,12 +222,13 @@ def commit(method, action, params, options) body = parse(e.response.body) end - if body['response'] + if body.nil? + no_content_response + elsif body['response'] success_response(body) elsif body['error'] error_response(body) end - rescue JSON::ParserError return unparsable_response(raw_response) end @@ -183,8 +239,8 @@ def success_response(body) true, response['status_message'], body, - :authorization => token(response), - :test => test? + authorization: token(response), + test: test? ) end @@ -193,26 +249,41 @@ def error_response(body) false, body['error_description'], body, - :authorization => nil, - :test => test? + authorization: nil, + test: test? + ) + end + + def no_content_response + Response.new( + true, + nil, + {}, + test: test? ) end def unparsable_response(raw_response) - message = 'Invalid JSON response received from Pin Payments. Please contact support@pin.net.au if you continue to receive this message.' + message = 'Invalid JSON response received from Pin Payments. Please contact support@pinpayments.com if you continue to receive this message.' message += " (The raw response returned by the API was #{raw_response.inspect})" return Response.new(false, message) end def token(response) - response['token'] + if response['token'].start_with?('cus') + "#{response.dig('card', 'token')};#{response['token']}" + else + response['token'] + end end def parse(body) - JSON.parse(body) + JSON.parse(body) unless body.nil? || body.length == 0 end def post_data(parameters = {}) + return nil unless parameters + parameters.to_json end end diff --git a/lib/active_merchant/billing/gateways/plexo.rb b/lib/active_merchant/billing/gateways/plexo.rb new file mode 100644 index 00000000000..348c17f51e6 --- /dev/null +++ b/lib/active_merchant/billing/gateways/plexo.rb @@ -0,0 +1,337 @@ +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class PlexoGateway < Gateway + self.test_url = 'https://api.testing.plexo.com.uy/v1/payments' + self.live_url = 'https://api.plexo.com.uy/v1/payments' + + self.supported_countries = ['UY'] + self.default_currency = 'UYU' + self.supported_cardtypes = %i[visa master american_express discover passcard edenred anda tarjeta-d] + + self.homepage_url = 'https://www.plexo.com.uy' + self.display_name = 'Plexo' + + APPENDED_URLS = %w(captures refunds cancellations verify) + AMOUNT_IN_RESPONSE = %w(authonly purchase /verify) + APPROVED_STATUS = %w(approved authorized) + + def initialize(options = {}) + requires!(options, :client_id, :api_key) + @credentials = options + super + end + + def purchase(money, payment, options = {}) + post = {} + build_auth_purchase_request(money, post, payment, options) + + commit('purchase', post, options) + end + + def authorize(money, payment, options = {}) + post = {} + build_auth_purchase_request(money, post, payment, options) + add_capture_type(post, options) + + commit('authonly', post, options) + end + + def capture(money, authorization, options = {}) + post = {} + post[:ReferenceId] = options[:reference_id] || generate_unique_id + post[:Amount] = amount(money) + + commit("/#{authorization}/captures", post, options) + end + + def refund(money, authorization, options = {}) + post = {} + post[:ReferenceId] = options[:reference_id] || generate_unique_id + post[:Type] = options[:refund_type] || 'refund' + post[:Description] = options[:description] + post[:Reason] = options[:reason] + post[:Amount] = amount(money) + + commit("/#{authorization}/refunds", post, options) + end + + def void(authorization, options = {}) + post = {} + post[:ReferenceId] = options[:reference_id] || generate_unique_id + post[:Description] = options[:description] + post[:Reason] = options[:reason] + + commit("/#{authorization}/cancellations", post, options) + end + + def verify(credit_card, options = {}) + post = {} + post[:ReferenceId] = options[:reference_id] || generate_unique_id + post[: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] + money = options[:verify_amount].to_i || 100 + + add_payment_method(post, credit_card, options) + add_metadata(post, options[:metadata]) + add_amount(money, post, options) + add_browser_details(post, options) + add_invoice_number(post, options) + + commit('/verify', post, options) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). + gsub(%r(("Number\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("Cvc\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("InvoiceNumber\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("MerchantId\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("Cryptogram\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]') + end + + private + + def encoded_credentials + Base64.encode64("#{@credentials[:client_id]}:#{@credentials[:api_key]}").delete("\n") + end + + def build_auth_purchase_request(money, post, payment, options) + post[:ReferenceId] = options[:reference_id] || generate_unique_id + post[:MerchantId] = options[:merchant_id] || @credentials[:merchant_id] + post[:Installments] = options[:installments] if options[:installments] + post[:StatementDescriptor] = options[:statement_descriptor] if options[:statement_descriptor] + post[:CustomerId] = options[:customer_id] if options[:customer_id] + 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 = {}) + { + 'Content-Type' => 'application/json', + 'Authorization' => "Basic #{encoded_credentials}" + } + end + + def add_capture_type(post, options) + post[:Capture] = {} + post[:Capture][:Method] = options.dig(:capture_type, :method) || 'manual' + end + + def add_items(post, items) + return unless items&.kind_of?(Array) + + post[:Items] = [] + + items.each do |option_item| + item = {} + item[:ReferenceId] = option_item[:reference_id] || generate_unique_id + item[:Name] = option_item[:name] if option_item[:name] + item[:Description] = option_item[:description] if option_item[:description] + item[:Quantity] = option_item[:quantity] if option_item[:quantity] + item[:Price] = option_item[:price] if option_item[:price] + item[:Discount] = option_item[:discount] if option_item[:discount] + + post[:Items].append(item) + end + end + + def add_metadata(post, metadata) + return unless metadata&.kind_of?(Hash) + + metadata.transform_keys! { |key| key.to_s.camelize.to_sym } + post[:Metadata] = metadata + end + + def add_amount(money, post, amount_options) + post[:Amount] = {} + + post[:Amount][:Currency] = amount_options[:currency] || self.default_currency + post[:Amount][:Total] = amount(money) + post[:Amount][:Details] = {} + add_amount_details(post[:Amount][:Details], amount_options[:amount_details]) if amount_options[:amount_details] + end + + def add_amount_details(amount_details, options) + return unless options + + amount_details[:TaxedAmount] = options[:taxed_amount] if options[:taxed_amount] + amount_details[:TipAmount] = options[:tip_amount] if options[:tip_amount] + amount_details[:DiscountAmount] = options[:discount_amount] if options[:discount_amount] + amount_details[:TaxableAmount] = options[:taxable_amount] if options[:taxable_amount] + add_tax(amount_details, options[:tax]) + end + + def add_tax(post, tax) + return unless tax + + post[:Tax] = {} + post[:Tax][:Type] = tax[:type] if tax[:type] + post[:Tax][:Amount] = tax[:amount] if tax[:amount] + post[:Tax][:Rate] = tax[:rate] if tax[:rate] + end + + def add_browser_details(post, browser_details) + return unless browser_details + + post[:BrowserDetails] = {} + post[:BrowserDetails][:DeviceFingerprint] = browser_details[:finger_print] if browser_details[:finger_print] + post[:BrowserDetails][:IpAddress] = browser_details[:ip] if browser_details[:ip] + end + + def add_invoice_number(post, options) + post[:InvoiceNumber] = options[:invoice_number] if options[:invoice_number] + end + + def add_payment_method(post, payment, options) + payment_method = build_payment_method(payment) + + if payment_method.present? + add_card_holder(payment_method[:NetworkToken] || payment_method[:Card], payment, options) + post[:paymentMethod] = payment_method + end + end + + def build_payment_method(payment) + case payment + when NetworkTokenizationCreditCard + { + source: 'network-token', + id: payment.brand, + NetworkToken: { + Number: payment.number, + ExpMonth: (format(payment.month, :two_digits) if payment.month), + ExpYear: (format(payment.year, :two_digits) if payment.year), + Cryptogram: payment.payment_cryptogram + } + } + when CreditCard + { + type: 'card', + Card: { + Number: payment.number, + ExpMonth: (format(payment.month, :two_digits) if payment.month), + ExpYear: (format(payment.year, :two_digits) if payment.year), + Cvc: payment.verification_value + } + } + end + end + + def add_card_holder(card, payment, options) + requires!(options, :email) + + cardholder = {} + cardholder[:FirstName] = payment.first_name if payment.first_name + cardholder[:LastName] = payment.last_name if payment.last_name + cardholder[:Email] = options[:email] + cardholder[:Birthdate] = options[:cardholder_birthdate] if options[:cardholder_birthdate] + cardholder[:Identification] = {} + cardholder[:Identification][:Type] = options[:identification_type] if options[:identification_type] + cardholder[:Identification][:Value] = options[:identification_value] if options[:identification_value] + add_billing_address(cardholder, options) + + card[:Cardholder] = cardholder + end + + def add_billing_address(cardholder, options) + return unless address = options[:billing_address] + + cardholder[:BillingAddress] = {} + cardholder[:BillingAddress][:City] = address[:city] + cardholder[:BillingAddress][:Country] = address[:country] + cardholder[:BillingAddress][:Line1] = address[:address1] + cardholder[:BillingAddress][:Line2] = address[:address2] + cardholder[:BillingAddress][:PostalCode] = address[:zip] + cardholder[:BillingAddress][:State] = address[:state] + end + + def parse(body) + return {} if body == '' + + JSON.parse(body) + end + + def build_url(action, base) + url = base + url += action if APPENDED_URLS.any? { |key| action.include?(key) } + url + end + + def get_authorization_from_url(url) + url.split('/')[1] + end + + def reorder_amount_fields(response) + return response unless response['amount'] + + amount_obj = response['amount'] + response['amount'] = amount_obj['total'].to_i if amount_obj['total'] + response['currency'] = amount_obj['currency'] if amount_obj['currency'] + response['amount_details'] = amount_obj['details'] if amount_obj['details'] + response + end + + def commit(action, parameters, options = {}) + base_url = (test? ? test_url : live_url) + url = build_url(action, base_url) + response = parse(ssl_post(url, parameters.to_json, header(options))) + response = reorder_amount_fields(response) if AMOUNT_IN_RESPONSE.include?(action) + + Response.new( + success_from(response), + message_from(response), + response, + authorization: authorization_from(response, action), + test: test?, + error_code: error_code_from(response) + ) + end + + def handle_response(response) + case response.code.to_i + when 200...300, 400, 401 + response.body + else + raise ResponseError.new(response) + end + end + + def success_from(response) + APPROVED_STATUS.include?(response['status']) + end + + def message_from(response) + response = response['transactions']&.first if response['transactions']&.is_a?(Array) + response['resultMessage'] || response['message'] + end + + def authorization_from(response, action = nil) + if action.include?('captures') + get_authorization_from_url(action) + else + response['id'] + end + end + + def error_code_from(response) + return if success_from(response) + + response = response['transactions']&.first if response['transactions']&.is_a?(Array) + response['resultCode'] || response['status'] + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/plugnpay.rb b/lib/active_merchant/billing/gateways/plugnpay.rb index 1b78b25c299..e383c0d4ef7 100644 --- a/lib/active_merchant/billing/gateways/plugnpay.rb +++ b/lib/active_merchant/billing/gateways/plugnpay.rb @@ -3,8 +3,8 @@ module Billing class PlugnpayGateway < Gateway class PlugnpayPostData < PostData # Fields that will be sent even if they are blank - self.required_fields = [:publisher_name, :publisher_password, - :card_amount, :card_name, :card_number, :card_exp, :orderID] + self.required_fields = %i[publisher_name publisher_password + card_amount card_name card_number card_exp orderID] end self.live_url = self.test_url = 'https://pay1.plugnpay.com/payment/pnpremote.cgi' @@ -16,7 +16,7 @@ class PlugnpayPostData < PostData 'U' => 'Issuer was not certified for card verification' } - CARD_CODE_ERRORS = %w( N S ) + CARD_CODE_ERRORS = %w(N S) AVS_MESSAGES = { 'A' => 'Street address matches billing information, zip/postal code does not', @@ -31,10 +31,10 @@ class PlugnpayPostData < PostData 'W' => '9-digit zip/postal code matches billing information, street address does not', 'X' => 'Street address and 9-digit zip/postal code matches billing information', 'Y' => 'Street address and 5-digit zip/postal code matches billing information', - 'Z' => '5-digit zip/postal code matches billing information, street address does not', + 'Z' => '5-digit zip/postal code matches billing information, street address does not' } - AVS_ERRORS = %w( A E N R W Z ) + AVS_ERRORS = %w(A E N R W Z) PAYMENT_GATEWAY_RESPONSES = { 'P01' => 'AVS Mismatch Failure', @@ -80,20 +80,20 @@ class PlugnpayPostData < PostData } TRANSACTIONS = { - :authorization => 'auth', - :purchase => 'auth', - :capture => 'mark', - :void => 'void', - :refund => 'return', - :credit => 'newreturn' + authorization: 'auth', + purchase: 'auth', + capture: 'mark', + void: 'void', + refund: 'return', + credit: 'newreturn' } - SUCCESS_CODES = [ 'pending', 'success' ] - FAILURE_CODES = [ 'badcard', 'fraud' ] + SUCCESS_CODES = %w[pending success] + FAILURE_CODES = %w[badcard fraud] self.default_currency = 'USD' self.supported_countries = ['US'] - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'http://www.plugnpay.com/' self.display_name = "Plug'n Pay" @@ -172,23 +172,27 @@ def refund(money, reference, options = {}) end private + def commit(action, post) - response = parse( ssl_post(self.live_url, post_data(action, post)) ) + response = parse(ssl_post(self.live_url, post_data(action, post))) success = SUCCESS_CODES.include?(response[:finalstatus]) message = success ? 'Success' : message_from(response) - Response.new(success, message, response, - :test => test?, - :authorization => response[:orderid], - :avs_result => { :code => response[:avs_code] }, - :cvv_result => response[:cvvresp] + Response.new( + success, + message, + response, + test: test?, + authorization: response[:orderid], + avs_result: { code: response[:avs_code] }, + cvv_result: response[:cvvresp] ) end def parse(body) body = CGI.unescape(body) results = {} - body.split('&').collect { |e| e.split('=') }.each do |key,value| + body.split('&').collect { |e| e.split('=') }.each do |key, value| results[key.downcase.to_sym] = normalize(value.to_s.strip) end @@ -218,7 +222,7 @@ def add_creditcard(post, creditcard) def add_customer_data(post, options) post[:email] = options[:email] - post[:dontsndmail] = 'yes' unless options[:send_email_confirmation] + post[:dontsndmail] = 'yes' unless options[:send_email_confirmation] post[:ipaddress] = options[:ip] end @@ -255,7 +259,7 @@ def add_addresses(post, options) post[:state] = shipping_address[:state] else post[:state] = 'ZZ' - post[:province] = shipping_address[:state] + post[:province] = shipping_address[:state] end post[:country] = shipping_address[:country] diff --git a/lib/active_merchant/billing/gateways/priority.rb b/lib/active_merchant/billing/gateways/priority.rb new file mode 100644 index 00000000000..d7b1f0f4cdb --- /dev/null +++ b/lib/active_merchant/billing/gateways/priority.rb @@ -0,0 +1,393 @@ +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class PriorityGateway < Gateway + # Sandbox and Production + self.test_url = 'https://sandbox.api.mxmerchant.com/checkout/v3/payment' + self.live_url = 'https://api.mxmerchant.com/checkout/v3/payment' + + class_attribute :test_url_verify, :live_url_verify, :test_auth, :live_auth, :test_env_verify, :live_env_verify, :test_url_batch, :live_url_batch, :test_url_jwt, :live_url_jwt, :merchant + + # Sandbox and Production - verify card + self.test_url_verify = 'https://sandbox-api2.mxmerchant.com/merchant/v1/bin' + self.live_url_verify = 'https://api2.mxmerchant.com/merchant/v1/bin' + + # Sandbox and Production - check batch status + self.test_url_batch = 'https://sandbox.api.mxmerchant.com/checkout/v3/batch' + self.live_url_batch = 'https://api.mxmerchant.com/checkout/v3/batch' + + # Sandbox and Production - generate jwt for verify card url + self.test_url_jwt = 'https://sandbox-api2.mxmerchant.com/security/v1/application/merchantId' + self.live_url_jwt = 'https://api2.mxmerchant.com/security/v1/application/merchantId' + + self.supported_countries = ['US'] + self.default_currency = 'USD' + self.supported_cardtypes = %i[visa master american_express discover] + + self.homepage_url = 'https://mxmerchant.com/' + self.display_name = 'Priority' + + def initialize(options = {}) + requires!(options, :merchant_id, :api_key, :secret) + super + end + + def basic_auth + Base64.strict_encode64("#{@options[:api_key]}:#{@options[:secret]}") + end + + def request_headers + { + 'Content-Type' => 'application/json', + 'Authorization' => "Basic #{basic_auth}" + } + end + + def request_verify_headers(jwt) + { + 'Authorization' => "Bearer #{jwt}" + } + end + + def purchase(amount, credit_card, options = {}) + params = {} + params['authOnly'] = false + params['isSettleFunds'] = true + + add_merchant_id(params) + add_amount(params, amount, options) + add_auth_purchase_params(params, options) + add_credit_card(params, credit_card, 'purchase', options) + + commit('purchase', params:) + end + + def authorize(amount, credit_card, options = {}) + params = {} + params['authOnly'] = true + params['isSettleFunds'] = false + + add_merchant_id(params) + add_amount(params, amount, options) + add_auth_purchase_params(params, options) + add_credit_card(params, credit_card, 'purchase', options) + + commit('purchase', params:) + end + + def credit(amount, credit_card, options = {}) + params = {} + params['authOnly'] = false + params['isSettleFunds'] = true + amount = -amount + + add_merchant_id(params) + add_amount(params, amount, options) + add_credit_params(params, credit_card, options) + commit('credit', params:) + end + + def refund(amount, authorization, options = {}) + params = {} + add_merchant_id(params) + params['paymentToken'] = payment_token(authorization) || options[:payment_token] + + # refund amounts must be negative + params['amount'] = ('-' + localized_amount(amount.to_f, options[:currency])).to_f + + commit('refund', params:) + end + + def capture(amount, authorization, options = {}) + params = {} + add_merchant_id(params) + add_amount(params, amount, options) + params['paymentToken'] = payment_token(authorization) || options[:payment_token] + add_auth_purchase_params(params, options) + + commit('capture', params:) + end + + def void(authorization, options = {}) + params = {} + + commit('void', params:, iid: payment_id(authorization)) + end + + def verify(credit_card, _options = {}) + jwt = create_jwt.params['jwtToken'] + + commit('verify', card_number: credit_card.number, jwt:) + end + + def get_payment_status(batch_id) + commit('get_payment_status', params: batch_id) + end + + def close_batch(batch_id) + commit('close_batch', params: batch_id) + end + + def create_jwt + commit('create_jwt', params: @options[:merchant_id]) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). + gsub(%r((number\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r((cvv\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]') + end + + private + + def add_amount(params, amount, options) + params['amount'] = localized_amount(amount.to_f, options[:currency]) + end + + def add_merchant_id(params) + params['merchantId'] = @options[:merchant_id] + end + + def add_auth_purchase_params(params, options) + add_replay_id(params, options) + add_purchases_data(params, options) + add_shipping_data(params, options) + add_pos_data(params, options) + add_additional_data(params, options) + end + + def add_credit_params(params, credit_card, options) + add_replay_id(params, options) + add_credit_card(params, credit_card, 'purchase', options) + add_additional_data(params, options) + end + + def add_replay_id(params, options) + params['replayId'] = options[:replay_id] if options[:replay_id] + end + + def add_credit_card(params, credit_card, action, options) + return unless credit_card&.is_a?(CreditCard) + + card_details = {} + card_details['expiryMonth'] = format(credit_card.month, :two_digits).to_s + card_details['expiryYear'] = format(credit_card.year, :two_digits).to_s + card_details['cardType'] = credit_card.brand + card_details['last4'] = credit_card.last_digits + card_details['cvv'] = credit_card.verification_value unless credit_card.verification_value.nil? + card_details['number'] = credit_card.number + card_details['avsStreet'] = options[:billing_address][:address1] if options[:billing_address] + card_details['avsZip'] = options[:billing_address][:zip] if !options[:billing_address].nil? && !options[:billing_address][:zip].nil? + + params['cardAccount'] = card_details + end + + def exp_date(credit_card) + "#{format(credit_card.month, :two_digits)}/#{format(credit_card.year, :two_digits)}" + end + + def add_additional_data(params, options) + params['isAuth'] = options[:is_auth].present? ? options[:is_auth] : 'true' + params['paymentType'] = options[:payment_type].present? ? options[:payment_type] : 'Sale' + params['tenderType'] = options[:tender_type].present? ? options[:tender_type] : 'Card' + params['taxExempt'] = options[:tax_exempt].present? ? options[:tax_exempt] : 'false' + params['taxAmount'] = options[:tax_amount] if options[:tax_amount] + params['shouldGetCreditCardLevel'] = options[:should_get_credit_card_level] if options[:should_get_credit_card_level] + params['source'] = options[:source] if options[:source] + params['invoice'] = options[:invoice] if options[:invoice] + params['isTicket'] = options[:is_ticket] if options[:is_ticket] + params['shouldVaultCard'] = options[:should_vault_card] if options[:should_vault_card] + params['sourceZip'] = options[:source_zip] if options[:source_zip] + params['authCode'] = options[:auth_code] if options[:auth_code] + params['achIndicator'] = options[:ach_indicator] if options[:ach_indicator] + params['bankAccount'] = options[:bank_account] if options[:bank_account] + params['meta'] = options[:meta] if options[:meta] + end + + def add_pos_data(params, options) + pos_data = {} + pos_data['cardholderPresence'] = options.dig(:pos_data, :cardholder_presence) || 'Ecom' + pos_data['deviceAttendance'] = options.dig(:pos_data, :device_attendance) || 'HomePc' + pos_data['deviceInputCapability'] = options.dig(:pos_data, :device_input_capability) || 'Unknown' + pos_data['deviceLocation'] = options.dig(:pos_data, :device_location) || 'HomePc' + pos_data['panCaptureMethod'] = options.dig(:pos_data, :pan_capture_method) || 'Manual' + pos_data['partialApprovalSupport'] = options.dig(:pos_data, :partial_approval_support) || 'NotSupported' + pos_data['pinCaptureCapability'] = options.dig(:pos_data, :pin_capture_capability) || 'Incapable' + + params['posData'] = pos_data + end + + def add_purchases_data(params, options) + return unless options[:purchases] + + params['purchases'] = [] + + options[:purchases].each do |purchase| + purchase_object = {} + + purchase_object['name'] = purchase[:name] if purchase[:name] + purchase_object['description'] = purchase[:description] if purchase[:description] + purchase_object['code'] = purchase[:code] if purchase[:code] + purchase_object['unitOfMeasure'] = purchase[:unit_of_measure] if purchase[:unit_of_measure] + purchase_object['unitPrice'] = purchase[:unit_price] if purchase[:unit_price] + purchase_object['quantity'] = purchase[:quantity] if purchase[:quantity] + purchase_object['taxRate'] = purchase[:tax_rate] if purchase[:tax_rate] + purchase_object['taxAmount'] = purchase[:tax_amount] if purchase[:tax_amount] + purchase_object['discountRate'] = purchase[:discount_rate] if purchase[:discount_rate] + purchase_object['discountAmount'] = purchase[:discount_amount] if purchase[:discount_amount] + purchase_object['extendedAmount'] = purchase[:extended_amount] if purchase[:extended_amount] + purchase_object['lineItemId'] = purchase[:line_item_id] if purchase[:line_item_id] + + params['purchases'].append(purchase_object) + end + end + + def add_shipping_data(params, options) + params['shipAmount'] = options[:ship_amount] if options[:ship_amount] + + shipping_country = shipping_country_from(options) + params['shipToCountry'] = shipping_country if shipping_country + + shipping_zip = shipping_zip_from(options) + params['shipToZip'] = shipping_zip if shipping_zip + end + + def shipping_country_from(options) + options[:ship_to_country] || options.dig(:shipping_address, :country) || options.dig(:billing_address, :country) + end + + def shipping_zip_from(options) + options[:ship_to_zip] || options.dig(:shipping_addres, :zip) || options.dig(:billing_address, :zip) + end + + def payment_token(authorization) + return unless authorization + return authorization unless authorization.include?('|') + + authorization.split('|').last + end + + def payment_id(authorization) + return unless authorization + return authorization unless authorization.include?('|') + + authorization.split('|').first + end + + def commit(action, params: '', iid: '', card_number: nil, jwt: '') + response = + begin + case action + when 'void' + parse(ssl_request(:delete, url(action, params, ref_number: iid), nil, request_headers)) + when 'verify' + parse(ssl_get(url(action, params, credit_card_number: card_number), request_verify_headers(jwt))) + when 'get_payment_status', 'create_jwt' + parse(ssl_get(url(action, params, ref_number: iid), request_headers)) + when 'close_batch' + parse(ssl_request(:put, url(action, params, ref_number: iid), nil, request_headers)) + else + parse(ssl_post(url(action, params), post_data(params), request_headers)) + end + rescue ResponseError => e + # currently Priority returns a 404 with no body on certain calls. In those cases we will substitute the response status from response.message + gateway_response = e.response.body.presence || e.response.message + parse(gateway_response) + end + + success = success_from(response, action) + Response.new( + success, + message_from(response), + response, + authorization: success ? authorization_from(response) : nil, + error_code: success || response == '' ? nil : error_from(response), + test: test? + ) + end + + def url(action, params, ref_number: '', credit_card_number: nil) + case action + when 'void' + base_url + "/#{ref_number}?force=true" + when 'verify' + (verify_url + '?search=') + credit_card_number.to_s[0..7] + when 'get_payment_status', 'close_batch' + batch_url + "/#{params}" + when 'create_jwt' + jwt_url + "/#{params}/token" + else + base_url + '?includeCustomerMatches=false&echo=true' + end + end + + def base_url + test? ? test_url : live_url + end + + def verify_url + test? ? self.test_url_verify : self.live_url_verify + end + + def jwt_url + test? ? self.test_url_jwt : self.live_url_jwt + end + + def batch_url + test? ? self.test_url_batch : self.live_url_batch + end + + def handle_response(response) + case response.code.to_i + when 204 + { status: 'Success' }.to_json + when 200...300 + response.body + else + raise ResponseError.new(response) + end + end + + def parse(body) + return {} if body.blank? + + parsed_response = JSON.parse(body) + parsed_response.is_a?(String) ? { 'message' => parsed_response } : parsed_response + rescue JSON::ParserError + message = 'Invalid JSON response received from Priority Gateway. Please contact Priority Gateway if you continue to receive this message.' + message += " (The raw response returned by the API was #{body.inspect})" + { + 'message' => message + } + end + + def success_from(response, action) + return !response['bank'].empty? if action == 'verify' && response['bank'] && !response.dig('bank', 'name').blank? + + %w[Approved Open Success Settled Voided].include?(response['status']) + end + + def message_from(response) + return response['details'][0] if response['details'] && response['details'][0] + + response['authMessage'] || response['message'] || response['status'] + end + + def authorization_from(response) + [response['id'], response['paymentToken']].join('|') + end + + def error_from(response) + response['errorCode'] || response['status'] + end + + def post_data(params) + params.to_json + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/pro_pay.rb b/lib/active_merchant/billing/gateways/pro_pay.rb index ef3eb1050a9..e19ed834a76 100644 --- a/lib/active_merchant/billing/gateways/pro_pay.rb +++ b/lib/active_merchant/billing/gateways/pro_pay.rb @@ -1,15 +1,15 @@ require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class ProPayGateway < Gateway self.test_url = 'https://xmltest.propay.com/API/PropayAPI.aspx' self.live_url = 'https://epay.propay.com/api/propayapi.aspx' - self.supported_countries = ['US', 'CA'] + self.supported_countries = %w[US CA] self.default_currency = 'USD' self.money_format = :cents - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'https://www.propay.com/' self.display_name = 'ProPay' @@ -133,12 +133,12 @@ class ProPayGateway < Gateway '99' => 'Generic decline or unable to parse issuer response code' } - def initialize(options={}) + def initialize(options = {}) requires!(options, :cert_str) super end - def purchase(money, payment, options={}) + def purchase(money, payment, options = {}) request = build_xml_request do |xml| add_invoice(xml, money, options) add_payment(xml, payment, options) @@ -151,7 +151,7 @@ def purchase(money, payment, options={}) commit(request) end - def authorize(money, payment, options={}) + def authorize(money, payment, options = {}) request = build_xml_request do |xml| add_invoice(xml, money, options) add_payment(xml, payment, options) @@ -164,7 +164,7 @@ def authorize(money, payment, options={}) commit(request) end - def capture(money, authorization, options={}) + def capture(money, authorization, options = {}) request = build_xml_request do |xml| add_invoice(xml, money, options) add_account(xml, options) @@ -175,7 +175,7 @@ def capture(money, authorization, options={}) commit(request) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) request = build_xml_request do |xml| add_invoice(xml, money, options) add_account(xml, options) @@ -186,11 +186,11 @@ def refund(money, authorization, options={}) commit(request) end - def void(authorization, options={}) + def void(authorization, options = {}) refund(nil, authorization, options) end - def credit(money, payment, options={}) + def credit(money, payment, options = {}) request = build_xml_request do |xml| add_invoice(xml, money, options) add_payment(xml, payment, options) @@ -201,7 +201,7 @@ def credit(money, payment, options={}) commit(request) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -234,7 +234,7 @@ def add_address(xml, options) xml.aptNum address[:address2] xml.city address[:city] xml.state address[:state] - xml.zip address[:zip] + xml.zip address[:zip].to_s.delete('-') end end @@ -253,7 +253,7 @@ def add_recurring(xml, options) end def parse(body) - results = {} + results = {} xml = Nokogiri::XML(body) resp = xml.xpath('//XMLResponse/XMLTrans') resp.children.each do |element| @@ -284,6 +284,7 @@ def success_from(response) def message_from(response) return 'Success' if success_from(response) + message = STATUS_RESPONSE_CODES[response[:status]] message += " - #{TRANSACTION_RESPONSE_CODES[response[:response_code]]}" if response[:response_code] @@ -295,9 +296,7 @@ def authorization_from(response) end def error_code_from(response) - unless success_from(response) - response[:status] - end + response[:status] unless success_from(response) end def build_xml_request @@ -317,8 +316,8 @@ def build_xml_request def underscore(camel_cased_word) camel_cased_word.to_s.gsub(/::/, '/'). - gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). - gsub(/([a-z\d])([A-Z])/,'\1_\2'). + gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2'). + gsub(/([a-z\d])([A-Z])/, '\1_\2'). tr('-', '_'). downcase end diff --git a/lib/active_merchant/billing/gateways/psigate.rb b/lib/active_merchant/billing/gateways/psigate.rb index e83b86c2c86..f6fa36088d3 100644 --- a/lib/active_merchant/billing/gateways/psigate.rb +++ b/lib/active_merchant/billing/gateways/psigate.rb @@ -1,7 +1,7 @@ require 'rexml/document' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # This class implements the Psigate gateway for the ActiveMerchant module. # # Modifications by Sean O'Hara ( sohara at sohara dot com ) @@ -38,7 +38,7 @@ class PsigateGateway < Gateway self.test_url = 'https://realtimestaging.psigate.com/xml' self.live_url = 'https://realtime.psigate.com/xml' - self.supported_cardtypes = [:visa, :master, :american_express] + self.supported_cardtypes = %i[visa master american_express] self.supported_countries = ['CA'] self.homepage_url = 'http://www.psigate.com/' self.display_name = 'Psigate' @@ -102,11 +102,14 @@ 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, - :test => test?, - :authorization => build_authorization(response) , - :avs_result => { :code => response[:avsresult] }, - :cvv_result => response[:cardidresult] + Response.new( + successful?(response), + message_from(response), + response, + test: test?, + authorization: build_authorization(response), + avs_result: { code: response[:avsresult] }, + cvv_result: response[:cardidresult] ) end @@ -119,7 +122,7 @@ def successful?(response) end def parse(xml) - response = {:message => 'Global Error Receipt', :complete => false} + response = { message: 'Global Error Receipt', complete: false } xml = REXML::Document.new(xml) xml.elements.each('//Result/*') do |node| @@ -144,43 +147,43 @@ def post_data(money, creditcard, options) def parameters(money, creditcard, options = {}) params = { # General order parameters - :StoreID => @options[:login], - :Passphrase => @options[:password], - :TestResult => options[:test_result], - :OrderID => options[:order_id], - :UserID => options[:user_id], - :Phone => options[:phone], - :Fax => options[:fax], - :Email => options[:email], - :TransRefNumber => options[:trans_ref_number], + StoreID: @options[:login], + Passphrase: @options[:password], + TestResult: options[:test_result], + OrderID: options[:order_id], + UserID: options[:user_id], + Phone: options[:phone], + Fax: options[:fax], + Email: options[:email], + TransRefNumber: options[:trans_ref_number], # Credit Card parameters - :PaymentType => 'CC', - :CardAction => options[:CardAction], + PaymentType: 'CC', + CardAction: options[:CardAction], # Financial parameters - :CustomerIP => options[:ip], - :SubTotal => amount(money), - :Tax1 => options[:tax1], - :Tax2 => options[:tax2], - :ShippingTotal => options[:shipping_total], + CustomerIP: options[:ip], + SubTotal: amount(money), + Tax1: options[:tax1], + Tax2: options[:tax2], + ShippingTotal: options[:shipping_total] } if creditcard exp_month = sprintf('%.2i', creditcard.month) unless creditcard.month.blank? - exp_year = creditcard.year.to_s[2,2] unless creditcard.year.blank? + exp_year = creditcard.year.to_s[2, 2] unless creditcard.year.blank? card_id_code = (creditcard.verification_value.blank? ? nil : '1') params.update( - :CardNumber => creditcard.number, - :CardExpMonth => exp_month, - :CardExpYear => exp_year, - :CardIDCode => card_id_code, - :CardIDNumber => creditcard.verification_value + CardNumber: creditcard.number, + CardExpMonth: exp_month, + CardExpYear: exp_year, + CardIDCode: card_id_code, + CardIDNumber: creditcard.verification_value ) end - if(address = (options[:billing_address] || options[:address])) + if (address = (options[:billing_address] || options[:address])) params[:Bname] = address[:name] || creditcard.name params[:Baddress1] = address[:address1] unless address[:address1].blank? params[:Baddress2] = address[:address2] unless address[:address2].blank? @@ -210,6 +213,7 @@ def message_from(response) return SUCCESS_MESSAGE else return FAILURE_MESSAGE if response[:errmsg].blank? + return response[:errmsg].gsub(/[^\w]/, ' ').split.join(' ').capitalize end end diff --git a/lib/active_merchant/billing/gateways/psl_card.rb b/lib/active_merchant/billing/gateways/psl_card.rb index 4bd7f56f977..11f20a2a1ad 100644 --- a/lib/active_merchant/billing/gateways/psl_card.rb +++ b/lib/active_merchant/billing/gateways/psl_card.rb @@ -17,11 +17,11 @@ class PslCardGateway < Gateway self.default_currency = 'GBP' self.supported_countries = ['GB'] - # Visa Credit, Visa Debit, Mastercard, Maestro, Solo, Electron, + # Visa Credit, Visa Debit, Mastercard, Maestro, Electron, # American Express, Diners Club, JCB, International Maestro, # Style, Clydesdale Financial Services, Other - self.supported_cardtypes = [ :visa, :master, :american_express, :diners_club, :jcb, :switch, :solo, :maestro ] + self.supported_cardtypes = %i[visa master american_express diners_club jcb maestro] self.homepage_url = 'http://www.paymentsolutionsltd.com/' self.display_name = 'PSL Payment Solutions' @@ -46,20 +46,20 @@ class PslCardGateway < Gateway 'USD' => 840 } - #The terminal used - only for swipe transactions, so hard coded to 32 for online + # The terminal used - only for swipe transactions, so hard coded to 32 for online EMV_TERMINAL_TYPE = 32 - #Different Dispatch types + # Different Dispatch types DISPATCH_LATER = 'LATER' DISPATCH_NOW = 'NOW' # Return codes APPROVED = '00' - #Nominal amount to authorize for a 'dispatch later' type - #The nominal amount is held straight away, when the goods are ready - #to be dispatched, PSL is informed and the full amount is the - #taken. + # Nominal amount to authorize for a 'dispatch later' type + # The nominal amount is held straight away, when the goods are ready + # to be dispatched, PSL is informed and the full amount is the + # taken. NOMINAL_AMOUNT = 101 AVS_CODE = { @@ -174,12 +174,6 @@ def add_credit_card(post, credit_card) post[:ExpMonth] = credit_card.month post[:ExpYear] = credit_card.year - if requires_start_date_or_issue_number?(credit_card) - post[:IssueNumber] = credit_card.issue_number unless credit_card.issue_number.blank? - post[:StartMonth] = credit_card.start_month unless credit_card.start_month.blank? - post[:StartYear] = credit_card.start_year unless credit_card.start_year.blank? - end - # CV2 check post[:AVSCV2Check] = credit_card.verification_value? ? 'YES' : 'NO' post[:CV2] = credit_card.verification_value if credit_card.verification_value? @@ -189,7 +183,7 @@ def add_address(post, options) address = options[:billing_address] || options[:address] return if address.nil? - post[:QAAddress] = [:address1, :address2, :city, :state].collect{|a| address[a]}.reject{|a| a.blank?}.join(' ') + post[:QAAddress] = %i[address1 address2 city state].collect { |a| address[a] }.reject(&:blank?).join(' ') post[:QAPostcode] = address[:zip] end @@ -217,7 +211,7 @@ def add_amount(post, money, dispatch_type, options) def add_purchase_details(post) post[:EchoAmount] = 'YES' - post[:SCBI] = 'YES' # Return information about the transaction + post[:SCBI] = 'YES' # Return information about the transaction post[:MessageType] = MESSAGE_TYPE end @@ -246,10 +240,9 @@ def currency_code(currency) # -a hash with all of the values returned in the PSL response # def parse(body) - fields = {} for line in body.split('&') - key, value = *line.scan( %r{^(\w+)\=(.*)$} ).flatten + key, value = *line.scan(%r{^(\w+)\=(.*)$}).flatten fields[key] = CGI.unescape(value) end fields.symbolize_keys @@ -264,13 +257,16 @@ def parse(body) # - ActiveMerchant::Billing::Response object # def commit(request) - response = parse( ssl_post(self.live_url, post_data(request)) ) - - 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]] } + response = parse(ssl_post(self.live_url, post_data(request))) + + 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]] } ) end diff --git a/lib/active_merchant/billing/gateways/qbms.rb b/lib/active_merchant/billing/gateways/qbms.rb index 85c9adf939a..2489cea5a7f 100644 --- a/lib/active_merchant/billing/gateways/qbms.rb +++ b/lib/active_merchant/billing/gateways/qbms.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class QbmsGateway < Gateway API_VERSION = '4.0' @@ -11,16 +11,16 @@ class QbmsGateway < Gateway self.homepage_url = 'http://payments.intuit.com/' self.display_name = 'QuickBooks Merchant Services' self.default_currency = 'USD' - self.supported_cardtypes = [ :visa, :master, :discover, :american_express, :diners_club, :jcb ] - self.supported_countries = [ 'US' ] + self.supported_cardtypes = %i[visa master discover american_express diners_club jcb] + self.supported_countries = ['US'] TYPES = { - :authorize => 'CustomerCreditCardAuth', - :capture => 'CustomerCreditCardCapture', - :purchase => 'CustomerCreditCardCharge', - :refund => 'CustomerCreditCardTxnVoidOrRefund', - :void => 'CustomerCreditCardTxnVoid', - :query => 'MerchantAccountQuery', + authorize: 'CustomerCreditCardAuth', + capture: 'CustomerCreditCardCapture', + purchase: 'CustomerCreditCardCharge', + refund: 'CustomerCreditCardTxnVoidOrRefund', + void: 'CustomerCreditCardTxnVoid', + query: 'MerchantAccountQuery' } # Creates a new QbmsGateway @@ -51,7 +51,7 @@ def initialize(options = {}) # * options -- A hash of optional parameters. # def authorize(money, creditcard, options = {}) - commit(:authorize, money, options.merge(:credit_card => creditcard)) + commit(:authorize, money, options.merge(credit_card: creditcard)) end # Perform a purchase, which is essentially an authorization and capture in a single operation. @@ -63,7 +63,7 @@ def authorize(money, creditcard, options = {}) # * options -- A hash of optional parameters. # def purchase(money, creditcard, options = {}) - commit(:purchase, money, options.merge(:credit_card => creditcard)) + commit(:purchase, money, options.merge(credit_card: creditcard)) end # Captures the funds from an authorized transaction. @@ -74,7 +74,7 @@ def purchase(money, creditcard, options = {}) # * authorization -- The authorization returned from the previous authorize request. # def capture(money, authorization, options = {}) - commit(:capture, money, options.merge(:transaction_id => authorization)) + commit(:capture, money, options.merge(transaction_id: authorization)) end # Void a previous transaction @@ -84,7 +84,7 @@ def capture(money, authorization, options = {}) # * authorization - The authorization returned from the previous authorize request. # def void(authorization, options = {}) - commit(:void, nil, options.merge(:transaction_id => authorization)) + commit(:void, nil, options.merge(transaction_id: authorization)) end # Credit an account. @@ -101,11 +101,11 @@ def void(authorization, options = {}) # def credit(money, identification, options = {}) ActiveMerchant.deprecated CREDIT_DEPRECATION_MESSAGE - refund(money, identification, options = {}) + refund(money, identification, {}) end def refund(money, identification, options = {}) - commit(:refund, money, options.merge(:transaction_id => identification)) + commit(:refund, money, options.merge(transaction_id: identification)) end # Query the merchant account status @@ -142,12 +142,15 @@ def commit(action, money, parameters) response = parse(type, data) message = (response[:status_message] || '').strip - 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) + 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) ) end @@ -167,16 +170,16 @@ def parse(type, body) if status_code != 0 return { - :status_code => status_code, - :status_message => signon.attributes['statusMessage'], + status_code:, + status_message: signon.attributes['statusMessage'] } end response = REXML::XPath.first(xml, "//QBMSXMLMsgsRs/#{type}Rs") results = { - :status_code => response.attributes['statusCode'].to_i, - :status_message => response.attributes['statusMessage'], + status_code: response.attributes['statusCode'].to_i, + status_message: response.attributes['statusMessage'] } response.elements.each do |e| @@ -195,10 +198,10 @@ def parse(type, body) end def build_request(type, money, parameters = {}) - xml = Builder::XmlMarkup.new(:indent => 0) + xml = Builder::XmlMarkup.new(indent: 0) - xml.instruct!(:xml, :version => '1.0', :encoding => 'utf-8') - xml.instruct!(:qbmsxml, :version => API_VERSION) + xml.instruct!(:xml, version: '1.0', encoding: 'utf-8') + xml.instruct!(:qbmsxml, version: API_VERSION) xml.tag!('QBMSXML') do xml.tag!('SignonMsgsRq') do diff --git a/lib/active_merchant/billing/gateways/quantum.rb b/lib/active_merchant/billing/gateways/quantum.rb index 65e7b9e6dcb..58a60bf4502 100644 --- a/lib/active_merchant/billing/gateways/quantum.rb +++ b/lib/active_merchant/billing/gateways/quantum.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # ActiveMerchant Implementation for Quantum Gateway XML Requester Service # Based on API Doc from 8/6/2009 # @@ -13,7 +13,7 @@ class QuantumGateway < Gateway self.live_url = self.test_url = 'https://secure.quantumgateway.com/cgi/xml_requester.php' # visa, master, american_express, discover - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.supported_countries = ['US'] self.default_currency = 'USD' self.money_format = :dollars @@ -44,7 +44,7 @@ def initialize(options = {}) # def authorize(money, creditcard, options = {}) setup_address_hash(options) - commit(build_auth_request(money, creditcard, options), options ) + commit(build_auth_request(money, creditcard, options), options) end # Capture an authorization that has previously been requested @@ -81,7 +81,7 @@ def setup_address_hash(options) def build_auth_request(money, creditcard, options) xml = Builder::XmlMarkup.new - add_common_credit_card_info(xml,'AUTH_ONLY') + add_common_credit_card_info(xml, 'AUTH_ONLY') add_purchase_data(xml, money) add_creditcard(xml, creditcard) add_address(xml, creditcard, options[:billing_address], options) @@ -94,15 +94,15 @@ def build_auth_request(money, creditcard, options) def build_capture_request(money, authorization, options) xml = Builder::XmlMarkup.new - add_common_credit_card_info(xml,'PREVIOUS_SALE') - transaction_id, _ = authorization_parts_from(authorization) + add_common_credit_card_info(xml, 'PREVIOUS_SALE') + transaction_id, = authorization_parts_from(authorization) add_transaction_id(xml, transaction_id) xml.target! end def build_purchase_request(money, creditcard, options) xml = Builder::XmlMarkup.new - add_common_credit_card_info(xml, @options[:ignore_avs] || @options[:ignore_cvv] ? 'SALES' : 'AUTH_CAPTURE') + add_common_credit_card_info(xml, @options[:ignore_avs] || @options[:ignore_cvv] ? 'SALES' : 'AUTH_CAPTURE') add_address(xml, creditcard, options[:billing_address], options) add_purchase_data(xml, money) add_creditcard(xml, creditcard) @@ -115,15 +115,15 @@ def build_purchase_request(money, creditcard, options) def build_void_request(authorization, options) xml = Builder::XmlMarkup.new - add_common_credit_card_info(xml,'VOID') - transaction_id, _ = authorization_parts_from(authorization) + add_common_credit_card_info(xml, 'VOID') + transaction_id, = authorization_parts_from(authorization) add_transaction_id(xml, transaction_id) xml.target! end def build_credit_request(money, authorization, options) xml = Builder::XmlMarkup.new - add_common_credit_card_info(xml,'RETURN') + add_common_credit_card_info(xml, 'RETURN') add_purchase_data(xml, money) transaction_id, cc = authorization_parts_from(authorization) add_transaction_id(xml, transaction_id) @@ -182,7 +182,7 @@ def add_creditcard(xml, creditcard) xml.tag! 'CreditCardNumber', creditcard.number xml.tag! 'ExpireMonth', format(creditcard.month, :two_digits) xml.tag! 'ExpireYear', format(creditcard.year, :four_digits) - xml.tag!('CVV2', creditcard.verification_value) unless (@options[:ignore_cvv] || creditcard.verification_value.blank? ) + xml.tag!('CVV2', creditcard.verification_value) unless @options[:ignore_cvv] || creditcard.verification_value.blank? end # Where we actually build the full SOAP request using builder @@ -215,11 +215,14 @@ def commit(request, options) authorization = success ? authorization_for(response) : nil end - Response.new(success, message, response, - :test => test?, - :authorization => authorization, - :avs_result => { :code => response[:AVSResponseCode] }, - :cvv_result => response[:CVV2ResponseCode] + Response.new( + success, + message, + response, + test: test?, + authorization:, + avs_result: { code: response[:AVSResponseCode] }, + cvv_result: response[:CVV2ResponseCode] ) end @@ -251,9 +254,9 @@ def parse(xml) def parse_element(reply, node) if node.has_elements? - node.elements.each{|e| parse_element(reply, e) } + node.elements.each { |e| parse_element(reply, e) } else - if node.parent.name =~ /item/ + if /item/.match?(node.parent.name) parent = node.parent.name + (node.parent.attributes['id'] ? '_' + node.parent.attributes['id'] : '') reply[(parent + '_' + node.name).to_sym] = node.text else @@ -270,7 +273,6 @@ def authorization_for(reply) def authorization_parts_from(authorization) authorization.split(/;/) end - end end end diff --git a/lib/active_merchant/billing/gateways/quickbooks.rb b/lib/active_merchant/billing/gateways/quickbooks.rb index ed51d7266dd..a5b5c809e5f 100644 --- a/lib/active_merchant/billing/gateways/quickbooks.rb +++ b/lib/active_merchant/billing/gateways/quickbooks.rb @@ -1,22 +1,19 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class QuickbooksGateway < Gateway self.test_url = 'https://sandbox.api.intuit.com' self.live_url = 'https://api.intuit.com' self.supported_countries = ['US'] self.default_currency = 'USD' - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners] + self.supported_cardtypes = %i[visa master american_express discover diners] self.homepage_url = 'http://payments.intuit.com' self.display_name = 'QuickBooks Payments' - ENDPOINT = '/quickbooks/v4/payments/charges' - OAUTH_ENDPOINTS = { - site: 'https://oauth.intuit.com', - request_token_path: '/oauth/v1/get_request_token', - authorize_url: 'https://appcenter.intuit.com/Connect/Begin', - access_token_path: '/oauth/v1/get_access_token' - } + BASE = '/quickbooks/v4/payments' + ENDPOINT = "#{BASE}/charges" + VOID_ENDPOINT = "#{BASE}/txn-requests" + REFRESH_URI = 'https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer' # https://developer.intuit.com/docs/0150_payments/0300_developer_guides/error_handling @@ -45,13 +42,21 @@ 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 = {}) - requires!(options, :consumer_key, :consumer_secret, :access_token, :token_secret, :realm) + # Quickbooks is deprecating OAuth 1.0 on December 17, 2019. + # OAuth 2.0 requires a client_id, client_secret, access_token, and refresh_token + # To maintain backwards compatibility, check for the presence of a refresh_token (only specified for OAuth 2.0) + # When present, validate that all OAuth 2.0 options are present + if options[:refresh_token] + requires!(options, :client_id, :client_secret, :access_token, :refresh_token) + else + requires!(options, :consumer_key, :consumer_secret, :access_token, :token_secret, :realm) + end @options = options super end @@ -62,7 +67,8 @@ def purchase(money, payment, options = {}) add_charge_data(post, payment, options) post[:capture] = 'true' - commit(ENDPOINT, post) + response = commit(ENDPOINT, post) + check_token_response(response, ENDPOINT, post, options) end def authorize(money, payment, options = {}) @@ -71,30 +77,46 @@ def authorize(money, payment, options = {}) add_charge_data(post, payment, options) post[:capture] = 'false' - commit(ENDPOINT, post) + response = commit(ENDPOINT, post) + check_token_response(response, ENDPOINT, post, options) end def capture(money, authorization, options = {}) post = {} - capture_uri = "#{ENDPOINT}/#{CGI.escape(authorization)}/capture" + authorization, = split_authorization(authorization) post[:amount] = localized_amount(money, currency(money)) add_context(post, options) - commit(capture_uri, post) + response = commit(capture_uri(authorization), post) + check_token_response(response, capture_uri(authorization), post, options) end def refund(money, authorization, options = {}) post = {} post[:amount] = localized_amount(money, currency(money)) add_context(post, options) + authorization, = split_authorization(authorization) + + response = commit(refund_uri(authorization), post) + check_token_response(response, refund_uri(authorization), post, options) + end - commit(refund_uri(authorization), post) + def void(authorization, options = {}) + _, request_id = split_authorization(authorization) + + response = commit(void_uri(request_id)) + check_token_response(response, void_uri(request_id), {}, options) end def verify(credit_card, options = {}) authorize(1.00, credit_card, options) end + def refresh + response = refresh_access_token + response_object(response) + end + def supports_scrubbing? true end @@ -106,8 +128,13 @@ 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]'). + gsub(%r((refresh_token=)\w+), '\1[FILTERED]'). + gsub(%r((Authorization: Bearer )[\w\-\.]+)i, '\1[FILTERED]\2') end private @@ -118,14 +145,15 @@ def add_charge_data(post, payment, options = {}) end def add_address(post, options) - return unless post[:card] && post[:card].kind_of?(Hash) + return unless post[:card]&.kind_of?(Hash) card_address = {} if address = options[:billing_address] || options[:address] card_address[:streetAddress] = address[:address1] card_address[:city] = address[:city] - card_address[:region] = address[:state] || address[:region] - card_address[:country] = address[:country] + region = address[:state] || address[:region] + card_address[:region] = region if region.present? + card_address[:country] = address[:country] if address[:country].present? card_address[:postalCode] = address[:zip] if address[:zip] end post[:card][:address] = card_address @@ -170,30 +198,30 @@ def commit(uri, body = {}, method = :post) # The QuickBooks API returns HTTP 4xx on failed transactions, which causes a # ResponseError raise, so we have to inspect the response and discern between # a legitimate HTTP error and an actual gateway transactional error. - response = begin - case method - when :post - ssl_post(endpoint, post_data(body), headers(:post, endpoint)) - when :get - ssl_request(:get, endpoint, nil, headers(:get, endpoint)) - else - raise ArgumentError, "Invalid HTTP method: #{method}. Valid methods are :post and :get" + headers = {} + response = + begin + headers = headers(method, endpoint) + method == :post ? ssl_post(endpoint, post_data(body), headers) : ssl_request(:get, endpoint, nil, headers) + rescue ResponseError => e + extract_response_body_or_raise(e) end - rescue ResponseError => e - extract_response_body_or_raise(e) - end - response_object(response) + response_object(response, headers) end - def response_object(raw_response) + def response_object(raw_response, headers = {}) parsed_response = parse(raw_response) + # Include access_token and refresh_token in params for OAuth 2.0 + parsed_response['access_token'] = @options[:access_token] if @options[:refresh_token] + parsed_response['refresh_token'] = @options[:refresh_token] if @options[:refresh_token] + Response.new( success?(parsed_response), message_from(parsed_response), parsed_response, - authorization: authorization_from(parsed_response), + authorization: authorization_from(parsed_response, headers), test: test?, cvv_result: cvv_code_from(parsed_response), error_code: errors_from(parsed_response), @@ -210,7 +238,10 @@ def post_data(data = {}) end def headers(method, uri) - raise ArgumentError, "Invalid HTTP method: #{method}. Valid methods are :post and :get" unless [:post, :get].include?(method) + return oauth_v2_headers if @options[:refresh_token] + + raise ArgumentError, "Invalid HTTP method: #{method}. Valid methods are :post and :get" unless %i[post get].include?(method) + request_uri = URI.parse(uri) # Following the guidelines from http://nouncer.com/oauth/authentication.html @@ -224,17 +255,17 @@ def headers(method, uri) } # prepare components for signature - oauth_signature_base_string = [method.to_s.upcase, request_uri.to_s, oauth_parameters.to_param].map{|v| CGI.escape(v) }.join('&') - oauth_signing_key = [@options[:consumer_secret], @options[:token_secret]].map{|v| CGI.escape(v)}.join('&') + oauth_signature_base_string = [method.to_s.upcase, request_uri.to_s, oauth_parameters.to_param].map { |v| CGI.escape(v) }.join('&') + oauth_signing_key = [@options[:consumer_secret], @options[:token_secret]].map { |v| CGI.escape(v) }.join('&') hmac_signature = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha1'), oauth_signing_key, oauth_signature_base_string) # append signature to required OAuth parameters - oauth_parameters[:oauth_signature] = CGI.escape(Base64.encode64(hmac_signature).chomp.gsub(/\n/, '')) + 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}\""} + oauth_headers += oauth_parameters.map { |k, v| "#{k}=\"#{v}\"" } { 'Content-type' => 'application/json', @@ -243,6 +274,45 @@ def headers(method, uri) } end + def oauth_v2_headers + { + 'Content-Type' => 'application/json', + 'Request-Id' => generate_unique_id, + 'Accept' => 'application/json', + 'Authorization' => "Bearer #{@options[:access_token]}" + } + end + + def check_token_response(response, endpoint, body = {}, options = {}) + return response unless @options[:refresh_token] + return response unless options[:allow_refresh] + return response unless response.params['code'] == 'AuthenticationFailed' + + refresh_access_token + commit(endpoint, body) + end + + def refresh_access_token + post = {} + post[:grant_type] = 'refresh_token' + post[:refresh_token] = @options[:refresh_token] + data = post.collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&') + + basic_auth = Base64.strict_encode64("#{@options[:client_id]}:#{@options[:client_secret]}") + headers = { + 'Content-Type' => 'application/x-www-form-urlencoded', + 'Accept' => 'application/json', + 'Authorization' => "Basic #{basic_auth}" + } + + response = ssl_post(REFRESH_URI, data, headers) + json_response = JSON.parse(response) + + @options[:access_token] = json_response['access_token'] if json_response['access_token'] + @options[:refresh_token] = json_response['refresh_token'] if json_response['refresh_token'] + response + end + def cvv_code_from(response) if response['errors'].present? FRAUD_WARNING_CODES.include?(response['errors'].first['code']) ? 'I' : '' @@ -252,21 +322,30 @@ def cvv_code_from(response) end def success?(response) - return FRAUD_WARNING_CODES.concat(['0']).include?(response['errors'].first['code']) if response['errors'] - - !['DECLINED', 'CANCELLED'].include?(response['status']) + return FRAUD_WARNING_CODES.concat(['0']).include?(response['errors'].first['code']) if response['errors'] + + !%w[DECLINED CANCELLED].include?(response['status']) && !%w[AuthenticationFailed AuthorizationFailed].include?(response['code']) end def message_from(response) - response['errors'].present? ? response['errors'].map {|error_hash| error_hash['message'] }.join(' ') : response['status'] + response['errors'].present? ? response['errors'].map { |error_hash| error_hash['message'] }.join(' ') : response['status'] end def errors_from(response) - response['errors'].present? ? STANDARD_ERROR_CODE_MAPPING[response['errors'].first['code']] : '' + if %w[AuthenticationFailed AuthorizationFailed].include?(response['code']) + response['code'] + else + response['errors'].present? ? STANDARD_ERROR_CODE_MAPPING[response['errors'].first['code']] : '' + end end - def authorization_from(response) - response['id'] + def authorization_from(response, headers = {}) + [response['id'], headers['Request-Id']].join('|') + end + + def split_authorization(authorization) + authorization, request_id = authorization.split('|') + [authorization, request_id] end def fraud_review_status_from(response) @@ -279,11 +358,20 @@ def extract_response_body_or_raise(response_error) rescue JSON::ParserError raise response_error end + response_error.response.body end def refund_uri(authorization) - "#{ENDPOINT}/#{CGI.escape(authorization)}/refunds" + "#{ENDPOINT}/#{CGI.escape(authorization.to_s)}/refunds" + end + + def capture_uri(authorization) + "#{ENDPOINT}/#{CGI.escape(authorization.to_s)}/capture" + end + + def void_uri(request_id) + "#{VOID_ENDPOINT}/#{CGI.escape(request_id.to_s)}/void" end end end diff --git a/lib/active_merchant/billing/gateways/quickpay.rb b/lib/active_merchant/billing/gateways/quickpay.rb index 34d36f93196..614c9ea0adc 100644 --- a/lib/active_merchant/billing/gateways/quickpay.rb +++ b/lib/active_merchant/billing/gateways/quickpay.rb @@ -4,8 +4,8 @@ require 'active_merchant/billing/gateways/quickpay/quickpay_v10' require 'active_merchant/billing/gateways/quickpay/quickpay_v4to7' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class QuickpayGateway < Gateway self.abstract_class = true @@ -19,8 +19,6 @@ def self.new(options = {}) QuickpayV10Gateway.new(options) end end - end end end - diff --git a/lib/active_merchant/billing/gateways/quickpay/quickpay_common.rb b/lib/active_merchant/billing/gateways/quickpay/quickpay_common.rb index b1b2c5e0099..25078bf9b5f 100644 --- a/lib/active_merchant/billing/gateways/quickpay/quickpay_common.rb +++ b/lib/active_merchant/billing/gateways/quickpay/quickpay_common.rb @@ -1,156 +1,154 @@ - module QuickpayCommon - MD5_CHECK_FIELDS = { 3 => { - :authorize => %w(protocol msgtype merchant ordernumber amount - currency autocapture cardnumber expirationdate - cvd cardtypelock testmode), + authorize: %w(protocol msgtype merchant ordernumber amount + currency autocapture cardnumber expirationdate + cvd cardtypelock testmode), - :capture => %w(protocol msgtype merchant amount finalize transaction), + capture: %w(protocol msgtype merchant amount finalize transaction), - :cancel => %w(protocol msgtype merchant transaction), + cancel: %w(protocol msgtype merchant transaction), - :refund => %w(protocol msgtype merchant amount transaction), + refund: %w(protocol msgtype merchant amount transaction), - :subscribe => %w(protocol msgtype merchant ordernumber cardnumber - expirationdate cvd cardtypelock description testmode), + subscribe: %w(protocol msgtype merchant ordernumber cardnumber + expirationdate cvd cardtypelock description testmode), - :recurring => %w(protocol msgtype merchant ordernumber amount - currency autocapture transaction), + recurring: %w(protocol msgtype merchant ordernumber amount + currency autocapture transaction), - :status => %w(protocol msgtype merchant transaction), + status: %w(protocol msgtype merchant transaction), - :chstatus => %w(protocol msgtype merchant) + chstatus: %w(protocol msgtype merchant) }, 4 => { - :authorize => %w(protocol msgtype merchant ordernumber amount - currency autocapture cardnumber expirationdate cvd - cardtypelock testmode fraud_remote_addr - fraud_http_accept fraud_http_accept_language - fraud_http_accept_encoding fraud_http_accept_charset - fraud_http_referer fraud_http_user_agent apikey), + authorize: %w(protocol msgtype merchant ordernumber amount + currency autocapture cardnumber expirationdate cvd + cardtypelock testmode fraud_remote_addr + fraud_http_accept fraud_http_accept_language + fraud_http_accept_encoding fraud_http_accept_charset + fraud_http_referer fraud_http_user_agent apikey), - :capture => %w(protocol msgtype merchant amount finalize transaction apikey), + capture: %w(protocol msgtype merchant amount finalize transaction apikey), - :cancel => %w(protocol msgtype merchant transaction apikey), + cancel: %w(protocol msgtype merchant transaction apikey), - :refund => %w(protocol msgtype merchant amount transaction apikey), + refund: %w(protocol msgtype merchant amount transaction apikey), - :subscribe => %w(protocol msgtype merchant ordernumber cardnumber - expirationdate cvd cardtypelock description testmode - fraud_remote_addr fraud_http_accept fraud_http_accept_language - fraud_http_accept_encoding fraud_http_accept_charset - fraud_http_referer fraud_http_user_agent apikey), + subscribe: %w(protocol msgtype merchant ordernumber cardnumber + expirationdate cvd cardtypelock description testmode + fraud_remote_addr fraud_http_accept fraud_http_accept_language + fraud_http_accept_encoding fraud_http_accept_charset + fraud_http_referer fraud_http_user_agent apikey), - :recurring => %w(protocol msgtype merchant ordernumber amount currency - autocapture transaction apikey), + recurring: %w(protocol msgtype merchant ordernumber amount currency + autocapture transaction apikey), - :status => %w(protocol msgtype merchant transaction apikey), + status: %w(protocol msgtype merchant transaction apikey), - :chstatus => %w(protocol msgtype merchant apikey) + chstatus: %w(protocol msgtype merchant apikey) }, 5 => { - :authorize => %w(protocol msgtype merchant ordernumber amount - currency autocapture cardnumber expirationdate cvd - cardtypelock testmode fraud_remote_addr - fraud_http_accept fraud_http_accept_language - fraud_http_accept_encoding fraud_http_accept_charset - fraud_http_referer fraud_http_user_agent apikey), + authorize: %w(protocol msgtype merchant ordernumber amount + currency autocapture cardnumber expirationdate cvd + cardtypelock testmode fraud_remote_addr + fraud_http_accept fraud_http_accept_language + fraud_http_accept_encoding fraud_http_accept_charset + fraud_http_referer fraud_http_user_agent apikey), - :capture => %w(protocol msgtype merchant amount finalize transaction apikey), + capture: %w(protocol msgtype merchant amount finalize transaction apikey), - :cancel => %w(protocol msgtype merchant transaction apikey), + cancel: %w(protocol msgtype merchant transaction apikey), - :refund => %w(protocol msgtype merchant amount transaction apikey), + refund: %w(protocol msgtype merchant amount transaction apikey), - :subscribe => %w(protocol msgtype merchant ordernumber cardnumber - expirationdate cvd cardtypelock description testmode - fraud_remote_addr fraud_http_accept fraud_http_accept_language - fraud_http_accept_encoding fraud_http_accept_charset - fraud_http_referer fraud_http_user_agent apikey), + subscribe: %w(protocol msgtype merchant ordernumber cardnumber + expirationdate cvd cardtypelock description testmode + fraud_remote_addr fraud_http_accept fraud_http_accept_language + fraud_http_accept_encoding fraud_http_accept_charset + fraud_http_referer fraud_http_user_agent apikey), - :recurring => %w(protocol msgtype merchant ordernumber amount currency - autocapture transaction apikey), + recurring: %w(protocol msgtype merchant ordernumber amount currency + autocapture transaction apikey), - :status => %w(protocol msgtype merchant transaction apikey), + status: %w(protocol msgtype merchant transaction apikey), - :chstatus => %w(protocol msgtype merchant apikey) + chstatus: %w(protocol msgtype merchant apikey) }, 6 => { - :authorize => %w(protocol msgtype merchant ordernumber amount - currency autocapture cardnumber expirationdate cvd - cardtypelock testmode fraud_remote_addr - fraud_http_accept fraud_http_accept_language - fraud_http_accept_encoding fraud_http_accept_charset - fraud_http_referer fraud_http_user_agent apikey), + authorize: %w(protocol msgtype merchant ordernumber amount + currency autocapture cardnumber expirationdate cvd + cardtypelock testmode fraud_remote_addr + fraud_http_accept fraud_http_accept_language + fraud_http_accept_encoding fraud_http_accept_charset + fraud_http_referer fraud_http_user_agent apikey), - :capture => %w(protocol msgtype merchant amount finalize transaction - apikey), + capture: %w(protocol msgtype merchant amount finalize transaction + apikey), - :cancel => %w(protocol msgtype merchant transaction apikey), + cancel: %w(protocol msgtype merchant transaction apikey), - :refund => %w(protocol msgtype merchant amount transaction apikey), + refund: %w(protocol msgtype merchant amount transaction apikey), - :subscribe => %w(protocol msgtype merchant ordernumber cardnumber - expirationdate cvd cardtypelock description testmode - fraud_remote_addr fraud_http_accept fraud_http_accept_language - fraud_http_accept_encoding fraud_http_accept_charset - fraud_http_referer fraud_http_user_agent apikey), + subscribe: %w(protocol msgtype merchant ordernumber cardnumber + expirationdate cvd cardtypelock description testmode + fraud_remote_addr fraud_http_accept fraud_http_accept_language + fraud_http_accept_encoding fraud_http_accept_charset + fraud_http_referer fraud_http_user_agent apikey), - :recurring => %w(protocol msgtype merchant ordernumber amount currency - autocapture transaction apikey), + recurring: %w(protocol msgtype merchant ordernumber amount currency + autocapture transaction apikey), - :status => %w(protocol msgtype merchant transaction apikey), + status: %w(protocol msgtype merchant transaction apikey), - :chstatus => %w(protocol msgtype merchant apikey) + chstatus: %w(protocol msgtype merchant apikey) }, 7 => { - :authorize => %w(protocol msgtype merchant ordernumber amount - currency autocapture cardnumber expirationdate cvd - acquirers cardtypelock testmode fraud_remote_addr - fraud_http_accept fraud_http_accept_language - fraud_http_accept_encoding fraud_http_accept_charset - fraud_http_referer fraud_http_user_agent apikey), + authorize: %w(protocol msgtype merchant ordernumber amount + currency autocapture cardnumber expirationdate cvd + acquirers cardtypelock testmode fraud_remote_addr + fraud_http_accept fraud_http_accept_language + fraud_http_accept_encoding fraud_http_accept_charset + fraud_http_referer fraud_http_user_agent apikey), - :capture => %w(protocol msgtype merchant amount finalize transaction - apikey), + capture: %w(protocol msgtype merchant amount finalize transaction + apikey), - :cancel => %w(protocol msgtype merchant transaction apikey), + cancel: %w(protocol msgtype merchant transaction apikey), - :refund => %w(protocol msgtype merchant amount transaction apikey), + refund: %w(protocol msgtype merchant amount transaction apikey), - :subscribe => %w(protocol msgtype merchant ordernumber amount currency - cardnumber expirationdate cvd acquirers cardtypelock - description testmode fraud_remote_addr fraud_http_accept - fraud_http_accept_language fraud_http_accept_encoding - fraud_http_accept_charset fraud_http_referer - fraud_http_user_agent apikey), + subscribe: %w(protocol msgtype merchant ordernumber amount currency + cardnumber expirationdate cvd acquirers cardtypelock + description testmode fraud_remote_addr fraud_http_accept + fraud_http_accept_language fraud_http_accept_encoding + fraud_http_accept_charset fraud_http_referer + fraud_http_user_agent apikey), - :recurring => %w(protocol msgtype merchant ordernumber amount currency - autocapture transaction apikey), + recurring: %w(protocol msgtype merchant ordernumber amount currency + autocapture transaction apikey), - :status => %w(protocol msgtype merchant transaction apikey), + status: %w(protocol msgtype merchant transaction apikey), - :chstatus => %w(protocol msgtype merchant apikey) + chstatus: %w(protocol msgtype merchant apikey) }, - + 10 => { - :authorize => %w(mobile_number acquirer autofee customer_id extras - zero_auth customer_ip), - :capture => %w( extras ), - :cancel => %w( extras ), - :refund => %w( extras ), - :subscribe => %w( variables branding_id), - :authorize_subscription => %w( mobile_number acquirer customer_ip), - :recurring => %w(auto_capture autofee zero_auth) + authorize: %w(mobile_number acquirer autofee customer_id extras + zero_auth customer_ip), + capture: %w(extras), + cancel: %w(extras), + refund: %w(extras), + subscribe: %w(variables branding_id), + authorize_subscription: %w(mobile_number acquirer customer_ip), + recurring: %w(auto_capture autofee zero_auth) } } - + RESPONSE_CODES = { 200 => 'OK', 201 => 'Created', @@ -165,24 +163,22 @@ module QuickpayCommon 409 => 'Conflict', 500 => 'Internal Server Error' } - + def self.included(base) base.default_currency = 'DKK' base.money_format = :cents - - base.supported_cardtypes = [:dankort, :forbrugsforeningen, :visa, :master, - :american_express, :diners_club, :jcb, :maestro] - base.supported_countries = ['DE', 'DK', 'ES', 'FI', 'FR', 'FO', 'GB', 'IS', 'NO', 'SE'] + + base.supported_countries = %w[DE DK ES FI FR FO GB IS NO SE] + base.supported_cardtypes = %i[dankort forbrugsforeningen visa master + american_express diners_club jcb maestro] base.homepage_url = 'http://quickpay.net/' base.display_name = 'QuickPay' - end - + def expdate(credit_card) year = format(credit_card.year, :two_digits) month = format(credit_card.month, :two_digits) "#{year}#{month}" end - end diff --git a/lib/active_merchant/billing/gateways/quickpay/quickpay_v10.rb b/lib/active_merchant/billing/gateways/quickpay/quickpay_v10.rb index f321a7c33b8..3061ba3305c 100644 --- a/lib/active_merchant/billing/gateways/quickpay/quickpay_v10.rb +++ b/lib/active_merchant/billing/gateways/quickpay/quickpay_v10.rb @@ -24,11 +24,11 @@ def purchase(money, credit_card_or_reference, options = {}) r.process { post = authorization_params(money, credit_card_or_reference, options) add_autocapture(post, false) - commit(synchronized_path("/payments/#{r.responses.last.params["id"]}/authorize"), post) + commit(synchronized_path("/payments/#{r.responses.last.params['id']}/authorize"), post) } r.process { post = capture_params(money, credit_card_or_reference, options) - commit(synchronized_path("/payments/#{r.responses.last.params["id"]}/capture"), post) + commit(synchronized_path("/payments/#{r.responses.last.params['id']}/capture"), post) } end end @@ -42,7 +42,7 @@ def authorize(money, credit_card_or_reference, options = {}) r.process { create_payment(money, options) } r.process { post = authorization_params(money, credit_card_or_reference, options) - commit(synchronized_path("/payments/#{r.responses.last.params["id"]}/authorize"), post) + commit(synchronized_path("/payments/#{r.responses.last.params['id']}/authorize"), post) } end end @@ -68,7 +68,7 @@ def refund(money, identification, options = {}) commit(synchronized_path("/payments/#{identification}/refund"), post) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -78,7 +78,7 @@ def verify(credit_card, options={}) def store(credit_card, options = {}) MultiResponse.run do |r| r.process { create_store(options) } - r.process { authorize_store(r.authorization, credit_card, options)} + r.process { authorize_store(r.authorization, credit_card, options) } end end @@ -99,201 +99,202 @@ def scrub(transcript) private - def authorization_params(money, credit_card_or_reference, options = {}) - post = {} - - add_amount(post, money, options) - add_credit_card_or_reference(post, credit_card_or_reference) - add_additional_params(:authorize, post, options) + def authorization_params(money, credit_card_or_reference, options = {}) + post = {} - post - end + add_amount(post, money, options) + add_credit_card_or_reference(post, credit_card_or_reference, options) + add_additional_params(:authorize, post, options) - def capture_params(money, credit_card, options = {}) - post = {} + post + end - add_amount(post, money, options) - add_additional_params(:capture, post, options) + def capture_params(money, credit_card, options = {}) + post = {} - post - end + add_amount(post, money, options) + add_additional_params(:capture, post, options) - def create_store(options = {}) - post = {} - commit('/cards', post) - end + post + end - def authorize_store(identification, credit_card, options = {}) - post = {} + def create_store(options = {}) + post = {} + commit('/cards', post) + end - add_credit_card_or_reference(post, credit_card, options) - commit(synchronized_path("/cards/#{identification}/authorize"), post) - end + def authorize_store(identification, credit_card, options = {}) + post = {} - def create_token(identification, options) - post = {} - commit(synchronized_path("/cards/#{identification}/tokens"), post) - end + add_credit_card_or_reference(post, credit_card, options) + commit(synchronized_path("/cards/#{identification}/authorize"), post) + end - def create_payment(money, options = {}) - post = {} - add_currency(post, money, options) - add_invoice(post, options) - commit('/payments', post) - end + def create_token(identification, options) + post = {} + commit(synchronized_path("/cards/#{identification}/tokens"), post) + end - def commit(action, params = {}) - success = false - begin - response = parse(ssl_post(self.live_url + action, params.to_json, headers)) - success = successful?(response) - rescue ResponseError => e - response = response_error(e.response.body) - rescue JSON::ParserError - response = json_error(response) - end + def create_payment(money, options = {}) + post = {} + add_currency(post, money, options) + add_invoice(post, options) + commit('/payments', post) + end - Response.new(success, message_from(success, response), response, - :test => test?, - :authorization => authorization_from(response) - ) + def commit(action, params = {}) + success = false + begin + response = parse(ssl_post(self.live_url + action, params.to_json, headers)) + success = successful?(response) + rescue ResponseError => e + response = response_error(e.response.body) + rescue JSON::ParserError + response = json_error(response) end - def authorization_from(response) - if response['token'] - response['token'].to_s - else - response['id'].to_s - end - end + Response.new( + success, + message_from(success, response), + response, + test: test?, + authorization: authorization_from(response) + ) + end - def add_currency(post, money, options) - post[:currency] = options[:currency] || currency(money) + def authorization_from(response) + if response['token'] + response['token'].to_s + else + response['id'].to_s end + end - def add_amount(post, money, options) - post[:amount] = options[:amount] || amount(money) - end + def add_currency(post, money, options) + post[:currency] = options[:currency] || currency(money) + end - def add_autocapture(post, value) - post[:auto_capture] = value - end + def add_amount(post, money, options) + post[:amount] = options[:amount] || amount(money) + end - def add_order_id(post, options) - requires!(options, :order_id) - post[:order_id] = format_order_id(options[:order_id]) - end + def add_autocapture(post, value) + post[:auto_capture] = value + end - def add_invoice(post, options) - add_order_id(post, options) + def add_order_id(post, options) + requires!(options, :order_id) + post[:order_id] = format_order_id(options[:order_id]) + end - if options[:billing_address] - post[:invoice_address] = map_address(options[:billing_address]) - end + def add_invoice(post, options) + add_order_id(post, options) - if options[:shipping_address] - post[:shipping_address] = map_address(options[:shipping_address]) - end + post[:invoice_address] = map_address(options[:billing_address]) if options[:billing_address] - [:metadata, :branding_id, :variables].each do |field| - post[field] = options[field] if options[field] - end + post[:shipping_address] = map_address(options[:shipping_address]) if options[:shipping_address] + + %i[metadata branding_id variables].each do |field| + post[field] = options[field] if options[field] end + end - def add_additional_params(action, post, options = {}) - MD5_CHECK_FIELDS[API_VERSION][action].each do |key| - key = key.to_sym - post[key] = options[key] if options[key] - end + def add_additional_params(action, post, options = {}) + MD5_CHECK_FIELDS[API_VERSION][action].each do |key| + key = key.to_sym + post[key] = options[key] if options[key] end + end - def add_credit_card_or_reference(post, credit_card_or_reference, options = {}) - post[:card] ||= {} - if credit_card_or_reference.is_a?(String) - post[:card][:token] = credit_card_or_reference - else - post[:card][:number] = credit_card_or_reference.number - post[:card][:cvd] = credit_card_or_reference.verification_value - post[:card][:expiration] = expdate(credit_card_or_reference) - post[:card][:issued_to] = credit_card_or_reference.name - end + def add_credit_card_or_reference(post, credit_card_or_reference, options = {}) + post[:card] ||= {} + if credit_card_or_reference.is_a?(String) + post[:card][:token] = credit_card_or_reference + else + post[:card][:number] = credit_card_or_reference.number + post[:card][:cvd] = credit_card_or_reference.verification_value + post[:card][:expiration] = expdate(credit_card_or_reference) + post[:card][:issued_to] = credit_card_or_reference.name end - def parse(body) - JSON.parse(body) + if options[:three_d_secure] + post[:card][:cavv] = options.dig(:three_d_secure, :cavv) + post[:card][:eci] = options.dig(:three_d_secure, :eci) + post[:card][:xav] = options.dig(:three_d_secure, :xid) end + end - def successful?(response) - has_error = response['errors'] - invalid_code = invalid_operation_code?(response) + def parse(body) + JSON.parse(body) + end - !(has_error || invalid_code) - end + def successful?(response) + has_error = response['errors'] + invalid_code = invalid_operation_code?(response) - def message_from(success, response) - success ? 'OK' : (response['message'] || invalid_operation_message(response) || 'Unknown error - please contact QuickPay') - end + !(has_error || invalid_code) + end - def invalid_operation_code?(response) - if response['operations'] - operation = response['operations'].last - operation && operation['qp_status_code'] != '20000' - end - end + def message_from(success, response) + success ? 'OK' : (response['message'] || invalid_operation_message(response) || 'Unknown error - please contact QuickPay') + end - def invalid_operation_message(response) - response['operations'] && response['operations'].last['qp_status_msg'] + def invalid_operation_code?(response) + if response['operations'] + operation = response['operations'].last + operation && operation['qp_status_code'] != '20000' end + end - def map_address(address) - return {} if address.nil? - requires!(address, :name, :address1, :city, :zip, :country) - country = Country.find(address[:country]) - mapped = { - :name => address[:name], - :street => address[:address1], - :city => address[:city], - :region => address[:address2], - :zip_code => address[:zip], - :country_code => country.code(:alpha3).value - } - mapped - end + def invalid_operation_message(response) + response['operations'] && response['operations'].last['qp_status_msg'] + end - def format_order_id(order_id) - truncate(order_id.to_s.gsub(/#/, ''), 20) - end + def map_address(address) + return {} if address.nil? + + requires!(address, :name, :address1, :city, :zip, :country) + country = Country.find(address[:country]) + { + name: address[:name], + street: address[:address1], + city: address[:city], + region: address[:address2], + zip_code: address[:zip], + country_code: country.code(:alpha3).value + } + end - def headers - auth = Base64.strict_encode64(":#{@options[:api_key]}") - { - 'Authorization' => 'Basic ' + auth, - 'User-Agent' => "Quickpay-v#{API_VERSION} ActiveMerchantBindings/#{ActiveMerchant::VERSION}", - 'Accept' => 'application/json', - 'Accept-Version' => "v#{API_VERSION}", - 'Content-Type' => 'application/json' - } - end + def format_order_id(order_id) + truncate(order_id.to_s.delete('#'), 20) + end - def response_error(raw_response) - begin - parse(raw_response) - rescue JSON::ParserError - json_error(raw_response) - end - end + def headers + auth = Base64.strict_encode64(":#{@options[:api_key]}") + { + 'Authorization' => 'Basic ' + auth, + 'User-Agent' => "Quickpay-v#{API_VERSION} ActiveMerchantBindings/#{ActiveMerchant::VERSION}", + 'Accept' => 'application/json', + 'Accept-Version' => "v#{API_VERSION}", + 'Content-Type' => 'application/json' + } + end - def json_error(raw_response) - msg = 'Invalid response received from the Quickpay API.' - msg += " (The raw response returned by the API was #{raw_response.inspect})" - { 'message' => msg } - end + def response_error(raw_response) + parse(raw_response) + rescue JSON::ParserError + json_error(raw_response) + end - def synchronized_path(path) - "#{path}?synchronized" - end + def json_error(raw_response) + msg = 'Invalid response received from the Quickpay API.' + msg += " (The raw response returned by the API was #{raw_response.inspect})" + { 'message' => msg } + end + def synchronized_path(path) + "#{path}?synchronized" + end end - end end diff --git a/lib/active_merchant/billing/gateways/quickpay/quickpay_v4to7.rb b/lib/active_merchant/billing/gateways/quickpay/quickpay_v4to7.rb index 95fae367036..efe62e4e3c5 100644 --- a/lib/active_merchant/billing/gateways/quickpay/quickpay_v4to7.rb +++ b/lib/active_merchant/billing/gateways/quickpay/quickpay_v4to7.rb @@ -2,11 +2,11 @@ require 'digest/md5' require 'active_merchant/billing/gateways/quickpay/quickpay_common' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class QuickpayV4to7Gateway < Gateway include QuickpayCommon - self.live_url = self.test_url = 'https://secure.quickpay.dk/api' + self.live_url = self.test_url = 'https://secure.quickpay.dk/api' APPROVED = '000' # The login is the QuickpayId @@ -141,6 +141,7 @@ def add_description(post, options) def add_testmode(post) return if post[:transaction].present? + post[:testmode] = test? ? '1' : '0' end @@ -163,9 +164,12 @@ 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, - :test => test?, - :authorization => response[:transaction] + Response.new( + successful?(response), + message_from(response), + response, + test: test?, + authorization: response[:transaction] ) end @@ -224,4 +228,3 @@ def format_order_number(number) end end end - diff --git a/lib/active_merchant/billing/gateways/qvalent.rb b/lib/active_merchant/billing/gateways/qvalent.rb index 6d59464a09c..5029bed6627 100644 --- a/lib/active_merchant/billing/gateways/qvalent.rb +++ b/lib/active_merchant/billing/gateways/qvalent.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class QvalentGateway < Gateway self.display_name = 'Qvalent' self.homepage_url = 'https://www.qvalent.com/' @@ -10,74 +10,87 @@ class QvalentGateway < Gateway self.supported_countries = ['AU'] self.default_currency = 'AUD' self.money_format = :cents - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :jcb, :diners] + self.supported_cardtypes = %i[visa master american_express discover jcb diners] - def initialize(options={}) - requires!(options, :username, :password, :merchant, :pem, :pem_password) + CVV_CODE_MAPPING = { + 'S' => 'D' + } + + def initialize(options = {}) + requires!(options, :username, :password, :merchant, :pem) super end - def purchase(amount, payment_method, options={}) + def purchase(amount, payment_method, options = {}) post = {} add_invoice(post, amount, options) add_order_number(post, options) add_payment_method(post, payment_method) add_verification_value(post, payment_method) + add_stored_credential_data(post, payment_method, options) add_customer_data(post, options) add_soft_descriptors(post, options) + add_customer_reference(post, options) commit('capture', post) end - def authorize(amount, payment_method, options={}) + def authorize(amount, payment_method, options = {}) post = {} add_invoice(post, amount, options) add_order_number(post, options) add_payment_method(post, payment_method) add_verification_value(post, payment_method) + add_stored_credential_data(post, payment_method, options) add_customer_data(post, options) add_soft_descriptors(post, options) + add_customer_reference(post, options) commit('preauth', post) end - def capture(amount, authorization, options={}) + def capture(amount, authorization, options = {}) post = {} add_invoice(post, amount, options) add_reference(post, authorization, options) add_customer_data(post, options) add_soft_descriptors(post, options) + add_customer_reference(post, options) commit('captureWithoutAuth', post) end - def refund(amount, authorization, options={}) + def refund(amount, authorization, options = {}) post = {} add_invoice(post, amount, options) add_reference(post, authorization, options) add_customer_data(post, options) add_soft_descriptors(post, options) + post['order.ECI'] = options[:eci] || 'SSL' + add_customer_reference(post, options) commit('refund', post) end # Credit requires the merchant account to be enabled for "Adhoc Refunds" - def credit(amount, payment_method, options={}) + def credit(amount, payment_method, options = {}) post = {} add_invoice(post, amount, options) add_order_number(post, options) add_payment_method(post, payment_method) add_customer_data(post, options) add_soft_descriptors(post, options) + add_customer_reference(post, options) commit('refund', post) end - def void(authorization, options={}) + def void(authorization, options = {}) post = {} add_reference(post, authorization, options) add_customer_data(post, options) add_soft_descriptors(post, options) + add_customer_reference(post, options) commit('reversal', post) end @@ -85,7 +98,7 @@ def void(authorization, options={}) def store(payment_method, options = {}) post = {} add_payment_method(post, payment_method) - add_card_reference(post) + add_card_reference(post, options) commit('registerAccount', post) end @@ -103,7 +116,7 @@ def scrub(transcript) private - CURRENCY_CODES = Hash.new{|h,k| raise ArgumentError.new("Unsupported currency: #{k}")} + CURRENCY_CODES = Hash.new { |_h, k| raise ArgumentError.new("Unsupported currency: #{k}") } CURRENCY_CODES['AUD'] = 'AUD' CURRENCY_CODES['INR'] = 'INR' @@ -120,7 +133,6 @@ def add_soft_descriptors(post, options) def add_invoice(post, money, options) post['order.amount'] = amount(money) post['card.currency'] = CURRENCY_CODES[options[:currency] || currency(money)] - post['order.ECI'] = options[:eci] || 'SSL' end def add_payment_method(post, payment_method) @@ -130,12 +142,61 @@ def add_payment_method(post, payment_method) post['card.expiryMonth'] = format(payment_method.month, :two_digits) end + def add_stored_credential_data(post, payment_method, options) + post['order.ECI'] = options[:eci] || eci(options) + if (stored_credential = options[:stored_credential]) && %w(visa master).include?(payment_method.brand) + post['card.posEntryMode'] = stored_credential[:initial_transaction] ? 'MANUAL' : 'STORED_CREDENTIAL' + stored_credential_usage(post, payment_method, options) unless stored_credential[:initiator] && stored_credential[:initiator] == 'cardholder' + post['order.authTraceId'] = stored_credential[:network_transaction_id] if stored_credential[:network_transaction_id] + end + end + + def stored_credential_usage(post, payment_method, options) + return unless payment_method.brand == 'visa' + + stored_credential = options[:stored_credential] + if stored_credential[:reason_type] == 'unscheduled' + if stored_credential[:initiator] == 'merchant' + post['card.storedCredentialUsage'] = 'UNSCHEDULED_MIT' + elsif stored_credential[:initiator] == 'customer' + post['card.storedCredentialUsage'] = 'UNSCHEDULED_CIT' + end + elsif stored_credential[:reason_type] == 'recurring' + post['card.storedCredentialUsage'] = 'RECURRING' + elsif stored_credential[:reason_type] == 'installment' + post['card.storedCredentialUsage'] = 'INSTALLMENT' + end + end + + def eci(options) + if options.dig(:stored_credential, :initial_transaction) + 'SSL' + elsif options.dig(:stored_credential, :initiator) && options[:stored_credential][:initiator] == 'cardholder' + 'MTO' + elsif options.dig(:stored_credential, :reason_type) + case options[:stored_credential][:reason_type] + when 'recurring' + 'REC' + when 'installment' + 'INS' + when 'unscheduled' + 'MTO' + end + else + 'SSL' + end + end + def add_verification_value(post, payment_method) post['card.CVN'] = payment_method.verification_value end - def add_card_reference(post) - post['customer.customerReferenceNumber'] = options[:order_id] + def add_card_reference(post, options) + post['customer.customerReferenceNumber'] = options[:customer_reference_number] || options[:order_id] + end + + def add_customer_reference(post, options) + post['customer.customerReferenceNumber'] = options[:customer_reference_number] if options[:customer_reference_number] end def add_reference(post, authorization, options) @@ -168,14 +229,22 @@ def commit(action, post) message_from(succeeded, raw), raw, authorization: raw['response.orderNumber'] || raw['response.customerReferenceNumber'], + cvv_result: cvv_result(succeeded, raw), error_code: error_code_from(succeeded, raw), test: test? ) end + def cvv_result(succeeded, raw) + return unless succeeded + + code = CVV_CODE_MAPPING[raw['response.cvnResponse']] || raw['response.cvnResponse'] + CVVResult.new(code) + end + def headers { - 'Content-Type' => 'application/x-www-form-urlencoded' + 'Content-Type' => 'application/x-www-form-urlencoded' } end @@ -197,7 +266,7 @@ def parse(body) def parse_element(response, node) if node.has_elements? - node.elements.each{|element| parse_element(response, element) } + node.elements.each { |element| parse_element(response, element) } else response[node.name.underscore.to_sym] = node.text end @@ -225,7 +294,7 @@ def message_from(succeeded, response) '12' => STANDARD_ERROR_CODE[:card_declined], '06' => STANDARD_ERROR_CODE[:processing_error], '01' => STANDARD_ERROR_CODE[:call_issuer], - '04' => STANDARD_ERROR_CODE[:pickup_card], + '04' => STANDARD_ERROR_CODE[:pickup_card] } def error_code_from(succeeded, response) diff --git a/lib/active_merchant/billing/gateways/rapyd.rb b/lib/active_merchant/billing/gateways/rapyd.rb new file mode 100644 index 00000000000..990814445ad --- /dev/null +++ b/lib/active_merchant/billing/gateways/rapyd.rb @@ -0,0 +1,425 @@ +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 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 = {}) + requires!(options, :secret_key, :access_key) + super + end + + def purchase(money, payment, options = {}) + post = {} + add_auth_purchase(post, money, payment, options) + post[:capture] = true unless payment.is_a?(Check) + + commit(:post, 'payments', post) + end + + def authorize(money, payment, options = {}) + post = {} + add_auth_purchase(post, money, payment, options) + post[:capture] = false unless payment.is_a?(Check) + + commit(:post, 'payments', post) + end + + def capture(money, authorization, options = {}) + post = {} + add_idempotency(options) + commit(:post, "payments/#{add_reference(authorization)}/capture", post) + end + + def refund(money, authorization, options = {}) + post = {} + post[:payment] = add_reference(authorization) + add_invoice(post, money, options) + add_metadata(post, options) + add_ewallet(post, options) + add_idempotency(options) + + commit(:post, 'refunds', post) + end + + def void(authorization, options = {}) + post = {} + add_idempotency(options) + commit(:delete, "payments/#{add_reference(authorization)}", post) + end + + def verify(credit_card, options = {}) + authorize(0, credit_card, options) + end + + def store(payment, options = {}) + post = {} + add_payment(post, payment, options) + add_customer_data(post, payment, options, 'store') + add_metadata(post, options) + add_ewallet(post, options) + add_payment_fields(post, options) + add_payment_urls(post, options, 'store') + add_address(post, payment, options) + add_idempotency(options) + commit(:post, 'customers', post) + end + + def unstore(customer) + commit(:delete, "customers/#{add_reference(customer)}", {}) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r((Access_key: )\w+), '\1[FILTERED]'). + gsub(%r(("number\\?":\\?")\d+), '\1[FILTERED]'). + gsub(%r(("account_number\\?":\\?")\d+), '\1[FILTERED]'). + gsub(%r(("routing_number\\?":\\?")\d+), '\1[FILTERED]'). + gsub(%r(("cvv\\?":\\?")\d+), '\1[FILTERED]') + end + + private + + def add_reference(authorization) + return unless authorization + + authorization.split('|')[0] + end + + 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_idempotency(options) + end + + def add_idempotency(options) + @options[:idempotency] = options[:idempotency_key] if options[:idempotency_key] + end + + def add_address(post, creditcard, options) + return unless address = options[:billing_address] + + post[:address] = {} + # name and line_1 are required at the gateway + post[:address][:name] = address[:name] if address[:name] + post[:address][:line_1] = address[:address1] if address[:address1] + post[:address][:line_2] = address[:address2] if address[:address2] + post[:address][:city] = address[:city] if address[:city] + post[:address][:state] = address[:state] if address[:state] + post[:address][:country] = address[:country] if address[:country] + post[:address][:zip] = address[:zip] if address[:zip] + post[:address][:phone_number] = address[:phone].gsub(/\D/, '') if address[:phone] + end + + 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) + if payment.is_a?(CreditCard) + add_creditcard(post, payment, options) + elsif payment.is_a?(Check) + add_ach(post, payment, options) + else + add_tokens(post, payment, options) + end + end + + def add_stored_credential(post, options) + add_network_reference_id(post, options) + add_initiation_type(post, options) + end + + def add_network_reference_id(post, options) + return unless (options[:stored_credential] && options[:stored_credential][:reason_type] == 'recurring') || options[:network_transaction_id] + + network_transaction_id = options[:network_transaction_id] || options[:stored_credential][:network_transaction_id] + post[:payment_method][:fields][:network_reference_id] = network_transaction_id unless network_transaction_id&.empty? + end + + def add_initiation_type(post, options) + return unless options[:stored_credential] || options[:initiation_type] + + initiation_type = options[:initiation_type] || options[:stored_credential][:reason_type] + post[:initiation_type] = initiation_type if initiation_type + end + + def add_creditcard(post, payment, options) + post[:payment_method] = {} + post[:payment_method][:fields] = {} + pm_fields = post[:payment_method][:fields] + + post[:payment_method][:type] = options[:pm_type] + pm_fields[:number] = payment.number + 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 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] = {} + + post[:payment_method][:type] = options[:pm_type] + post[:payment_method][:fields][:proof_of_authorization] = options[:proof_of_authorization] + post[:payment_method][:fields][:first_name] = payment.first_name if payment.first_name + post[:payment_method][:fields][:last_name] = payment.last_name if payment.last_name + post[:payment_method][:fields][:routing_number] = payment.routing_number + post[:payment_method][:fields][:account_number] = payment.account_number + post[:payment_method][:fields][:payment_purpose] = options[:payment_purpose] if options[:payment_purpose] + end + + def add_tokens(post, payment, options) + return unless payment.respond_to?(:split) + + customer_id, card_id = payment.split('|') + + post[:customer] = customer_id unless send_customer_object?(options) + post[:payment_method] = card_id + end + + def add_3ds(post, payment, options) + if options[:execute_threed] == true + post[:payment_method_options] = { '3d_required' => true } 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) + post[:metadata] = options[:metadata] if options[:metadata] + end + + def add_ewallet(post, options) + post[:ewallet] = options[:ewallet_id] if options[:ewallet_id] + end + + def add_payment_fields(post, options) + post[:description] = options[:description] if options[:description] + post[:statement_descriptor] = options[:statement_descriptor] if options[:statement_descriptor] + post[:save_payment_method] = options[:save_payment_method] if options[:save_payment_method] + end + + def add_payment_urls(post, options, action = '') + if action == 'store' + url_location = post[:payment_method] + else + url_location = post + end + + url_location[:complete_payment_url] = options[:complete_payment_url] if options[:complete_payment_url] + url_location[:error_payment_url] = options[:error_payment_url] if options[:error_payment_url] + end + + def add_customer_data(post, payment, options, action = '') + phone_number = options.dig(:billing_address, :phone) || options.dig(:billing_address, :phone_number) + post[:phone_number] = phone_number.gsub(/\D/, '') unless phone_number.nil? + post[: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 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 address(options) + return unless address = options[:billing_address] + + formatted_address = {} + + formatted_address[:name] = address[:name] if address[:name] + formatted_address[:line_1] = address[:address1] if address[:address1] + formatted_address[:line_2] = address[:address2] if address[:address2] + formatted_address[:city] = address[:city] if address[:city] + formatted_address[:state] = address[:state] if address[:state] + formatted_address[:country] = address[:country] if address[:country] + formatted_address[:zip] = address[:zip] if address[:zip] + formatted_address[:phone_number] = address[:phone].gsub(/\D/, '') if address[:phone] + + formatted_address + end + + def add_customer_id(post, options) + post[:customer] = options[:customer_id] if options[:customer_id] + end + + def parse(body) + return {} if body.empty? || body.nil? + + 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) + rel_path = "#{method}/v1/#{action}" + response = api_request(method, url(action, @options[:url_override]), rel_path, parameters) + + Response.new( + success_from(response), + message_from(response), + response, + authorization: authorization_from(response), + avs_result: avs_result(response), + cvv_result: cvv_result(response), + 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 + + def headers(rel_path, payload) + salt = SecureRandom.base64(12) + timestamp = Time.new.to_i.to_s + { + 'Content-Type' => 'application/json', + 'access_key' => @options[:access_key], + 'salt' => salt, + 'timestamp' => timestamp, + '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}" + Base64.urlsafe_encode64(OpenSSL::HMAC.hexdigest('sha256', @options[:secret_key], signature)) + end + + def avs_result(response) + return nil unless (code = response.dig('data', 'payment_method_data', 'acs_check')) + + AVSResult.new(code:) + end + + def cvv_result(response) + return nil unless (code = response.dig('data', 'payment_method_data', 'cvv_check')) + + CVVResult.new(code) + end + + def success_from(response) + response.dig('status', 'status') == 'SUCCESS' && response.dig('status', 'error') != 'ERR' + end + + def message_from(response) + case response.dig('status', 'status') + when 'ERROR' + response.dig('status', 'message') == '' ? response.dig('status', 'error_code') : response.dig('status', 'message') + else + response.dig('status', 'status') + end + end + + def authorization_from(response) + id = response.dig('data') ? response.dig('data', 'id') : response.dig('status', 'operation_id') + + "#{id}|#{response.dig('data', 'default_payment_method')}" + end + + def error_code_from(response) + response.dig('status', 'error_code') || response.dig('status', 'response_code') || '' + end + + def handle_response(response) + case response.code.to_i + when 200...300, 400, 401, 404 + response.body + else + raise ResponseError.new(response) + end + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/reach.rb b/lib/active_merchant/billing/gateways/reach.rb new file mode 100644 index 00000000000..02068850914 --- /dev/null +++ b/lib/active_merchant/billing/gateways/reach.rb @@ -0,0 +1,284 @@ +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class ReachGateway < Gateway + self.test_url = 'https://checkout.rch.how/' + self.live_url = 'https://checkout.rch.io/' + + self.supported_countries = %w(AE AG AL AM AT AU AW AZ BA BB BD BE BF BG BH BJ BM BN BO BR BS BW BZ CA CD CF + CH CI CL CM CN CO CR CU CV CY CZ DE DJ DK DM DO DZ EE EG ES ET FI FJ FK FR GA + GB GD GE GG GH GI GN GR GT GU GW GY HK HN HR HU ID IE IL IM IN IS IT JE JM JO + JP KE KG KH KM KN KR KW KY KZ LA LC LK LR LT LU LV LY MA MD MK ML MN MO MR MS + MT MU MV MW MX MY MZ NA NC NE NG NI NL NO NP NZ OM PA PE PF PG PH PK PL PT PY + QA RO RS RW SA SB SC SE SG SH SI SK SL SN SO SR ST SV SY SZ TD TG TH TN TO TR + TT TV TW TZ UG US UY UZ VC VN VU WF WS YE ZM) + + self.default_currency = 'USD' + self.supported_cardtypes = %i[visa diners_club american_express jcb master discover maestro] + + self.homepage_url = 'https://www.withreach.com/' + self.display_name = 'Reach' + self.currencies_without_fractions = %w(BIF BYR CLF CLP CVE DJF GNF ISK JPY KMF KRW PYG RWF UGX UYI VND VUV XAF XOF XPF IDR MGA MRO) + + API_VERSION = 'v2.22'.freeze + STANDARD_ERROR_CODE_MAPPING = {} + PAYMENT_METHOD_MAP = { + american_express: 'AMEX', + cabal: 'CABAL', + check: 'ACH', + dankort: 'DANKORT', + diners_club: 'DINERS', + discover: 'DISC', + elo: 'ELO', + jcb: 'JCB', + maestro: 'MAESTRO', + master: 'MC', + naranja: 'NARANJA', + union_pay: 'UNIONPAY', + visa: 'VISA' + } + + def initialize(options = {}) + requires!(options, :merchant_id, :secret) + super + end + + def authorize(money, payment, options = {}) + request = build_checkout_request(money, payment, options) + add_custom_fields_data(request, options) + add_customer_data(request, options, payment) + add_stored_credentials(request, options) + post = { request:, card: add_payment(payment, options) } + if options[:stored_credential] + MultiResponse.run(:use_first_response) do |r| + r.process { commit('checkout', post) } + r.process do + r2 = get_network_payment_reference(r.responses[0]) + r.params[:network_transaction_id] = r2.message + r2 + end + end + else + commit('checkout', post) + end + end + + def purchase(money, payment, options = {}) + options[:capture] = true + authorize(money, payment, options) + end + + def capture(money, authorization, options = {}) + post = { request: { MerchantId: @options[:merchant_id], OrderId: authorization } } + commit('capture', post) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r(((MerchantId)[% \w]+%\d{2})[\w -]+), '\1[FILTERED]'). + gsub(%r((signature=)[\w%]+), '\1[FILTERED]\2'). + gsub(%r((Number%22%3A%22)\d+), '\1[FILTERED]\2'). + gsub(%r((VerificationCode%22%3A)\d+), '\1[FILTERED]\2') + end + + def refund(amount, authorization, options = {}) + currency = options[:currency] || currency(options[:amount]) + post = { + request: { + MerchantId: @options[:merchant_id], + OrderId: authorization, + ReferenceId: options[:order_id] || options[:reference_id], + Amount: localized_amount(amount, currency) + } + } + commit('refund', post) + end + + def void(authorization, options = {}) + post = { + request: { + MerchantId: @options[:merchant_id], + OrderId: authorization + } + } + + commit('cancel', post) + end + + def verify(credit_card, options = {}) + MultiResponse.run(:use_first_response) do |r| + r.process { authorize(100, credit_card, options) } + r.process(:ignore_result) { void(r.authorization, options) } + end + end + + private + + def build_checkout_request(amount, payment, options) + currency = options[:currency] || currency(options[:amount]) + { + MerchantId: @options[:merchant_id], + ReferenceId: options[:order_id], + ConsumerCurrency: currency, + Capture: options[:capture] || false, + PaymentMethod: PAYMENT_METHOD_MAP.fetch(payment.brand.to_sym, 'unsupported'), + Items: [ + Sku: options[:item_sku] || SecureRandom.alphanumeric, + ConsumerPrice: localized_amount(amount, currency), + Quantity: (options[:item_quantity] || 1) + ] + } + end + + def add_payment(payment, options) + ntid = options.dig(:stored_credential, :network_transaction_id) + cvv_or_previos_reference = (ntid ? { PreviousNetworkPaymentReference: ntid } : { VerificationCode: payment.verification_value }) + { + Name: payment.name, + Number: payment.number, + Expiry: { Month: payment.month, Year: payment.year } + }.merge!(cvv_or_previos_reference) + end + + def add_customer_data(request, options, payment) + address = options[:billing_address] || options[:address] + + return if address.blank? + + request[:Consumer] = { + Name: payment.respond_to?(:name) ? payment.name : address[:name], + Email: options[:email], + Address: address[:address1], + City: address[:city], + Country: address[:country] + }.compact + end + + def add_stored_credentials(request, options) + request[:PaymentModel] = payment_model(options) || '' + request[:DeviceFingerprint] = options[:device_fingerprint] if options[:device_fingerprint] + end + + def payment_model(options) + stored_credential = options[:stored_credential] + return options[:payment_model] if options[:payment_model] + return 'CIT-One-Time' unless stored_credential + + payment_model_options = { + initial_transaction: { + 'cardholder' => { + 'installment' => 'CIT-Setup-Scheduled', + 'unscheduled' => 'CIT-Setup-Unscheduled-MIT', + 'recurring' => 'CIT-Setup-Unscheduled' + } + }, + no_initial_transaction: { + 'cardholder' => { + 'unscheduled' => 'CIT-Subsequent-Unscheduled' + }, + 'merchant' => { + 'recurring' => 'MIT-Subsequent-Scheduled', + 'unscheduled' => 'MIT-Subsequent-Unscheduled' + } + } + } + initial = stored_credential[:initial_transaction] ? :initial_transaction : :no_initial_transaction + payment_model_options[initial].dig(stored_credential[:initiator], stored_credential[:reason_type]) + end + + def add_custom_fields_data(request, options) + add_shipping_data(request, options) if options[:taxes].present? + request[:RateOfferId] = options[:rate_offer_id] if options[:rate_offer_id].present? + request[:Items] = options[:items] if options[:items].present? + end + + def add_shipping_data(request, options) + request[:Shipping] = { + ConsumerPrice: options[:price], + ConsumerTaxes: options[:taxes], + ConsumerDuty: options[:duty] + } + request[:Consignee] = { + Name: options[:consignee_name], + Address: options[:consignee_address], + City: options[:consignee_city], + Country: options[:consignee_country] + } + end + + def sign_body(body) + Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', @options[:secret].encode('utf-8'), body.encode('utf-8'))) + end + + def parse(body) + hash_response = URI.decode_www_form(body).to_h + hash_response['response'] = JSON.parse(hash_response['response']) + + hash_response + end + + def format_and_sign(post) + post[:request] = post[:request].to_json + post[:card] = post[:card].to_json if post[:card].present? + post[:signature] = sign_body(post[:request]) + post + end + + def get_network_payment_reference(response) + parameters = { request: { MerchantId: @options[:merchant_id], OrderId: response.params['response']['OrderId'] } } + body = post_data format_and_sign(parameters) + + raw_response = ssl_request :post, url('query'), body, {} + response = parse(raw_response) + message = response.dig('response', 'Payment', 'NetworkPaymentReference') + Response.new(true, message, {}) + end + + def commit(action, parameters) + body = post_data format_and_sign(parameters) + raw_response = ssl_post url(action), body + response = parse(raw_response) + + Response.new( + success_from(response), + message_from(response) || '', + response, + authorization: authorization_from(response['response']), + # avs_result: AVSResult.new(code: response['some_avs_response_key']), + # cvv_result: CVVResult.new(response['some_cvv_response_key']), + test: test?, + error_code: error_code_from(response) + ) + rescue ActiveMerchant::ResponseError => e + Response.new(false, (e.response.body.present? ? e.response.body : e.response.msg), {}, test: test?) + end + + def success_from(response) + response.dig('response', 'Error').blank? + end + + def message_from(response) + success_from(response) ? '' : response.dig('response', 'Error', 'ReasonCode') + end + + def authorization_from(response) + response['OrderId'] + end + + def post_data(params) + params.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&') + end + + def error_code_from(response) + response['response']['Error']['Code'] unless success_from(response) + end + + def url(action) + "#{test? ? test_url : live_url}#{API_VERSION}/#{action}" + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/realex.rb b/lib/active_merchant/billing/gateways/realex.rb index 08280b4b76b..d639b04d91a 100644 --- a/lib/active_merchant/billing/gateways/realex.rb +++ b/lib/active_merchant/billing/gateways/realex.rb @@ -26,15 +26,12 @@ class RealexGateway < Gateway 'visa' => 'VISA', 'american_express' => 'AMEX', 'diners_club' => 'DINERS', - 'switch' => 'SWITCH', - 'solo' => 'SWITCH', - 'laser' => 'LASER', 'maestro' => 'MC' } self.money_format = :cents self.default_currency = 'EUR' - self.supported_cardtypes = [ :visa, :master, :american_express, :diners_club, :switch, :solo, :laser ] + self.supported_cardtypes = %i[visa master american_express diners_club] self.supported_countries = %w(IE GB FR BE NL LU IT US CA ES) self.homepage_url = 'http://www.realexpayments.com/' self.display_name = 'Realex' @@ -45,7 +42,8 @@ class RealexGateway < Gateway def initialize(options = {}) requires!(options, :login, :password) - options[:refund_hash] = Digest::SHA1.hexdigest(options[:rebate_secret]) if options.has_key?(:rebate_secret) + options[:refund_hash] = Digest::SHA1.hexdigest(options[:rebate_secret]) if options[:rebate_secret].present? + options[:credit_hash] = Digest::SHA1.hexdigest(options[:refund_secret]) if options[:refund_secret].present? super end @@ -73,9 +71,9 @@ def refund(money, authorization, options = {}) commit(request) end - def credit(money, authorization, options = {}) - ActiveMerchant.deprecated CREDIT_DEPRECATION_MESSAGE - refund(money, authorization, options) + def credit(money, creditcard, options = {}) + request = build_credit_request(money, creditcard, options) + commit(request) end def void(authorization, options = {}) @@ -83,17 +81,25 @@ def void(authorization, options = {}) commit(request) end + def verify(credit_card, options = {}) + requires!(options, :order_id) + + request = build_verify_request(credit_card, options) + commit(request) + end + def supports_scrubbing true end def scrub(transcript) transcript. - gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). - gsub(%r(()\d+())i, '\1[FILTERED]\2') + gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). + gsub(%r(()\d+())i, '\1[FILTERED]\2') end private + def commit(request) response = parse(ssl_post(self.live_url, request)) @@ -101,8 +107,8 @@ def commit(request) (response[:result] == '00'), message_from(response), response, - :test => (response[:message] =~ %r{\[ test system \]}), - :authorization => authorization_from(response), + test: (response[:message] =~ %r{\[ test system \]}), + authorization: authorization_from(response), avs_result: AVSResult.new(code: response[:avspostcoderesponse]), cvv_result: CVVResult.new(response[:cvnresult]) ) @@ -113,7 +119,7 @@ def parse(xml) doc = Nokogiri::XML(xml) doc.xpath('//response/*').each do |node| - if (node.elements.size == 0) + if node.elements.size == 0 response[node.name.downcase.to_sym] = normalize(node.text) else node.elements.each do |childnode| @@ -132,7 +138,7 @@ def authorization_from(parsed) def build_purchase_or_authorization_request(action, money, credit_card, options) timestamp = new_timestamp - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 xml.tag! 'request', 'timestamp' => timestamp, 'type' => 'auth' do add_merchant_details(xml, options) xml.tag! 'orderid', sanitize_order_id(options[:order_id]) @@ -140,7 +146,12 @@ def build_purchase_or_authorization_request(action, money, credit_card, options) add_card(xml, credit_card) xml.tag! 'autosettle', 'flag' => auto_settle_flag(action) add_signed_digest(xml, timestamp, @options[:login], sanitize_order_id(options[:order_id]), amount(money), (options[:currency] || currency(money)), credit_card.number) - add_network_tokenization_card(xml, credit_card) if credit_card.is_a?(NetworkTokenizationCreditCard) + if credit_card.is_a?(NetworkTokenizationCreditCard) + add_network_tokenization_card(xml, credit_card) + else + add_three_d_secure(xml, options) + end + add_stored_credential(xml, options) add_comments(xml, options) add_address_and_customer_info(xml, options) end @@ -149,7 +160,7 @@ def build_purchase_or_authorization_request(action, money, credit_card, options) def build_capture_request(money, authorization, options) timestamp = new_timestamp - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 xml.tag! 'request', 'timestamp' => timestamp, 'type' => 'settle' do add_merchant_details(xml, options) add_amount(xml, money, options) @@ -162,7 +173,7 @@ def build_capture_request(money, authorization, options) def build_refund_request(money, authorization, options) timestamp = new_timestamp - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 xml.tag! 'request', 'timestamp' => timestamp, 'type' => 'rebate' do add_merchant_details(xml, options) add_transaction_identifiers(xml, authorization, options) @@ -175,9 +186,25 @@ def build_refund_request(money, authorization, options) xml.target! end + def build_credit_request(money, credit_card, options) + timestamp = new_timestamp + xml = Builder::XmlMarkup.new indent: 2 + xml.tag! 'request', 'timestamp' => timestamp, 'type' => 'credit' do + add_merchant_details(xml, options) + xml.tag! 'orderid', sanitize_order_id(options[:order_id]) + add_amount(xml, money, options) + add_card(xml, credit_card) + xml.tag! 'refundhash', @options[:credit_hash] if @options[:credit_hash] + xml.tag! 'autosettle', 'flag' => 1 + add_comments(xml, options) + add_signed_digest(xml, timestamp, @options[:login], sanitize_order_id(options[:order_id]), amount(money), (options[:currency] || currency(money)), credit_card.number) + end + xml.target! + end + def build_void_request(authorization, options) timestamp = new_timestamp - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 xml.tag! 'request', 'timestamp' => timestamp, 'type' => 'void' do add_merchant_details(xml, options) add_transaction_identifiers(xml, authorization, options) @@ -187,16 +214,31 @@ def build_void_request(authorization, options) xml.target! end + # Verify initiates an OTB (Open To Buy) request + def build_verify_request(credit_card, options) + timestamp = new_timestamp + xml = Builder::XmlMarkup.new indent: 2 + xml.tag! 'request', 'timestamp' => timestamp, 'type' => 'otb' do + add_merchant_details(xml, options) + xml.tag! 'orderid', sanitize_order_id(options[:order_id]) + add_card(xml, credit_card) + add_comments(xml, options) + add_signed_digest(xml, timestamp, @options[:login], sanitize_order_id(options[:order_id]), credit_card.number) + end + xml.target! + end + def add_address_and_customer_info(xml, options) billing_address = options[:billing_address] || options[:address] shipping_address = options[:shipping_address] + ipv4_address = ipv4?(options[:ip]) ? options[:ip] : nil - return unless billing_address || shipping_address || options[:customer] || options[:invoice] || options[:ip] + return unless billing_address || shipping_address || options[:customer] || options[:invoice] || ipv4_address xml.tag! 'tssinfo' do xml.tag! 'custnum', options[:customer] if options[:customer] xml.tag! 'prodid', options[:invoice] if options[:invoice] - xml.tag! 'custipaddress', options[:ip] if options[:ip] + xml.tag! 'custipaddress', options[:ip] if ipv4_address if billing_address xml.tag! 'address', 'type' => 'billing' do @@ -216,9 +258,7 @@ def add_address_and_customer_info(xml, options) def add_merchant_details(xml, options) xml.tag! 'merchantid', @options[:login] - if options[:account] || @options[:account] - xml.tag! 'account', (options[:account] || @options[:account]) - end + xml.tag! 'account', (options[:account] || @options[:account]) if options[:account] || @options[:account] end def add_transaction_identifiers(xml, authorization, options) @@ -230,6 +270,7 @@ def add_transaction_identifiers(xml, authorization, options) def add_comments(xml, options) return unless options[:description] + xml.tag! 'comments' do xml.tag! 'comment', options[:description], 'id' => 1 end @@ -245,7 +286,7 @@ def add_card(xml, credit_card) xml.tag! 'expdate', expiry_date(credit_card) xml.tag! 'chname', credit_card.name xml.tag! 'type', CARD_MAPPING[card_brand(credit_card).to_s] - xml.tag! 'issueno', credit_card.issue_number + xml.tag! 'issueno', '' xml.tag! 'cvn' do xml.tag! 'number', credit_card.verification_value xml.tag! 'presind', (options['presind'] || (credit_card.verification_value? ? 1 : nil)) @@ -260,14 +301,49 @@ def add_network_tokenization_card(xml, payment) end xml.tag! 'supplementarydata' do xml.tag! 'item', 'type' => 'mobile' do - xml.tag! 'field01', payment.source.to_s.gsub('_','-') + xml.tag! 'field01', payment.source.to_s.tr('_', '-') + end + end + end + + def add_three_d_secure(xml, options) + return unless three_d_secure = options[:three_d_secure] + + version = three_d_secure.fetch(:version, '') + xml.tag! 'mpi' do + if /^2/.match?(version) + xml.tag! 'authentication_value', three_d_secure[:cavv] + xml.tag! 'ds_trans_id', three_d_secure[:ds_transaction_id] + else + xml.tag! 'cavv', three_d_secure[:cavv] + xml.tag! 'xid', three_d_secure[:xid] + version = '1' end + xml.tag! 'eci', three_d_secure[:eci] + xml.tag! 'message_version', version + end + end + + def add_stored_credential(xml, options) + return unless stored_credential = options[:stored_credential] + + xml.tag! 'storedcredential' do + xml.tag! 'type', stored_credential_type(stored_credential[:reason_type]) + xml.tag! 'initiator', stored_credential[:initiator] + xml.tag! 'sequence', stored_credential[:initial_transaction] ? 'first' : 'subsequent' + xml.tag! 'srd', stored_credential[:network_transaction_id] end end + def stored_credential_type(reason) + return 'oneoff' if reason == 'unscheduled' + + reason + end + def format_address_code(address) code = [address[:zip].to_s, address[:address1].to_s + address[:address2].to_s] - code.collect{|e| e.gsub(/\D/, '')}.reject{|e| e.empty?}.join('|') + code.collect { |e| e.gsub(/\D/, '') }.reject(&:empty?).join('|') end def new_timestamp @@ -288,32 +364,37 @@ def expiry_date(credit_card) end def message_from(response) - message = nil case response[:result] when '00' - message = SUCCESS + SUCCESS when '101' - message = response[:message] + response[:message] when '102', '103' - message = DECLINED + DECLINED when /^2[0-9][0-9]/ - message = BANK_ERROR + BANK_ERROR when /^3[0-9][0-9]/ - message = REALEX_ERROR + REALEX_ERROR when /^5[0-9][0-9]/ - message = response[:message] + response[:message] when '600', '601', '603' - message = ERROR + ERROR when '666' - message = CLIENT_DEACTIVATED + CLIENT_DEACTIVATED else - message = DECLINED + DECLINED end end def sanitize_order_id(order_id) order_id.to_s.gsub(/[^a-zA-Z0-9\-_]/, '') end + + def ipv4?(ip_address) + return false if ip_address.nil? + + !ip_address.match(/\A\d+\.\d+\.\d+\.\d+\z/).nil? + end end end end diff --git a/lib/active_merchant/billing/gateways/redsys.rb b/lib/active_merchant/billing/gateways/redsys.rb index 2fb8ecfcd02..9f88736c307 100644 --- a/lib/active_merchant/billing/gateways/redsys.rb +++ b/lib/active_merchant/billing/gateways/redsys.rb @@ -1,8 +1,9 @@ # coding: utf-8 + require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # = Redsys Merchant Gateway # # Gateway support for the Spanish "Redsys" payment gateway system. This is @@ -35,15 +36,15 @@ module Billing #:nodoc: # # class RedsysGateway < Gateway - self.live_url = 'https://sis.sermepa.es/sis/operaciones' + 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 # Not all card types may be activated by the bank! - self.supported_cardtypes = [:visa, :master, :american_express, :jcb, :diners_club] + self.supported_cardtypes = %i[visa master american_express jcb diners_club unionpay patagonia_365 tarjeta_sol] self.homepage_url = 'http://www.redsys.es/' self.display_name = 'Redsys' @@ -90,11 +91,11 @@ class RedsysGateway < Gateway # More operations are supported by the gateway itself, but # are not supported in this library. SUPPORTED_TRANSACTIONS = { - :purchase => 'A', - :authorize => '1', - :capture => '2', - :refund => '3', - :cancel => '9' + purchase: '0', + authorize: '1', + capture: '2', + refund: '3', + cancel: '9' } # These are the text meanings sent back by the acquirer when @@ -125,6 +126,7 @@ class RedsysGateway < Gateway 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', @@ -169,6 +171,10 @@ class RedsysGateway < Gateway 9914 => 'KO Confirmation' } + # Expected values as per documentation + THREE_DS_V1 = '1.0.2' + THREE_DS_V2 = '2.1.0' + # Creates a new instance # # Redsys requires a login and secret_key, and optionally also accepts a @@ -192,68 +198,75 @@ def purchase(money, payment, options = {}) requires!(options, :order_id) data = {} - add_action(data, :purchase) + add_action(data, :purchase, options) add_amount(data, money, options) add_order(data, options[:order_id]) add_payment(data, payment) + add_external_mpi_fields(data, options) + add_three_ds_data(data, options) if options[:execute_threed] + add_stored_credential_options(data, options) data[:description] = options[:description] data[:store_in_vault] = options[:store] + data[:sca_exemption] = options[:sca_exemption] + data[:sca_exemption_direct_payment_enabled] = options[:sca_exemption_direct_payment_enabled] - commit data + commit data, options end def authorize(money, payment, options = {}) requires!(options, :order_id) data = {} - add_action(data, :authorize) + add_action(data, :authorize, options) add_amount(data, money, options) add_order(data, options[:order_id]) add_payment(data, payment) + add_external_mpi_fields(data, options) + add_three_ds_data(data, options) if options[:execute_threed] + add_stored_credential_options(data, options) data[:description] = options[:description] data[:store_in_vault] = options[:store] + data[:sca_exemption] = options[:sca_exemption] + data[:sca_exemption_direct_payment_enabled] = options[:sca_exemption_direct_payment_enabled] - commit data + commit data, options end def capture(money, authorization, options = {}) data = {} add_action(data, :capture) add_amount(data, money, options) - order_id, _, _ = split_authorization(authorization) + order_id, = split_authorization(authorization) add_order(data, order_id) data[:description] = options[:description] - commit data + commit data, options end def void(authorization, options = {}) data = {} add_action(data, :cancel) order_id, amount, currency = split_authorization(authorization) - add_amount(data, amount, :currency => currency) + add_amount(data, amount, currency:) add_order(data, order_id) data[:description] = options[:description] - commit data + commit data, options end def refund(money, authorization, options = {}) data = {} add_action(data, :refund) add_amount(data, money, options) - order_id, _, _ = split_authorization(authorization) + order_id, = split_authorization(authorization) add_order(data, order_id) data[:description] = options[:description] - commit data + commit data, options end def verify(creditcard, options = {}) - MultiResponse.run(:use_first_response) do |r| - r.process { authorize(100, creditcard, options) } - r.process(:ignore_result) { void(r.authorization, options) } - end + purchase(0, creditcard, options) end def supports_scrubbing @@ -265,8 +278,10 @@ def scrub(transcript) gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). gsub(%r((%3CDS_MERCHANT_PAN%3E)\d+(%3C%2FDS_MERCHANT_PAN%3E))i, '\1[FILTERED]\2'). gsub(%r((%3CDS_MERCHANT_CVV2%3E)\d+(%3C%2FDS_MERCHANT_CVV2%3E))i, '\1[FILTERED]\2'). + gsub(%r((<DS_MERCHANT_PAN>)\d+(</DS_MERCHANT_PAN>))i, '\1[FILTERED]\2'). gsub(%r(()\d+())i, '\1[FILTERED]\2'). gsub(%r(()\d+())i, '\1[FILTERED]\2'). + gsub(%r((<DS_MERCHANT_CVV2>)\d+(</DS_MERCHANT_CVV2>))i, '\1[FILTERED]\2'). gsub(%r((DS_MERCHANT_CVV2)%2F%3E%0A%3C%2F)i, '\1[BLANK]'). gsub(%r((DS_MERCHANT_CVV2)%2F%3E%3C)i, '\1[BLANK]'). gsub(%r((DS_MERCHANT_CVV2%3E)(%3C%2FDS_MERCHANT_CVV2))i, '\1[BLANK]\2'). @@ -277,8 +292,8 @@ def scrub(transcript) private - def add_action(data, action) - data[:action] = transaction_code(action) + def add_action(data, action, options = {}) + data[:action] = options[:execute_threed].present? ? '0' : transaction_code(action) end def add_amount(data, money, options) @@ -294,6 +309,10 @@ def url test? ? test_url : live_url end + def webservice_url + test? ? 'https://sis-t.redsys.es:25443/sis/services/SerClsWSEntradaV2' : 'https://sis.redsys.es/sis/services/SerClsWSEntradaV2' + end + def add_payment(data, card) if card.is_a?(String) data[:credit_card_token] = card @@ -302,37 +321,120 @@ def add_payment(data, card) year = sprintf('%.4i', card.year) month = sprintf('%.2i', card.month) data[:card] = { - :name => name, - :pan => card.number, - :date => "#{year[2..3]}#{month}", - :cvv => card.verification_value + name:, + pan: card.number, + date: "#{year[2..3]}#{month}", + cvv: card.verification_value } end end - def commit(data) - parse(ssl_post(url, "entrada=#{CGI.escape(xml_request_from(data))}", headers)) + def add_external_mpi_fields(data, options) + return unless options[:three_d_secure] + + if options[:three_d_secure][:version] == THREE_DS_V2 + data[:threeDSServerTransID] = options[:three_d_secure][:three_ds_server_trans_id] if options[:three_d_secure][:three_ds_server_trans_id] + data[:dsTransID] = options[:three_d_secure][:ds_transaction_id] if options[:three_d_secure][:ds_transaction_id] + data[:authenticacionValue] = options[:three_d_secure][:cavv] if options[:three_d_secure][:cavv] + data[:protocolVersion] = options[:three_d_secure][:version] if options[:three_d_secure][:version] + data[:authenticacionMethod] = options[:authentication_method] if options[:authentication_method] + data[:authenticacionType] = options[:authentication_type] if options[:authentication_type] + data[:authenticacionFlow] = options[:authentication_flow] if options[:authentication_flow] + data[:eci_v2] = options[:three_d_secure][:eci] if options[:three_d_secure][:eci] + elsif options[:three_d_secure][:version] == THREE_DS_V1 + data[:txid] = options[:three_d_secure][:xid] if options[:three_d_secure][:xid] + data[:cavv] = options[:three_d_secure][:cavv] if options[:three_d_secure][:cavv] + data[:eci_v1] = options[:three_d_secure][:eci] if options[:three_d_secure][:eci] + end end - def headers - { - 'Content-Type' => 'application/x-www-form-urlencoded' - } + def add_stored_credential_options(data, options) + return unless options[:stored_credential] + + case options[:stored_credential][:initial_transaction] + when true + data[:DS_MERCHANT_COF_INI] = 'S' + when false + data[:DS_MERCHANT_COF_INI] = 'N' + data[:DS_MERCHANT_COF_TXNID] = options[:stored_credential][:network_transaction_id] if options[:stored_credential][:network_transaction_id] + end + + case options[:stored_credential][:reason_type] + when 'recurring' + data[:DS_MERCHANT_COF_TYPE] = 'R' + when 'installment' + data[:DS_MERCHANT_COF_TYPE] = 'I' + when 'unscheduled' + return + end + end + + def add_three_ds_data(data, options) + data[:three_ds_data] = { threeDSInfo: 'CardData' } if options[:execute_threed] == true end - def xml_request_from(data) + def determine_peticion_type(data) + three_ds_info = data.dig(:three_ds_data, :threeDSInfo) + return 'iniciaPeticion' if three_ds_info == 'CardData' + return 'trataPeticion' if three_ds_info == 'AuthenticationData' || + three_ds_info == 'ChallengeResponse' || + data[:sca_exemption] == 'MIT' + end + + def use_webservice_endpoint?(data, options) + options[:use_webservice_endpoint].to_s == 'true' || data[:three_ds_data] || data[:sca_exemption] == 'MIT' + end + + def commit(data, options = {}) + xmlreq = xml_request_from(data, options) + + if use_webservice_endpoint?(data, options) + peticion_type = determine_peticion_type(data) + + request = <<-REQUEST + + + + + + + + + + + REQUEST + parse(ssl_post(webservice_url, request, headers(peticion_type)), peticion_type) + else + parse(ssl_post(url, "entrada=#{CGI.escape(xmlreq)}", headers), peticion_type) + end + end + + def headers(peticion_type = nil) + if peticion_type + { + 'Content-Type' => 'text/xml', + 'SOAPAction' => peticion_type + } + else + { + 'Content-Type' => 'application/x-www-form-urlencoded' + } + end + end + + def xml_request_from(data, options = {}) if sha256_authentication? - build_sha256_xml_request(data) + build_sha256_xml_request(data, options) else - build_sha1_xml_request(data) + build_sha1_xml_request(data, options) end end def build_signature(data) str = data[:amount] + - data[:order_id].to_s + - @options[:login].to_s + - data[:currency] + data[:order_id].to_s + + @options[:login].to_s + + data[:currency] if card = data[:card] str << card[:pan] @@ -350,30 +452,32 @@ def build_signature(data) Digest::SHA1.hexdigest(str) end - def build_sha256_xml_request(data) + def build_sha256_xml_request(data, options = {}) xml = Builder::XmlMarkup.new xml.instruct! xml.REQUEST do - build_merchant_data(xml, data) + build_merchant_data(xml, data, options) xml.DS_SIGNATUREVERSION 'HMAC_SHA256_V1' - xml.DS_SIGNATURE sign_request(merchant_data_xml(data), data[:order_id]) + xml.DS_SIGNATURE sign_request(merchant_data_xml(data, options), data[:order_id]) end xml.target! end - def build_sha1_xml_request(data) - xml = Builder::XmlMarkup.new :indent => 2 - build_merchant_data(xml, data) + def build_sha1_xml_request(data, options = {}) + xml = Builder::XmlMarkup.new indent: 2 + build_merchant_data(xml, data, options) xml.target! end - def merchant_data_xml(data) + def merchant_data_xml(data, options = {}) xml = Builder::XmlMarkup.new - build_merchant_data(xml, data) + build_merchant_data(xml, data, options) xml.target! end - def build_merchant_data(xml, data) + def build_merchant_data(xml, data, options = {}) + # See https://sis-t.redsys.es:25443/sis/services/SerClsWSEntradaV2/wsdl/SerClsWSEntradaV2.wsdl + # (which results from calling #webservice_url + '?WSDL', https://sis-t.redsys.es:25443/sis/services/SerClsWSEntradaV2?WSDL) xml.DATOSENTRADA do # Basic elements xml.DS_Version 0.1 @@ -381,45 +485,109 @@ def build_merchant_data(xml, data) xml.DS_MERCHANT_AMOUNT data[:amount] xml.DS_MERCHANT_ORDER data[:order_id] xml.DS_MERCHANT_TRANSACTIONTYPE data[:action] - xml.DS_MERCHANT_PRODUCTDESCRIPTION data[:description] - xml.DS_MERCHANT_TERMINAL @options[:terminal] + if data[:description] && use_webservice_endpoint?(data, options) + xml.DS_MERCHANT_PRODUCTDESCRIPTION CGI.escape(data[:description]) + else + xml.DS_MERCHANT_PRODUCTDESCRIPTION data[:description] + end + xml.DS_MERCHANT_TERMINAL options[:terminal] || @options[:terminal] xml.DS_MERCHANT_MERCHANTCODE @options[:login] xml.DS_MERCHANT_MERCHANTSIGNATURE build_signature(data) unless sha256_authentication? + peticion_type = determine_peticion_type(data) if data[:three_ds_data] + if peticion_type == 'iniciaPeticion' && data[:sca_exemption] + xml.DS_MERCHANT_EXCEP_SCA 'Y' + else + xml.DS_MERCHANT_EXCEP_SCA data[:sca_exemption] if data[:sca_exemption] + xml.DS_MERCHANT_DIRECTPAYMENT data[:sca_exemption_direct_payment_enabled] || 'true' if data[:sca_exemption] == 'MIT' + end + # Only when card is present if data[:card] - xml.DS_MERCHANT_TITULAR data[:card][:name] + if data[:card][:name] && use_webservice_endpoint?(data, options) + xml.DS_MERCHANT_TITULAR CGI.escape(data[:card][:name]) + else + xml.DS_MERCHANT_TITULAR data[:card][:name] + end xml.DS_MERCHANT_PAN data[:card][:pan] xml.DS_MERCHANT_EXPIRYDATE data[:card][:date] xml.DS_MERCHANT_CVV2 data[:card][:cvv] xml.DS_MERCHANT_IDENTIFIER 'REQUIRED' if data[:store_in_vault] + + build_merchant_mpi_external(xml, data) + elsif data[:credit_card_token] xml.DS_MERCHANT_IDENTIFIER data[:credit_card_token] xml.DS_MERCHANT_DIRECTPAYMENT 'true' end + + # Set moto flag only if explicitly requested via moto field + # Requires account configuration to be able to use + xml.DS_MERCHANT_DIRECTPAYMENT 'moto' if options.dig(:moto) && options.dig(:metadata, :manual_entry) + + xml.DS_MERCHANT_EMV3DS data[:three_ds_data].to_json if data[:three_ds_data] + + if options[:stored_credential] + 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 - def parse(data) + def build_merchant_mpi_external(xml, data) + return unless data[:txid] || data[:threeDSServerTransID] + + ds_merchant_mpi_external = {} + ds_merchant_mpi_external[:TXID] = data[:txid] if data[:txid] + ds_merchant_mpi_external[:CAVV] = data[:cavv] if data[:cavv] + ds_merchant_mpi_external[:ECI] = data[:eci_v1] if data[:eci_v1] + + ds_merchant_mpi_external[:threeDSServerTransID] = data[:threeDSServerTransID] if data[:threeDSServerTransID] + ds_merchant_mpi_external[:dsTransID] = data[:dsTransID] if data[:dsTransID] + ds_merchant_mpi_external[:authenticacionValue] = data[:authenticacionValue] if data[:authenticacionValue] + ds_merchant_mpi_external[:protocolVersion] = data[:protocolVersion] if data[:protocolVersion] + ds_merchant_mpi_external[:Eci] = data[:eci_v2] if data[:eci_v2] + ds_merchant_mpi_external[:authenticacionMethod] = data[:authenticacionMethod] if data[:authenticacionMethod] + ds_merchant_mpi_external[:authenticacionType] = data[:authenticacionType] if data[:authenticacionType] + ds_merchant_mpi_external[:authenticacionFlow] = data[:authenticacionFlow] if data[:authenticacionFlow] + + xml.DS_MERCHANT_MPIEXTERNAL ds_merchant_mpi_external.to_json unless ds_merchant_mpi_external.empty? + xml.target! + end + + def parse(data, action) params = {} success = false message = '' - options = @options.merge(:test => test?) + options = @options.merge(test: test?) xml = Nokogiri::XML(data) code = xml.xpath('//RETORNOXML/CODIGO').text - if code == '0' + + if code == '0' && xml.xpath('//RETORNOXML/OPERACION').present? op = xml.xpath('//RETORNOXML/OPERACION') op.children.each do |element| params[element.name.downcase.to_sym] = element.text end - if validate_signature(params) message = response_text(params[:ds_response]) options[:authorization] = build_authorization(params) - success = is_success_response?(params[:ds_response]) + success = success_response?(params[:ds_response]) else message = 'Response failed validation check' end + elsif %w[iniciaPeticion trataPeticion].include?(action) + vxml = Nokogiri::XML(data).remove_namespaces!.xpath("//Envelope/Body/#{action}Response/#{action}Return").inner_text + xml = Nokogiri::XML(vxml) + node = (action == 'iniciaPeticion' ? 'INFOTARJETA' : 'OPERACION') + op = xml.xpath("//RETORNOXML/#{node}") + op.children.each do |element| + params[element.name.downcase.to_sym] = element.text + end + message = response_text_3ds(xml, params) + options[:authorization] = build_authorization(params) + success = params.size > 0 && success_response?(params[:ds_response]) else # Some kind of programmer error with the request! message = "#{code} ERROR" @@ -431,17 +599,17 @@ def parse(data) def validate_signature(data) if sha256_authentication? sig = Base64.strict_encode64(mac256(get_key(data[:ds_order].to_s), xml_signed_fields(data))) - sig.upcase == data[:ds_signature].to_s.upcase + sig.casecmp(data[:ds_signature].to_s).zero? else str = data[:ds_amount] + - data[:ds_order].to_s + - data[:ds_merchantcode] + - data[:ds_currency] + - data[:ds_response] + - data[:ds_cardnumber].to_s + - data[:ds_transactiontype].to_s + - data[:ds_securepayment].to_s + - @options[:secret_key] + data[:ds_order].to_s + + data[:ds_merchantcode] + + data[:ds_currency] + + data[:ds_response] + + data[:ds_cardnumber].to_s + + data[:ds_transactiontype].to_s + + data[:ds_securepayment].to_s + + @options[:secret_key] sig = Digest::SHA1.hexdigest(str) data[:ds_signature].to_s.downcase == sig @@ -460,6 +628,7 @@ def split_authorization(authorization) def currency_code(currency) return currency if currency =~ /^\d+$/ raise ArgumentError, "Unknown currency #{currency}" unless CURRENCY_CODES[currency] + CURRENCY_CODES[currency] end @@ -470,16 +639,30 @@ def transaction_code(type) def response_text(code) code = code.to_i code = 0 if code < 100 - RESPONSE_TEXTS[code] || 'Unkown code, please check in manual' + RESPONSE_TEXTS[code] || 'Unknown code, please check in manual' end - def is_success_response?(code) + def response_text_3ds(xml, params) + code = xml.xpath('//RETORNOXML/CODIGO').text + message = '' + if code != '0' + message = "#{code} ERROR" + elsif params[:ds_emv3ds] + three_ds_data = JSON.parse(params[:ds_emv3ds]) + message = three_ds_data['threeDSInfo'] + elsif params[:ds_response] + message = response_text(params[:ds_response]) + end + message + end + + def success_response?(code) (code.to_i < 100) || [400, 481, 500, 900].include?(code.to_i) end def clean_order_id(order_id) cleansed = order_id.gsub(/[^\da-zA-Z]/, '') - if cleansed =~ /^\d{4}/ + if /^\d{4}/.match?(cleansed) cleansed[0..11] else '%04d%s' % [rand(0..9999), cleansed[0...8]] @@ -500,14 +683,13 @@ def encrypt(key, order_id) cipher = OpenSSL::Cipher.new('DES3') cipher.encrypt - cipher.key = Base64.strict_decode64(key) + cipher.key = Base64.urlsafe_decode64(key) # The OpenSSL default of an all-zeroes ("\\0") IV is used. cipher.padding = 0 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) @@ -516,13 +698,13 @@ def mac256(key, data) def xml_signed_fields(data) xml_signed_fields = data[:ds_amount] + data[:ds_order] + data[:ds_merchantcode] + - data[:ds_currency] + data[:ds_response] + data[:ds_currency] + data[:ds_response] - if data[:ds_cardnumber] - xml_signed_fields += data[:ds_cardnumber] - end + xml_signed_fields += data[:ds_cardnumber] if data[:ds_cardnumber] + + xml_signed_fields += data[:ds_emv3ds] if data[:ds_emv3ds] - xml_signed_fields += data[:ds_transactiontype] + data[:ds_securepayment] + xml_signed_fields + data[:ds_transactiontype] + data[:ds_securepayment] end def get_key(order_id) 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..467be69c953 --- /dev/null +++ b/lib/active_merchant/billing/gateways/redsys_rest.rb @@ -0,0 +1,545 @@ +# 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 patagonia_365 tarjeta_sol] + 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' + } + + THREEDS_EXEMPTIONS = { + corporate_card: 'COR', + delegated_authentication: 'ATD', + low_risk: 'TRA', + low_value: 'LWV', + stored_credential: 'MIT', + trusted_merchant: 'NDF' + } + + # 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_stored_credentials(post, options) + add_threeds_exemption_data(post, 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_stored_credentials(post, options) + add_threeds_exemption_data(post, 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:) + 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) + merchant_parameters = filter_merchant_parameters(transcript) + + transcript. + gsub(%r((Ds_MerchantParameters=)(\w+)), '\1' + merchant_parameters.to_s + '\3'). + 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] || options[:three_ds_exemption_type] == 'moto' + + 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, payment_method) + year = sprintf('%.4i', payment_method.year) + month = sprintf('%.2i', payment_method.month) + + if payment_method.is_a?(NetworkTokenizationCreditCard) + post[:Ds_Merchant_TokenData] = { + tokenCryptogram: payment_method.payment_cryptogram, + expirationDate: "#{year[2..3]}#{month}", + token: payment_method.number + } + else + name = [payment_method.first_name, payment_method.last_name].join(' ').slice(0, 60) + post['DS_MERCHANT_TITULAR'] = CGI.escape(name) + post['DS_MERCHANT_PAN'] = payment_method.number + post['DS_MERCHANT_EXPIRYDATE'] = "#{year[2..3]}#{month}" + post['DS_MERCHANT_CVV2'] = payment_method.verification_value if payment_method.verification_value.present? + end + 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 + + begin + response = JSON.parse(Base64.decode64(payload)).transform_keys!(&:downcase).with_indifferent_access + rescue JSON::ParserError + response = JSON.parse(Base64.urlsafe_decode64(payload)).transform_keys!(&:downcase).with_indifferent_access + end + + 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, post, options), + 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 add_stored_credentials(post, options) + return unless stored_credential = options[:stored_credential] + + post[:DS_MERCHANT_COF_INI] = stored_credential[:initial_transaction] ? 'S' : 'N' + + post[:DS_MERCHANT_COF_TYPE] = case stored_credential[:reason_type] + when 'recurring' + 'R' + when 'installment' + 'I' + else + 'C' + end + + post[:DS_MERCHANT_IDENTIFIER] = 'REQUIRED' if stored_credential[:initiator] == 'cardholder' + post[:DS_MERCHANT_COF_TXNID] = stored_credential[:network_transaction_id] if stored_credential[:network_transaction_id] + end + + def add_threeds_exemption_data(post, options) + return unless options[:three_ds_exemption_type] + + if options[:three_ds_exemption_type] == 'moto' + post[:DS_MERCHANT_DIRECTPAYMENT] = 'MOTO' + else + exemption = options[:three_ds_exemption_type].to_sym + post[:DS_MERCHANT_EXCEP_SCA] = THREEDS_EXEMPTIONS[exemption] + end + 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) || [195, 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, post, options) + array_resp = if success_from(response, options) && options[:execute_threed] # 3DS Case + [post[:DS_MERCHANT_AMOUNT], post[:DS_MERCHANT_CURRENCY]] + else + [response[:ds_amount], response[:ds_currency]] + end + ([response[:ds_order]] << array_resp).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 + + def filter_merchant_parameters(transcript) + # Enhancement, the gateway response with base64 and it contians sensible data. + # Decode + Scrub + Encode the returned sensitive dat. + pre_filter_data = transcript.match(%r(Ds_MerchantParameters=(\w+))) + return unless pre_filter_data + + decoded_pre_filter_data = Base64.decode64(pre_filter_data[1]) + + filter_data = decoded_pre_filter_data. + gsub(%r((PAN\":\")(\d+)), '\1[FILTERED]'). + gsub(%r((CVV2\":\")(\d+)), '\1[FILTERED]'). + gsub(%r((token\":\")(\d+)), '\1[FILTERED]'). + gsub(%r((tokenCryptogram\":\")([^*]*?")), '\1[FILTERED]"') + + Base64.strict_encode64(filter_data) + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/s5.rb b/lib/active_merchant/billing/gateways/s5.rb index 6e5ec5e80a7..1097476dbad 100644 --- a/lib/active_merchant/billing/gateways/s5.rb +++ b/lib/active_merchant/billing/gateways/s5.rb @@ -1,14 +1,14 @@ require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class S5Gateway < Gateway self.test_url = 'https://test.ctpe.io/payment/ctpe' self.live_url = 'https://ctpe.io/payment/ctpe' self.supported_countries = ['DK'] self.default_currency = 'EUR' - self.supported_cardtypes = [:visa, :master, :maestro] + self.supported_cardtypes = %i[visa master maestro] self.homepage_url = 'http://www.s5.dk/' self.display_name = 'S5' @@ -22,12 +22,12 @@ class S5Gateway < Gateway 'store' => 'CC.RG' } - def initialize(options={}) + def initialize(options = {}) requires!(options, :sender, :channel, :login, :password) super end - def purchase(money, payment, options={}) + def purchase(money, payment, options = {}) request = build_xml_request do |xml| add_identification(xml, options) add_payment(xml, money, 'sale', options) @@ -39,7 +39,7 @@ def purchase(money, payment, options={}) commit(request) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) request = build_xml_request do |xml| add_identification(xml, options, authorization) add_payment(xml, money, 'refund', options) @@ -48,7 +48,7 @@ def refund(money, authorization, options={}) commit(request) end - def authorize(money, payment, options={}) + def authorize(money, payment, options = {}) request = build_xml_request do |xml| add_identification(xml, options) add_payment(xml, money, 'authonly', options) @@ -60,7 +60,7 @@ def authorize(money, payment, options={}) commit(request) end - def capture(money, authorization, options={}) + def capture(money, authorization, options = {}) request = build_xml_request do |xml| add_identification(xml, options, authorization) add_payment(xml, money, 'capture', options) @@ -69,7 +69,7 @@ def capture(money, authorization, options={}) commit(request) end - def void(authorization, options={}) + def void(authorization, options = {}) request = build_xml_request do |xml| add_identification(xml, options, authorization) add_payment(xml, nil, 'void', options) @@ -89,14 +89,13 @@ def store(payment, options = {}) commit(request) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } end end - def supports_scrubbing? true end @@ -129,21 +128,22 @@ 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}" xml.Brand payment_method.brand xml.Expiry(year: payment_method.year, month: payment_method.month) - xml.Verification payment_method.verification_value + xml.Verification payment_method.verification_value end + else + xml.Account(registration: payment_method) end end def add_customer(xml, creditcard, options) return unless creditcard.respond_to?(:number) + address = options[:billing_address] xml.Customer do xml.Contact do @@ -181,7 +181,7 @@ def add_recurrence_mode(xml, options) end def parse(body) - results = {} + results = {} xml = Nokogiri::XML(body) resp = xml.xpath('//Response/Transaction/Identification') resp.children.each do |element| diff --git a/lib/active_merchant/billing/gateways/safe_charge.rb b/lib/active_merchant/billing/gateways/safe_charge.rb index a2c8022655e..6cd899f98b5 100644 --- a/lib/active_merchant/billing/gateways/safe_charge.rb +++ b/lib/active_merchant/billing/gateways/safe_charge.rb @@ -1,29 +1,40 @@ require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class SafeChargeGateway < Gateway self.test_url = 'https://process.sandbox.safecharge.com/service.asmx/Process' self.live_url = 'https://process.safecharge.com/service.asmx/Process' - self.supported_countries = ['AT', 'BE', 'BG', 'CY', 'CZ', 'DE', 'DK', 'EE', 'GR', 'ES', 'FI', 'FR', 'HR', 'HU', 'IE', 'IS', 'IT', 'LI', 'LT', 'LU', 'LV', 'MT', 'NL', 'NO', 'PL', 'PT', 'RO', 'SE', 'SE', 'SI', 'SK', 'GB', 'US'] + self.supported_countries = %w[AT AU BE BG CA CY CZ DE DK EE GR ES FI FR GI HK HR HU IE IS IT LI LT LU LV MT MX NL NO PL PT RO SE SG SI SK GB US] self.default_currency = 'USD' - self.supported_cardtypes = [:visa, :master] + self.supported_cardtypes = %i[visa master] self.homepage_url = 'https://www.safecharge.com' self.display_name = 'SafeCharge' VERSION = '4.1.0' - def initialize(options={}) + def initialize(options = {}) requires!(options, :client_login_id, :client_password) super end - def purchase(money, payment, options={}) + def purchase(money, payment, options = {}) post = {} - post[:sg_APIType] = 1 if options[:three_d_secure] - trans_type = options[:three_d_secure] ? 'Sale3D' : 'Sale' + + # Determine if 3DS is requested, or there is standard external MPI data + if options[:three_d_secure] + if options[:three_d_secure].is_a?(Hash) + add_external_mpi_data(post, options) + else + post[:sg_APIType] = 1 + trans_type = 'Sale3D' + end + end + + trans_type ||= 'Sale' + add_transaction_data(trans_type, post, money, options) add_payment(post, payment, options) add_customer_details(post, payment, options) @@ -31,8 +42,10 @@ def purchase(money, payment, options={}) commit(post) end - def authorize(money, payment, options={}) + def authorize(money, payment, options = {}) post = {} + + add_external_mpi_data(post, options) if options[:three_d_secure]&.is_a?(Hash) add_transaction_data('Auth', post, money, options) add_payment(post, payment, options) add_customer_details(post, payment, options) @@ -40,46 +53,50 @@ def authorize(money, payment, options={}) commit(post) end - def capture(money, authorization, options={}) + def capture(money, authorization, options = {}) post = {} auth, transaction_id, token, exp_month, exp_year, _, original_currency = authorization.split('|') - add_transaction_data('Settle', post, money, (options.merge!({currency: original_currency}))) + add_transaction_data('Settle', post, money, options.merge!({ currency: original_currency })) 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_Email] = options[:email] commit(post) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) post = {} auth, transaction_id, token, exp_month, exp_year, _, original_currency = authorization.split('|') - add_transaction_data('Credit', post, money, (options.merge!({currency: original_currency}))) + 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 - def credit(money, payment, options={}) + def credit(money, payment, options = {}) post = {} + add_payment(post, payment, options) add_transaction_data('Credit', post, money, options) - post[:sg_CreditType] = 1 + add_customer_details(post, payment, options) + + options[:unreferenced_refund].to_s == 'true' ? post[:sg_CreditType] = 2 : post[:sg_CreditType] = 1 commit(post) end - def void(authorization, options={}) + def void(authorization, options = {}) post = {} auth, transaction_id, token, exp_month, exp_year, original_amount, original_currency = authorization.split('|') - add_transaction_data('Void', post, (original_amount.to_f * 100), (options.merge!({currency: original_currency}))) + add_transaction_data('Void', post, (original_amount.to_f * 100), options.merge!({ currency: original_currency })) post[:sg_CreditType] = 2 post[:sg_AuthCode] = auth post[:sg_TransactionID] = transaction_id @@ -90,11 +107,8 @@ def void(authorization, options={}) commit(post) end - def verify(credit_card, options={}) - MultiResponse.run(:use_first_response) do |r| - r.process { authorize(100, credit_card, options) } - r.process(:ignore_result) { void(r.authorization, options) } - end + def verify(credit_card, options = {}) + authorize(0, credit_card, options) end def supports_scrubbing? @@ -111,9 +125,11 @@ def scrub(transcript) private def add_transaction_data(trans_type, post, money, options) + currency = options[:currency] || currency(money) + post[:sg_TransType] = trans_type - post[:sg_Currency] = (options[:currency] || currency(money)) - post[:sg_Amount] = amount(money) + post[:sg_Currency] = currency + post[:sg_Amount] = localized_amount(money, currency) post[:sg_ClientLoginID] = @options[:client_login_id] post[:sg_ClientPassword] = @options[:client_password] post[:sg_ResponseFormat] = '4' @@ -128,32 +144,75 @@ def add_transaction_data(trans_type, post, money, options) post[:sg_Descriptor] = options[:merchant_descriptor] if options[:merchant_descriptor] post[:sg_MerchantPhoneNumber] = options[:merchant_phone_number] if options[:merchant_phone_number] post[:sg_MerchantName] = options[:merchant_name] if options[:merchant_name] + post[:sg_ProductID] = options[:product_id] if options[:product_id] + post[:sg_NotUseCVV] = options[:not_use_cvv].to_s == 'true' ? 1 : 0 unless options[:not_use_cvv].nil? end - def add_payment(post, payment, options={}) - post[:sg_NameOnCard] = payment.name - post[:sg_CardNumber] = payment.number - post[:sg_ExpMonth] = format(payment.month, :two_digits) - post[:sg_ExpYear] = format(payment.year, :two_digits) + def add_payment(post, payment, options = {}) + 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] - post[:sg_Zip] = address[:zip] if address[:zip] - post[:sg_Country] = address[:country] if address[:country] - post[:sg_Phone] = address[:phone] if address[:phone] + post[:sg_Zip] = address[:zip] if address[:zip] + post[:sg_Country] = address[:country] if address[:country] + post[:sg_Phone] = address[:phone] if address[:phone] + post[:sg_middleName] = options[:middle_name] if options[:middle_name] + post[:sg_doCardHolderNameVerification] = options[:card_holder_verification] if options[:card_holder_verification] end post[:sg_Email] = options[:email] end + def add_external_mpi_data(post, options) + post[:sg_ECI] = options[:three_d_secure][:eci] if options[:three_d_secure][:eci] + post[:sg_CAVV] = options[:three_d_secure][:cavv] if options[:three_d_secure][:cavv] + post[:sg_dsTransID] = options[:three_d_secure][:ds_transaction_id] if options[:three_d_secure][:ds_transaction_id] + post[:sg_threeDSProtocolVersion] = options[:three_d_secure][:ds_transaction_id] ? '2' : '1' + post[:sg_Xid] = options[:three_d_secure][:xid] + post[:sg_IsExternalMPI] = 1 + post[:sg_EnablePartialApproval] = options[:is_partial_approval] + post[:sg_challengePreference] = options[:three_d_secure][:challenge_preference] if options[:three_d_secure][:challenge_preference] + end + def parse(xml) response = {} @@ -181,7 +240,7 @@ def childnode_to_response(response, childnode) end def element_name_to_symbol(response, childnode) - name = "#{childnode.name.downcase}" + name = childnode.name.downcase response[name.to_sym] = childnode.text end @@ -207,6 +266,7 @@ def success_from(response) def message_from(response) return 'Success' if success_from(response) + response[:reason_codes] || response[:reason] end @@ -226,9 +286,9 @@ def split_authorization(authorization) auth_code, transaction_id, token, month, year, original_amount = authorization.split('|') { - auth_code: auth_code, - transaction_id: transaction_id, - token: token, + auth_code:, + transaction_id:, + token:, exp_month: month, exp_year: year, original_amount: amount(original_amount.to_f * 100) @@ -240,20 +300,19 @@ def post_data(params) params.map do |key, value| next if value != false && value.blank? + "#{key}=#{CGI.escape(value.to_s)}" end.compact.join('&') end def error_code_from(response) - unless success_from(response) - response[:ex_err_code] || response[:err_code] - end + response[:ex_err_code] || response[:err_code] unless success_from(response) end def underscore(camel_cased_word) camel_cased_word.to_s.gsub(/::/, '/'). - gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). - gsub(/([a-z\d])([A-Z])/,'\1_\2'). + gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2'). + gsub(/([a-z\d])([A-Z])/, '\1_\2'). tr('-', '_'). downcase end diff --git a/lib/active_merchant/billing/gateways/sage.rb b/lib/active_merchant/billing/gateways/sage.rb index f4a029284dc..e586263f9c1 100644 --- a/lib/active_merchant/billing/gateways/sage.rb +++ b/lib/active_merchant/billing/gateways/sage.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class SageGateway < Gateway include Empty @@ -7,20 +7,20 @@ class SageGateway < Gateway self.homepage_url = 'Sage Payment Solutions' self.live_url = 'https://www.sagepayments.net/cgi-bin' - self.supported_countries = ['US', 'CA'] - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :jcb, :diners_club] + self.supported_countries = %w[US CA] + self.supported_cardtypes = %i[visa master american_express discover jcb diners_club] TRANSACTIONS = { - :purchase => '01', - :authorization => '02', - :capture => '11', - :void => '04', - :credit => '06', - :refund => '10' + purchase: '01', + authorization: '02', + capture: '11', + void: '04', + credit: '06', + refund: '10' } SOURCE_CARD = 'bankcard' - SOURCE_ECHECK = 'virtual_check' + SOURCE_ECHECK = 'virtual_check' def initialize(options = {}) requires!(options, :login, :password) @@ -77,7 +77,7 @@ def credit(money, payment_method, options = {}) commit(:credit, post, source) end - def refund(money, reference, options={}) + def refund(money, reference, options = {}) post = {} add_reference(post, reference) add_transaction_data(post, money, options) @@ -97,7 +97,7 @@ def supports_scrubbing? end def scrub(transcript) - force_utf8(transcript). + force_utf8(transcript). gsub(%r((M_id=)[^&]*), '\1[FILTERED]'). gsub(%r((M_key=)[^&]*), '\1[FILTERED]'). gsub(%r((C_cardnumber=)[^&]*), '\1[FILTERED]'). @@ -115,7 +115,8 @@ def scrub(transcript) # use the same method as in pay_conex def force_utf8(string) return nil unless string - binary = string.encode('BINARY', invalid: :replace, undef: :replace, replace: '?') # Needed for Ruby 2.0 since #encode is a no-op if the string is already UTF-8. It's not needed for Ruby 2.1 and up since it's not a no-op there. + + binary = string.encode('BINARY', invalid: :replace, undef: :replace, replace: '?') # Needed for Ruby 2.0 since #encode is a no-op if the string is already UTF-8. It's not needed for Ruby 2.1 and up since it's not a no-op there. binary.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?') end @@ -179,9 +180,9 @@ def parse(data, source) def parse_check(data) response = {} - response[:success] = data[1,1] - response[:code] = data[2,6].strip - response[:message] = data[8,32].strip + response[:success] = data[1, 1] + response[:code] = data[2, 6].strip + response[:message] = data[8, 32].strip response[:risk] = data[40, 2] response[:reference] = data[42, 10] @@ -194,9 +195,9 @@ def parse_check(data) def parse_credit_card(data) response = {} - response[:success] = data[1,1] - response[:code] = data[2,6] - response[:message] = data[8,32].strip + response[:success] = data[1, 1] + response[:code] = data[2, 6] + response[:message] = data[8, 32].strip response[:front_end] = data[40, 2] response[:cvv_result] = data[42, 1] response[:avs_result] = data[43, 1].strip @@ -214,7 +215,7 @@ def add_invoice(post, options) end def add_reference(post, reference) - ref, _ = reference.to_s.split(';') + ref, = reference.to_s.split(';') post[:T_reference] = ref end @@ -227,7 +228,7 @@ def add_customer_data(post, options) end def add_addresses(post, options) - billing_address = options[:billing_address] || options[:address] || {} + billing_address = options[:billing_address] || options[:address] || {} post[:C_address] = billing_address[:address1] post[:C_city] = billing_address[:city] @@ -259,11 +260,14 @@ 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, - :test => test?, - :authorization => authorization_from(response, source), - :avs_result => { :code => response[:avs_result] }, - :cvv_result => response[:cvv_result] + 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] ) end @@ -296,7 +300,6 @@ def vault end class SageVault - def initialize(options, gateway) @live_url = 'https://www.sagepayments.net/web_services/wsVault/wsVault.asmx' @options = options @@ -364,9 +367,12 @@ def exp_date(credit_card) end def commit(action, request) - response = parse(@gateway.ssl_post(@live_url, - build_soap_request(action, request), - build_headers(action)) + response = parse( + @gateway.ssl_post( + @live_url, + build_soap_request(action, request), + build_headers(action) + ) ) case action @@ -378,7 +384,10 @@ def commit(action, request) message = success ? 'Succeeded' : 'Failed' end - Response.new(success, message, response, + Response.new( + success, + message, + response, authorization: response[:guid] ) end diff --git a/lib/active_merchant/billing/gateways/sage_pay.rb b/lib/active_merchant/billing/gateways/sage_pay.rb index 97282fc835f..d7fab6d6104 100644 --- a/lib/active_merchant/billing/gateways/sage_pay.rb +++ b/lib/active_merchant/billing/gateways/sage_pay.rb @@ -1,40 +1,38 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class SagePayGateway < Gateway cattr_accessor :simulate self.simulate = false 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' TRANSACTIONS = { - :purchase => 'PAYMENT', - :credit => 'REFUND', - :authorization => 'DEFERRED', - :capture => 'RELEASE', - :void => 'VOID', - :abort => 'ABORT', - :store => 'TOKEN', - :unstore => 'REMOVETOKEN', - :repeat => 'REPEAT' + purchase: 'PAYMENT', + credit: 'REFUND', + authorization: 'DEFERRED', + capture: 'RELEASE', + void: 'VOID', + abort: 'ABORT', + store: 'TOKEN', + unstore: 'REMOVETOKEN', + repeat: 'REPEAT' } CREDIT_CARDS = { - :visa => 'VISA', - :master => 'MC', - :delta => 'DELTA', - :solo => 'SOLO', - :switch => 'MAESTRO', - :maestro => 'MAESTRO', - :american_express => 'AMEX', - :electron => 'UKE', - :diners_club => 'DC', - :jcb => 'JCB' + visa: 'VISA', + master: 'MC', + delta: 'DELTA', + maestro: 'MAESTRO', + american_express: 'AMEX', + electron: 'UKE', + diners_club: 'DC', + jcb: 'JCB' } AVS_CODE = { @@ -54,8 +52,8 @@ class SagePayGateway < Gateway OPTIONAL_REQUEST_FIELDS = { paypal_callback_url: :PayPalCallbackURL, basket: :Basket, - gift_aid_payment: :GiftAidPayment , - apply_avscv2: :ApplyAVSCV2 , + gift_aid_payment: :GiftAidPayment, + apply_avscv2: :ApplyAVSCV2, apply_3d_secure: :Apply3DSecure, account_type: :AccountType, billing_agreement: :BillingAgreement, @@ -65,14 +63,14 @@ class SagePayGateway < Gateway vendor_data: :VendorData, language: :Language, website: :Website, - recipient_account_number: :FIRecipientAcctNumber , - recipient_surname: :FIRecipientSurname , - recipient_postcode: :FIRecipientPostcode , + recipient_account_number: :FIRecipientAcctNumber, + recipient_surname: :FIRecipientSurname, + recipient_postcode: :FIRecipientPostcode, recipient_dob: :FIRecipientDoB } - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :jcb, :switch, :solo, :maestro, :diners_club] - self.supported_countries = ['GB', 'IE'] + self.supported_countries = %w[GB IE] + self.supported_cardtypes = %i[visa master american_express discover jcb maestro diners_club] self.default_currency = 'GBP' self.homepage_url = 'http://www.sagepay.com' @@ -80,6 +78,7 @@ class SagePayGateway < Gateway def initialize(options = {}) requires!(options, :login) + @protocol_version = options.fetch(:protocol_version, '3.00') super end @@ -88,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) @@ -103,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) @@ -117,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) @@ -126,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) @@ -138,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) @@ -152,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) @@ -160,11 +169,12 @@ def store(credit_card, options = {}) def unstore(token, options = {}) post = {} + add_override_protocol_version(options) add_token(post, token) commit(:unstore, post) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -183,6 +193,59 @@ def scrub(transcript) end 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 @@ -214,18 +277,18 @@ def add_related_reference(post, identification) def add_amount(post, money, options) currency = options[:currency] || currency(money) - add_pair(post, :Amount, localized_amount(money, currency), :required => true) - add_pair(post, :Currency, currency, :required => true) + add_pair(post, :Amount, localized_amount(money, currency), required: true) + add_pair(post, :Currency, currency, required: true) end def add_currency(post, money, options) currency = options[:currency] || currency(money) - add_pair(post, :Currency, currency, :required => true) + add_pair(post, :Currency, currency, required: true) end # doesn't actually use the currency -- dodgy! def add_release_amount(post, money, options) - add_pair(post, :ReleaseAmount, amount(money), :required => true) + add_pair(post, :ReleaseAmount, amount(money), required: true) end def add_customer_data(post, options) @@ -249,7 +312,7 @@ def add_address(post, options) add_pair(post, :BillingAddress1, truncate(billing_address[:address1], 100)) add_pair(post, :BillingAddress2, truncate(billing_address[:address2], 100)) add_pair(post, :BillingCity, truncate(billing_address[:city], 40)) - add_pair(post, :BillingState, truncate(billing_address[:state], 2)) if is_usa(billing_address[:country]) + add_pair(post, :BillingState, truncate(billing_address[:state], 2)) if usa?(billing_address[:country]) add_pair(post, :BillingCountry, truncate(billing_address[:country], 2)) add_pair(post, :BillingPhone, sanitize_phone(billing_address[:phone])) add_pair(post, :BillingPostCode, truncate(billing_address[:zip], 10)) @@ -262,7 +325,7 @@ def add_address(post, options) add_pair(post, :DeliveryAddress1, truncate(shipping_address[:address1], 100)) add_pair(post, :DeliveryAddress2, truncate(shipping_address[:address2], 100)) add_pair(post, :DeliveryCity, truncate(shipping_address[:city], 40)) - add_pair(post, :DeliveryState, truncate(shipping_address[:state], 2)) if is_usa(shipping_address[:country]) + add_pair(post, :DeliveryState, truncate(shipping_address[:state], 2)) if usa?(shipping_address[:country]) add_pair(post, :DeliveryCountry, truncate(shipping_address[:country], 2)) add_pair(post, :DeliveryPhone, sanitize_phone(shipping_address[:phone])) add_pair(post, :DeliveryPostCode, truncate(shipping_address[:zip], 10)) @@ -270,7 +333,7 @@ def add_address(post, options) end def add_invoice(post, options) - add_pair(post, :VendorTxCode, sanitize_order_id(options[:order_id]), :required => true) + add_pair(post, :VendorTxCode, sanitize_order_id(options[:order_id]), required: true) add_pair(post, :Description, truncate(options[:description] || options[:order_id], 100)) end @@ -287,15 +350,10 @@ def add_payment_method(post, payment_method, options) end def add_credit_card(post, credit_card) - add_pair(post, :CardHolder, truncate(credit_card.name, 50), :required => true) - add_pair(post, :CardNumber, credit_card.number, :required => true) + add_pair(post, :CardHolder, truncate(credit_card.name, 50), required: true) + add_pair(post, :CardNumber, credit_card.number, required: true) - add_pair(post, :ExpiryDate, format_date(credit_card.month, credit_card.year), :required => true) - - if requires_start_date_or_issue_number?(credit_card) - add_pair(post, :StartDate, format_date(credit_card.start_month, credit_card.start_year)) - add_pair(post, :IssueNumber, credit_card.issue_number) - end + add_pair(post, :ExpiryDate, format_date(credit_card.month, credit_card.year), required: true) add_pair(post, :CardType, map_card_type(credit_card)) add_pair(post, :CV2, credit_card.verification_value) @@ -318,11 +376,12 @@ def sanitize_order_id(order_id) def sanitize_phone(phone) return nil unless phone + cleansed = phone.to_s.gsub(/[^0-9+]/, '') truncate(cleansed, 20) end - def is_usa(country) + def usa?(country) truncate(country, 2) == 'US' end @@ -349,16 +408,19 @@ def format_date(month, year) end def commit(action, parameters) - response = parse( ssl_post(url_for(action), post_data(action, parameters)) ) - - 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'] ], + response = parse(ssl_post(url_for(action), post_data(action, parameters))) + + 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 @@ -367,11 +429,11 @@ def authorization_from(response, params, action) when :store response['Token'] else - [ params[:VendorTxCode], + [params[:VendorTxCode], response['VPSTxId'] || params[:VPSTxId], response['TxAuthNo'], response['SecurityKey'] || params[:SecurityKey], - action ].join(';') + action].join(';') end end @@ -385,37 +447,42 @@ def url_for(action) end def build_url(action) - endpoint = case action + endpoint = + case action when :purchase, :authorization then 'vspdirect-register' when :store then 'directtoken' else TRANSACTIONS[action].downcase - end + end "#{test? ? self.test_url : self.live_url}/#{endpoint}.vsp" end def build_simulator_url(action) - endpoint = [ :purchase, :authorization ].include?(action) ? 'VSPDirectGateway.asp' : "VSPServerGateway.asp?Service=Vendor#{TRANSACTIONS[action].capitalize}Tx" + endpoint = %i[purchase authorization].include?(action) ? 'VSPDirectGateway.asp' : "VSPServerGateway.asp?Service=Vendor#{TRANSACTIONS[action].capitalize}Tx" "#{self.simulator_url}/#{endpoint}" end def message_from(response) - response['Status'] == APPROVED ? 'Success' : (response['StatusDetail'] || 'Unspecified error') # simonr 20080207 can't actually get non-nil blanks, so this is shorter + response['Status'] == APPROVED ? 'Success' : (response['StatusDetail'] || 'Unspecified error') # simonr 20080207 can't actually get non-nil blanks, so this is shorter end def post_data(action, parameters = {}) parameters.update( - :Vendor => @options[:login], - :TxType => TRANSACTIONS[action], - :VPSProtocol => @options.fetch(:protocol_version, '3.00') + Vendor: @options[:login], + TxType: TRANSACTIONS[action], + VPSProtocol: @protocol_version ) - if(application_id && (application_id != Gateway.application_id)) - parameters.update(:ReferrerID => application_id) - end + parameters.update(ReferrerID: application_id) if application_id && (application_id != Gateway.application_id) 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 @@ -433,9 +500,9 @@ def add_pair(post, key, value, options = {}) def past_purchase_reference?(payment_method) return false unless payment_method.is_a?(String) - payment_method.split(';').last == 'purchase' + + %w(purchase repeat).include?(payment_method.split(';').last) end end - end end diff --git a/lib/active_merchant/billing/gateways/sallie_mae.rb b/lib/active_merchant/billing/gateways/sallie_mae.rb index 510273a1fc4..efdc5538137 100644 --- a/lib/active_merchant/billing/gateways/sallie_mae.rb +++ b/lib/active_merchant/billing/gateways/sallie_mae.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class SallieMaeGateway < Gateway self.live_url = self.test_url = 'https://trans.salliemae.com/cgi-bin/process.cgi' @@ -7,7 +7,7 @@ class SallieMaeGateway < Gateway self.supported_countries = ['US'] # The card types supported by the payment gateway - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] # The homepage URL of the gateway self.homepage_url = 'http://www.salliemae.com/' @@ -120,9 +120,12 @@ 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, - :test => test?, - :authorization => response['refcode'] + Response.new( + successful?(response), + message_from(response), + response, + test: test?, + authorization: response['refcode'] ) end @@ -140,4 +143,3 @@ def message_from(response) end end end - diff --git a/lib/active_merchant/billing/gateways/secure_net.rb b/lib/active_merchant/billing/gateways/secure_net.rb index 0bf4f8ff55b..1de3b6218e9 100644 --- a/lib/active_merchant/billing/gateways/secure_net.rb +++ b/lib/active_merchant/billing/gateways/secure_net.rb @@ -1,25 +1,24 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class SecureNetGateway < Gateway - API_VERSION = '4.0' TRANSACTIONS = { - :auth_only => '0000', - :auth_capture => '0100', - :prior_auth_capture => '0200', - :void => '0400', - :credit => '0500' + auth_only: '0000', + auth_capture: '0100', + prior_auth_capture: '0200', + void: '0400', + credit: '0500' } XML_ATTRIBUTES = { - 'xmlns' => 'http://gateway.securenet.com/API/Contracts', - 'xmlns:i' => 'http://www.w3.org/2001/XMLSchema-instance' - } + 'xmlns' => 'http://gateway.securenet.com/API/Contracts', + 'xmlns:i' => 'http://www.w3.org/2001/XMLSchema-instance' + } NIL_ATTRIBUTE = { 'i:nil' => 'true' } self.supported_countries = ['US'] - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'http://www.securenet.com/' self.display_name = 'SecureNet' @@ -28,8 +27,8 @@ class SecureNetGateway < Gateway APPROVED, DECLINED = 1, 2 - CARD_CODE_ERRORS = %w( N S ) - AVS_ERRORS = %w( A E N R W Z ) + CARD_CODE_ERRORS = %w(N S) + AVS_ERRORS = %w(A E N R W Z) def initialize(options = {}) requires!(options, :login, :password) @@ -80,11 +79,14 @@ def commit(request) data = ssl_post(url, xml, 'Content-Type' => 'text/xml') response = parse(data) - 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] + 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] ) end @@ -136,13 +138,9 @@ def add_credit_card(xml, creditcard) end def add_customer_data(xml, options) - if options.has_key? :customer - xml.tag! 'CUSTOMERID', options[:customer] - end + xml.tag! 'CUSTOMERID', options[:customer] if options.has_key? :customer - if options.has_key? :ip - xml.tag! 'CUSTOMERIP', options[:ip] - end + xml.tag! 'CUSTOMERIP', options[:ip] if options.has_key? :ip end def add_address(xml, creditcard, options) @@ -161,7 +159,7 @@ def add_address(xml, creditcard, options) xml.tag! 'FIRSTNAME', creditcard.first_name xml.tag! 'LASTNAME', creditcard.last_name xml.tag! 'PHONE', address[:phone].to_s - xml.tag! 'STATE', address[:state].blank? ? 'n/a' : address[:state] + xml.tag! 'STATE', address[:state].blank? ? 'n/a' : address[:state] xml.tag! 'ZIP', address[:zip].to_s end end @@ -182,14 +180,13 @@ def add_address(xml, creditcard, options) xml.tag! 'LASTNAME', address[:last_name].to_s end - xml.tag! 'STATE', address[:state].blank? ? 'n/a' : address[:state] + xml.tag! 'STATE', address[:state].blank? ? 'n/a' : address[:state] xml.tag! 'ZIP', address[:zip].to_s end else xml.tag!('CUSTOMER_SHIP', NIL_ATTRIBUTE) do end end - end def add_merchant_key(xml, options) @@ -248,7 +245,7 @@ def parse(xml) def recurring_parse_element(response, node) if node.has_elements? - node.elements.each{|e| recurring_parse_element(response, e) } + node.elements.each { |e| recurring_parse_element(response, e) } else response[node.name.underscore.to_sym] = node.text end @@ -262,7 +259,6 @@ def split_authorization(authorization) def build_authorization(response) [response[:transactionid], response[:transactionamount], response[:last4_digits]].join('|') end - end end end diff --git a/lib/active_merchant/billing/gateways/secure_pay.rb b/lib/active_merchant/billing/gateways/secure_pay.rb index 68d32c5b5c2..d8cf41ebbe6 100644 --- a/lib/active_merchant/billing/gateways/secure_pay.rb +++ b/lib/active_merchant/billing/gateways/secure_pay.rb @@ -1,7 +1,7 @@ require 'active_merchant/billing/gateways/authorize_net' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class SecurePayGateway < Gateway API_VERSION = '3.1' @@ -17,12 +17,12 @@ class SecurePayGateway < Gateway self.default_currency = 'USD' self.supported_countries = %w(US CA GB AU) - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb] + self.supported_cardtypes = %i[visa master american_express discover diners_club jcb] self.homepage_url = 'http://www.securepay.com/' self.display_name = 'SecurePay' - CARD_CODE_ERRORS = %w( N S ) - AVS_ERRORS = %w( A E N R W Z ) + CARD_CODE_ERRORS = %w(N S) + AVS_ERRORS = %w(A E N R W Z) AVS_REASON_CODES = %w(27 45) TRANSACTION_ALREADY_ACTIONED = %w(310 311) @@ -56,12 +56,15 @@ def commit(action, money, parameters) message = message_from(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] + 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] ) end @@ -76,17 +79,16 @@ 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], - :avs_result_code => fields[AVS_RESULT_CODE], - :transaction_id => fields[TRANSACTION_ID], - :card_code => fields[CARD_CODE_RESPONSE_CODE], - :authorization_code => fields[AUTHORIZATION_CODE], - :cardholder_authentication_code => fields[CARDHOLDER_AUTH_CODE] + { + response_code: fields[RESPONSE_CODE].to_i, + response_reason_code: fields[RESPONSE_REASON_CODE], + response_reason_text: fields[RESPONSE_REASON_TEXT], + avs_result_code: fields[AVS_RESULT_CODE], + transaction_id: fields[TRANSACTION_ID], + card_code: fields[CARD_CODE_RESPONSE_CODE], + authorization_code: fields[AUTHORIZATION_CODE], + cardholder_authentication_code: fields[CARDHOLDER_AUTH_CODE] } - results end def post_data(action, parameters = {}) @@ -102,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) @@ -115,7 +116,7 @@ def add_invoice(post, options) post[:description] = options[:description] end - def add_creditcard(post, creditcard, options={}) + def add_creditcard(post, creditcard, options = {}) post[:card_num] = creditcard.number post[:card_code] = creditcard.verification_value if creditcard.verification_value? post[:exp_date] = expdate(creditcard) @@ -123,7 +124,7 @@ def add_creditcard(post, creditcard, options={}) post[:last_name] = creditcard.last_name end - def add_payment_source(params, source, options={}) + def add_payment_source(params, source, options = {}) add_creditcard(params, source, options) end @@ -137,17 +138,11 @@ def add_customer_data(post, options) post[:cust_id] = options[:customer] if Float(options[:customer]) rescue nil end - if options.has_key? :ip - post[:customer_ip] = options[:ip] - end + post[:customer_ip] = options[:ip] if options.has_key? :ip - if options.has_key? :cardholder_authentication_value - post[:cardholder_authentication_value] = options[:cardholder_authentication_value] - end + post[:cardholder_authentication_value] = options[:cardholder_authentication_value] if options.has_key? :cardholder_authentication_value - if options.has_key? :authentication_indicator - post[:authentication_indicator] = options[:authentication_indicator] - end + post[:authentication_indicator] = options[:authentication_indicator] if options.has_key? :authentication_indicator end # x_duplicate_window won't be sent by default, because sending it changes the response. @@ -165,7 +160,7 @@ def add_address(post, options) post[:zip] = address[:zip].to_s post[:city] = address[:city].to_s post[:country] = address[:country].to_s - post[:state] = address[:state].blank? ? 'n/a' : address[:state] + post[:state] = address[:state].blank? ? 'n/a' : address[:state] end if address = options[:shipping_address] @@ -177,16 +172,14 @@ def add_address(post, options) post[:ship_to_zip] = address[:zip].to_s post[:ship_to_city] = address[:city].to_s post[:ship_to_country] = address[:country].to_s - post[:ship_to_state] = address[:state].blank? ? 'n/a' : address[:state] + post[:ship_to_state] = address[:state].blank? ? 'n/a' : address[:state] end end def message_from(results) if results[:response_code] == DECLINED - return CVVResult.messages[ results[:card_code] ] if CARD_CODE_ERRORS.include?(results[:card_code]) - if AVS_REASON_CODES.include?(results[:response_reason_code]) && AVS_ERRORS.include?(results[:avs_result_code]) - return AVSResult.messages[ results[:avs_result_code] ] - end + return CVVResult.messages[results[:card_code]] if CARD_CODE_ERRORS.include?(results[:card_code]) + return AVSResult.messages[results[:avs_result_code]] if AVS_REASON_CODES.include?(results[:response_reason_code]) && AVS_ERRORS.include?(results[:avs_result_code]) end (results[:response_reason_text] ? results[:response_reason_text].chomp('.') : '') @@ -198,4 +191,3 @@ def split(response) end end end - diff --git a/lib/active_merchant/billing/gateways/secure_pay_au.rb b/lib/active_merchant/billing/gateways/secure_pay_au.rb index f4e569253a3..4847e1d80d8 100644 --- a/lib/active_merchant/billing/gateways/secure_pay_au.rb +++ b/lib/active_merchant/billing/gateways/secure_pay_au.rb @@ -1,21 +1,21 @@ require 'rexml/document' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class SecurePayAuGateway < Gateway API_VERSION = 'xml-4.2' PERIODIC_API_VERSION = 'spxml-3.0' class_attribute :test_periodic_url, :live_periodic_url - self.test_url = 'https://api.securepay.com.au/test/payment' + self.test_url = 'https://test.api.securepay.com.au/xmlapi/payment' self.live_url = 'https://api.securepay.com.au/xmlapi/payment' self.test_periodic_url = 'https://test.securepay.com.au/xmlapi/periodic' self.live_periodic_url = 'https://api.securepay.com.au/xmlapi/periodic' self.supported_countries = ['AU'] - self.supported_cardtypes = [:visa, :master, :american_express, :diners_club, :jcb] + self.supported_cardtypes = %i[visa master american_express diners_club jcb] # The homepage URL of the gateway self.homepage_url = 'http://securepay.com.au' @@ -23,9 +23,6 @@ class SecurePayAuGateway < Gateway # The name of the gateway self.display_name = 'SecurePay' - class_attribute :request_timeout - self.request_timeout = 60 - self.money_format = :cents self.default_currency = 'AUD' @@ -35,32 +32,36 @@ class SecurePayAuGateway < Gateway # 10 Preauthorise # 11 Preauth Complete (Advice) TRANSACTIONS = { - :purchase => 0, - :authorization => 10, - :capture => 11, - :void => 6, - :refund => 4 + purchase: 0, + authorization: 10, + capture: 11, + void: 6, + refund: 4 } PERIODIC_ACTIONS = { - :add_triggered => 'add', - :remove_triggered => 'delete', - :trigger => 'trigger' + add_triggered: 'add', + remove_triggered: 'delete', + trigger: 'trigger' } PERIODIC_TYPES = { - :add_triggered => 4, - :remove_triggered => nil, - :trigger => nil + add_triggered: 4, + remove_triggered: nil, + trigger: nil } - SUCCESS_CODES = [ '00', '08', '11', '16', '77' ] + SUCCESS_CODES = %w[00 08 11 16 77] def initialize(options = {}) requires!(options, :login, :password) super end + def request_timeout + @options[:request_timeout] || 60 + end + def purchase(money, credit_card_or_stored_id, options = {}) if credit_card_or_stored_id.respond_to?(:number) requires!(options, :order_id) @@ -181,11 +182,14 @@ def build_request(action, body) end 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, - :test => test?, - :authorization => authorization_from(response) + response = parse(ssl_post(test? ? self.test_url : self.live_url, build_request(action, request), { 'Content-Type' => 'text/xml; charset=utf-8' })) + + Response.new( + success?(response), + message_from(response), + response, + test: test?, + authorization: authorization_from(response) ) end @@ -204,6 +208,7 @@ def build_periodic_item(action, money, credit_card, options) end xml.tag! 'amount', amount(money) xml.tag! 'periodicType', PERIODIC_TYPES[action] if PERIODIC_TYPES[action] + xml.tag! 'transactionReference', options[:order_id] if options[:order_id] xml.target! end @@ -238,12 +243,14 @@ def build_periodic_request(body) def commit_periodic(request) my_request = build_periodic_request(request) - #puts my_request - response = parse(ssl_post(test? ? self.test_periodic_url : self.live_periodic_url, my_request)) - - Response.new(success?(response), message_from(response), response, - :test => test?, - :authorization => authorization_from(response) + response = parse(ssl_post(test? ? self.test_periodic_url : self.live_periodic_url, my_request, { 'Content-Type' => 'text/xml; charset=utf-8' })) + + Response.new( + success?(response), + message_from(response), + response, + test: test?, + authorization: authorization_from(response) ) end @@ -277,7 +284,7 @@ def parse(body) def parse_element(response, node) if node.has_elements? - node.elements.each{|element| parse_element(response, element) } + node.elements.each { |element| parse_element(response, element) } else response[node.name.underscore.to_sym] = node.text end diff --git a/lib/active_merchant/billing/gateways/secure_pay_tech.rb b/lib/active_merchant/billing/gateways/secure_pay_tech.rb index 5a6036afcd1..9cec69e2c82 100644 --- a/lib/active_merchant/billing/gateways/secure_pay_tech.rb +++ b/lib/active_merchant/billing/gateways/secure_pay_tech.rb @@ -1,8 +1,8 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class SecurePayTechGateway < Gateway class SecurePayTechPostData < PostData - self.required_fields = [ :OrderReference, :CardNumber, :CardExpiry, :CardHolderName, :CardType, :MerchantID, :MerchantKey, :Amount, :Currency ] + self.required_fields = %i[OrderReference CardNumber CardExpiry CardHolderName CardType MerchantID MerchantKey Amount Currency] end self.live_url = self.test_url = 'https://tx.securepaytech.com/web/HttpPostPurchase' @@ -21,7 +21,7 @@ class SecurePayTechPostData < PostData self.default_currency = 'NZD' self.supported_countries = ['NZ'] - self.supported_cardtypes = [:visa, :master, :american_express, :diners_club] + self.supported_cardtypes = %i[visa master american_express diners_club] self.homepage_url = 'http://www.securepaytech.com/' self.display_name = 'SecurePayTech' @@ -82,11 +82,14 @@ def parse(body) end 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, - :test => test?, - :authorization => response[:merchant_transaction_reference] + response = parse(ssl_post(self.live_url, post_data(action, post))) + + Response.new( + response[:result_code] == 1, + message_from(response), + response, + test: test?, + authorization: response[:merchant_transaction_reference] ) end @@ -102,4 +105,3 @@ def post_data(action, post) end end end - diff --git a/lib/active_merchant/billing/gateways/securion_pay.rb b/lib/active_merchant/billing/gateways/securion_pay.rb index 4d3035d3336..be3e3b16643 100644 --- a/lib/active_merchant/billing/gateways/securion_pay.rb +++ b/lib/active_merchant/billing/gateways/securion_pay.rb @@ -1,16 +1,15 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class SecurionPayGateway < Gateway self.test_url = 'https://api.securionpay.com/' self.live_url = 'https://api.securionpay.com/' - - self.supported_countries = %w(AL AD AT BY BE BG HR CY CZ RE DK EE IS FI FR DE GI GR HU IS IE IT IL LV LI LT LU - MK MT MD MC NL NO PL PT RO RU MA RS SK SI ES SE CH UA GB KI CI ME) + self.supported_countries = %w(AD BE BG CH CY CZ DE DK EE ES FI FO FR GI GL GR GS GT HR HU IE IS IT LI LR LT + LU LV MC MT MU MV MW NL NO PL RO SE SI) self.default_currency = 'USD' self.money_format = :cents - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :jcb, :diners_club] + self.supported_cardtypes = %i[visa master american_express discover jcb diners_club] self.homepage_url = 'https://securionpay.com/' self.display_name = 'SecurionPay' @@ -32,17 +31,17 @@ class SecurionPayGateway < Gateway 'expired_token' => STANDARD_ERROR_CODE[:card_declined] } - def initialize(options={}) + def initialize(options = {}) requires!(options, :secret_key) super end - def purchase(money, payment, options={}) + def purchase(money, payment, options = {}) post = create_post_for_auth_or_purchase(money, payment, options) commit('charges', post, options) end - def authorize(money, payment, options={}) + def authorize(money, payment, options = {}) post = create_post_for_auth_or_purchase(money, payment, options) post[:captured] = 'false' commit('charges', post, options) @@ -64,7 +63,7 @@ def void(authorization, options = {}) commit("charges/#{CGI.escape(authorization)}/refund", {}, options) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -74,11 +73,11 @@ def verify(credit_card, options={}) def store(credit_card, options = {}) if options[:customer_id].blank? MultiResponse.run() do |r| - #create charge object + # create charge object r.process { authorize(100, credit_card, options) } - #create customer and save card + # create customer and save card r.process { create_customer_add_card(r.authorization, options) } - #void the charge + # void the charge r.process(:ignore_result) { void(r.params['metadata']['chargeId'], options) } end else @@ -134,6 +133,7 @@ def create_post_for_auth_or_purchase(money, payment, options) add_creditcard(post, payment, options) add_customer(post, payment, options) add_customer_data(post, options) + add_external_three_ds(post, options) if options[:email] post[:metadata] = {} post[:metadata][:email] = options[:email] @@ -141,6 +141,40 @@ def create_post_for_auth_or_purchase(money, payment, options) post end + def add_external_three_ds(post, options) + return if options[:three_d_secure].blank? + + post[:threeDSecure] = { + external: { + version: options[:three_d_secure][:version], + authenticationValue: options[:three_d_secure][:cavv], + acsTransactionId: options[:three_d_secure][:acs_transaction_id], + status: options[:three_d_secure][:authentication_response_status], + eci: options[:three_d_secure][:eci] + }.merge(xid_or_ds_trans_id(options[:three_d_secure])) + } + end + + def xid_or_ds_trans_id(three_ds) + if three_ds[:version].to_f >= 2.0 + { dsTransactionId: three_ds[:ds_transaction_id] } + else + { xid: three_ds[:xid] } + end + end + + def validate_three_ds_params(three_ds) + errors = {} + supported_version = %w{1.0.2 2.1.0 2.2.0}.include?(three_ds[:version]) + supported_auth_response = ['Y', 'N', 'U', 'R', 'E', 'A', nil].include?(three_ds[:status]) + + errors[:three_ds_version] = 'ThreeDs version not supported' unless supported_version + errors[:auth_response] = 'Authentication response value not supported' unless supported_auth_response + errors.compact! + + errors.present? ? Response.new(false, 'ThreeDs data is invalid', errors) : nil + end + def add_amount(post, money, options, include_currency = false) currency = (options[:currency] || default_currency) post[:amount] = localized_amount(money, currency) @@ -159,14 +193,16 @@ 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 end def add_address(post, options) - return unless post[:card] && post[:card].kind_of?(Hash) + return unless post[:card]&.kind_of?(Hash) + if address = options[:billing_address] post[:card][:addressLine1] = address[:address1] if address[:address1] post[:card][:addressLine2] = address[:address2] if address[:address2] @@ -182,34 +218,49 @@ def parse(body) end def commit(url, parameters = nil, options = {}, method = nil) + if parameters.present? && parameters[:threeDSecure].present? + three_ds_errors = validate_three_ds_params(parameters[:threeDSecure][:external]) + return three_ds_errors if three_ds_errors + 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']), + 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) - begin - parse(raw_response) - rescue JSON::ParserError - json_error(raw_response) - end + parse(raw_response) + rescue JSON::ParserError + json_error(raw_response) end def post_data(params) @@ -217,6 +268,7 @@ def post_data(params) params.map do |key, value| next if value.blank? + if value.is_a?(Hash) h = {} value.each do |k, v| @@ -249,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' => { @@ -260,7 +312,7 @@ def json_error(raw_response) end def test? - (@options[:secret_key] && @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 new file mode 100644 index 00000000000..32a085bef96 --- /dev/null +++ b/lib/active_merchant/billing/gateways/shift4.rb @@ -0,0 +1,351 @@ +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class Shift4Gateway < Gateway + self.test_url = 'https://utgapi.shift4test.com/api/rest/v1/' + self.live_url = 'https://utg.shift4api.net/api/rest/v1/' + + self.supported_countries = %w(US CA CU HT DO PR JM TT GP MQ BS BB LC CW AW VC VI GD AG DM KY KN SX TC MF VG BQ AI BL MS) + self.default_currency = 'USD' + self.supported_cardtypes = %i[visa master american_express discover] + + self.homepage_url = 'https://shift4.com' + self.display_name = 'Shift4' + + RECURRING_TYPE_TRANSACTIONS = %w(recurring installment) + TRANSACTIONS_WITHOUT_RESPONSE_CODE = %w(accesstoken add) + SUCCESS_TRANSACTION_STATUS = %w(A) + DISALLOWED_ENTRY_MODE_ACTIONS = %w(capture refund add verify) + URL_POSTFIX_MAPPING = { + 'accesstoken' => 'credentials', + 'add' => 'tokens', + 'verify' => 'cards' + } + + def initialize(options = {}) + requires!(options, :client_guid, :auth_token) + @client_guid = options[:client_guid] + @auth_token = options[:auth_token] + @access_token = options[:access_token] + super + end + + def purchase(money, payment_method, options = {}) + post = {} + action = 'sale' + + payment_method = get_card_token(payment_method) if payment_method.is_a?(String) + add_datetime(post, options) + add_invoice(post, money, options) + add_clerk(post, options) + add_transaction(post, options) + add_card(action, post, payment_method, options) + add_card_present(post, options) + add_customer(post, payment_method, options) + + commit(action, post, options) + end + + def authorize(money, payment_method, options = {}) + post = {} + action = 'authorization' + + payment_method = get_card_token(payment_method) if payment_method.is_a?(String) + add_datetime(post, options) + add_invoice(post, money, options) + add_clerk(post, options) + add_transaction(post, options) + add_card(action, post, payment_method, options) + add_card_present(post, options) + add_customer(post, payment_method, options) + + commit(action, post, options) + end + + def capture(money, authorization, options = {}) + post = {} + action = 'capture' + options[:invoice] = get_invoice(authorization) + + add_datetime(post, options) + add_invoice(post, money, options) + add_clerk(post, options) + add_transaction(post, options) + add_card(action, post, get_card_token(authorization), options) + + commit(action, post, options) + end + + def refund(money, payment_method, options = {}) + post = {} + action = 'refund' + + add_datetime(post, options) + add_invoice(post, money, options) + add_clerk(post, options) + add_transaction(post, options) + card_token = payment_method.is_a?(CreditCard) ? get_card_token(payment_method) : payment_method + add_card(action, post, card_token, options) + add_card_present(post, options) + + commit(action, post, options) + end + + alias credit refund + + def void(authorization, options = {}) + options[:invoice] = get_invoice(authorization) + commit('invoice', {}, options) + end + + def verify(credit_card, options = {}) + post = {} + action = 'verify' + post[:transaction] = {} + + add_datetime(post, options) + add_card(action, post, credit_card, options) + add_customer(post, credit_card, options) + add_card_on_file(post[:transaction], options) + + commit(action, post, options) + end + + def store(credit_card, options = {}) + post = {} + action = 'add' + + add_datetime(post, options) + add_card(action, post, credit_card, options) + add_customer(post, credit_card, options) + + commit(action, post, options) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r(("Number\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("expirationDate\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("FirstName\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("LastName\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("securityCode\\?":{\\?"\w+\\?":\d+,\\?"value\\?":\\?")\d*)i, '\1[FILTERED]') + end + + def setup_access_token + post = {} + add_credentials(post, options) + add_datetime(post, options) + + response = commit('accesstoken', post, request_headers('accesstoken', options)) + raise OAuthResponseError.new(response, response.params.fetch('result', [{}]).first.dig('error', 'longText')) unless response.success? + + response.params['result'].first['credential']['accessToken'] + end + + private + + def add_credentials(post, options) + post[:credential] = {} + post[:credential][:clientGuid] = @client_guid + post[:credential][:authToken] = @auth_token + end + + def add_clerk(post, options) + post[:clerk] = {} + post[:clerk][:numericId] = options[:clerk_id] || '1' + end + + def add_invoice(post, money, options) + post[:amount] = {} + post[:amount][:total] = amount(money.to_f) + post[:amount][:tax] = options[:tax].to_f || 0.0 + end + + def add_datetime(post, options) + post[:dateTime] = options[:date_time] || current_date_time(options) + end + + def add_transaction(post, options) + post[:transaction] = {} + post[:transaction][:invoice] = options[:invoice] || (Time.new.to_i.to_s[1..3] + rand.to_s[2..7]) + post[:transaction][:notes] = options[:notes] if options[:notes].present? + post[:transaction][:vendorReference] = options[:order_id] + + add_purchase_card(post[:transaction], options) + add_card_on_file(post[:transaction], options) + end + + def add_card(action, post, payment_method, options) + post[:card] = {} + post[:card][:entryMode] = options[:entry_mode] || 'M' unless DISALLOWED_ENTRY_MODE_ACTIONS.include?(action) + if payment_method.is_a?(CreditCard) + post[:card][:expirationDate] = "#{format(payment_method.month, :two_digits)}#{format(payment_method.year, :two_digits)}" + post[:card][:number] = payment_method.number + post[:card][:securityCode] = {} + post[:card][:securityCode][:indicator] = 1 + post[:card][:securityCode][:value] = payment_method.verification_value + else + post[:card] = {} if post[:card].nil? + post[:card][:token] = {} + post[:card][:token][:value] = payment_method + post[:card][:expirationDate] = options[:expiration_date] if options[:expiration_date] + end + end + + def add_card_present(post, options) + post[:card] = {} unless post[:card].present? + + post[:card][:present] = options[:card_present] || 'N' + end + + def add_customer(post, card, options) + address = options[:billing_address] || {} + + post[:customer] = {} + post[:customer][:addressLine1] = address[:address1] if address[:address1] + post[:customer][:postalCode] = address[:zip] if address[:zip] && !address[:zip]&.to_s&.empty? + post[:customer][:firstName] = card.first_name if card.is_a?(CreditCard) && card.first_name + post[:customer][:lastName] = card.last_name if card.is_a?(CreditCard) && card.last_name + post[:customer][:emailAddress] = options[:email] if options[:email] + post[:customer][:ipAddress] = options[:ip] if options[:ip] + end + + def add_purchase_card(post, options) + return unless options[:customer_reference] || options[:destination_postal_code] || options[:product_descriptors] + + post[:purchaseCard] = {} + post[:purchaseCard][:customerReference] = options[:customer_reference] if options[:customer_reference] + post[:purchaseCard][:destinationPostalCode] = options[:destination_postal_code] if options[:destination_postal_code] + post[:purchaseCard][:productDescriptors] = options[:product_descriptors] if options[:product_descriptors] + end + + def add_card_on_file(post, options) + return unless options[:stored_credential] || options[:usage_indicator] || options[:indicator] || options[:scheduled_indicator] || options[:transaction_id] + + stored_credential = options[:stored_credential] || {} + post[:cardOnFile] = {} + post[:cardOnFile][:usageIndicator] = options[:usage_indicator] || (stored_credential[:initial_transaction] ? '01' : '02') + post[:cardOnFile][:indicator] = options[:indicator] || '01' + post[:cardOnFile][:scheduledIndicator] = options[:scheduled_indicator] || (RECURRING_TYPE_TRANSACTIONS.include?(stored_credential[:reason_type]) ? '01' : '02') + post[:cardOnFile][:transactionId] = options[:transaction_id] || stored_credential[:network_transaction_id] if options[:transaction_id] || stored_credential[:network_transaction_id] + end + + def commit(action, parameters, option) + url_postfix = URL_POSTFIX_MAPPING[action] || 'transactions' + url = (test? ? "#{test_url}#{url_postfix}/#{action}" : "#{live_url}#{url_postfix}/#{action}") + if action == 'invoice' + response = parse(ssl_request(:delete, url, parameters.to_json, request_headers(action, option))) + else + response = parse(ssl_post(url, parameters.to_json, request_headers(action, option))) + end + + Response.new( + success_from(action, response), + message_from(action, response), + response, + authorization: authorization_from(action, response), + avs_result: avs_result_from(response), + test: test?, + error_code: error_code_from(action, response) + ) + end + + def handle_response(response) + case response.code.to_i + when 200...300, 400, 401, 500 + response.body + else + raise ResponseError.new(response) + end + end + + def parse(body) + return {} if body == '' + + JSON.parse(body) + end + + def message_from(action, response) + if success_from(action, response) + 'Transaction successful' + else + error(response)&.dig('longText') || + response['result'].first&.dig('transaction', 'hostresponse', 'reasonDescription') || + response['result'].first&.dig('transaction', 'hostResponse', 'reasonDescription') || + 'Transaction declined' + end + end + + def error_code_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&.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 + + 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) + return unless success_from(action, response) + + authorization = response.dig('result', 0, 'card', 'token', 'value').to_s + invoice = response.dig('result', 0, 'transaction', 'invoice') + authorization += "|#{invoice}" if invoice + authorization + end + + def get_card_token(authorization) + authorization.is_a?(CreditCard) ? authorization : authorization.split('|')[0] + end + + def get_invoice(authorization) + authorization.is_a?(CreditCard) ? authorization : authorization.split('|')[1] + end + + def request_headers(action, options) + headers = { + 'Content-Type' => 'application/json' + } + headers['AccessToken'] = @access_token + headers['Invoice'] = options[:invoice] if action != 'capture' && options[:invoice].present? + headers['InterfaceVersion'] = '1' + headers['InterfaceName'] = 'Spreedly' + headers['CompanyName'] = 'Spreedly' + headers + end + + def success_from(action, response) + success = error(response).nil? + success &&= SUCCESS_TRANSACTION_STATUS.include?(response['result'].first['transaction']['responseCode']) unless TRANSACTIONS_WITHOUT_RESPONSE_CODE.include?(action) + success + end + + def error(response) + server_error = { 'longText' => response['error'] } if response['error'] + server_error || response['result'].first['error'] + end + + def current_date_time(options = {}) + time_zone = options[:merchant_time_zone] || 'Pacific Time (US & Canada)' + time = Time.now.in_time_zone(time_zone) + offset = Time.now.in_time_zone(time_zone).formatted_offset + + time.strftime('%Y-%m-%dT%H:%M:%S.%3N') + offset + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/shift4_v2.rb b/lib/active_merchant/billing/gateways/shift4_v2.rb new file mode 100644 index 00000000000..e19e503afad --- /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 new file mode 100644 index 00000000000..a41912bfed5 --- /dev/null +++ b/lib/active_merchant/billing/gateways/simetrik.rb @@ -0,0 +1,374 @@ +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class SimetrikGateway < Gateway + self.test_url = 'https://payments.sta.simetrik.com/v1' + self.live_url = 'https://payments.simetrik.com/v1' + + class_attribute :test_auth_url, :live_auth_url, :test_audience, :live_audience + self.test_auth_url = 'https://tenant-payments-dev.us.auth0.com/oauth/token' + self.live_auth_url = 'https://tenant-payments-prod.us.auth0.com/oauth/token' + + self.test_audience = 'https://tenant-payments-dev.us.auth0.com/api/v2/' + self.live_audience = 'https://tenant-payments-prod.us.auth0.com/api/v2/' + + self.supported_countries = %w(PE AR) + self.default_currency = 'USD' + self.supported_cardtypes = %i[visa master american_express discover] + + self.homepage_url = 'https://www.simetrik.com' + self.display_name = 'Simetrik' + + STANDARD_ERROR_CODE_MAPPING = { + 'R101' => STANDARD_ERROR_CODE[:incorrect_number], + 'R102' => STANDARD_ERROR_CODE[:invalid_number], + 'R103' => STANDARD_ERROR_CODE[:invalid_expiry_date], + 'R104' => STANDARD_ERROR_CODE[:invalid_cvc], + 'R105' => STANDARD_ERROR_CODE[:expired_card], + 'R106' => STANDARD_ERROR_CODE[:incorrect_cvc], + 'R107' => STANDARD_ERROR_CODE[:incorrect_pin], + 'R201' => STANDARD_ERROR_CODE[:incorrect_zip], + 'R202' => STANDARD_ERROR_CODE[:incorrect_address], + 'R301' => STANDARD_ERROR_CODE[:card_declined], + 'R302' => STANDARD_ERROR_CODE[:processing_error], + 'R303' => STANDARD_ERROR_CODE[:call_issuer], + 'R304' => STANDARD_ERROR_CODE[:pick_up_card], + 'R305' => STANDARD_ERROR_CODE[:processing_error], + 'R306' => STANDARD_ERROR_CODE[:processing_error], + 'R307' => STANDARD_ERROR_CODE[:processing_error], + 'R401' => STANDARD_ERROR_CODE[:config_error], + 'R402' => STANDARD_ERROR_CODE[:test_mode_live_card], + 'R403' => STANDARD_ERROR_CODE[:unsupported_feature] + + } + + def initialize(options = {}) + requires!(options, :client_id, :client_secret) + super + @access_token = options[:access_token] || {} + sign_access_token() + end + + def authorize(money, payment, options = {}) + requires!(options, :token_acquirer) + + post = {} + add_forward_route(post, options) + add_forward_payload(post, money, payment, options) + add_stored_credential(post, options) + commit('authorize', post, { token_acquirer: options[:token_acquirer] }) + end + + def capture(money, authorization, options = {}) + requires!(options, :token_acquirer) + post = { + forward_payload: { + amount: { + total_amount: amount(money).to_f, + currency: (options[:currency] || currency(money)) + }, + transaction: { + id: authorization + }, + acquire_extra_options: options[:acquire_extra_options] || {} + } + } + post[:forward_payload][:amount][:vat] = options[:vat].to_f / 100 if options[:vat] + + add_forward_route(post, options) + commit('capture', post, { token_acquirer: options[:token_acquirer] }) + end + + def refund(money, authorization, options = {}) + requires!(options, :token_acquirer) + post = { + forward_payload: { + amount: { + total_amount: amount(money).to_f, + currency: (options[:currency] || currency(money)) + }, + transaction: { + id: authorization + }, + acquire_extra_options: options[:acquire_extra_options] || {} + } + } + post[:forward_payload][:transaction][:comment] = options[:comment] if options[:comment] + + add_forward_route(post, options) + commit('refund', post, { token_acquirer: options[:token_acquirer] }) + end + + def void(authorization, options = {}) + requires!(options, :token_acquirer) + post = { + forward_payload: { + transaction: { + id: authorization + }, + acquire_extra_options: options[:acquire_extra_options] || {} + } + } + add_forward_route(post, options) + commit('void', post, { token_acquirer: options[:token_acquirer] }) + end + + def purchase(money, payment, options = {}) + requires!(options, :token_acquirer) + + post = {} + add_forward_route(post, options) + add_forward_payload(post, money, payment, options) + + add_stored_credential(post, options) + commit('charge', post, { token_acquirer: options[:token_acquirer] }) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r((Authorization: Bearer ).+), '\1[FILTERED]'). + gsub(%r(("client_secret\\?":\\?")\w+), '\1[FILTERED]'). + gsub(%r(("number\\?":\\?")\d+), '\1[FILTERED]'). + gsub(%r(("security_code\\?":\\?")\d+), '\1[FILTERED]') + end + + private + + def add_forward_route(post, options) + forward_route = {} + forward_route[:trace_id] = options[:trace_id] if options[:trace_id] + + forward_route[:psp_extra_fields] = options[:psp_extra_fields] || {} + post[:forward_route] = forward_route + end + + def add_forward_payload(post, money, payment, options) + forward_payload = {} + add_user(forward_payload, options[:user]) if options[:user] + add_order(forward_payload, money, options) + add_payment_method(forward_payload, payment, options[:payment_method]) if options[:payment_method] || payment + + forward_payload[:payment_method] = {} unless forward_payload[:payment_method] + forward_payload[:payment_method][:card] = {} unless forward_payload[:payment_method][:card] + add_address('billing_address', forward_payload[:payment_method][:card], options[:billing_address]) if options[:billing_address] + + add_three_ds_fields(forward_payload[:authentication] = {}, options[:three_ds_fields]) if options[:three_ds_fields] + add_sub_merchant(forward_payload, options[:sub_merchant]) if options[:sub_merchant] + forward_payload[:acquire_extra_options] = options[:acquire_extra_options] || {} + post[:forward_payload] = forward_payload + end + + def add_sub_merchant(post, sub_merchant_options) + sub_merchant = {} + sub_merchant[:merchant_id] = sub_merchant_options[:merchant_id] if sub_merchant_options[:merchant_id] + sub_merchant[:extra_params] = sub_merchant_options[:extra_params] if sub_merchant_options[:extra_params] + sub_merchant[:mcc] = sub_merchant_options[:mcc] if sub_merchant_options[:mcc] + sub_merchant[:name] = sub_merchant_options[:name] if sub_merchant_options[:name] + sub_merchant[:address] = sub_merchant_options[:address] if sub_merchant_options[:address] + sub_merchant[:postal_code] = sub_merchant_options[:postal_code] if sub_merchant_options[:postal_code] + sub_merchant[:url] = sub_merchant_options[:url] if sub_merchant_options[:url] + sub_merchant[:phone_number] = sub_merchant_options[:phone_number] if sub_merchant_options[:phone_number] + + post[:sub_merchant] = sub_merchant + end + + def add_payment_method(post, payment, payment_method_options) + payment_method = {} + opts = nil + opts = payment_method_options[:card] if payment_method_options + add_card(payment_method, payment, opts) if opts || payment + + post[:payment_method] = payment_method + end + + def add_three_ds_fields(post, three_ds_options) + three_ds = {} + three_ds[:version] = three_ds_options[:version] if three_ds_options[:version] + three_ds[:eci] = three_ds_options[:eci] if three_ds_options[:eci] + three_ds[:cavv] = three_ds_options[:cavv] if three_ds_options[:cavv] + three_ds[:ds_transaction_id] = three_ds_options[:ds_transaction_id] if three_ds_options[:ds_transaction_id] + three_ds[:acs_transaction_id] = three_ds_options[:acs_transaction_id] if three_ds_options[:acs_transaction_id] + three_ds[:xid] = three_ds_options[:xid] if three_ds_options[:xid] + three_ds[:enrolled] = three_ds_options[:enrolled] if three_ds_options[:enrolled] + three_ds[:cavv_algorithm] = three_ds_options[:cavv_algorithm] if three_ds_options[:cavv_algorithm] + three_ds[:directory_response_status] = three_ds_options[:directory_response_status] if three_ds_options[:directory_response_status] + three_ds[:authentication_response_status] = three_ds_options[:authentication_response_status] if three_ds_options[:authentication_response_status] + three_ds[:three_ds_server_trans_id] = three_ds_options[:three_ds_server_trans_id] if three_ds_options[:three_ds_server_trans_id] + + post[:three_ds_fields] = three_ds + end + + def add_card(post, card, card_options = {}) + card_hash = {} + card_hash[:number] = card.number + card_hash[:exp_month] = card.month + card_hash[:exp_year] = card.year + card_hash[:security_code] = card.verification_value + card_hash[:type] = card.brand + card_hash[:holder_first_name] = card.first_name + card_hash[:holder_last_name] = card.last_name + post[:card] = card_hash + end + + def add_user(post, user_options) + user = {} + user[:id] = user_options[:id] if user_options[:id] + user[:email] = user_options[:email] if user_options[:email] + + post[:user] = user + end + + def add_stored_credential(post, options) + return unless options[:stored_credential] + + check_initiator = %w[merchant cardholder].any? { |item| item == options[:stored_credential][:initiator] } + check_reason_type = %w[recurring installment unscheduled].any? { |item| item == options[:stored_credential][:reason_type] } + post[:forward_payload][:authentication] = {} unless post[:forward_payload].key?(:authentication) + post[:forward_payload][:authentication][:stored_credential] = options[:stored_credential] if check_initiator && check_reason_type + end + + def add_order(post, money, options) + return unless options[:order] || money + + order = {} + order_options = options[:order] || {} + order[:id] = options[:order_id] if options[:order_id] + order[:description] = options[:description] if options[:description] + order[:installments] = order_options[:installments].to_i if order_options[:installments] + order[:datetime_local_transaction] = order_options[:datetime_local_transaction] if order_options[:datetime_local_transaction] + + add_amount(order, money, options) + add_address('shipping_address', order, options) + + post[:order] = order + end + + def add_amount(post, money, options) + amount_obj = {} + amount_obj[:total_amount] = amount(money).to_f + amount_obj[:currency] = (options[:currency] || currency(money)) + amount_obj[:vat] = options[:vat].to_f / 100 if options[:vat] + + post[:amount] = amount_obj + end + + def add_address(tag, post, options) + return unless address_options = options[:shipping_address] + + address = {} + address[:name] = address_options[:name] if address_options[:name] + address[:address1] = address_options[:address1] if address_options[:address1] + address[:address2] = address_options[:address2] if address_options[:address2] + address[:company] = address_options[:company] if address_options[:company] + address[:city] = address_options[:city] if address_options[:city] + address[:state] = address_options[:state] if address_options[:state] + address[:zip] = address_options[:zip] if address_options[:zip] + address[:country] = address_options[:country] if address_options[:country] + address[:phone] = address_options[:phone] if address_options[:phone] + address[:fax] = address_options[:fax] if address_options[:fax] + + post[tag] = address + end + + def parse(body) + JSON.parse(body) + end + + def commit(action, parameters, url_params = {}) + begin + response = JSON.parse ssl_post(url(action, url_params), post_data(parameters), authorized_headers()) + rescue ResponseError => e + case e.response.code.to_i + when 400...499 + response = JSON.parse e.response.body + else + raise e + end + end + + Response.new( + success_from(response['code']), + message_from(response), + response, + authorization: authorization_from(response), + avs_result: AVSResult.new(code: avs_code_from(response)), + cvv_result: CVVResult.new(cvv_code_from(response)), + test: test?, + error_code: error_code_from(response) + ) + end + + def avs_code_from(response) + response['avs_result'] + end + + def cvv_code_from(response) + response['cvv_result'] + end + + def success_from(code) + code == 'S001' + end + + def message_from(response) + response['message'] + end + + def url(action, url_params) + "#{test? ? test_url : live_url}/#{url_params[:token_acquirer]}/#{action}" + end + + def post_data(data = {}) + data.to_json + end + + def authorization_from(response) + response['simetrik_authorization_id'] + end + + def error_code_from(response) + STANDARD_ERROR_CODE_MAPPING[response['code']] unless success_from(response['code']) + end + + def authorized_headers + { + 'content-Type' => 'application/json', + 'Authorization' => "Bearer #{sign_access_token()}" + } + end + + # if this method is refactored, ensure that the client_secret is properly scrubbed + def sign_access_token + fetch_access_token() if Time.new.to_i > (@access_token[:expires_at] || 0) + 10 + @access_token[:access_token] + end + + def auth_url + (test? ? test_auth_url : live_auth_url) + end + + def fetch_access_token + login_info = {} + login_info[:client_id] = @options[:client_id] + login_info[:client_secret] = @options[:client_secret] + login_info[:audience] = test? ? test_audience : live_audience + login_info[:grant_type] = 'client_credentials' + + 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 +end diff --git a/lib/active_merchant/billing/gateways/skip_jack.rb b/lib/active_merchant/billing/gateways/skip_jack.rb index 6de7356d1de..81ad151fb43 100644 --- a/lib/active_merchant/billing/gateways/skip_jack.rb +++ b/lib/active_merchant/billing/gateways/skip_jack.rb @@ -1,8 +1,7 @@ -#!ruby19 # encoding: utf-8 -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class SkipJackGateway < Gateway API_VERSION = '?.?' @@ -13,16 +12,16 @@ class SkipJackGateway < Gateway ADVANCED_PATH = '/evolvcc/evolvcc.aspx' ACTIONS = { - :authorization => 'AuthorizeAPI', - :change_status => 'SJAPI_TransactionChangeStatusRequest', - :get_status => 'SJAPI_TransactionStatusRequest' + authorization: 'AuthorizeAPI', + change_status: 'SJAPI_TransactionChangeStatusRequest', + get_status: 'SJAPI_TransactionStatusRequest' } SUCCESS_MESSAGE = 'The transaction was successful.' MONETARY_CHANGE_STATUSES = ['SETTLE', 'AUTHORIZE', 'AUTHORIZE ADDITIONAL', 'CREDIT', 'SPLITSETTLE'] - CARD_CODE_ERRORS = %w( N S "" ) + CARD_CODE_ERRORS = %w(N S "") CARD_CODE_MESSAGES = { 'M' => 'Card verification number matched', @@ -33,7 +32,7 @@ class SkipJackGateway < Gateway '' => 'Transaction failed because incorrect card verification number was entered or no number was entered' } - AVS_ERRORS = %w( A B C E I N O P R W Z ) + AVS_ERRORS = %w(A B C E I N O P R W Z) AVS_MESSAGES = { 'A' => 'Street address matches billing information, zip/postal code does not', @@ -52,7 +51,7 @@ class SkipJackGateway < Gateway 'W' => '9-digit zip/postal code matches billing information, street address does not', 'X' => 'Street address and 9-digit zip/postal code matches billing information', 'Y' => 'Street address and 5-digit zip/postal code matches billing information', - 'Z' => '5-digit zip/postal code matches billing information, street address does not', + 'Z' => '5-digit zip/postal code matches billing information, street address does not' } CHANGE_STATUS_ERROR_MESSAGES = { @@ -161,8 +160,8 @@ class SkipJackGateway < Gateway '-117' => 'POS Check Invalid Cashier Number' } - self.supported_countries = ['US', 'CA'] - self.supported_cardtypes = [:visa, :master, :american_express, :jcb, :discover, :diners_club] + self.supported_countries = %w[US CA] + self.supported_cardtypes = %i[visa master american_express jcb discover diners_club] self.homepage_url = 'http://www.skipjack.com/' self.display_name = 'SkipJack' @@ -214,7 +213,7 @@ def purchase(money, creditcard, options = {}) # # * :force_settlement -- Force the settlement to occur as soon as possible. This option is not supported by other gateways. See the SkipJack API reference for more details def capture(money, authorization, options = {}) - post = { } + post = {} add_status_action(post, 'SETTLE') add_forced_settlement(post, options) add_transaction_id(post, authorization) @@ -243,7 +242,7 @@ def credit(money, identification, options = {}) end def status(order_id) - commit(:get_status, nil, :szOrderNumber => order_id) + commit(:get_status, nil, szOrderNumber: order_id) end private @@ -261,21 +260,24 @@ def add_status_action(post, action) end def commit(action, money, parameters) - response = parse( ssl_post( url_for(action), post_data(action, money, parameters) ), action ) + 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, - :test => test?, - :authorization => response[:szTransactionFileName] || parameters[:szTransactionId], - :avs_result => { :code => response[:szAVSResponseCode] }, - :cvv_result => response[:szCVV2ResponseCode] + 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] ) end def url_for(action) result = test? ? self.test_url : self.live_url result += advanced? && action == :authorization ? ADVANCED_PATH : BASIC_PATH - result += "?#{ACTIONS[action]}" + result + "?#{ACTIONS[action]}" end def add_credentials(params, action) @@ -301,9 +303,9 @@ def parse(body, action) when :authorization parse_authorization_response(body) when :get_status - parse_status_response(body, [ :SerialNumber, :TransactionAmount, :TransactionStatusCode, :TransactionStatusMessage, :OrderNumber, :TransactionDateTime, :TransactionID, :ApprovalCode, :BatchNumber ]) + parse_status_response(body, %i[SerialNumber TransactionAmount TransactionStatusCode TransactionStatusMessage OrderNumber TransactionDateTime TransactionID ApprovalCode BatchNumber]) else - parse_status_response(body, [ :SerialNumber, :TransactionAmount, :DesiredStatus, :StatusResponse, :StatusResponseMessage, :OrderNumber, :AuditID ]) + parse_status_response(body, %i[SerialNumber TransactionAmount DesiredStatus StatusResponse StatusResponseMessage OrderNumber AuditID]) end end @@ -318,7 +320,7 @@ def split_line(line) def authorize_response_map(body) lines = split_lines(body) keys, values = split_line(lines[0]), split_line(lines[1]) - Hash[*(keys.zip(values).flatten)].symbolize_keys + Hash[*keys.zip(values).flatten].symbolize_keys end def parse_authorization_response(body) @@ -330,10 +332,10 @@ def parse_authorization_response(body) def parse_status_response(body, response_keys) lines = split_lines(body) - keys = [ :szSerialNumber, :szErrorCode, :szNumberRecords] + keys = %i[szSerialNumber szErrorCode szNumberRecords] values = split_line(lines[0])[0..2] - result = Hash[*(keys.zip(values).flatten)] + result = Hash[*keys.zip(values).flatten] result[:szErrorMessage] = '' result[:success] = (result[:szErrorCode] == '0') @@ -354,8 +356,8 @@ def parse_status_response(body, response_keys) def post_data(action, money, params = {}) add_credentials(params, action) add_amount(params, action, money) - sorted_params = params.to_a.sort{|a,b| a.to_s <=> b.to_s}.reverse - sorted_params.collect { |key, value| "#{key.to_s}=#{CGI.escape(value.to_s)}" }.join('&') + sorted_params = params.to_a.sort_by(&:to_s).reverse + sorted_params.collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&') end def add_transaction_id(post, transaction_id) @@ -369,14 +371,14 @@ def add_invoice(post, options) post[:OrderDescription] = options[:description] if order_items = options[:items] - post[:OrderString] = order_items.collect { |item| "#{item[:sku]}~#{item[:description].tr('~','-')}~#{item[:declared_value]}~#{item[:quantity]}~#{item[:taxable]}~~~~~~~~#{item[:tax_rate]}~||"}.join + post[:OrderString] = order_items.collect { |item| "#{item[:sku]}~#{item[:description].tr('~', '-')}~#{item[:declared_value]}~#{item[:quantity]}~#{item[:taxable]}~~~~~~~~#{item[:tax_rate]}~||" }.join else post[:OrderString] = '1~None~0.00~0~N~||' end end def add_creditcard(post, creditcard) - post[:AccountNumber] = creditcard.number + post[:AccountNumber] = creditcard.number post[:Month] = creditcard.month post[:Year] = creditcard.year post[:CVV2] = creditcard.verification_value if creditcard.verification_value? @@ -435,6 +437,7 @@ def message_from_authorization(response) return CARD_CODE_MESSAGES[response[:szCVV2ResponseCode]] if CARD_CODE_ERRORS.include?(response[:szCVV2ResponseCode]) return AVS_MESSAGES[response[:szAVSResponseMessage]] if AVS_ERRORS.include?(response[:szAVSResponseCode]) return RETURN_CODE_MESSAGES[response[:szReturnCode]] if response[:szReturnCode] != '1' + return response[:szAuthorizationDeclinedMessage] end end diff --git a/lib/active_merchant/billing/gateways/smart_ps.rb b/lib/active_merchant/billing/gateways/smart_ps.rb index 2b572aaa9d5..e0ea422bcff 100644 --- a/lib/active_merchant/billing/gateways/smart_ps.rb +++ b/lib/active_merchant/billing/gateways/smart_ps.rb @@ -1,9 +1,8 @@ require File.join(File.dirname(__FILE__), '..', 'check.rb') -module ActiveMerchant #:nodoc: - module Billing #:nodoc: - class SmartPs < Gateway #:nodoc: - +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class SmartPs < Gateway # :nodoc: ## # This is the base gateway for processors who use the smartPS processing system @@ -23,7 +22,7 @@ def initialize(options = {}) def authorize(money, creditcard, options = {}) post = {} add_invoice(post, options) - add_payment_source(post, creditcard,options) + add_payment_source(post, creditcard, options) add_address(post, options[:billing_address] || options[:address]) add_address(post, options[:shipping_address], 'shipping') add_customer_data(post, options) @@ -48,13 +47,13 @@ def purchase(money, payment_source, options = {}) end def capture(money, authorization, options = {}) - post ={} + post = {} post[:transactionid] = authorization commit('capture', money, post) end def void(authorization, options = {}) - post ={} + post = {} post[:transactionid] = authorization commit('void', nil, post) end @@ -65,7 +64,7 @@ def credit(money, payment_source, options = {}) add_payment_source(post, payment_source, options) add_address(post, options[:billing_address] || options[:address]) add_customer_data(post, options) - add_sku(post,options) + add_sku(post, options) add_currency(post, money, options) add_processor(post, options) commit('credit', money, post) @@ -106,7 +105,6 @@ def amend(auth, options = {}) commit('update', nil, post) end - def delete(vault_id) post = {} post[:customer_vault] = 'delete_customer' @@ -119,36 +117,33 @@ def delete(vault_id) def store(payment_source, options = {}) post = {} billing_id = options.delete(:billing_id).to_s || true - add_payment_source(post, payment_source, :store => billing_id) + add_payment_source(post, payment_source, store: billing_id) add_address(post, options[:billing_address] || options[:address]) add_customer_data(post, options) commit(nil, nil, post) end - alias_method :unstore, :delete + alias unstore delete private + def add_customer_data(post, options) - if options.has_key? :email - post[:email] = options[:email] - end + post[:email] = options[:email] if options.has_key? :email - if options.has_key? :ip - post[:ipaddress] = options[:ip] - end + post[:ipaddress] = options[:ip] if options.has_key? :ip end - def add_address(post, address,prefix='') - prefix +='_' unless prefix.blank? - unless address.blank? or address.values.blank? - post[prefix+'address1'] = address[:address1].to_s - post[prefix+'address2'] = address[:address2].to_s unless address[:address2].blank? - post[prefix+'company'] = address[:company].to_s - post[prefix+'phone'] = address[:phone].to_s - post[prefix+'zip'] = address[:zip].to_s - post[prefix+'city'] = address[:city].to_s - post[prefix+'country'] = address[:country].to_s - post[prefix+'state'] = address[:state].blank? ? 'n/a' : address[:state] + def add_address(post, address, prefix = '') + prefix += '_' unless prefix.blank? + unless address.blank? || address.values.blank? + post[prefix + 'address1'] = address[:address1].to_s + post[prefix + 'address2'] = address[:address2].to_s unless address[:address2].blank? + post[prefix + 'company'] = address[:company].to_s + post[prefix + 'phone'] = address[:phone].to_s + post[prefix + 'zip'] = address[:zip].to_s + post[prefix + 'city'] = address[:city].to_s + post[prefix + 'country'] = address[:country].to_s + post[prefix + 'state'] = address[:state].blank? ? 'n/a' : address[:state] end end @@ -168,7 +163,7 @@ def add_invoice(post, options) post[:orderid] = options[:order_id].to_s.gsub(/[^\w.]/, '') end - def add_payment_source(params, source, options={}) + def add_payment_source(params, source, options = {}) case determine_funding_source(source) when :vault then add_customer_vault_id(params, source) when :credit_card then add_creditcard(params, source, options) @@ -185,9 +180,9 @@ def add_creditcard(post, creditcard, options) post[:customer_vault] = 'add_customer' post[:customer_vault_id] = options[:store] unless options[:store] == true end - post[:ccnumber] = creditcard.number + post[:ccnumber] = creditcard.number post[:cvv] = creditcard.verification_value if creditcard.verification_value? - post[:ccexp] = expdate(creditcard) + post[:ccexp] = expdate(creditcard) post[:firstname] = creditcard.first_name post[:lastname] = creditcard.last_name end @@ -206,7 +201,7 @@ def add_check(post, check, options) post[:account_type] = check.account_type # The customer's type of ACH account end - def add_sku(post,options) + def add_sku(post, options) post['product_sku_#'] = options[:sku] || options['product_sku_#'] end @@ -221,7 +216,7 @@ def add_eci(post, options) def parse(body) results = {} body.split(/&/).each do |pair| - key,val = pair.split(/=/) + key, val = pair.split(/=/) results[key] = val end @@ -229,15 +224,17 @@ def parse(body) end 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, - :authorization => (response['transactionid'] || response['customer_vault_id']), - :test => test?, - :cvv_result => response['cvvresponse'], - :avs_result => { :code => response['avsresponse'] } + 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, + authorization: (response['transactionid'] || response['customer_vault_id']), + test: test?, + cvv_result: response['cvvresponse'], + avs_result: { code: response['avsresponse'] } ) - end def expdate(creditcard) @@ -247,7 +244,6 @@ def expdate(creditcard) "#{month}#{year[-2..-1]}" end - def message_from(response) case response['responsetext'] when 'SUCCESS', 'Approved', nil # This is dubious, but responses from UPDATE are nil. @@ -261,18 +257,17 @@ def message_from(response) def post_data(action, parameters = {}) post = {} - post[:username] = @options[:login] + post[:username] = @options[:login] 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) case when source.is_a?(String) then :vault - when CreditCard.card_companies.keys.include?(card_brand(source)) then :credit_card + when CreditCard.card_companies.include?(card_brand(source)) then :credit_card when card_brand(source) == 'check' then :check else raise ArgumentError, 'Unsupported funding source provided' end @@ -280,4 +275,3 @@ def determine_funding_source(source) end end end - diff --git a/lib/active_merchant/billing/gateways/so_easy_pay.rb b/lib/active_merchant/billing/gateways/so_easy_pay.rb index cb80073bff0..9b28eaeef6c 100644 --- a/lib/active_merchant/billing/gateways/so_easy_pay.rb +++ b/lib/active_merchant/billing/gateways/so_easy_pay.rb @@ -1,16 +1,16 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class SoEasyPayGateway < Gateway self.live_url = self.test_url = 'https://secure.soeasypay.com/gateway.asmx' self.money_format = :cents - self.supported_countries = [ - 'US', 'CA', 'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', - 'FI', 'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', - 'MT', 'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE', 'GB', - 'IS', 'NO', 'CH' + self.supported_countries = %w[ + US CA AT BE BG HR CY CZ DK EE + FI FR DE GR HU IE IT LV LT LU + MT NL PL PT RO SK SI ES SE GB + IS NO CH ] - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :maestro, :jcb, :solo, :diners_club] + self.supported_cardtypes = %i[visa master american_express discover maestro jcb diners_club] self.homepage_url = 'http://www.soeasypay.com/' self.display_name = 'SoEasyPay' @@ -39,11 +39,11 @@ def capture(money, authorization, options = {}) commit('CaptureTransaction', do_capture(money, authorization, options), options) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) commit('RefundTransaction', do_refund(money, authorization, options), options) end - def void(authorization, options={}) + def void(authorization, options = {}) commit('CancelTransaction', do_void(authorization, options), options) end @@ -114,7 +114,7 @@ def fill_credentials(soap, options) def fill_cardholder(soap, card, options) ch_info = options[:billing_address] || options[:address] - soap.tag!('customerIP',options[:ip].to_s) + soap.tag!('customerIP', options[:ip].to_s) name = card.name || ch_info[:name] soap.tag!('cardHolderName', name.to_s) address = ch_info[:address1] || '' @@ -139,7 +139,7 @@ def fill_card(soap, card) soap.tag!('cardExpireYear', card.year.to_s) end - def fill_order_info(soap, money, options, skip_currency=false) + def fill_order_info(soap, money, options, skip_currency = false) soap.tag!('orderID', options[:order_id].to_s) soap.tag!('orderDescription', "Order #{options[:order_id]}") soap.tag!('amount', amount(money).to_s) @@ -149,7 +149,7 @@ def fill_order_info(soap, money, options, skip_currency=false) def parse(response, action) result = {} document = REXML::Document.new(response) - response_element = document.root.get_elements("//[@xsi:type='tns:#{action}Response']").first + response_element = document.root.get_elements("//*[@xsi:type='tns:#{action}Response']").first response_element.elements.each do |element| result[element.name.underscore] = element.text end @@ -157,30 +157,33 @@ def parse(response, action) end def commit(soap_action, soap, options) - headers = {'SOAPAction' => "\"urn:Interface##{soap_action}\"", - 'Content-Type' => 'text/xml; charset=utf-8'} + headers = { 'SOAPAction' => "\"urn:Interface##{soap_action}\"", + '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', - response['errormessage'], - response, - :test => test?, - :authorization => response['transaction_id']) + return Response.new( + response['errorcode'] == '000', + response['errormessage'], + response, + test: test?, + authorization: response['transaction_id'] + ) end def build_soap(request) - retval = Builder::XmlMarkup.new(:indent => 2) - retval.instruct!(:xml, :version => '1.0', :encoding => 'utf-8') + retval = Builder::XmlMarkup.new(indent: 2) + retval.instruct!(:xml, version: '1.0', encoding: 'utf-8') retval.tag!('soap:Envelope', { - 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', - 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema', - 'xmlns:soapenc' => 'http://schemas.xmlsoap.org/soap/encoding/', - 'xmlns:tns' => 'urn:Interface', - 'xmlns:types' => 'urn:Interface/encodedTypes', - 'xmlns:soap' => 'http://schemas.xmlsoap.org/soap/envelope/'}) do - retval.tag!('soap:Body', {'soap:encodingStyle'=>'http://schemas.xmlsoap.org/soap/encoding/'}) do + 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', + 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema', + 'xmlns:soapenc' => 'http://schemas.xmlsoap.org/soap/encoding/', + 'xmlns:tns' => 'urn:Interface', + 'xmlns:types' => 'urn:Interface/encodedTypes', + 'xmlns:soap' => 'http://schemas.xmlsoap.org/soap/envelope/' + }) do + retval.tag!('soap:Body', { 'soap:encodingStyle' => 'http://schemas.xmlsoap.org/soap/encoding/' }) do retval.tag!("tns:#{request}") do - retval.tag!("#{request}Request", {'xsi:type'=>"tns:#{request}Request"}) do + retval.tag!("#{request}Request", { 'xsi:type' => "tns:#{request}Request" }) do yield retval end end @@ -188,7 +191,6 @@ def build_soap(request) end retval.target! end - end end end diff --git a/lib/active_merchant/billing/gateways/spreedly_core.rb b/lib/active_merchant/billing/gateways/spreedly_core.rb index 20a92886494..b78c0c3f13f 100644 --- a/lib/active_merchant/billing/gateways/spreedly_core.rb +++ b/lib/active_merchant/billing/gateways/spreedly_core.rb @@ -1,7 +1,7 @@ require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # Public: This gateway allows you to interact with any gateway you've # created in Spreedly (https://spreedly.com). It's an adapter which can be # particularly useful if you already have code interacting with @@ -14,7 +14,7 @@ class SpreedlyCoreGateway < Gateway MT MU MV MX MY NL NO NZ OM PH PL PT QA RO SA SE SG SI SK SM TR TT UM US VA VN ZA) - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'https://spreedly.com' self.display_name = 'Spreedly' self.money_format = :cents @@ -35,19 +35,13 @@ def initialize(options = {}) # Public: Run a purchase transaction. # # money - The monetary amount of the transaction in cents. - # payment_method - The CreditCard or the Spreedly payment method token. + # payment_method - The CreditCard or Check or the Spreedly payment method token. # options - A hash of options: # :store - Retain the payment method if the purchase # succeeds. Defaults to false. (optional) def purchase(money, payment_method, options = {}) - if payment_method.is_a?(String) - purchase_with_token(money, payment_method, options) - else - MultiResponse.run do |r| - r.process { save_card(options[:store], payment_method, options) } - r.process { purchase_with_token(money, r.authorization, options) } - end - end + request = build_transaction_request(money, payment_method, options) + commit("gateways/#{options[:gateway_token] || @options[:gateway_token]}/purchase.xml", request) end # Public: Run an authorize transaction. @@ -58,17 +52,11 @@ def purchase(money, payment_method, options = {}) # :store - Retain the payment method if the authorize # succeeds. Defaults to false. (optional) def authorize(money, payment_method, options = {}) - if payment_method.is_a?(String) - authorize_with_token(money, payment_method, options) - else - MultiResponse.run do |r| - r.process { save_card(options[:store], payment_method, options) } - r.process { authorize_with_token(money, r.authorization, options) } - end - end + request = build_transaction_request(money, payment_method, options) + commit("gateways/#{@options[:gateway_token]}/authorize.xml", request) end - def capture(money, authorization, options={}) + def capture(money, authorization, options = {}) request = build_xml_request('transaction') do |doc| add_invoice(doc, money, options) end @@ -76,19 +64,19 @@ def capture(money, authorization, options={}) commit("transactions/#{authorization}/capture.xml", request) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) request = build_xml_request('transaction') do |doc| add_invoice(doc, money, options) + add_extra_options(:gateway_specific_fields, doc, options) end commit("transactions/#{authorization}/credit.xml", request) end - def void(authorization, options={}) + def void(authorization, options = {}) commit("transactions/#{authorization}/void.xml", '') end - # Public: Determine whether a credit card is chargeable card and available for purchases. # # payment_method - The CreditCard or the Spreedly payment method token. @@ -110,7 +98,7 @@ def verify(payment_method, options = {}) # # credit_card - The CreditCard to store # options - A standard ActiveMerchant options hash - def store(credit_card, options={}) + def store(credit_card, options = {}) retain = (options.has_key?(:retain) ? options[:retain] : true) save_card(retain, credit_card, options) end @@ -120,7 +108,7 @@ def store(credit_card, options={}) # # credit_card - The CreditCard to store # options - A standard ActiveMerchant options hash - def unstore(authorization, options={}) + def unstore(authorization, options = {}) commit("payment_methods/#{authorization}/redact.xml", '', :put) end @@ -129,7 +117,7 @@ def find(transaction_token) commit("transactions/#{transaction_token}.xml", nil, :get) end - alias_method :status, :find + alias status find def supports_scrubbing? true @@ -156,32 +144,25 @@ def save_card(retain, credit_card, options) end def purchase_with_token(money, payment_method_token, options) - request = auth_purchase_request(money, payment_method_token, options) + request = build_transaction_request(money, payment_method_token, options) commit("gateways/#{options[:gateway_token] || @options[:gateway_token]}/purchase.xml", request) end def authorize_with_token(money, payment_method_token, options) - request = auth_purchase_request(money, payment_method_token, options) + request = build_transaction_request(money, payment_method_token, options) commit("gateways/#{@options[:gateway_token]}/authorize.xml", request) end def verify_with_token(payment_method_token, options) - request = build_xml_request('transaction') do |doc| - add_invoice(doc, nil, options) - doc.payment_method_token(payment_method_token) - doc.retain_on_success(true) if options[:store] - add_extra_options(:gateway_specific_fields, doc, options) - end - + request = build_transaction_request(nil, payment_method_token, options) commit("gateways/#{@options[:gateway_token]}/verify.xml", request) end - def auth_purchase_request(money, payment_method_token, options) + def build_transaction_request(money, payment_method, options) build_xml_request('transaction') do |doc| add_invoice(doc, money, options) + add_payment_method(doc, payment_method, options) add_extra_options(:gateway_specific_fields, doc, options) - doc.payment_method_token(payment_method_token) - doc.retain_on_success(true) if options[:store] end end @@ -191,6 +172,23 @@ def add_invoice(doc, money, options) doc.order_id(options[:order_id]) doc.ip(options[:ip]) if options[:ip] doc.description(options[:description]) if options[:description] + + doc.merchant_name_descriptor(options[:merchant_name_descriptor]) if options[:merchant_name_descriptor] + doc.merchant_location_descriptor(options[:merchant_location_descriptor]) if options[:merchant_location_descriptor] + end + + def add_payment_method(doc, payment_method, options) + doc.retain_on_success(true) if options[:store] + + if payment_method.is_a?(String) + doc.payment_method_token(payment_method) + elsif payment_method.is_a?(CreditCard) + add_credit_card(doc, payment_method, options) + elsif payment_method.is_a?(Check) + add_bank_account(doc, payment_method, options) + else + raise TypeError, 'Payment method not supported' + end end def add_credit_card(doc, credit_card, options) @@ -211,6 +209,17 @@ def add_credit_card(doc, credit_card, options) end end + def add_bank_account(doc, bank_account, options) + doc.bank_account do + doc.first_name(bank_account.first_name) + doc.last_name(bank_account.last_name) + doc.bank_routing_number(bank_account.routing_number) + doc.bank_account_number(bank_account.account_number) + doc.bank_account_type(bank_account.account_type) + doc.bank_account_holder_type(bank_account.account_holder_type) + end + end + def add_extra_options(type, doc, options) doc.send(type) do extra_options_to_doc(doc, options[type]) @@ -219,6 +228,7 @@ def add_extra_options(type, doc, options) def extra_options_to_doc(doc, value) return doc.text value unless value.kind_of? Hash + value.each do |k, v| doc.send(k) do extra_options_to_doc(doc, v) @@ -231,7 +241,7 @@ def parse(xml) doc = Nokogiri::XML(xml) doc.root.xpath('*').each do |node| - if (node.elements.empty?) + if node.elements.empty? response[node.name.downcase.to_sym] = node.text else node.elements.each do |childnode| @@ -244,19 +254,26 @@ def parse(xml) end def childnode_to_response(response, node, childnode) - name = "#{node.name.downcase}_#{childnode.name.downcase}" - if name == 'payment_method_data' && !childnode.elements.empty? - response[name.to_sym] = Hash.from_xml(childnode.to_s).values.first + node_name = node.name.downcase + childnode_name = childnode.name.downcase + composed_name = "#{node_name}_#{childnode_name}" + + childnodes_present = !childnode.elements.empty? + + if childnodes_present && composed_name == 'payment_method_data' + response[composed_name.to_sym] = Hash.from_xml(childnode.to_s).values.first + elsif childnodes_present && node_name == 'gateway_specific_response_fields' + response[node_name.to_sym] = { + childnode_name => Hash.from_xml(childnode.to_s).values.first + } else - response[name.to_sym] = childnode.text + response[composed_name.to_sym] = childnode.text 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 @@ -273,10 +290,10 @@ def commit(relative_url, request, method = :post, authorization_field = :token) def response_from(raw_response, authorization_field) parsed = parse(raw_response) options = { - :authorization => parsed[authorization_field], - :test => (parsed[:on_test_gateway] == 'true'), - :avs_result => { :code => parsed[:response_avs_code] }, - :cvv_result => parsed[:response_cvv_code] + authorization: parsed[authorization_field], + test: (parsed[:on_test_gateway] == 'true'), + avs_result: { code: parsed[:response_avs_code] }, + cvv_result: parsed[:response_cvv_code] } Response.new(parsed[:succeeded] == 'true', parsed[:message] || parsed[:error], parsed, options) diff --git a/lib/active_merchant/billing/gateways/stripe.rb b/lib/active_merchant/billing/gateways/stripe.rb index aeaac0b4b82..eaaaf7039f3 100644 --- a/lib/active_merchant/billing/gateways/stripe.rb +++ b/lib/active_merchant/billing/gateways/stripe.rb @@ -1,18 +1,22 @@ require 'active_support/core_ext/hash/slice' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + # This gateway uses an older version of the Stripe API. + # To utilize the updated {Payment Intents API}[https://stripe.com/docs/api/payment_intents], integrate with the StripePaymentIntents gateway 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 = { @@ -21,10 +25,12 @@ class StripeGateway < Gateway 'unchecked' => 'P' } - self.supported_countries = %w(AT AU BE BR CA CH DE DK ES FI FR GB HK IE IT JP LU MX NL NO NZ PT SE SG US) + DEFAULT_API_VERSION = '2020-08-27' + + self.supported_countries = %w(AE AT AU BE BG BR CA CH CY CZ DE DK EE ES FI FR GB GR HK HU IE IN IT JP LT LU LV MT MX MY NL NO NZ PL PT RO SE SG SI SK US) self.default_currency = 'USD' self.money_format = :cents - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :jcb, :diners_club, :maestro] + self.supported_cardtypes = %i[visa master american_express discover jcb diners_club maestro unionpay] self.currencies_without_fractions = %w(BIF CLP DJF GNF JPY KMF KRW MGA PYG RWF VND VUV XAF XOF XPF UGX) self.homepage_url = 'https://stripe.com/' @@ -44,12 +50,13 @@ class StripeGateway < Gateway 'processing_error' => STANDARD_ERROR_CODE[:processing_error], 'incorrect_pin' => STANDARD_ERROR_CODE[:incorrect_pin], 'test_mode_live_card' => STANDARD_ERROR_CODE[:test_mode_live_card], - 'pickup_card' => STANDARD_ERROR_CODE[:pickup_card] + 'pickup_card' => STANDARD_ERROR_CODE[:pickup_card], + 'amount_too_small' => STANDARD_ERROR_CODE[:invalid_amount] } BANK_ACCOUNT_HOLDER_TYPE_MAPPING = { 'personal' => 'individual', - 'business' => 'company', + 'business' => 'company' } MINIMUM_AUTHORIZE_AMOUNTS = { @@ -76,21 +83,15 @@ def initialize(options = {}) end def authorize(money, payment, options = {}) - MultiResponse.run do |r| - if payment.is_a?(ApplePayPaymentToken) - r.process { tokenize_apple_pay_token(payment) } - payment = StripePaymentToken.new(r.params['token']) if r.success? - end - r.process do - post = create_post_for_auth_or_purchase(money, payment, options) - if emv_payment?(payment) - add_application_fee(post, options) - else - post[:capture] = 'false' - end - commit(:post, 'charges', post, options) - end - end.responses.last + if ach?(payment) + direct_bank_error = 'Direct bank account transactions are not supported for authorize.' + return Response.new(false, direct_bank_error) + end + + post = create_post_for_auth_or_purchase(money, payment, options) + add_application_fee(post, options) if emv_payment?(payment) + post[:capture] = 'false' + commit(:post, 'charges', post, options) end # To create a charge on a card or a token, call @@ -106,36 +107,33 @@ def purchase(money, payment, options = {}) return Response.new(false, direct_bank_error) end - MultiResponse.run do |r| - if payment.is_a?(ApplePayPaymentToken) - r.process { tokenize_apple_pay_token(payment) } - payment = StripePaymentToken.new(r.params['token']) if r.success? - end - r.process do - post = create_post_for_auth_or_purchase(money, payment, options) - post[:card][:processing_method] = 'quick_chip' if quickchip_payment?(payment) - commit(:post, 'charges', post, options) - end - end.responses.last + post = create_post_for_auth_or_purchase(money, payment, options) + post[:card][:processing_method] = 'quick_chip' if quickchip_payment?(payment) + commit(:post, 'charges', post, options) end def capture(money, authorization, options = {}) post = {} if emv_tc_response = options.delete(:icc_data) - post[:card] = { emv_approval_data: emv_tc_response } - commit(:post, "charges/#{CGI.escape(authorization)}", post, options) + # update the charge with emv data if card present + update = {} + update[:card] = { emv_approval_data: emv_tc_response } + commit(:post, "charges/#{CGI.escape(authorization)}", update, options) else add_application_fee(post, options) add_amount(post, money, options) add_exchange_rate(post, options) - commit(:post, "charges/#{CGI.escape(authorization)}/capture", post, options) end + + commit(:post, "charges/#{CGI.escape(authorization)}/capture", post, options) end def void(identification, options = {}) post = {} - post[:metadata] = options[:metadata] if options[:metadata] + post[:reverse_transfer] = options[:reverse_transfer] if options[:reverse_transfer] + add_metadata(post, options) + post[:reason] = options[:reason] if options[:reason] post[:expand] = [:charge] commit(:post, "charges/#{CGI.escape(identification)}/refunds", post, options) end @@ -145,17 +143,25 @@ def refund(money, identification, options = {}) add_amount(post, money, options) post[:refund_application_fee] = true if options[:refund_application_fee] post[:reverse_transfer] = options[:reverse_transfer] if options[:reverse_transfer] - post[:metadata] = options[:metadata] if options[:metadata] + add_metadata(post, options) + post[:reason] = options[:reason] if options[:reason] post[:expand] = [:charge] - MultiResponse.run(:first) do |r| - r.process { commit(:post, "charges/#{CGI.escape(identification)}/refunds", post, options) } + response = commit(:post, "charges/#{CGI.escape(identification)}/refunds", post, options) - if options[:refund_fee_amount] && options[:refund_fee_amount].to_s != '0' - r.process { fetch_application_fee(identification, options) } - r.process { refund_application_fee(options[:refund_fee_amount].to_i, application_fee_from_response(r.responses.last), options) } + if response.success? && options[:refund_fee_amount] && options[:refund_fee_amount].to_s != '0' + charge = api_request(:get, "charges/#{CGI.escape(identification)}", nil, options) + + if application_fee = charge['application_fee'] + fee_refund_options = { + currency: options[:currency], # currency isn't used by Stripe here, but we need it for #add_amount + key: @fee_refund_api_key + } + refund_application_fee(options[:refund_fee_amount].to_i, application_fee, fee_refund_options) end end + + response end def verify(payment, options = {}) @@ -166,21 +172,10 @@ def verify(payment, options = {}) end end - def application_fee_from_response(response) - return unless response.success? - response.params['application_fee'] unless response.params['application_fee'].empty? - end - def refund_application_fee(money, identification, options = {}) - return Response.new(false, 'Application fee id could not be found') unless identification - post = {} add_amount(post, money, options) - options.merge!(:key => @fee_refund_api_key) if @fee_refund_api_key - options.delete(:stripe_account) - - refund_fee = commit(:post, "application_fees/#{CGI.escape(identification)}/refunds", post, options) - application_fee_response!(refund_fee, "Application fee could not be refunded: #{refund_fee.message}") + commit(:post, "application_fees/#{CGI.escape(identification)}/refunds", post, options) end # Note: creating a new credit card will not change the customer's existing default credit card (use :set_default => true) @@ -188,14 +183,10 @@ def store(payment, options = {}) params = {} post = {} - if payment.is_a?(ApplePayPaymentToken) - token_exchange_response = tokenize_apple_pay_token(payment) - params = { card: token_exchange_response.params['token']['id'] } if token_exchange_response.success? - elsif payment.is_a?(StripePaymentToken) - add_payment_token(params, payment, options) - elsif payment.is_a?(Check) + if payment.is_a?(Check) bank_token_response = tokenize_bank_account(payment) return bank_token_response unless bank_token_response.success? + params = { source: bank_token_response.params['token']['id'] } else add_creditcard(params, payment, options) @@ -213,15 +204,12 @@ def store(payment, options = {}) # The /cards endpoint does not update other customer parameters. r.process { commit(:post, "customers/#{CGI.escape(options[:customer])}/cards", params, options) } - if options[:set_default] and r.success? and !r.params['id'].blank? - post[:default_card] = r.params['id'] - end + post[:default_card] = r.params['id'] if options[:set_default] && r.success? && !r.params['id'].blank? - if post.count > 0 - r.process { update_customer(options[:customer], post) } - end + r.process { update_customer(options[:customer], post.merge(expand: [:sources])) } if post.count > 0 end else + post[:expand] = [:sources] commit(:post, 'customers', post.merge(params), options) end end @@ -246,17 +234,6 @@ def unstore(identification, options = {}, deprecated_options = {}) commit(:delete, "customers/#{CGI.escape(customer_id)}/cards/#{CGI.escape(card_id)}", nil, options) end - def tokenize_apple_pay_token(apple_pay_payment_token, options = {}) - token_response = api_request(:post, "tokens?pk_token=#{CGI.escape(apple_pay_payment_token.payment_data.to_json)}") - success = !token_response.key?('error') - - if success && token_response.key?('id') - Response.new(success, nil, token: token_response) - else - Response.new(success, token_response['error']['message']) - end - end - def verify_credentials begin ssl_get(live_url + 'charges/nonexistent', headers) @@ -274,21 +251,37 @@ def supports_scrubbing? def scrub(transcript) transcript. gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). + gsub(%r((Authorization: Bearer )\w+), '\1[FILTERED]'). gsub(%r((&?three_d_secure\[cryptogram\]=)[\w=]*(&?)), '\1[FILTERED]\2'). - gsub(%r((card\[cryptogram\]=)[^&]+(&?)), '\1[FILTERED]\2'). - gsub(%r((card\[cvc\]=)\d+), '\1[FILTERED]'). - gsub(%r((card\[emv_approval_data\]=)[^&]+(&?)), '\1[FILTERED]\2'). - gsub(%r((card\[emv_auth_data\]=)[^&]+(&?)), '\1[FILTERED]\2'). - gsub(%r((card\[encrypted_pin\]=)[^&]+(&?)), '\1[FILTERED]\2'). - gsub(%r((card\[encrypted_pin_key_id\]=)[\w=]+(&?)), '\1[FILTERED]\2'). - gsub(%r((card\[number\]=)\d+), '\1[FILTERED]'). - gsub(%r((card\[swipe_data\]=)[^&]+(&?)), '\1[FILTERED]\2') + gsub(%r(((\[card\]|card)\[cryptogram\]=)[^&]+(&?)), '\1[FILTERED]\3'). + gsub(%r(((\[card\]|card)\[cvc\]=)\d+), '\1[FILTERED]'). + gsub(%r(((\[card\]|card)\[emv_approval_data\]=)[^&]+(&?)), '\1[FILTERED]\3'). + gsub(%r(((\[card\]|card)\[emv_auth_data\]=)[^&]+(&?)), '\1[FILTERED]\3'). + gsub(%r(((\[card\]|card)\[encrypted_pin\]=)[^&]+(&?)), '\1[FILTERED]\3'). + gsub(%r(((\[card\]|card)\[encrypted_pin_key_id\]=)[\w=]+(&?)), '\1[FILTERED]\3'). + 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\]\[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? true end + # Helper method to prevent hitting the external_account limit from remote test runs + def delete_latest_test_external_account(account) + return unless test? + + auth_header = { 'Authorization' => 'Basic ' + Base64.strict_encode64(options[:login].to_s + ':').strip } + url = "#{live_url}accounts/#{CGI.escape(account)}/external_accounts" + accounts_response = JSON.parse(ssl_get("#{url}?limit=100", auth_header)) + to_delete = accounts_response['data'].reject { |ac| ac['default_for_currency'] } + ssl_request(:delete, "#{url}/#{to_delete.first['id']}", nil, auth_header) + end + private class StripePaymentToken < PaymentToken @@ -297,15 +290,59 @@ def type end end + def create_source(money, payment, type, options = {}) + post = {} + add_amount(post, money, options, true) + post[:type] = type + if type == 'card' + add_creditcard(post, payment, options, true) + add_source_owner(post, payment, options) + elsif type == 'three_d_secure' + post[:three_d_secure] = { card: payment } + post[:redirect] = { return_url: options[:redirect_url] } + end + commit(:post, 'sources', post, options) + end + + def show_source(source_id, options) + commit(:get, "sources/#{source_id}", nil, options) + end + + def create_webhook_endpoint(options, events) + post = {} + post[:url] = options[:callback_url] + post[:enabled_events] = events + post[:connect] = true if options[:stripe_account] + options.delete(:stripe_account) + commit(:post, 'webhook_endpoints', post, options) + end + + def delete_webhook_endpoint(options) + commit(:delete, "webhook_endpoints/#{options[:webhook_id]}", {}, options) + end + + def show_webhook_endpoint(options) + options.delete(:stripe_account) + commit(:get, "webhook_endpoints/#{options[:webhook_id]}", nil, options) + end + + def list_webhook_endpoints(options) + params = {} + params[:limit] = options[:limit] if options[:limit] + options.delete(:stripe_account) + commit(:get, "webhook_endpoints?#{post_data(params)}", nil, options) + end + def create_post_for_auth_or_purchase(money, payment, options) post = {} - if payment.is_a?(StripePaymentToken) - add_payment_token(post, payment, options) - else - add_creditcard(post, payment, options) - end + add_creditcard(post, payment, options) + add_charge_details(post, money, payment, options) + post + end + # Used internally by Spreedly to populate the charge object for 3DS 1.0 transactions + def add_charge_details(post, money, payment, options) if emv_payment?(payment) add_statement_address(post, options) add_emv_metadata(post, payment) @@ -314,15 +351,20 @@ def create_post_for_auth_or_purchase(money, payment, options) add_customer_data(post, options) post[:description] = options[:description] post[:statement_descriptor] = options[:statement_description] + post[:statement_descriptor_suffix] = options[:statement_descriptor_suffix] if options[:statement_descriptor_suffix] post[:receipt_email] = options[:receipt_email] if options[:receipt_email] add_customer(post, payment, options) add_flags(post, options) end add_metadata(post, options) + add_shipping_address(post, payment, options) add_application_fee(post, options) add_exchange_rate(post, options) add_destination(post, options) + add_level_three(post, options) + add_connected_account(post, options) + add_radar_data(post, options) post end @@ -348,6 +390,19 @@ def add_destination(post, options) end end + def add_level_three(post, options) + level_three = {} + + copy_when_present(level_three, [:merchant_reference], options) + copy_when_present(level_three, [:customer_reference], options) + copy_when_present(level_three, [:shipping_address_zip], options) + copy_when_present(level_three, [:shipping_from_zip], options) + copy_when_present(level_three, [:shipping_amount], options) + copy_when_present(level_three, [:line_items], options) + + post[:level3] = level_three unless level_three.empty? + end + def add_expand_parameters(post, options) post[:expand] ||= [] post[:expand].concat(Array.wrap(options[:expand]).map(&:to_sym)).uniq! @@ -355,13 +410,13 @@ def add_expand_parameters(post, options) def add_external_account(post, card_params, payment) external_account = {} - external_account[:object] ='card' + external_account[:object] = 'card' external_account[:currency] = (options[:currency] || currency(payment)).downcase post[:external_account] = external_account.merge(card_params[:card]) end def add_customer_data(post, options) - metadata_options = [:description, :ip, :user_agent, :referrer] + metadata_options = %i[description ip user_agent referrer] post.update(options.slice(*metadata_options)) post[:external_id] = options[:order_id] @@ -369,7 +424,8 @@ def add_customer_data(post, options) end def add_address(post, options) - return unless post[:card] && post[:card].kind_of?(Hash) + return unless post[:card]&.kind_of?(Hash) + if address = options[:billing_address] || options[:address] post[:card][:address_line1] = address[:address1] if address[:address1] post[:card][:address_line2] = address[:address2] if address[:address2] @@ -382,7 +438,7 @@ def add_address(post, options) def add_statement_address(post, options) return unless statement_address = options[:statement_address] - return unless [:address1, :city, :zip, :state].all? { |key| statement_address[key].present? } + return unless %i[address1 city zip state].all? { |key| statement_address[key].present? } post[:statement_address] = {} post[:statement_address][:line1] = statement_address[:address1] @@ -392,11 +448,12 @@ def add_statement_address(post, options) post[:statement_address][:state] = statement_address[:state] end - def add_creditcard(post, creditcard, options) + def add_creditcard(post, creditcard, options, use_sources = false) card = {} if emv_payment?(creditcard) add_emv_creditcard(post, creditcard.icc_data) post[:card][:read_method] = 'contactless' if creditcard.read_method == 'contactless' + post[:card][:read_method] = 'contactless_magstripe_mode' if creditcard.read_method == 'contactless_magstripe' if creditcard.encrypted_pin_cryptogram.present? && creditcard.encrypted_pin_ksn.present? post[:card][:encrypted_pin] = creditcard.encrypted_pin_cryptogram post[:card][:encrypted_pin_key_id] = creditcard.encrypted_pin_ksn @@ -414,7 +471,7 @@ def add_creditcard(post, creditcard, options) card[:exp_month] = creditcard.month card[:exp_year] = creditcard.year card[:cvc] = creditcard.verification_value if creditcard.verification_value? - card[:name] = creditcard.name if creditcard.name + card[:name] = creditcard.name if creditcard.name && !use_sources end if creditcard.is_a?(NetworkTokenizationCreditCard) @@ -424,7 +481,7 @@ def add_creditcard(post, creditcard, options) end post[:card] = card - add_address(post, options) + add_address(post, options) unless use_sources elsif creditcard.kind_of?(String) if options[:track_data] card[:swipe_data] = options[:track_data] @@ -443,20 +500,13 @@ def add_emv_creditcard(post, icc_data, options = {}) post[:card] = { emv_auth_data: icc_data } end - def add_payment_token(post, token, options = {}) - post[:card] = token.payment_data['id'] - end - def add_customer(post, payment, options) - if options[:customer] && !payment.respond_to?(:number) - ActiveMerchant.deprecated 'Passing the customer in the options is deprecated. Just use the response.authorization instead.' - post[:customer] = options[:customer] - end + post[:customer] = options[:customer] if options[:customer] && !payment.respond_to?(:number) end def add_flags(post, options) post[:uncaptured] = true if options[:uncaptured] - post[:recurring] = true if (options[:eci] == 'recurring' || options[:recurring]) + post[:recurring] = true if options[:eci] == 'recurring' || options[:recurring] end def add_metadata(post, options = {}) @@ -464,7 +514,6 @@ def add_metadata(post, options = {}) post[:metadata].merge!(options[:metadata]) if options[:metadata] post[:metadata][:email] = options[:email] if options[:email] post[:metadata][:order_id] = options[:order_id] if options[:order_id] - post.delete(:metadata) if post[:metadata].empty? end def add_emv_metadata(post, creditcard) @@ -472,69 +521,155 @@ def add_emv_metadata(post, creditcard) post[:metadata][:card_read_method] = creditcard.read_method if creditcard.respond_to?(:read_method) end - def fetch_application_fee(identification, options = {}) - options.merge!(:key => @fee_refund_api_key) + def add_shipping_address(post, payment, options = {}) + return unless shipping = options[:shipping_address] + return unless shipping_name = shipping[:name] + + post[:shipping] = {} + + post[:shipping][:name] = shipping_name + post[:shipping][:address] = {} + post[:shipping][:address][:line1] = shipping[:address1] + post[:shipping][:address][:line2] = shipping[:address2] if shipping[:address2] + post[:shipping][:address][:city] = shipping[:city] if shipping[:city] + post[:shipping][:address][:country] = shipping[:country] if shipping[:country] + post[:shipping][:address][:state] = shipping[:state] if shipping[:state] + post[:shipping][:address][:postal_code] = shipping[:zip] if shipping[:zip] + post[:shipping][:phone] = shipping[:phone_number] if shipping[:phone_number] + end + + def add_source_owner(post, creditcard, options) + post[:owner] = {} + post[:owner][:name] = creditcard.name if creditcard.respond_to?(:name) && creditcard.name + post[:owner][:email] = options[:email] if options[:email] + + if address = options[:billing_address] || options[:address] + owner_address = {} + owner_address[:line1] = address[:address1] if address[:address1] + owner_address[:line2] = address[:address2] if address[:address2] + owner_address[:country] = address[:country] if address[:country] + owner_address[:postal_code] = address[:zip] if address[:zip] + owner_address[:state] = address[:state] if address[:state] + owner_address[:city] = address[:city] if address[:city] + + post[:owner][:phone] = address[:phone] if address[:phone] + post[:owner][:address] = owner_address + end + end + + def add_connected_account(post, options = {}) + post[:on_behalf_of] = options[:on_behalf_of] if options[:on_behalf_of] + + return unless options[:transfer_destination] + + post[:transfer_data] = { destination: options[:transfer_destination] } + post[:transfer_data][:amount] = options[:transfer_amount] if options[:transfer_amount] + post[:transfer_group] = options[:transfer_group] if options[:transfer_group] + post[:application_fee_amount] = options[:application_fee_amount] if options[:application_fee_amount] + end + + def add_radar_data(post, options = {}) + radar_options = {} + radar_options[:session] = options[:radar_session_id] if options[:radar_session_id] + radar_options[:skip_rules] = ['all'] if options[:skip_radar_rules] + + post[:radar_options] = radar_options unless radar_options.empty? + end + + def add_header_fields(response) + return unless @response_headers.present? - fetch_charge = commit(:get, "charges/#{CGI.escape(identification)}", nil, options) - application_fee_response!(fetch_charge, "Application fee id could not be retrieved: #{fetch_charge.message}") + headers = {} + headers['response_headers'] = {} + headers['response_headers']['idempotent_replayed'] = @response_headers['idempotent-replayed'] if @response_headers['idempotent-replayed'] + headers['response_headers']['stripe_should_retry'] = @response_headers['stripe-should-retry'] if @response_headers['stripe-should-retry'] + + response.merge!(headers) end - def application_fee_response!(response, message) - response.success? ? response : Response.new(false, message) + def add_card_response_field(response) + return if @card_3d_supported.nil? + + card_details = {} + card_details['three_d_secure_usage_supported'] = @card_3d_supported + + response.merge!(card_details) end def parse(body) - JSON.parse(body) + response = JSON.parse(body) + add_header_fields(response) + add_card_response_field(response) + + response end def post_data(params) return nil unless params - params.map do |key, value| + flatten_params([], params).join('&') + end + + def flatten_params(flattened, params, prefix = nil) + params.each do |key, value| next if value != false && value.blank? + + flattened_key = prefix.nil? ? key : "#{prefix}[#{key}]" if value.is_a?(Hash) - h = {} - value.each do |k, v| - h["#{key}[#{k}]"] = v unless v.blank? - end - post_data(h) + flatten_params(flattened, value, flattened_key) elsif value.is_a?(Array) - value.map { |v| "#{key}[]=#{CGI.escape(v.to_s)}" }.join('&') + flatten_array(flattened, value, flattened_key) + else + flattened << "#{flattened_key}=#{CGI.escape(value.to_s)}" + end + end + flattened + end + + def flatten_array(flattened, array, prefix) + array.each_with_index do |item, idx| + key = "#{prefix}[#{idx}]" + if item.is_a?(Hash) + flatten_params(flattened, item, key) + elsif item.is_a?(Array) + flatten_array(flattened, item, key) else - "#{key}=#{CGI.escape(value.to_s)}" + flattened << "#{key}=#{CGI.escape(item.to_s)}" end - end.compact.join('&') + 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(method = :post, options = {}) headers = { - 'Authorization' => 'Basic ' + Base64.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 + 'X-Stripe-Client-User-Metadata' => { ip: options[:ip] }.to_json } - headers.merge!('Idempotency-Key' => idempotency_key) if idempotency_key - headers.merge!('Stripe-Account' => options[:stripe_account]) if options[:stripe_account] + headers['Idempotency-Key'] = options[:idempotency_key] if options[:idempotency_key] && method != :get + headers['Stripe-Account'] = options[:stripe_account] if options[:stripe_account] headers end def stripe_client_user_agent(options) return user_agent unless options[:application] - JSON.dump(JSON.parse(user_agent).merge!({application: options[:application]})) + + JSON.dump(JSON.parse(user_agent).merge!({ application: options[:application] })) end def api_version(options) - options[:version] || @options[:version] || '2015-04-07' + options[:version] || @options[:version] || self.class::DEFAULT_API_VERSION end def api_request(method, endpoint, parameters = nil, options = {}) raw_response = response = nil begin - raw_response = ssl_request(method, self.live_url + endpoint, post_data(parameters), headers(options)) + raw_response = ssl_request(method, self.live_url + endpoint, post_data(parameters), headers(method, options)) response = parse(raw_response) rescue ResponseError => e raw_response = e.response.body @@ -547,54 +682,81 @@ def api_request(method, endpoint, parameters = nil, options = {}) def commit(method, url, parameters = nil, options = {}) add_expand_parameters(parameters, options) if parameters - response = api_request(method, url, parameters, options) - - success = success_from(response) - - 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']] + return Response.new(false, 'Invalid API Key provided') unless key_valid?(options) - Response.new(success, + response = api_request(method, url, parameters, options) + response['webhook_id'] = options[:webhook_id] if options[:webhook_id] + success = success_from(response, options) + + 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), - :avs_result => { :code => avs_code }, - :cvv_result => cvc_code, - :emv_authorization => emv_authorization_from_response(response), - :error_code => success ? nil : error_code_from(response) + test: response_is_test?(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) ) end - def authorization_from(success, url, method, response) - return response.fetch('error', {})['charge'] unless success + 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, options) + return error_id(response, url) unless success if url == 'customers' - [response['id'], response['sources']['data'].first['id']].join('|') - elsif method == :post && url.match(/customers\/.*\/cards/) - [response['customer'], response['id']].join('|') + [response['id'], response.dig('sources', 'data').first&.dig('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 end + def error_id(response, url) + response.dig('error', 'charge') || response.dig('error', 'setup_intent', 'id') || response['id'] + end + def message_from(success, response) - success ? 'Transaction approved' : response.fetch('error', {'message' => 'No error details'})['message'] + success ? 'Transaction approved' : response.fetch('error', { 'message' => 'No error details' })['message'] end - def success_from(response) + def success_from(response, options) !response.key?('error') && response['status'] != 'failed' end - def response_error(raw_response) - begin - parse(raw_response) - rescue JSON::ParserError - json_error(raw_response) + # Override the regular handle response so we can access the headers + # set header fields and values so we can add them to the response body + def handle_response(response) + @response_headers = response.each_header.to_h if response.respond_to?(:header) + case response.code.to_i + when 200...300 + response.body + else + raise ResponseError.new(response) end end + def response_error(raw_response) + parse(raw_response) + rescue JSON::ParserError + json_error(raw_response) + end + def json_error(raw_response) msg = 'Invalid response received from the Stripe API. Please contact support@stripe.com if you continue to receive this message.' msg += " (The raw response returned by the API was #{raw_response.inspect})" @@ -624,7 +786,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) @@ -653,8 +818,8 @@ def tokenize_bank_account(bank_account, options = {}) country: 'US', currency: 'usd', routing_number: bank_account.routing_number, - name: bank_account.name, - account_holder_type: account_holder_type, + account_holder_name: bank_account.name, + account_holder_type: } } @@ -679,8 +844,26 @@ def ach?(payment_method) def auth_minimum_amount(options) return 100 unless options[:currency] + return MINIMUM_AUTHORIZE_AMOUNTS[options[:currency].upcase] || 100 end + + def copy_when_present(dest, dest_path, source, source_path = nil) + source_path ||= dest_path + source_path.each do |key| + return nil unless source[key] + + source = source[key] + end + + if source + dest_path.first(dest_path.size - 1).each do |key| + dest[key] ||= {} + dest = dest[key] + end + dest[dest_path.last] = source + end + end end end end diff --git a/lib/active_merchant/billing/gateways/stripe_payment_intents.rb b/lib/active_merchant/billing/gateways/stripe_payment_intents.rb new file mode 100644 index 00000000000..fb7430241bd --- /dev/null +++ b/lib/active_merchant/billing/gateways/stripe_payment_intents.rb @@ -0,0 +1,758 @@ +require 'active_support/core_ext/hash/slice' + +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + # This gateway uses the current Stripe {Payment Intents API}[https://stripe.com/docs/api/payment_intents]. + # For the legacy API, see the Stripe gateway + class StripePaymentIntentsGateway < StripeGateway + ALLOWED_METHOD_STATES = %w[automatic manual].freeze + ALLOWED_CANCELLATION_REASONS = %w[duplicate fraudulent requested_by_customer abandoned].freeze + CREATE_INTENT_ATTRIBUTES = %i[description statement_descriptor_suffix statement_descriptor receipt_email save_payment_method] + 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' + } + + def create_intent(money, payment_method, options = {}) + MultiResponse.run do |r| + 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 + r.process do + post = {} + add_amount(post, money, options, true) + add_capture_method(post, options) + add_confirmation_method(post, options) + add_customer(post, options) + + 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_info(post, payment_method, options) + 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_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) + end + commit(:post, 'payment_intents', post, options) + end + end + end + + 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 = {} + 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_network_token_info(post, payment_method, options) + add_payment_method_types(post, options) + CONFIRM_INTENT_ATTRIBUTES.each do |attribute| + add_whitelisted_attribute(post, options, attribute) + end + + commit(:post, "payment_intents/#{intent_id}/confirm", post, options) + end + + 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 = { + 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 + + # wallet_type is only passed for non-tokenized GooglePay which acts as a CreditCard + if options[:wallet_type] + post[:metadata] ||= {} + post[:metadata][:input_method] = 'GooglePay' + 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) + post_data.merge!({ + payment_method_types: ['card'], + payment_method_data: { type: 'card', card: { token: payment_method } } + }) + end + + def update_intent(money, intent_id, payment_method, options = {}) + post = {} + add_amount(post, money, options) + + 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_network_token_info(post, payment_method, options) + add_payment_method_types(post, options) + add_customer(post, options) + add_metadata(post, options) + add_shipping_address(post, options) + add_connected_account(post, options) + add_fulfillment_date(post, options) + + UPDATE_INTENT_ATTRIBUTES.each do |attribute| + add_whitelisted_attribute(post, options, attribute) + end + commit(:post, "payment_intents/#{intent_id}", post, options) + end + + def create_setup_intent(payment_method, options = {}) + MultiResponse.run do |r| + r.process do + post = {} + add_customer(post, options) + + 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 + + add_network_token_info(post, payment_method, 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) + add_exemption(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 = {}) + # Retrieving a setup_intent passing 'expand[]=latest_attempt' allows the caller to + # check for a network_transaction_id and ds_transaction_id + # eg (latest_attempt -> payment_method_details -> card -> network_transaction_id) + # + # Being able to retrieve these fields enables payment flows that rely on MIT exemptions, e.g: off_session + commit(:get, "setup_intents/#{setup_intent_id}?expand[]=latest_attempt", nil, options) + end + + def authorize(money, payment_method, options = {}) + create_intent(money, payment_method, options.merge!(confirm: true, capture_method: 'manual')) + end + + def purchase(money, payment_method, options = {}) + create_intent(money, payment_method, options.merge!(confirm: true, capture_method: 'automatic')) + end + + def capture(money, intent_id, options = {}) + post = {} + currency = options[:currency] || currency(money) + post[:amount_to_capture] = localized_amount(money, currency) + if options[:transfer_amount] + post[:transfer_data] = {} + post[:transfer_data][:amount] = options[:transfer_amount] + end + post[:application_fee_amount] = options[:application_fee] if options[:application_fee] + options = format_idempotency_key(options, 'capture') + commit(:post, "payment_intents/#{intent_id}/capture", post, options) + end + + def void(intent_id, options = {}) + post = {} + post[:cancellation_reason] = options[:cancellation_reason] if ALLOWED_CANCELLATION_REASONS.include?(options[:cancellation_reason]) + commit(:post, "payment_intents/#{intent_id}/cancel", post, options) + end + + def refund(money, intent_id, options = {}) + if intent_id.include?('pi_') + intent = api_request(:get, "payment_intents/#{intent_id}", nil, options) + + return Response.new(false, intent['error']['message'], intent) if intent['error'] + + charge_id = intent.try(:[], 'charges').try(:[], 'data').try(:[], 0).try(:[], 'id') + + if charge_id.nil? + error_message = "No associated charge for #{intent['id']}" + error_message << "; payment_intent has a status of #{intent['status']}" if intent.try(:[], 'status') && intent.try(:[], 'status') != 'succeeded' + return Response.new(false, error_message, intent) + end + else + charge_id = intent_id + end + + super(money, charge_id, options) + end + + # Note: Not all payment methods are currently supported by the {Payment Methods API}[https://stripe.com/docs/payments/payment-methods] + # Current implementation will create a PaymentMethod object if the method is a token or credit card + # All other types will default to legacy Stripe store + def store(payment_method, options = {}) + params = {} + # If customer option is provided, create a payment method and attach to customer id + # Otherwise, create a customer, then attach + 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?(ActiveMerchant::Billing::CreditCard) + result = add_payment_method_token(params, payment_method, options) + return result if result.is_a?(ActiveMerchant::Billing::Response) + + 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? + commit(:post, "payment_methods/#{params[:payment_method]}/attach", attach_parameters, options) + else + super(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('|') + commit(:post, "payment_methods/#{payment_method}/detach", nil, options) + else + super(identification, options, deprecated_options) + end + end + + def verify(payment_method, options = {}) + create_setup_intent(payment_method, options.merge!({ confirm: true, verify: true })) + end + + def setup_purchase(money, options = {}) + requires!(options, :payment_method_types) + post = {} + add_currency(post, options, money) + add_amount(post, money, options) + add_payment_method_types(post, options) + add_metadata(post, options) + commit(:post, 'payment_intents', post, options) + end + + def supports_network_tokenization? + true + end + + private + + def error_id(response, url) + if url.end_with?('payment_intents') + response.dig('error', 'payment_intent', 'id') || super + else + super + end + end + + 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 + + def add_connected_account(post, options = {}) + super(post, options) + post[:application_fee_amount] = options[:application_fee] if options[:application_fee] + end + + def add_whitelisted_attribute(post, options, attribute) + post[attribute] = options[attribute] if options[attribute] + end + + def add_capture_method(post, options) + capture_method = options[:capture_method].to_s + post[:capture_method] = capture_method if ALLOWED_METHOD_STATES.include?(capture_method) + end + + def add_confirmation_method(post, options) + confirmation_method = options[:confirmation_method].to_s + post[:confirmation_method] = confirmation_method if ALLOWED_METHOD_STATES.include?(confirmation_method) + end + + def add_customer(post, options) + customer = options[:customer].to_s + post[:customer] = customer if customer.start_with?('cus_') + end + + def add_fulfillment_date(post, options) + post[:fulfillment_date] = options[:fulfillment_date].to_i if options[:fulfillment_date] + end + + def add_metadata(post, options = {}) + super + + 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 = {} + + level_three[:merchant_reference] = options[:merchant_reference] if options[:merchant_reference] + level_three[:customer_reference] = options[:customer_reference] if options[:customer_reference] + level_three[:shipping_address_zip] = options[:shipping_address_zip] if options[:shipping_address_zip] + level_three[:shipping_from_zip] = options[:shipping_from_zip] if options[:shipping_from_zip] + level_three[:shipping_amount] = options[:shipping_amount] if options[:shipping_amount] + level_three[:line_items] = options[:line_items] if options[:line_items] + + post[:level3] = level_three unless level_three.empty? + end + + def add_return_url(post, options) + return unless options[:confirm] + + post[:confirm] = options[:confirm] + post[:return_url] = options[:return_url] if options[:return_url] + end + + def add_payment_method_token(post, payment_method, options, responses = []) + case payment_method + when String + extract_token_from_string_and_maybe_add_customer_id(post, payment_method) + when ActiveMerchant::Billing::CreditCard + 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] || payment_method.number[-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_info(post, payment_method, options) + # wallet_type is only passed for non-tokenized GooglePay which acts as a CreditCard + if options[:wallet_type] + post[:metadata] ||= {} + post[:metadata][:input_method] = 'GooglePay' + end + + return unless payment_method.is_a?(NetworkTokenizationCreditCard) && options.dig(:stored_credential, :initiator) != 'merchant' + return if digital_wallet_payment_method?(payment_method) && options[:new_ap_gp_route] != true + + post[:payment_method_options] ||= {} + post[:payment_method_options][:card] ||= {} + post[:payment_method_options][:card][:network_token] ||= {} + post[:payment_method_options][:card][:network_token].merge!({ + cryptogram: payment_method.respond_to?(:payment_cryptogram) ? payment_method.payment_cryptogram : options[:cryptogram], + electronic_commerce_indicator: format_eci(payment_method, options) + }.compact) + 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, + tokenization_method: DIGITAL_WALLETS[payment_method.source] + } + } + } + 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 + + def extract_token_from_string_and_maybe_add_customer_id(post, payment_method) + if payment_method.include?('|') + customer_id, payment_method = payment_method.split('|') + post[:customer] = customer_id + end + + if payment_method.include?('tok_') + add_payment_method_card_data_token(post, payment_method) + else + post[:payment_method] = payment_method + end + end + + def tokenize_apple_google(payment, options = {}) + tokenization_method = payment.source == :google_pay ? :android_pay : payment.source + post = { + card: { + number: payment.number, + exp_month: payment.month, + exp_year: payment.year, + tokenization_method:, + eci: payment.eci, + 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'] + Response.new(success, nil, token: token_response) + elsif token_response['error']['message'] + Response.new(false, "The tokenization process fails. #{token_response['error']['message']}") + else + Response.new(false, "The tokenization process fails. #{token_response}") + end + end + + 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, responses) + payment_method_response = create_payment_method(payment_method, options) + return payment_method_response if payment_method_response.failure? + + add_card_3d_secure_usage_supported(payment_method_response) + + responses << payment_method_response + add_payment_method_token(post, payment_method_response.params['id'], options) + end + + def add_payment_method_types(post, options) + payment_method_types = options[:payment_method_types] if options[:payment_method_types] + return if payment_method_types.nil? + + post[:payment_method_types] = Array(payment_method_types) + end + + def add_exemption(post, options = {}) + return unless options[:confirm] && options[:moto] + + post[:payment_method_options] ||= {} + post[:payment_method_options][:card] ||= {} + post[:payment_method_options][:card][:moto] = true if options[:moto] + end + + # 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 = {}) + stored_credential = options[:stored_credential] + return unless stored_credential && !stored_credential.values.all?(&:nil?) + + post[:payment_method_options] ||= {} + post[:payment_method_options][:card] ||= {} + + 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. + 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 %w(setup_on_session stored_on_session).include?(stored_credential_type) + 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 = {}) + return unless options[:network_transaction_id] + + post[:payment_method_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] + end + + def add_claim_without_transaction_id(post, options = {}) + return if options[:stored_credential] || options[:network_transaction_id] || options[:ds_transaction_id] + return unless options[:claim_without_transaction_id] + + post[:payment_method_options] ||= {} + post[:payment_method_options][:card] ||= {} + post[:payment_method_options][:card][:mit_exemption] = {} + + # Stripe PI accepts claim_without_transaction_id for transactions without transaction ids. + # Gateway validation for this field occurs through a different service, before the transaction request is sent to the gateway. + 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] + + post[:error_on_requires_action] = true if options[:error_on_requires_action] + end + + def request_three_d_secure(post, options = {}) + return unless options[:request_three_d_secure] && %w(any automatic challenge).include?(options[:request_three_d_secure]) + + post[:payment_method_options] ||= {} + post[:payment_method_options][:card] ||= {} + post[:payment_method_options][:card][:request_three_d_secure] = options[:request_three_d_secure] + end + + def add_external_three_d_secure_auth_data(post, options = {}) + return unless options[:three_d_secure]&.is_a?(Hash) + + three_d_secure = options[:three_d_secure] + post[:payment_method_options] ||= {} + post[:payment_method_options][:card] ||= {} + post[:payment_method_options][:card][:three_d_secure] ||= {} + post[:payment_method_options][:card][:three_d_secure][:version] = three_d_secure[:version] || (three_d_secure[:ds_transaction_id] ? '2.2.0' : '1.0.2') + post[:payment_method_options][:card][:three_d_secure][:electronic_commerce_indicator] = three_d_secure[:eci] if three_d_secure[:eci] + post[:payment_method_options][:card][:three_d_secure][:cryptogram] = three_d_secure[:cavv] if three_d_secure[:cavv] + post[:payment_method_options][:card][:three_d_secure][:transaction_id] = three_d_secure[:ds_transaction_id] || three_d_secure[:xid] + end + + def setup_future_usage(post, options = {}) + post[:setup_future_usage] = options[:setup_future_usage] if %w(on_session off_session).include?(options[:setup_future_usage]) + post[:off_session] = options[:off_session] if off_session_request?(options) + post + end + + def add_billing_address(post, payment_method, options = {}) + return if payment_method.nil? || payment_method.is_a?(String) + + post[:payment_method_data] ||= {} + if billing = options[:billing_address] || options[:address] + post[:payment_method_data][:billing_details] = add_address(billing, options) + end + + 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: } + end + end + + def add_shipping_address(post, options = {}) + return unless shipping = options[:shipping_address] + + 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 + + # This surfaces the three_d_secure_usage.supported field and saves it as an instance variable so that we can access it later on in the response + def add_card_3d_secure_usage_supported(response) + return unless response.params['card'] && response.params['card']['three_d_secure_usage'] + + @card_3d_supported = response.params['card']['three_d_secure_usage']['supported'] + end + + def format_idempotency_key(options, suffix) + return options unless options[:idempotency_key] + + options.merge(idempotency_key: "#{options[:idempotency_key]}-#{suffix}") + end + + def success_from(response, options) + if response['status'] == 'requires_action' && !options[:execute_threed] + response['error'] = {} + response['error']['message'] = 'Received unexpected 3DS authentication response, but a 3DS initiation flag was not included in the request.' + return false + end + + super(response, options) + end + + def add_currency(post, options, money) + post[:currency] = options[:currency] || currency(money) + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/sum_up.rb b/lib/active_merchant/billing/gateways/sum_up.rb new file mode 100644 index 00000000000..ae6cffa7ebf --- /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[:partner_id] && options[:order_id] ? "#{options[:partner_id]}-#{options[:order_id]}" : 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 802a033188f..ffb9ef0e340 100644 --- a/lib/active_merchant/billing/gateways/swipe_checkout.rb +++ b/lib/active_merchant/billing/gateways/swipe_checkout.rb @@ -1,22 +1,19 @@ require 'json' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class SwipeCheckoutGateway < Gateway TRANSACTION_APPROVED_MSG = 'Transaction approved' TRANSACTION_DECLINED_MSG = 'Transaction declined' - LIVE_URLS = { - 'NZ' => 'https://api.swipehq.com', - 'CA' => 'https://api.swipehq.ca' - } + self.live_url = 'https://api.swipehq.com' self.test_url = 'https://api.swipehq.com' TRANSACTION_API = '/createShopifyTransaction.php' - self.supported_countries = %w[ NZ CA ] + self.supported_countries = %w[NZ CA] self.default_currency = 'NZD' - self.supported_cardtypes = [:visa, :master] + self.supported_cardtypes = %i[visa master] self.homepage_url = 'https://www.swipehq.com/checkout' self.display_name = 'Swipe Checkout' self.money_format = :dollars @@ -61,7 +58,7 @@ def add_customer_data(post, creditcard, options) post[:address] = "#{address[:address1]}, #{address[:address2]}" post[:city] = address[:city] post[:country] = address[:country] - post[:mobile] = address[:phone] # API only has a "mobile" field, no "phone" + post[:mobile] = address[:phone] # API only has a "mobile" field, no "phone" end def add_invoice(post, options) @@ -107,12 +104,13 @@ 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) @@ -120,37 +118,36 @@ def commit(action, money, parameters) rescue ResponseError => e build_error_response("ssl_post() with url #{url} raised ResponseError: #{e}") rescue JSON::ParserError => e - msg = 'Invalid response received from the Swipe Checkout API. ' + - 'Please contact support@optimizerhq.com if you continue to receive this message.' + + msg = 'Invalid response received from the Swipe Checkout API. ' \ + 'Please contact support@optimizerhq.com if you continue to receive this message.' \ " (Full error message: #{e})" build_error_response(msg) end end end - def call_api(api, params=nil) + def call_api(api, params = nil) params ||= {} params[:merchant_id] = @options[:login] params[:api_key] = @options[:api_key] # ssl_post() returns the response body as a string on success, # or raises a ResponseError exception on failure - JSON.parse(ssl_post(url(@options[:region], api), params.to_query)) + JSON.parse(ssl_post(url(api), params.to_query)) end - def url(region, api) - ((test? ? self.test_url : LIVE_URLS[region]) + api) + def url(api) + (test? ? self.test_url : self.live_url) + api end - def build_error_response(message, params={}) + def build_error_response(message, params = {}) Response.new( false, message, params, - :test => test? + test: test? ) end end end end - diff --git a/lib/active_merchant/billing/gateways/telr.rb b/lib/active_merchant/billing/gateways/telr.rb index 9d8085fb175..ee43ed43343 100644 --- a/lib/active_merchant/billing/gateways/telr.rb +++ b/lib/active_merchant/billing/gateways/telr.rb @@ -1,23 +1,23 @@ require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class TelrGateway < Gateway self.display_name = 'Telr' self.homepage_url = 'http://www.telr.com/' self.live_url = 'https://secure.telr.com/gateway/remote.xml' - self.supported_countries = ['AE', 'IN', 'SA'] + self.supported_countries = %w[AE IN SA] self.default_currency = 'AED' self.money_format = :dollars - self.supported_cardtypes = [:visa, :master, :american_express, :maestro, :solo, :jcb] + self.supported_cardtypes = %i[visa master american_express maestro jcb] CVC_CODE_TRANSLATOR = { 'Y' => 'M', 'N' => 'N', 'X' => 'P', - 'E' => 'U', + 'E' => 'U' } AVS_CODE_TRANSLATOR = { @@ -28,12 +28,12 @@ class TelrGateway < Gateway 'E' => 'R' } - def initialize(options={}) + def initialize(options = {}) requires!(options, :merchant_id, :api_key) super end - def purchase(amount, payment_method, options={}) + def purchase(amount, payment_method, options = {}) commit(:purchase, amount, options[:currency]) do |doc| add_invoice(doc, 'sale', amount, payment_method, options) add_payment_method(doc, payment_method, options) @@ -41,7 +41,7 @@ def purchase(amount, payment_method, options={}) end end - def authorize(amount, payment_method, options={}) + def authorize(amount, payment_method, options = {}) commit(:authorize, amount, options[:currency]) do |doc| add_invoice(doc, 'auth', amount, payment_method, options) add_payment_method(doc, payment_method, options) @@ -49,26 +49,26 @@ def authorize(amount, payment_method, options={}) end end - def capture(amount, authorization, options={}) + def capture(amount, authorization, options = {}) commit(:capture) do |doc| add_invoice(doc, 'capture', amount, authorization, options) end end - def void(authorization, options={}) + def void(authorization, options = {}) _, amount, currency = split_authorization(authorization) commit(:void) do |doc| - add_invoice(doc, 'void', amount.to_i, authorization, options.merge(currency: currency)) + add_invoice(doc, 'void', amount.to_i, authorization, options.merge(currency:)) end end - def refund(amount, authorization, options={}) + def refund(amount, authorization, options = {}) commit(:refund) do |doc| add_invoice(doc, 'refund', amount, authorization, options) end end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) commit(:verify) do |doc| add_invoice(doc, 'verify', 100, credit_card, options) add_payment_method(doc, credit_card, options) @@ -78,7 +78,7 @@ def verify(credit_card, options={}) def verify_credentials response = void('0') - !['01', '04'].include?(response.error_code) + !%w[01 04].include?(response.error_code) end def supports_scrubbing? @@ -87,9 +87,9 @@ def supports_scrubbing? 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'). + gsub(%r(()[^<]+(<))i, '\1[FILTERED]\2') end private @@ -109,6 +109,7 @@ def add_invoice(doc, action, money, payment_method, options) def add_payment_method(doc, payment_method, options) return if payment_method.is_a?(String) + doc.card do doc.number(payment_method.number) doc.cvv(payment_method.verification_value) @@ -121,6 +122,7 @@ def add_payment_method(doc, payment_method, options) def add_customer_data(doc, payment_method, options) return if payment_method.is_a?(String) + doc.billing do doc.name do doc.first(payment_method.first_name) @@ -140,15 +142,14 @@ def add_address(doc, options) doc.city(address[:city] || 'City') doc.line1(address[:address1] || 'Address') return unless address + doc.line2(address[:address2]) if address[:address2] doc.zip(address[:zip]) if address[:zip] doc.region(address[:state]) if address[:state] end def add_ref(doc, action, payment_method) - if ['capture', 'refund', 'void'].include?(action) || payment_method.is_a?(String) - doc.ref(split_authorization(payment_method)[0]) - end + doc.ref(split_authorization(payment_method)[0]) if %w[capture refund void].include?(action) || payment_method.is_a?(String) end def add_authentication(doc) @@ -161,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) @@ -190,7 +191,6 @@ def root_attributes def build_xml_request builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml| xml.remote do |doc| - add_authentication(doc) yield(doc) end @@ -215,24 +215,23 @@ def parse(xml) response = {} doc = Nokogiri::XML(xml) - doc.root.xpath('*').each do |node| - if (node.elements.size == 0) + doc.root&.xpath('*')&.each do |node| + if node.elements.size == 0 response[node.name.downcase.to_sym] = node.text else node.elements.each do |childnode| - name = "#{childnode.name.downcase}" + name = childnode.name.downcase response[name.to_sym] = childnode.text end end - end unless doc.root.nil? + end response end 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) @@ -252,9 +251,7 @@ def message_from(succeeded, response) end def error_code_from(succeeded, response) - unless succeeded - response[:code] - end + response[:code] unless succeeded end def cvv_result(parsed) diff --git a/lib/active_merchant/billing/gateways/tns.rb b/lib/active_merchant/billing/gateways/tns.rb index 0aa15904050..b84da916ef5 100644 --- a/lib/active_merchant/billing/gateways/tns.rb +++ b/lib/active_merchant/billing/gateways/tns.rb @@ -3,20 +3,21 @@ module Billing class TnsGateway < Gateway include MastercardGateway - class_attribute :live_na_url, :live_ap_url, :test_na_url, :test_ap_url + class_attribute :live_na_url, :live_ap_url, :live_eu_url, :test_na_url, :test_ap_url, :test_eu_url - self.live_na_url = 'https://secure.na.tnspayments.com/api/rest/version/36/' - self.test_na_url = 'https://secure.na.tnspayments.com/api/rest/version/36/' + VERSION = '52' - self.live_ap_url = 'https://secure.ap.tnspayments.com/api/rest/version/36/' - self.test_ap_url = 'https://secure.ap.tnspayments.com/api/rest/version/36/' + self.live_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.live_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/' self.supported_countries = %w(AR AU BR FR DE HK MX NZ SG GB US) self.default_currency = 'USD' - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb, :maestro, :laser] - + self.supported_cardtypes = %i[visa master american_express discover diners_club jcb maestro] end end end diff --git a/lib/active_merchant/billing/gateways/trans_first.rb b/lib/active_merchant/billing/gateways/trans_first.rb index 337a4f03d9b..d015b63f05a 100644 --- a/lib/active_merchant/billing/gateways/trans_first.rb +++ b/lib/active_merchant/billing/gateways/trans_first.rb @@ -1,12 +1,11 @@ -module ActiveMerchant #:nodoc: - - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class TransFirstGateway < Gateway self.test_url = 'https://ws.cert.transfirst.com' self.live_url = 'https://webservices.primerchants.com' self.supported_countries = ['US'] - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'http://www.transfirst.com/' self.display_name = 'TransFirst' @@ -19,7 +18,7 @@ class TransFirstGateway < Gateway purchase_echeck: 'ACHDebit', refund: 'CreditCardCredit', refund_echeck: 'ACHVoidTransaction', - void: 'CreditCardAutoRefundorVoid', + void: 'CreditCardAutoRefundorVoid' } ENDPOINTS = { @@ -47,7 +46,7 @@ def purchase(money, payment, options = {}) commit((payment.is_a?(Check) ? :purchase_echeck : :purchase), post) end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) post = {} transaction_id, payment_type = split_authorization(authorization) @@ -58,10 +57,10 @@ def refund(money, authorization, options={}) commit((payment_type == 'check' ? :refund_echeck : :refund), post) end - def void(authorization, options={}) + def void(authorization, options = {}) post = {} - transaction_id, _ = split_authorization(authorization) + transaction_id, = split_authorization(authorization) add_pair(post, :TransID, transaction_id) commit(:void, post) @@ -134,6 +133,7 @@ def add_echeck(post, payment) def add_or_use_default(payment_data, default_value) return payment_data.capitalize if payment_data + return default_value end @@ -166,6 +166,9 @@ def parse(data) end end + response + rescue StandardError + response[:message] = data&.to_s&.strip response end @@ -175,10 +178,10 @@ def commit(action, params) success_from(response), message_from(response), response, - :test => test?, - :authorization => authorization_from(response), - :avs_result => { :code => response[:avs_code] }, - :cvv_result => response[:cvv2_code] + test: test?, + authorization: authorization_from(response), + avs_result: { code: response[:avs_code] }, + cvv_result: response[:cvv2_code] ) end @@ -219,8 +222,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 546a3d4d0c1..6a622c1c4cf 100644 --- a/lib/active_merchant/billing/gateways/trans_first_transaction_express.rb +++ b/lib/active_merchant/billing/gateways/trans_first_transaction_express.rb @@ -1,7 +1,7 @@ require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class TransFirstTransactionExpressGateway < Gateway self.display_name = 'TransFirst Transaction Express' self.homepage_url = 'http://transactionexpress.com/' @@ -12,7 +12,7 @@ class TransFirstTransactionExpressGateway < Gateway self.supported_countries = ['US'] self.default_currency = 'USD' self.money_format = :cents - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club] + self.supported_cardtypes = %i[visa master american_express discover diners_club] V1_NAMESPACE = 'http://postilion/realtime/merchantframework/xsd/v1/' SOAPENV_NAMESPACE = 'http://schemas.xmlsoap.org/soap/envelope/' @@ -152,7 +152,7 @@ class TransFirstTransactionExpressGateway < Gateway 'R1' => 'The transaction was declined or returned, because the cardholder requested that payment of all recurring or installment payment transactions for a specific merchant account be stopped/ Reserved for client-specific use (declined)', 'Q1' => 'Card Authentication failed/ Reserved for client-specific use (declined)', 'XA' => 'Forward to Issuer/ Reserved for client-specific use (declined)', - 'XD' => 'Forward to Issuer/ Reserved for client-specific use (declined)', + 'XD' => 'Forward to Issuer/ Reserved for client-specific use (declined)' } EXTENDED_RESPONSE_MESSAGES = { @@ -179,15 +179,15 @@ class TransFirstTransactionExpressGateway < Gateway refund_echeck: 16, void_echeck: 16, - wallet_sale: 14, + wallet_sale: 14 } - def initialize(options={}) + def initialize(options = {}) requires!(options, :gateway_id, :reg_key) super end - def purchase(amount, payment_method, options={}) + def purchase(amount, payment_method, options = {}) if credit_card?(payment_method) action = :purchase request = build_xml_transaction_request do |doc| @@ -216,7 +216,7 @@ def purchase(amount, payment_method, options={}) commit(action, request) end - def authorize(amount, payment_method, options={}) + def authorize(amount, payment_method, options = {}) if credit_card?(payment_method) request = build_xml_transaction_request do |doc| add_credit_card(doc, payment_method) @@ -234,7 +234,7 @@ def authorize(amount, payment_method, options={}) commit(:authorize, request) end - def capture(amount, authorization, options={}) + def capture(amount, authorization, options = {}) transaction_id = split_authorization(authorization)[1] request = build_xml_transaction_request do |doc| add_amount(doc, amount) @@ -244,7 +244,7 @@ def capture(amount, authorization, options={}) commit(:capture, request) end - def void(authorization, options={}) + def void(authorization, options = {}) action, transaction_id = split_authorization(authorization) request = build_xml_transaction_request do |doc| @@ -254,7 +254,7 @@ def void(authorization, options={}) commit(void_type(action), request) end - def refund(amount, authorization, options={}) + def refund(amount, authorization, options = {}) action, transaction_id = split_authorization(authorization) request = build_xml_transaction_request do |doc| @@ -265,7 +265,7 @@ def refund(amount, authorization, options={}) commit(refund_type(action), request) end - def credit(amount, payment_method, options={}) + def credit(amount, payment_method, options = {}) request = build_xml_transaction_request do |doc| add_pan(doc, payment_method) add_amount(doc, amount) @@ -274,7 +274,7 @@ def credit(amount, payment_method, options={}) commit(:credit, request) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) request = build_xml_transaction_request do |doc| add_credit_card(doc, credit_card) add_contact(doc, credit_card.name, options) @@ -283,7 +283,7 @@ def verify(credit_card, options={}) commit(:verify, request) end - def store(payment_method, options={}) + def store(payment_method, options = {}) store_customer_request = build_xml_payment_storage_request do |doc| store_customer_details(doc, payment_method.name, options) end @@ -291,6 +291,7 @@ def store(payment_method, options={}) MultiResponse.run do |r| r.process { commit(:store, store_customer_request) } return r unless r.success? && r.params['custId'] + customer_id = r.params['custId'] store_payment_method_request = build_xml_payment_storage_request do |doc| @@ -316,12 +317,13 @@ def scrub(transcript) gsub(%r((<[^>]+pan>)[^<]+(<))i, '\1[FILTERED]\2'). gsub(%r((<[^>]+sec>)[^<]+(<))i, '\1[FILTERED]\2'). gsub(%r((<[^>]+id>)[^<]+(<))i, '\1[FILTERED]\2'). - gsub(%r((<[^>]+regKey>)[^<]+(<))i, '\1[FILTERED]\2') + gsub(%r((<[^>]+regKey>)[^<]+(<))i, '\1[FILTERED]\2'). + gsub(%r((<[^>]+acctNr>)[^<]+(<))i, '\1[FILTERED]\2') end private - CURRENCY_CODES = Hash.new{|h,k| raise ArgumentError.new("Unsupported currency: #{k}")} + CURRENCY_CODES = Hash.new { |_h, k| raise ArgumentError.new("Unsupported currency: #{k}") } CURRENCY_CODES['USD'] = '840' def headers @@ -333,11 +335,12 @@ def headers def commit(action, request) request = add_transaction_code_to_request(request, action) - raw_response = begin - ssl_post(url, request, headers) - rescue ActiveMerchant::ResponseError => e - e.response.body - end + raw_response = + begin + ssl_post(url, request, headers) + rescue ActiveMerchant::ResponseError => e + e.response.body + end response = parse(raw_response) @@ -383,6 +386,7 @@ def success_from(response) def error_code_from(succeeded, response) return if succeeded + response['errorCode'] || response['rspCode'] end @@ -434,32 +438,24 @@ 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) + def build_xml_request(wrapper, merchant_product_type = nil) Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml| xml['soapenv'].Envelope('xmlns:soapenv' => SOAPENV_NAMESPACE) do xml['soapenv'].Body do @@ -478,11 +474,11 @@ def add_transaction_code_to_request(request, action) doc = Nokogiri::XML::Document.parse(request) merc_nodeset = doc.xpath('//v1:merc', 'v1' => V1_NAMESPACE) - merc_nodeset.after "#{TRANSACTION_CODES[action]}" + merc_nodeset.after "#{TRANSACTION_CODES[action]}" doc.root.to_xml end - def add_merchant(doc, product_type=nil) + def add_merchant(doc, product_type = nil) doc['v1'].merc do doc['v1'].id @options[:gateway_id] doc['v1'].regKey @options[:reg_key] @@ -532,22 +528,22 @@ def add_pan(doc, payment_method) def add_contact(doc, fullname, options) doc['v1'].contact do - doc['v1'].fullName fullname + doc['v1'].fullName fullname unless fullname.blank? doc['v1'].coName options[:company_name] if options[:company_name] doc['v1'].title options[:title] if options[:title] if (billing_address = options[:billing_address]) if billing_address[:phone] doc['v1'].phone do - doc['v1'].type (options[:phone_number_type] || '4') + doc['v1'].type(options[:phone_number_type] || '4') doc['v1'].nr billing_address[:phone].gsub(/\D/, '') end end doc['v1'].addrLn1 billing_address[:address1] if billing_address[:address1] - doc['v1'].addrLn2 billing_address[:address2] if billing_address[:address2] + doc['v1'].addrLn2 billing_address[:address2] unless billing_address[:address2].blank? doc['v1'].city billing_address[:city] if billing_address[:city] doc['v1'].state billing_address[:state] if billing_address[:state] - doc['v1'].zipCode billing_address[:zip] if billing_address[:zip] + doc['v1'].zipCode billing_address[:zip].delete('-') if billing_address[:zip] doc['v1'].ctry 'US' end @@ -557,12 +553,12 @@ def add_contact(doc, fullname, options) if (shipping_address = options[:shipping_address]) doc['v1'].ship do - doc['v1'].fullName fullname + doc['v1'].fullName fullname unless fullname.blank? doc['v1'].addrLn1 shipping_address[:address1] if shipping_address[:address1] - doc['v1'].addrLn2 shipping_address[:address2] if shipping_address[:address2] + doc['v1'].addrLn2 shipping_address[:address2] unless shipping_address[:address2].blank? doc['v1'].city shipping_address[:city] if shipping_address[:city] doc['v1'].state shipping_address[:state] if shipping_address[:state] - doc['v1'].zipCode shipping_address[:zip] if shipping_address[:zip] + doc['v1'].zipCode shipping_address[:zip].delete('-') if shipping_address[:zip] doc['v1'].phone shipping_address[:phone].gsub(/\D/, '') if shipping_address[:phone] doc['v1'].email shipping_address[:email] if shipping_address[:email] end @@ -572,7 +568,7 @@ def add_contact(doc, fullname, options) def add_name(doc, payment_method) doc['v1'].contact do - doc['v1'].fullName payment_method.name + doc['v1'].fullName payment_method.name unless payment_method.name.blank? end end diff --git a/lib/active_merchant/billing/gateways/transact_pro.rb b/lib/active_merchant/billing/gateways/transact_pro.rb index 5855527bfc0..a403cd9e3e6 100644 --- a/lib/active_merchant/billing/gateways/transact_pro.rb +++ b/lib/active_merchant/billing/gateways/transact_pro.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # For more information visit {Transact Pro Services}[https://www.transactpro.lv/business/] # # This gateway was formerly associated with www.1stpayments.net @@ -12,17 +12,17 @@ class TransactProGateway < Gateway self.supported_countries = ['US'] self.default_currency = 'USD' self.money_format = :cents - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'https://www.transactpro.lv/business/online-payments-acceptance' self.display_name = 'Transact Pro' - def initialize(options={}) + def initialize(options = {}) requires!(options, :guid, :password, :terminal) super end - def purchase(amount, payment, options={}) + def purchase(amount, payment, options = {}) post = PostData.new add_invoice(post, amount, options) add_payment(post, payment) @@ -32,7 +32,7 @@ def purchase(amount, payment, options={}) post[:rs] = @options[:terminal] MultiResponse.run do |r| - r.process{commit('init', post)} + r.process { commit('init', post) } r.process do post = PostData.new post[:init_transaction_id] = r.authorization @@ -44,7 +44,7 @@ def purchase(amount, payment, options={}) end end - def authorize(amount, payment, options={}) + def authorize(amount, payment, options = {}) post = PostData.new add_invoice(post, amount, options) add_payment(post, payment) @@ -54,7 +54,7 @@ def authorize(amount, payment, options={}) post[:rs] = @options[:terminal] MultiResponse.run do |r| - r.process{commit('init_dms', post)} + r.process { commit('init_dms', post) } r.process do post = PostData.new post[:init_transaction_id] = r.authorization @@ -66,11 +66,9 @@ def authorize(amount, payment, options={}) end end - def capture(amount, authorization, options={}) + def capture(amount, authorization, options = {}) identifier, original_amount = split_authorization(authorization) - if amount && (amount != original_amount) - raise ArgumentError.new("Partial capture is not supported, and #{amount.inspect} != #{original_amount.inspect}") - end + raise ArgumentError.new("Partial capture is not supported, and #{amount.inspect} != #{original_amount.inspect}") if amount && (amount != original_amount) post = PostData.new add_credentials(post) @@ -80,7 +78,7 @@ def capture(amount, authorization, options={}) commit('charge_hold', post, original_amount) end - def refund(amount, authorization, options={}) + def refund(amount, authorization, options = {}) identifier, original_amount = split_authorization(authorization) post = PostData.new @@ -91,7 +89,7 @@ def refund(amount, authorization, options={}) commit('refund', post) end - def void(authorization, options={}) + def void(authorization, options = {}) identifier, amount = split_authorization(authorization) post = PostData.new @@ -101,7 +99,7 @@ def void(authorization, options={}) commit('cancel_dms', post) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -158,14 +156,14 @@ def add_payment_cc(post, credit_card) post[:expire] = "#{month}/#{year[2..3]}" end - def add_credentials(post, key=:guid) + def add_credentials(post, key = :guid) post[key] = @options[:guid] post[:pwd] = Digest::SHA1.hexdigest(@options[:password]) end def parse(body) - if body =~ /^ID:/ - body.split('~').reduce(Hash.new) { |h,v| + if /^ID:/.match?(body) + body.split('~').reduce(Hash.new) { |h, v| m = v.match('(.*?):(.*)') h.merge!(m[1].underscore.to_sym => m[2]) } @@ -174,13 +172,13 @@ def parse(body) { status: 'success', id: m[2] } : { status: 'failure', message: m[2] } else - Hash[ status: body ] + { status: body } end end - def commit(action, parameters, amount=nil) + def commit(action, parameters, amount = nil) url = (test? ? test_url : live_url) - response = parse(ssl_post(url, post_data(action,parameters))) + response = parse(ssl_post(url, post_data(action, parameters))) Response.new( success_from(response), @@ -199,7 +197,7 @@ def authorization_from(parameters, response, amount) end def split_authorization(authorization) - if authorization =~ /|/ + if /|/.match?(authorization) identifier, amount = authorization.split('|') [identifier, amount.to_i] else diff --git a/lib/active_merchant/billing/gateways/transax.rb b/lib/active_merchant/billing/gateways/transax.rb index c11c830bac9..9e24476ad18 100644 --- a/lib/active_merchant/billing/gateways/transax.rb +++ b/lib/active_merchant/billing/gateways/transax.rb @@ -1,23 +1,21 @@ -require File.join(File.dirname(__FILE__),'smart_ps.rb') +require File.join(File.dirname(__FILE__), 'smart_ps.rb') -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class TransaxGateway < SmartPs self.live_url = self.test_url = 'https://secure.nelixtransax.net/api/transact.php' - + # The countries the gateway supports merchants from as 2 digit ISO country codes self.supported_countries = ['US'] - + # The card types supported by the payment gateway - self.supported_cardtypes = [:visa, :master, :american_express, :discover] - + self.supported_cardtypes = %i[visa master american_express discover] + # The homepage URL of the gateway self.homepage_url = 'https://www.nelixtransax.com/' - + # The name of the gateway self.display_name = 'NELiX TransaX' - end end end - diff --git a/lib/active_merchant/billing/gateways/transnational.rb b/lib/active_merchant/billing/gateways/transnational.rb index bce37f5eede..b401b58d30a 100644 --- a/lib/active_merchant/billing/gateways/transnational.rb +++ b/lib/active_merchant/billing/gateways/transnational.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class TransnationalGateway < NetworkMerchantsGateway self.homepage_url = 'http://www.tnbci.com/' self.display_name = 'Transnational' @@ -7,4 +7,3 @@ class TransnationalGateway < NetworkMerchantsGateway end end end - diff --git a/lib/active_merchant/billing/gateways/trexle.rb b/lib/active_merchant/billing/gateways/trexle.rb index b4601860b27..80933b2b710 100644 --- a/lib/active_merchant/billing/gateways/trexle.rb +++ b/lib/active_merchant/billing/gateways/trexle.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class TrexleGateway < Gateway self.test_url = 'https://core.trexle.com/api/v1' self.live_url = 'https://core.trexle.com/api/v1' @@ -10,7 +10,7 @@ class TrexleGateway < Gateway GI GR HK HU ID IE IL IM IN IS IT JO KW LB LI LK LT LU LV MC MT MU MV MX MY NL NO NZ OM PH PL PT QA RO SA SE SG SI SK SM TR TT UM US VA VN ZA) - self.supported_cardtypes = [:visa, :master, :american_express] + self.supported_cardtypes = %i[visa master american_express] self.homepage_url = 'https://trexle.com' self.display_name = 'Trexle' @@ -90,6 +90,7 @@ def scrub(transcript) gsub(/(number\\?":\\?")(\d*)/, '\1[FILTERED]'). gsub(/(cvc\\?":\\?")(\d*)/, '\1[FILTERED]') end + private def add_amount(post, money, options) @@ -105,6 +106,7 @@ def add_customer_data(post, options) def add_address(post, creditcard, options) return if creditcard.kind_of?(String) + address = (options[:billing_address] || options[:address]) return unless address @@ -135,7 +137,7 @@ def add_creditcard(post, creditcard) name: creditcard.name ) elsif creditcard.kind_of?(String) - if creditcard =~ /^token_/ + if /^token_/.match?(creditcard) post[:card_token] = creditcard else post[:customer_token] = creditcard @@ -153,33 +155,34 @@ def headers(params = {}) result['X-Safe-Card'] = params[:safe_card] if params[:safe_card] result end - + def commit(method, action, params, options) url = "#{test? ? test_url : live_url}/#{action}" raw_response = ssl_request(method, url, post_data(params), headers(options)) parsed_response = parse(raw_response) - success_response(parsed_response) + success_response(parsed_response) rescue ResponseError => e error_response(parse(e.response.body)) rescue JSON::ParserError unparsable_response(raw_response) end - + def success_response(body) return invalid_response unless body['response'] - + response = body['response'] Response.new( - true, - response['status_message'], - body, - authorization: token(response), - test: test? + true, + response['status_message'], + body, + authorization: token(response), + test: test? ) end def error_response(body) return invalid_response unless body['error'] + Response.new( false, body['error'], @@ -194,7 +197,7 @@ def unparsable_response(raw_response) message += " (The raw response returned by the API was #{raw_response.inspect})" return Response.new(false, message) end - + def invalid_response message = 'Invalid response.' return Response.new(false, message) @@ -206,7 +209,8 @@ def token(response) def parse(body) return {} if body.blank? - JSON.parse(body) + + JSON.parse(body) end def post_data(parameters = {}) diff --git a/lib/active_merchant/billing/gateways/trust_commerce.rb b/lib/active_merchant/billing/gateways/trust_commerce.rb index 87872c59122..1f0b203260a 100644 --- a/lib/active_merchant/billing/gateways/trust_commerce.rb +++ b/lib/active_merchant/billing/gateways/trust_commerce.rb @@ -4,8 +4,8 @@ # Falls back to an SSL post to TrustCommerce end -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # TO USE: # First, make sure you have everything setup correctly and all of your dependencies in place with: # @@ -19,10 +19,10 @@ module Billing #:nodoc: # Next, create a credit card object using a TC approved test card. # # creditcard = ActiveMerchant::Billing::CreditCard.new( - # :number => '4111111111111111', - # :month => 8, - # :year => 2006, - # :first_name => 'Longbob', + # :number => '4111111111111111', + # :month => 8, + # :year => 2006, + # :first_name => 'Longbob', # :last_name => 'Longsen' # ) # @@ -67,7 +67,7 @@ module Billing #:nodoc: class TrustCommerceGateway < Gateway self.live_url = self.test_url = 'https://vault.trustcommerce.com/trans/' - SUCCESS_TYPES = ['approved', 'accepted'] + SUCCESS_TYPES = %w[approved accepted] DECLINE_CODES = { 'decline' => 'The credit card was declined', @@ -104,8 +104,10 @@ class TrustCommerceGateway < Gateway TEST_LOGIN = 'TestMerchant' TEST_PASSWORD = 'password' + VOIDABLE_ACTIONS = %w(preauth sale postauth credit) + self.money_format = :cents - self.supported_cardtypes = [:visa, :master, :discover, :american_express, :diners_club, :jcb] + self.supported_cardtypes = %i[visa master discover american_express diners_club jcb] self.supported_countries = ['US'] self.homepage_url = 'http://www.trustcommerce.com/' self.display_name = 'TrustCommerce' @@ -149,13 +151,16 @@ def test? def authorize(money, creditcard_or_billing_id, options = {}) parameters = { - :amount => amount(money), + amount: amount(money) } add_order_id(parameters, options) + add_aggregator(parameters, options) add_customer_data(parameters, options) add_payment_source(parameters, creditcard_or_billing_id) add_addresses(parameters, options) + add_custom_fields(parameters, options) + commit('preauth', parameters) end @@ -163,13 +168,16 @@ def authorize(money, creditcard_or_billing_id, options = {}) # to process a purchase are an amount in cents or a money object and a creditcard object or billingid string. def purchase(money, creditcard_or_billing_id, options = {}) parameters = { - :amount => amount(money), + amount: amount(money) } add_order_id(parameters, options) + add_aggregator(parameters, options) add_customer_data(parameters, options) add_payment_source(parameters, creditcard_or_billing_id) add_addresses(parameters, options) + add_custom_fields(parameters, options) + commit('sale', parameters) end @@ -177,10 +185,13 @@ def purchase(money, creditcard_or_billing_id, options = {}) # postauth, we preserve active_merchant's nomenclature of capture() for consistency with the rest of the library. To process # a postauthorization with TC, you need an amount in cents or a money object, and a TC transid. def capture(money, authorization, options = {}) + transaction_id, = split_authorization(authorization) parameters = { - :amount => amount(money), - :transid => authorization, + amount: amount(money), + transid: transaction_id } + add_aggregator(parameters, options) + add_custom_fields(parameters, options) commit('postauth', parameters) end @@ -188,11 +199,16 @@ def capture(money, authorization, options = {}) # refund() allows you to return money to a card that was previously billed. You need to supply the amount, in cents or a money object, # that you want to refund, and a TC transid for the transaction that you are refunding. def refund(money, identification, options = {}) + transaction_id, = split_authorization(identification) + parameters = { - :amount => amount(money), - :transid => identification + amount: amount(money), + transid: transaction_id } + add_aggregator(parameters, options) + add_custom_fields(parameters, options) + commit('credit', parameters) end @@ -210,17 +226,32 @@ def credit(money, identification, options = {}) # TrustCommerce to allow for reversal transactions before you can use this # method. # + # void() is also used to to cancel a capture (postauth), purchase (sale), + # or refund (credit) or a before it is sent for settlement. + # # NOTE: AMEX preauth's cannot be reversed. If you want to clear it more # quickly than the automatic expiration (7-10 days), you will have to # capture it and then immediately issue a credit for the same amount # which should clear the customers credit card with 48 hours according to # TC. def void(authorization, options = {}) + transaction_id, original_action = split_authorization(authorization) + action = (VOIDABLE_ACTIONS - ['preauth']).include?(original_action) ? 'void' : 'reversal' + parameters = { - :transid => authorization, + transid: transaction_id } - commit('reversal', parameters) + add_aggregator(parameters, options) + add_custom_fields(parameters, 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 @@ -237,29 +268,30 @@ def void(authorization, options = {}) def recurring(money, creditcard, options = {}) ActiveMerchant.deprecated RECURRING_DEPRECATION_MESSAGE - requires!(options, [:periodicity, :bimonthly, :monthly, :biweekly, :weekly, :yearly, :daily] ) - - cycle = case options[:periodicity] - when :monthly - '1m' - when :bimonthly - '2m' - when :weekly - '1w' - when :biweekly - '2w' - when :yearly - '1y' - when :daily - '1d' - end + requires!(options, %i[periodicity bimonthly monthly biweekly weekly yearly daily]) + + cycle = + case options[:periodicity] + when :monthly + '1m' + when :bimonthly + '2m' + when :weekly + '1w' + when :biweekly + '2w' + when :yearly + '1y' + when :daily + '1d' + end parameters = { - :amount => amount(money), - :cycle => cycle, - :verify => options[:verify] || 'y', - :billingid => options[:billingid] || nil, - :payments => options[:payments] || nil, + amount: amount(money), + cycle:, + verify: options[:verify] || 'y', + billingid: options[:billingid] || nil, + payments: options[:payments] || nil } add_creditcard(parameters, creditcard) @@ -273,12 +305,14 @@ def recurring(money, creditcard, options = {}) def store(creditcard, options = {}) parameters = { - :verify => options[:verify] || 'y', - :billingid => options[:billingid] || options[:billing_id] || nil, + verify: options[:verify] || 'y', + billingid: options[:billingid] || options[:billing_id] || nil } add_creditcard(parameters, creditcard) add_addresses(parameters, options) + add_custom_fields(parameters, options) + commit('store', parameters) end @@ -286,9 +320,11 @@ def store(creditcard, options = {}) # unstore() the information will be removed and a Response object will be returned indicating the success of the action. def unstore(identification, options = {}) parameters = { - :billingid => identification, + billingid: identification } + add_custom_fields(parameters, options) + commit('unstore', parameters) end @@ -300,18 +336,44 @@ def scrub(transcript) transcript. gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). gsub(%r((&?cc=)\d*(&?)), '\1[FILTERED]\2'). - gsub(%r((&?cvv=)\d*(&?)), '\1[FILTERED]\2') + gsub(%r((&?password=)[^&]+(&?)), '\1[FILTERED]\2'). + gsub(%r((&?cvv=)\d*(&?)), '\1[FILTERED]\2'). + gsub(%r((&?account=)\d*(&?)), '\1[FILTERED]\2') end private + + def add_custom_fields(params, options) + options[:custom_fields]&.each do |key, value| + params[key.to_sym] = value + end + end + + def add_aggregator(params, options) + if @options[:aggregator_id] || application_id != Gateway.application_id + params[:aggregators] = 1 + params[:aggregator1] = @options[:aggregator_id] || application_id + end + end + def add_payment_source(params, source) if source.is_a?(String) add_billing_id(params, source) + elsif card_brand(source) == 'check' + add_check(params, source) else add_creditcard(params, source) end end + def add_check(params, check) + params[:media] = 'ach' + params[:routing] = check.routing_number + params[:account] = check.account_number + params[:savings] = 'y' if check.account_type == 'savings' + params[:name] = check.name + end + def add_creditcard(params, creditcard) params[:media] = 'cc' params[:name] = creditcard.name @@ -352,7 +414,7 @@ def add_addresses(params, options) params[:shipto_address2] = shipping_address[:address2] unless shipping_address[:address2].blank? params[:shipto_city] = shipping_address[:city] unless shipping_address[:city].blank? params[:shipto_state] = shipping_address[:state] unless shipping_address[:state].blank? - params[:shipto_zip] = shipping_address[:zip] unless shipping_address[:zip].blank? + params[:shipto_zip] = shipping_address[:zip] unless shipping_address[:zip].blank? params[:shipto_country] = shipping_address[:country] unless shipping_address[:country].blank? end end @@ -361,16 +423,14 @@ def clean_and_stringify_params(parameters) # TCLink wants us to send a hash with string keys, and activemerchant pushes everything around with # symbol keys. Before sending our input to TCLink, we convert all our keys to strings and dump the symbol keys. # We also remove any pairs with nil values, as these confuse TCLink. - parameters.keys.reverse.each do |key| - if parameters[key] - parameters[key.to_s] = parameters[key] - end + parameters.keys.reverse_each do |key| + parameters[key.to_s] = parameters[key] if parameters[key] parameters.delete(key) end end def post_data(parameters) - parameters.collect { |key, value| "#{key}=#{ CGI.escape(value.to_s)}" }.join('&') + parameters.collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&') end def commit(action, parameters) @@ -382,19 +442,22 @@ def commit(action, parameters) clean_and_stringify_params(parameters) data = if tclink? - TCLink.send(parameters) - else - parse( ssl_post(self.live_url, post_data(parameters)) ) - end + TCLink.send(parameters) + else + parse(ssl_post(self.live_url, post_data(parameters))) + end # 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, - :test => test?, - :authorization => data['transid'], - :cvv_result => data['cvv'], - :avs_result => { :code => data['avs'] } + Response.new( + success, + message, + data, + test: test?, + authorization: authorization_from(action, data), + cvv_result: data['cvv'], + avs_result: { code: data['avs'] } ) end @@ -402,7 +465,7 @@ def parse(body) results = {} body.split(/\n/).each do |pair| - key,val = pair.split(/=/) + key, val = pair.split(/=/) results[key] = val end @@ -422,6 +485,20 @@ def message_from(data) end end + def authorization_from(action, data) + case action + when 'store' + data['billingid'] + when *VOIDABLE_ACTIONS + "#{data['transid']}|#{action}" + else + data['transid'] + end + end + + def split_authorization(authorization) + authorization&.split('|') + end end end end diff --git a/lib/active_merchant/billing/gateways/usa_epay.rb b/lib/active_merchant/billing/gateways/usa_epay.rb index b43e355f61c..3b2072551e0 100644 --- a/lib/active_merchant/billing/gateways/usa_epay.rb +++ b/lib/active_merchant/billing/gateways/usa_epay.rb @@ -1,19 +1,18 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: ## # Delegates to the appropriate gateway, either the Transaction or Advanced # depending on options passed to new. # class UsaEpayGateway < Gateway - self.abstract_class = true ## - # Creates an instance of UsaEpayTransactionGateway by default, but if + # Creates an instance of UsaEpayTransactionGateway by default, but if # :software id or :live_url are passed in the options hash it will # create an instance of UsaEpayAdvancedGateway. # - def self.new(options={}) + def self.new(options = {}) if options.has_key?(:software_id) || options.has_key?(:live_url) UsaEpayAdvancedGateway.new(options) else diff --git a/lib/active_merchant/billing/gateways/usa_epay_advanced.rb b/lib/active_merchant/billing/gateways/usa_epay_advanced.rb index d831807ec29..b0165de09c0 100644 --- a/lib/active_merchant/billing/gateways/usa_epay_advanced.rb +++ b/lib/active_merchant/billing/gateways/usa_epay_advanced.rb @@ -1,8 +1,8 @@ require 'securerandom' require 'digest' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # ==== USA ePay Advanced SOAP Interface # # This class encapsulates USA ePay's Advanced SOAP Interface. The Advanced Soap Interface allows @@ -64,197 +64,197 @@ module Billing #:nodoc: class UsaEpayAdvancedGateway < Gateway API_VERSION = '1.4' - TEST_URL_BASE = 'https://sandbox.usaepay.com/soap/gate/' #:nodoc: - LIVE_URL_BASE = 'https://www.usaepay.com/soap/gate/' #:nodoc: + TEST_URL_BASE = 'https://sandbox.usaepay.com/soap/gate/' # :nodoc: + LIVE_URL_BASE = 'https://www.usaepay.com/soap/gate/' # :nodoc: self.test_url = TEST_URL_BASE self.live_url = LIVE_URL_BASE - FAILURE_MESSAGE = 'Default Failure' #:nodoc: + FAILURE_MESSAGE = 'Default Failure' # :nodoc: self.supported_countries = ['US'] - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb] + self.supported_cardtypes = %i[visa master american_express discover diners_club jcb] self.homepage_url = 'http://www.usaepay.com/' self.display_name = 'USA ePay Advanced SOAP Interface' CUSTOMER_PROFILE_OPTIONS = { - :id => [:string, 'CustomerID'], # merchant assigned number - :notes => [:string, 'Notes'], - :data => [:string, 'CustomData'], - :url => [:string, 'URL'] - } #:nodoc: + id: [:string, 'CustomerID'], # merchant assigned number + notes: [:string, 'Notes'], + data: [:string, 'CustomData'], + url: [:string, 'URL'] + } # :nodoc: CUSTOMER_RECURRING_BILLING_OPTIONS = { - :enabled => [:boolean, 'Enabled'], - :schedule => [:string, 'Schedule'], - :number_left => [:integer, 'NumLeft'], - :currency => [:string, 'Currency'], - :description => [:string, 'Description'], - :order_id => [:string, 'OrderID'], - :user => [:string, 'User'], - :source => [:string, 'Source'], - :send_receipt => [:boolean, 'SendReceipt'], - :receipt_note => [:string, 'ReceiptNote'] - } #:nodoc: + enabled: [:boolean, 'Enabled'], + schedule: [:string, 'Schedule'], + number_left: [:integer, 'NumLeft'], + currency: [:string, 'Currency'], + description: [:string, 'Description'], + order_id: [:string, 'OrderID'], + user: [:string, 'User'], + source: [:string, 'Source'], + send_receipt: [:boolean, 'SendReceipt'], + receipt_note: [:string, 'ReceiptNote'] + } # :nodoc: CUSTOMER_POINT_OF_SALE_OPTIONS = { - :price_tier => [:string, 'PriceTier'], - :tax_class => [:string, 'TaxClass'], - :lookup_code => [:string, 'LookupCode'] - } #:nodoc: + price_tier: [:string, 'PriceTier'], + tax_class: [:string, 'TaxClass'], + lookup_code: [:string, 'LookupCode'] + } # :nodoc: CUSTOMER_OPTIONS = [ CUSTOMER_PROFILE_OPTIONS, CUSTOMER_RECURRING_BILLING_OPTIONS, CUSTOMER_POINT_OF_SALE_OPTIONS - ].inject(:merge) #:nodoc: + ].inject(:merge) # :nodoc: COMMON_ADDRESS_OPTIONS = { - :first_name => [:string, 'FirstName'], - :last_name => [:string, 'LastName'], - :city => [:string, 'City'], - :state => [:string, 'State'], - :zip => [:string, 'Zip'], - :country => [:string, 'Country'], - :phone => [:string, 'Phone'], - :email => [:string, 'Email'], - :fax => [:string, 'Fax'], - :company => [:string, 'Company'] - } #:nodoc: + first_name: [:string, 'FirstName'], + last_name: [:string, 'LastName'], + city: [:string, 'City'], + state: [:string, 'State'], + zip: [:string, 'Zip'], + country: [:string, 'Country'], + phone: [:string, 'Phone'], + email: [:string, 'Email'], + fax: [:string, 'Fax'], + company: [:string, 'Company'] + } # :nodoc: ADDRESS_OPTIONS = [ COMMON_ADDRESS_OPTIONS, { - :address1 => [:string, 'Street'], - :address2 => [:string, 'Street2'], + address1: [:string, 'Street'], + address2: [:string, 'Street2'] } - ].inject(:merge) #:nodoc + ].inject(:merge) # :nodoc CUSTOMER_UPDATE_DATA_FIELDS = [ CUSTOMER_PROFILE_OPTIONS, CUSTOMER_RECURRING_BILLING_OPTIONS, COMMON_ADDRESS_OPTIONS, { - :address1 => [:string, 'Address'], - :address2 => [:string, 'Address2'], + address1: [:string, 'Address'], + address2: [:string, 'Address2'] }, { - :card_number => [:string, 'CardNumber'], - :card_exp => [:string, 'CardExp'], - :account => [:string, 'Account'], - :routing => [:string, 'Routing'], - :check_format => [:string, 'CheckFormat'], - :record_type => [:string, 'RecordType'], + card_number: [:string, 'CardNumber'], + card_exp: [:string, 'CardExp'], + account: [:string, 'Account'], + routing: [:string, 'Routing'], + check_format: [:string, 'CheckFormat'], + record_type: [:string, 'RecordType'] } - ].inject(:merge) #:nodoc + ].inject(:merge) # :nodoc CUSTOMER_TRANSACTION_REQUEST_OPTIONS = { - :command => [:string, 'Command'], - :ignore_duplicate => [:boolean, 'IgnoreDuplicate'], - :client_ip => [:string, 'ClientIP'], - :customer_receipt => [:boolean, 'CustReceipt'], - :customer_email => [:boolean, 'CustReceiptEmail'], - :customer_template => [:boolean, 'CustReceiptName'], - :merchant_receipt => [:boolean, 'MerchReceipt'], - :merchant_email => [:boolean, 'MerchReceiptEmail'], - :merchant_template => [:boolean, 'MerchReceiptName'], - :recurring => [:boolean, 'isRecurring'], - :verification_value => [:string, 'CardCode'], - :software => [:string, 'Software'] - } #:nodoc: + command: [:string, 'Command'], + ignore_duplicate: [:boolean, 'IgnoreDuplicate'], + client_ip: [:string, 'ClientIP'], + customer_receipt: [:boolean, 'CustReceipt'], + customer_email: [:boolean, 'CustReceiptEmail'], + customer_template: [:boolean, 'CustReceiptName'], + merchant_receipt: [:boolean, 'MerchReceipt'], + merchant_email: [:boolean, 'MerchReceiptEmail'], + merchant_template: [:boolean, 'MerchReceiptName'], + recurring: [:boolean, 'isRecurring'], + verification_value: [:string, 'CardCode'], + software: [:string, 'Software'] + } # :nodoc: TRANSACTION_REQUEST_OBJECT_OPTIONS = { - :command => [:string, 'Command'], - :ignore_duplicate => [:boolean, 'IgnoreDuplicate'], - :authorization_code => [:string, 'AuthCode'], - :reference_number => [:string, 'RefNum'], - :account_holder => [:string, 'AccountHolder'], - :client_ip => [:string, 'ClientIP'], - :customer_id => [:string, 'CustomerID'], - :customer_receipt => [:boolean, 'CustReceipt'], - :customer_template => [:boolean, 'CustReceiptName'], - :software => [:string, 'Software'] - } #:nodoc: + command: [:string, 'Command'], + ignore_duplicate: [:boolean, 'IgnoreDuplicate'], + authorization_code: [:string, 'AuthCode'], + reference_number: [:string, 'RefNum'], + account_holder: [:string, 'AccountHolder'], + client_ip: [:string, 'ClientIP'], + customer_id: [:string, 'CustomerID'], + customer_receipt: [:boolean, 'CustReceipt'], + customer_template: [:boolean, 'CustReceiptName'], + software: [:string, 'Software'] + } # :nodoc: TRANSACTION_DETAIL_OPTIONS = { - :invoice => [:string, 'Invoice'], - :po_number => [:string, 'PONum'], - :order_id => [:string, 'OrderID'], - :clerk => [:string, 'Clerk'], - :terminal => [:string, 'Terminal'], - :table => [:string, 'Table'], - :description => [:string, 'Description'], - :comments => [:string, 'Comments'], - :allow_partial_auth => [:boolean, 'AllowPartialAuth'], - :currency => [:string, 'Currency'], - :non_tax => [:boolean, 'NonTax'], - } #:nodoc: + invoice: [:string, 'Invoice'], + po_number: [:string, 'PONum'], + order_id: [:string, 'OrderID'], + clerk: [:string, 'Clerk'], + terminal: [:string, 'Terminal'], + table: [:string, 'Table'], + description: [:string, 'Description'], + comments: [:string, 'Comments'], + allow_partial_auth: [:boolean, 'AllowPartialAuth'], + currency: [:string, 'Currency'], + non_tax: [:boolean, 'NonTax'] + } # :nodoc: TRANSACTION_DETAIL_MONEY_OPTIONS = { - :amount => [:double, 'Amount'], - :tax => [:double, 'Tax'], - :tip => [:double, 'Tip'], - :non_tax => [:boolean, 'NonTax'], - :shipping => [:double, 'Shipping'], - :discount => [:double, 'Discount'], - :subtotal => [:double, 'Subtotal'] - } #:nodoc: + amount: [:double, 'Amount'], + tax: [:double, 'Tax'], + tip: [:double, 'Tip'], + non_tax: [:boolean, 'NonTax'], + shipping: [:double, 'Shipping'], + discount: [:double, 'Discount'], + subtotal: [:double, 'Subtotal'] + } # :nodoc: CREDIT_CARD_DATA_OPTIONS = { - :magnetic_stripe => [:string, 'MagStripe'], - :dukpt => [:string, 'DUKPT'], - :signature => [:string, 'Signature'], - :terminal_type => [:string, 'TermType'], - :magnetic_support => [:string, 'MagSupport'], - :xid => [:string, 'XID'], - :cavv => [:string, 'CAVV'], - :eci => [:integer, 'ECI'], - :internal_card_authorization => [:boolean, 'InternalCardAuth'], - :pares => [:string, 'Pares'] - } #:nodoc: + magnetic_stripe: [:string, 'MagStripe'], + dukpt: [:string, 'DUKPT'], + signature: [:string, 'Signature'], + terminal_type: [:string, 'TermType'], + magnetic_support: [:string, 'MagSupport'], + xid: [:string, 'XID'], + cavv: [:string, 'CAVV'], + eci: [:integer, 'ECI'], + internal_card_authorization: [:boolean, 'InternalCardAuth'], + pares: [:string, 'Pares'] + } # :nodoc: CHECK_DATA_OPTIONS = { - :drivers_license => [:string, 'DriversLicense'], - :drivers_license_state => [:string, 'DriversLicenseState'], - :record_type => [:string, 'RecordType'], - :aux_on_us => [:string, 'AuxOnUS'], - :epc_code => [:string, 'EpcCode'], - :front_image => [:string, 'FrontImage'], - :back_image => [:string, 'BackImage'] - } #:nodoc: + drivers_license: [:string, 'DriversLicense'], + drivers_license_state: [:string, 'DriversLicenseState'], + record_type: [:string, 'RecordType'], + aux_on_us: [:string, 'AuxOnUS'], + epc_code: [:string, 'EpcCode'], + front_image: [:string, 'FrontImage'], + back_image: [:string, 'BackImage'] + } # :nodoc: RECURRING_BILLING_OPTIONS = { - :schedule => [:string, 'Schedule'], - :number_left => [:integer, 'NumLeft'], - :enabled => [:boolean, 'Enabled'] - } #:nodoc: + schedule: [:string, 'Schedule'], + number_left: [:integer, 'NumLeft'], + enabled: [:boolean, 'Enabled'] + } # :nodoc: AVS_RESULTS = { - 'Y' => %w( YYY Y YYA YYD ), - 'Z' => %w( NYZ Z ), - 'A' => %w( YNA A YNY ), - 'N' => %w( NNN N NN ), - 'X' => %w( YYX X ), - 'W' => %w( NYW W ), - 'XXW' => %w( XXW ), - 'XXU' => %w( XXU ), - 'R' => %w( XXR R U E ), - 'S' => %w( XXS S ), - 'XXE' => %w( XXE ), - 'G' => %w( XXG G C I ), - 'B' => %w( YYG B M ), - 'D' => %w( GGG D ), - 'P' => %w( YGG P ) + 'Y' => %w(YYY Y YYA YYD), + 'Z' => %w(NYZ Z), + 'A' => %w(YNA A YNY), + 'N' => %w(NNN N NN), + 'X' => %w(YYX X), + 'W' => %w(NYW W), + 'XXW' => %w(XXW), + 'XXU' => %w(XXU), + 'R' => %w(XXR R U E), + 'S' => %w(XXS S), + 'XXE' => %w(XXE), + 'G' => %w(XXG G C I), + 'B' => %w(YYG B M), + 'D' => %w(GGG D), + 'P' => %w(YGG P) }.inject({}) do |map, (type, codes)| codes.each { |code| map[code] = type } map - end #:nodoc: + end # :nodoc: AVS_CUSTOM_MESSAGES = { 'XXW' => 'Card number not on file.', 'XXU' => 'Address information not verified for domestic transaction.', 'XXE' => 'Address verification not allowed for card type.' - } #:nodoc: + } # :nodoc: # Create a new gateway. # @@ -272,8 +272,8 @@ def initialize(options = {}) requires!(options, :login, :password) if options[:software_id] - self.live_url = "#{LIVE_URL_BASE}#{options[:software_id].to_s}" - self.test_url = "#{TEST_URL_BASE}#{options[:software_id].to_s}" + self.live_url = "#{LIVE_URL_BASE}#{options[:software_id]}" + self.test_url = "#{TEST_URL_BASE}#{options[:software_id]}" else self.live_url = options[:live_url].to_s self.test_url = options[:test_url].to_s if options[:test_url] @@ -289,43 +289,43 @@ def initialize(options = {}) # # Note: See run_transaction for additional options. # - def purchase(money, creditcard, options={}) - run_sale(options.merge!(:amount => money, :payment_method => creditcard)) + def purchase(money, creditcard, options = {}) + run_sale(options.merge!(amount: money, payment_method: creditcard)) end # Authorize an amount on a credit card or account. # # Note: See run_transaction for additional options. # - def authorize(money, creditcard, options={}) - run_auth_only(options.merge!(:amount => money, :payment_method => creditcard)) + def authorize(money, creditcard, options = {}) + run_auth_only(options.merge!(amount: money, payment_method: creditcard)) end # Capture an authorized transaction. # # Note: See run_transaction for additional options. # - def capture(money, identification, options={}) - capture_transaction(options.merge!(:amount => money, :reference_number => identification)) + def capture(money, identification, options = {}) + capture_transaction(options.merge!(amount: money, reference_number: identification)) end # Void a previous transaction that has not been settled. # # Note: See run_transaction for additional options. # - def void(identification, options={}) - void_transaction(options.merge!(:reference_number => identification)) + def void(identification, options = {}) + void_transaction(options.merge!(reference_number: identification)) end # Refund a previous transaction. # # Note: See run_transaction for additional options. # - def refund(money, identification, options={}) - refund_transaction(options.merge!(:amount => money, :reference_number => identification)) + def refund(money, identification, options = {}) + refund_transaction(options.merge!(amount: money, reference_number: identification)) end - def credit(money, identification, options={}) + def credit(money, identification, options = {}) ActiveMerchant.deprecated CREDIT_DEPRECATION_MESSAGE refund(money, identification, options) end @@ -368,7 +368,7 @@ def credit(money, identification, options={}) # ==== Response # * #message -- customer number assigned by gateway # - def add_customer(options={}) + def add_customer(options = {}) request = build_request(__method__, options) commit(__method__, request) end @@ -381,7 +381,7 @@ def add_customer(options={}) # ==== Options # * Same as add_customer # - def update_customer(options={}) + def update_customer(options = {}) requires! options, :customer_number request = build_request(__method__, options) @@ -429,7 +429,7 @@ def update_customer(options={}) # ==== Response # * #message -- boolean; Returns true if successful. Exception thrown all failures. # - def quick_update_customer(options={}) + def quick_update_customer(options = {}) requires! options, :customer_number requires! options, :update_data @@ -444,7 +444,7 @@ def quick_update_customer(options={}) # ==== Required # * :customer_number # - def enable_customer(options={}) + def enable_customer(options = {}) requires! options, :customer_number request = build_request(__method__, options) @@ -456,7 +456,7 @@ def enable_customer(options={}) # ==== Required # * :customer_number # - def disable_customer(options={}) + def disable_customer(options = {}) requires! options, :customer_number request = build_request(__method__, options) @@ -479,7 +479,7 @@ def disable_customer(options={}) # ==== Response # * #message -- method_id of new customer payment method # - def add_customer_payment_method(options={}) + def add_customer_payment_method(options = {}) requires! options, :customer_number request = build_request(__method__, options) @@ -494,7 +494,7 @@ def add_customer_payment_method(options={}) # ==== Response # * #message -- either a single hash or an array of hashes of payment methods # - def get_customer_payment_methods(options={}) + def get_customer_payment_methods(options = {}) requires! options, :customer_number request = build_request(__method__, options) @@ -510,7 +510,7 @@ def get_customer_payment_methods(options={}) # ==== Response # * #message -- hash of payment method # - def get_customer_payment_method(options={}) + def get_customer_payment_method(options = {}) requires! options, :customer_number, :method_id request = build_request(__method__, options) @@ -531,7 +531,7 @@ def get_customer_payment_method(options={}) # ==== Response # * #message -- hash of payment method # - def update_customer_payment_method(options={}) + def update_customer_payment_method(options = {}) requires! options, :method_id request = build_request(__method__, options) @@ -544,7 +544,7 @@ def update_customer_payment_method(options={}) # * :customer_number # * :method_id # - def delete_customer_payment_method(options={}) + def delete_customer_payment_method(options = {}) requires! options, :customer_number, :method_id request = build_request(__method__, options) @@ -556,7 +556,7 @@ def delete_customer_payment_method(options={}) # ==== Required # * :customer_number # - def delete_customer(options={}) + def delete_customer(options = {}) requires! options, :customer_number request = build_request(__method__, options) @@ -607,7 +607,7 @@ def delete_customer(options={}) # ==== Response # * #message -- transaction response hash # - def run_customer_transaction(options={}) + def run_customer_transaction(options = {}) requires! options, :customer_number, :command, :amount request = build_request(__method__, options) @@ -671,15 +671,15 @@ def run_customer_transaction(options={}) # ==== Response # * #message -- transaction response hash # - def run_transaction(options={}) + def run_transaction(options = {}) request = build_request(__method__, options) commit(__method__, request) end - TRANSACTION_METHODS = [ - :run_sale, :run_auth_only, :run_credit, - :run_check_sale, :run_check_credit - ] #:nodoc: + TRANSACTION_METHODS = %i[ + run_sale run_auth_only run_credit + run_check_sale run_check_credit + ] # :nodoc: TRANSACTION_METHODS.each do |method| define_method method do |options| @@ -699,7 +699,7 @@ def run_transaction(options={}) # ==== Response # * #message -- transaction response hash # - def post_auth(options={}) + def post_auth(options = {}) requires! options, :authorization_code request = build_request(__method__, options) @@ -721,7 +721,7 @@ def post_auth(options={}) # ==== Response # * #message -- transaction response hash # - def capture_transaction(options={}) + def capture_transaction(options = {}) requires! options, :reference_number request = build_request(__method__, options) @@ -738,7 +738,7 @@ def capture_transaction(options={}) # ==== Response # * #message -- transaction response hash # - def void_transaction(options={}) + def void_transaction(options = {}) requires! options, :reference_number request = build_request(__method__, options) @@ -757,7 +757,7 @@ def void_transaction(options={}) # ==== Response # * #message -- transaction response hash # - def refund_transaction(options={}) + def refund_transaction(options = {}) requires! options, :reference_number, :amount request = build_request(__method__, options) @@ -777,7 +777,7 @@ def refund_transaction(options={}) # ==== Response # * #message -- transaction response hash # - def override_transaction(options={}) + def override_transaction(options = {}) requires! options, :reference_number request = build_request(__method__, options) @@ -820,7 +820,7 @@ def override_transaction(options={}) # ==== Response # * #message -- transaction response hash # - def run_quick_sale(options={}) + def run_quick_sale(options = {}) requires! options, :reference_number, :amount request = build_request(__method__, options) @@ -858,7 +858,7 @@ def run_quick_sale(options={}) # ==== Response # * #message -- transaction response hash # - def run_quick_credit(options={}) + def run_quick_credit(options = {}) requires! options, :reference_number request = build_request(__method__, options) @@ -875,7 +875,7 @@ def run_quick_credit(options={}) # ==== Response # * #message -- transaction hash # - def get_transaction(options={}) + def get_transaction(options = {}) requires! options, :reference_number request = build_request(__method__, options) @@ -892,7 +892,7 @@ def get_transaction(options={}) # * response.message -- message of the referenced transaction # * response.authorization -- same as :reference_number in options # - def get_transaction_status(options={}) + def get_transaction_status(options = {}) requires! options, :reference_number request = build_request(__method__, options) @@ -990,7 +990,7 @@ def get_transaction_status(options={}) # ==== Response # * #message -- hash; keys are the field values # - def get_transaction_custom(options={}) + def get_transaction_custom(options = {}) requires! options, :reference_number, :fields request = build_request(__method__, options) @@ -1005,7 +1005,7 @@ def get_transaction_custom(options={}) # ==== Response # * #message -- check trace hash # - def get_check_trace(options={}) + def get_check_trace(options = {}) requires! options, :reference_number request = build_request(__method__, options) @@ -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 @@ -1078,7 +1080,7 @@ def build_add_customer(soap, options) end end - def build_customer(soap, options, type, add_customer_data=false) + def build_customer(soap, options, type, add_customer_data = false) soap.tag! "ns1:#{type}" do build_token soap, options build_tag soap, :integer, 'CustNum', options[:customer_number] @@ -1297,7 +1299,7 @@ def build_get_account_details(soap, options) def build_customer_data(soap, options) soap.CustomerData 'xsi:type' => 'ns1:CustomerObject' do - CUSTOMER_OPTIONS.each do |k,v| + CUSTOMER_OPTIONS.each do |k, v| build_tag soap, v[0], v[1], options[k] end build_billing_address soap, options @@ -1310,7 +1312,7 @@ def build_customer_payments(soap, options) if options[:payment_methods] length = options[:payment_methods].length soap.PaymentMethods 'SOAP-ENC:arrayType' => "ns1:PaymentMethod[#{length}]", - 'xsi:type' =>'ns1:PaymentMethodArray' do + 'xsi:type' => 'ns1:PaymentMethodArray' do build_customer_payment_methods soap, options end 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] @@ -1345,9 +1346,7 @@ def build_credit_card_or_check(soap, payment_method) when payment_method[:method].kind_of?(ActiveMerchant::Billing::Check) build_tag soap, :string, 'Account', payment_method[:method].account_number build_tag soap, :string, 'Routing', payment_method[:method].routing_number - unless payment_method[:method].account_type.nil? - build_tag soap, :string, 'AccountType', payment_method[:method].account_type.capitalize - end + build_tag soap, :string, 'AccountType', payment_method[:method].account_type.capitalize unless payment_method[:method].account_type.nil? build_tag soap, :string, 'DriversLicense', options[:drivers_license] build_tag soap, :string, 'DriversLicenseState', options[:drivers_license_state] build_tag soap, :string, 'RecordType', options[:record_type] @@ -1370,7 +1369,7 @@ def build_customer_payment_methods(soap, options) def build_customer_transaction(soap, options) soap.Parameters 'xsi:type' => 'ns1:CustomerTransactionRequest' do build_transaction_detail soap, options - CUSTOMER_TRANSACTION_REQUEST_OPTIONS.each do |k,v| + CUSTOMER_TRANSACTION_REQUEST_OPTIONS.each do |k, v| build_tag soap, v[0], v[1], options[k] end build_custom_fields soap, options @@ -1380,13 +1379,14 @@ def build_customer_transaction(soap, options) # Transaction Helpers =========================================== - def build_transaction_request_object(soap, options, name='Params') + def build_transaction_request_object(soap, options, name = 'Params') soap.tag! name, 'xsi:type' => 'ns1:TransactionRequestObject' do - TRANSACTION_REQUEST_OBJECT_OPTIONS.each do |k,v| + TRANSACTION_REQUEST_OBJECT_OPTIONS.each do |k, v| build_tag soap, v[0], v[1], options[k] end case - when options[:payment_method] == nil + when options[:payment_method].nil? + nil when options[:payment_method].kind_of?(ActiveMerchant::Billing::CreditCard) build_credit_card_data soap, options when options[:payment_method].kind_of?(ActiveMerchant::Billing::Check) @@ -1405,10 +1405,10 @@ def build_transaction_request_object(soap, options, name='Params') def build_transaction_detail(soap, options) soap.Details 'xsi:type' => 'ns1:TransactionDetail' do - TRANSACTION_DETAIL_OPTIONS.each do |k,v| + TRANSACTION_DETAIL_OPTIONS.each do |k, v| build_tag soap, v[0], v[1], options[k] end - TRANSACTION_DETAIL_MONEY_OPTIONS.each do |k,v| + TRANSACTION_DETAIL_MONEY_OPTIONS.each do |k, v| build_tag soap, v[0], v[1], amount(options[k]) end end @@ -1424,7 +1424,7 @@ def build_credit_card_data(soap, options) end build_tag soap, :string, 'CardCode', options[:payment_method].verification_value build_tag soap, :boolean, 'CardPresent', options[:card_present] || false - CREDIT_CARD_DATA_OPTIONS.each do |k,v| + CREDIT_CARD_DATA_OPTIONS.each do |k, v| build_tag soap, v[0], v[1], options[k] end end @@ -1433,9 +1433,7 @@ def build_credit_card_data(soap, options) def build_card_expiration(options) month = options[:payment_method].month year = options[:payment_method].year - unless month.nil? || year.nil? - "#{"%02d" % month}#{year.to_s[-2..-1]}" - end + "#{'%02d' % month}#{year.to_s[-2..-1]}" unless month.nil? || year.nil? end def build_check_data(soap, options) @@ -1444,7 +1442,7 @@ def build_check_data(soap, options) build_tag soap, :string, 'Account', options[:payment_method].account_number build_tag soap, :string, 'Routing', options[:payment_method].routing_number build_tag soap, :string, 'AccountType', options[:payment_method].account_type.capitalize - CHECK_DATA_OPTIONS.each do |k,v| + CHECK_DATA_OPTIONS.each do |k, v| build_tag soap, v[0], v[1], options[k] end end @@ -1456,7 +1454,7 @@ def build_recurring_billing(soap, options) build_tag soap, :double, 'Amount', amount(options[:recurring][:amount]) build_tag soap, :string, 'Next', options[:recurring][:next].strftime('%Y-%m-%d') if options[:recurring][:next] build_tag soap, :string, 'Expire', options[:recurring][:expire].strftime('%Y-%m-%d') if options[:recurring][:expire] - RECURRING_BILLING_OPTIONS.each do |k,v| + RECURRING_BILLING_OPTIONS.each do |k, v| build_tag soap, v[0], v[1], options[:recurring][k] end end @@ -1475,11 +1473,9 @@ def build_transaction_field_array(soap, options) def build_billing_address(soap, options) if options[:billing_address] - if options[:billing_address][:name] - options[:billing_address][:first_name], options[:billing_address][:last_name] = split_names(options[:billing_address][:name]) - end + options[:billing_address][:first_name], options[:billing_address][:last_name] = split_names(options[:billing_address][:name]) if options[:billing_address][:name] soap.BillingAddress 'xsi:type' => 'ns1:Address' do - ADDRESS_OPTIONS.each do |k,v| + ADDRESS_OPTIONS.each do |k, v| build_tag soap, v[0], v[1], options[:billing_address][k] end end @@ -1488,11 +1484,9 @@ def build_billing_address(soap, options) def build_shipping_address(soap, options) if options[:shipping_address] - if options[:shipping_address][:name] - options[:shipping_address][:first_name], options[:shipping_address][:last_name] = split_names(options[:shipping_address][:name]) - end + options[:shipping_address][:first_name], options[:shipping_address][:last_name] = split_names(options[:shipping_address][:name]) if options[:shipping_address][:name] soap.ShippingAddress 'xsi:type' => 'ns1:Address' do - ADDRESS_OPTIONS.each do |k,v| + ADDRESS_OPTIONS.each do |k, v| build_tag soap, v[0], v[1], options[:shipping_address][k] end end @@ -1502,7 +1496,7 @@ def build_shipping_address(soap, options) def build_field_value_array(soap, tag_name, type, custom_data, fields) soap.tag! tag_name, 'SOAP-ENC:arryType' => "xsd:#{type}[#{options.length}]", 'xsi:type' => "ns1:#{type}Array" do custom_data.each do |k, v| - build_field_value soap, fields[k][1], v, fields[k][0] if fields.keys.include? k + build_field_value soap, fields[k][1], v, fields[k][0] if fields.key?(k) end end end @@ -1527,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) @@ -1537,53 +1531,54 @@ def commit(action, request) def build_response(action, soap) response_params, success, message, authorization, avs, cvv = parse(action, soap) - response_params.merge!('soap_response' => soap) if @options[:soap_response] + response_params['soap_response'] = soap if @options[:soap_response] Response.new( success, message, response_params, - :test => test?, - :authorization => authorization, - :avs_result => avs_from(avs), - :cvv_result => cvv + test: test?, + authorization:, + avs_result: avs_from(avs), + cvv_result: cvv ) end def avs_from(avs) - avs_params = { :code => avs } - avs_params.merge!(:message => AVS_CUSTOM_MESSAGES[avs]) if AVS_CUSTOM_MESSAGES.key?(avs) + avs_params = { code: avs } + avs_params[:message] = AVS_CUSTOM_MESSAGES[avs] if AVS_CUSTOM_MESSAGES.key?(avs) avs_params end def parse(action, soap) xml = REXML::Document.new(soap) root = REXML::XPath.first(xml, '//SOAP-ENV:Body') - response = root ? parse_element(root[0]) : { :response => soap } + response = root ? parse_element(root[0]) : { response: soap } success, message, authorization, avs, cvv = false, FAILURE_MESSAGE, nil, nil, nil - fault = (!response) || (response.length < 1) || response.has_key?('faultcode') + fault = !response || (response.length < 1) || response.has_key?('faultcode') return [response, success, response['faultstring'], authorization, avs, cvv] if fault if response.respond_to?(:[]) && p = response["#{action}_return"] if p.respond_to?(:key?) && p.key?('result_code') - success = p['result_code'] == 'A' ? true : false + success = p['result_code'] == 'A' authorization = p['ref_num'] avs = AVS_RESULTS[p['avs_result_code']] cvv = p['card_code_result_code'] else success = true end - message = case action - when :get_customer_payment_methods - p['item'] - when :get_transaction_custom - items = p['item'].kind_of?(Array) ? p['item'] : [p['item']] - items.inject({}) { |hash, item| hash[item['field']] = item['value']; hash } - else - p - end + message = + case action + when :get_customer_payment_methods + p['item'] + when :get_transaction_custom + items = p['item'].kind_of?(Array) ? p['item'] : [p['item']] + items.inject({}) { |hash, item| hash[item['field']] = item['value']; hash } + else + p + end elsif response.respond_to?(:[]) && p = response[:response] message = p # when response is html end @@ -1613,7 +1608,6 @@ def parse_element(node) response end - end end end diff --git a/lib/active_merchant/billing/gateways/usa_epay_transaction.rb b/lib/active_merchant/billing/gateways/usa_epay_transaction.rb index b2f4a7fb590..a1d148549d3 100644 --- a/lib/active_merchant/billing/gateways/usa_epay_transaction.rb +++ b/lib/active_merchant/billing/gateways/usa_epay_transaction.rb @@ -1,23 +1,23 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: - +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class UsaEpayTransactionGateway < Gateway self.live_url = 'https://www.usaepay.com/gate' self.test_url = 'https://sandbox.usaepay.com/gate' - self.supported_cardtypes = [:visa, :master, :american_express] + self.supported_cardtypes = %i[visa master american_express] self.supported_countries = ['US'] self.homepage_url = 'http://www.usaepay.com/' self.display_name = 'USA ePay' TRANSACTIONS = { - :authorization => 'cc:authonly', - :purchase => 'cc:sale', - :capture => 'cc:capture', - :refund => 'cc:refund', - :void => 'cc:void', - :void_release => 'cc:void:release', - :check_purchase => 'check:sale' + authorization: 'cc:authonly', + purchase: 'cc:sale', + capture: 'cc:capture', + refund: 'cc:refund', + void: 'cc:void', + void_release: 'cc:void:release', + check_purchase: 'check:sale', + store: 'cc:save' } STANDARD_ERROR_CODE_MAPPING = { @@ -34,9 +34,9 @@ class UsaEpayTransactionGateway < Gateway '10110' => STANDARD_ERROR_CODE[:incorrect_address], '10111' => STANDARD_ERROR_CODE[:incorrect_address], '10127' => STANDARD_ERROR_CODE[:card_declined], - '10128' => STANDARD_ERROR_CODE[:processing_error], - '10132' => STANDARD_ERROR_CODE[:processing_error], - '00043' => STANDARD_ERROR_CODE[:call_issuer] + '00043' => STANDARD_ERROR_CODE[:call_issuer], + '10205' => STANDARD_ERROR_CODE[:card_declined], + '10204' => STANDARD_ERROR_CODE[:pickup_card] } def initialize(options = {}) @@ -44,17 +44,20 @@ def initialize(options = {}) super end - def authorize(money, credit_card, options = {}) + def authorize(money, payment, options = {}) post = {} add_amount(post, money) add_invoice(post, options) - add_payment(post, credit_card) - unless credit_card.track_data.present? - add_address(post, credit_card, options) + add_payment(post, payment) + unless payment.is_a?(CreditCard) && payment.track_data.present? + add_address(post, payment, options) add_customer_data(post, options) end add_split_payments(post, options) + add_recurring_fields(post, options) + add_custom_fields(post, options) + add_line_items(post, options) add_test_mode(post, options) commit(:authorization, post) @@ -65,19 +68,22 @@ def purchase(money, payment, options = {}) add_amount(post, money) add_invoice(post, options) - add_payment(post, payment) + add_payment(post, payment, options) unless payment.respond_to?(:track_data) && payment.track_data.present? add_address(post, payment, options) add_customer_data(post, options) end add_split_payments(post, options) + add_recurring_fields(post, options) + add_custom_fields(post, options) + add_line_items(post, options) add_test_mode(post, options) payment.respond_to?(:routing_number) ? commit(:check_purchase, post) : commit(:purchase, post) end def capture(money, authorization, options = {}) - post = { :refNum => authorization } + post = { refNum: authorization } add_amount(post, money) add_test_mode(post, options) @@ -85,13 +91,19 @@ def capture(money, authorization, options = {}) end def refund(money, authorization, options = {}) - post = { :refNum => authorization } + post = { refNum: authorization } add_amount(post, money) add_test_mode(post, options) commit(:refund, post) end + def store(payment, options = {}) + post = {} + add_payment(post, payment, options) + commit(:store, post) + end + def verify(creditcard, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(1, creditcard, options) } @@ -102,7 +114,7 @@ def verify(creditcard, options = {}) # Pass `no_release: true` to keep the void from immediately settling def void(authorization, options = {}) command = (options[:no_release] ? :void : :void_release) - post = { :refNum => authorization } + post = { refNum: authorization } add_test_mode(post, options) commit(command, post) end @@ -148,13 +160,9 @@ def add_customer_data(post, options) end end - if options.has_key? :customer - post[:custid] = options[:customer] - end + post[:custid] = options[:customer] if options.has_key? :customer - if options.has_key? :ip - post[:ip] = options[:ip] - end + post[:ip] = options[:ip] if options.has_key? :ip end def add_address(post, payment, options) @@ -192,18 +200,28 @@ def address_key(prefix, key) end def add_invoice(post, options) - post[:invoice] = options[:order_id] + post[:invoice] = options[:invoice] + post[:orderid] = options[:order_id] post[:description] = options[:description] end - def add_payment(post, payment) + def add_payment(post, payment, options = {}) if payment.respond_to?(:routing_number) + post[:checkformat] = options[:check_format] if options[:check_format] + if payment.account_type + account_type = payment.account_type.to_s.capitalize + raise ArgumentError, 'account_type must be checking or savings' unless %w(Checking Savings).include?(account_type) + + post[:accounttype] = account_type + end post[:account] = payment.account_number post[:routing] = payment.routing_number post[:name] = payment.name unless payment.name.blank? elsif payment.respond_to?(:track_data) && payment.track_data.present? post[:magstripe] = payment.track_data post[:cardpresent] = true + elsif payment.is_a?(String) + post[:card] = payment else post[:card] = payment.number post[:cvv2] = payment.verification_value if payment.verification_value? @@ -220,6 +238,7 @@ def add_test_mode(post, options) # see: http://wiki.usaepay.com/developer/transactionapi#split_payments def add_split_payments(post, options) return unless options[:split_payments].is_a?(Array) + options[:split_payments].each_with_index do |payment, index| prefix = '%02d' % (index + 2) post["#{prefix}key"] = payment[:key] @@ -231,40 +250,94 @@ def add_split_payments(post, options) post['onError'] = options[:on_error] || 'Void' end + def add_recurring_fields(post, options) + return unless options[:recurring_fields].is_a?(Hash) + + options[:recurring_fields].each do |key, value| + if value == true + value = 'yes' + elsif value == false + next + end + + value = amount(value) if key == :bill_amount + + post[key.to_s.delete('_')] = value + end + end + + # see: https://wiki.usaepay.com/developer/transactionapi#merchant_defined_custom_fields + def add_custom_fields(post, options) + return unless options[:custom_fields].is_a?(Hash) + + options[:custom_fields].each do |index, custom| + raise ArgumentError.new('Cannot specify custom field with index 0') if index.to_s.to_i.zero? + + post["custom#{index}"] = custom + end + end + + # see: https://wiki.usaepay.com/developer/transactionapi#line_item_details + def add_line_items(post, options) + return unless options[:line_items].is_a?(Array) + + options[:line_items].each_with_index do |line_item, index| + %w(product_ref_num sku qty name description taxable tax_rate tax_amount commodity_code discount_rate discount_amount).each do |key| + post["line#{index}#{key.delete('_')}"] = line_item[key.to_sym] if line_item.has_key?(key.to_sym) + end + + { + quantity: 'qty', + unit: 'um' + }.each do |key, umkey| + post["line#{index}#{umkey}"] = line_item[key.to_sym] if line_item.has_key?(key.to_sym) + end + + post["line#{index}cost"] = amount(line_item[:cost]) + end + end + def parse(body) fields = {} for line in body.split('&') - key, value = *line.scan( %r{^(\w+)\=(.*)$} ).flatten + key, value = *line.scan(%r{^(\w+)\=(.*)$}).flatten fields[key] = CGI.unescape(value.to_s) end { - :status => fields['UMstatus'], - :auth_code => fields['UMauthCode'], - :ref_num => fields['UMrefNum'], - :batch => fields['UMbatch'], - :avs_result => fields['UMavsResult'], - :avs_result_code => fields['UMavsResultCode'], - :cvv2_result => fields['UMcvv2Result'], - :cvv2_result_code => fields['UMcvv2ResultCode'], - :vpas_result_code => fields['UMvpasResultCode'], - :result => fields['UMresult'], - :error => fields['UMerror'], - :error_code => fields['UMerrorcode'], - :acs_url => fields['UMacsurl'], - :payload => fields['UMpayload'] - }.delete_if{|k, v| v.nil?} + status: fields['UMstatus'], + auth_code: fields['UMauthCode'], + ref_num: fields['UMrefNum'], + card_ref: fields['UMcardRef'], + batch: fields['UMbatch'], + avs_result: fields['UMavsResult'], + avs_result_code: fields['UMavsResultCode'], + cvv2_result: fields['UMcvv2Result'], + cvv2_result_code: fields['UMcvv2ResultCode'], + vpas_result_code: fields['UMvpasResultCode'], + result: fields['UMresult'], + error: fields['UMerror'], + error_code: fields['UMerrorcode'], + acs_url: fields['UMacsurl'], + payload: fields['UMpayload'] + }.delete_if { |_k, v| v.nil? } end def commit(action, parameters) url = (test? ? self.test_url : self.live_url) response = parse(ssl_post(url, post_data(action, parameters))) - Response.new(response[:status] == 'Approved', message_from(response), response, - :test => test?, - :authorization => response[:ref_num], - :cvv_result => response[:cvv2_result_code], - :avs_result => { :code => response[:avs_result_code] }, - :error_code => STANDARD_ERROR_CODE_MAPPING[response[:error_code]] + 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, + test: test?, + authorization: authorization_from(action, response), + cvv_result: response[:cvv2_result_code], + avs_result: { code: response[:avs_result_code] }, + error_code: ) end @@ -273,17 +346,22 @@ def message_from(response) return 'Success' else return 'Unspecified error' if response[:error].blank? + return response[:error] end end + def authorization_from(action, response) + return (action == :store ? response[:card_ref] : response[:ref_num]) + end + def post_data(action, parameters = {}) parameters[:command] = TRANSACTIONS[action] parameters[:key] = @options[:login] parameters[:software] = 'Active Merchant' parameters[:testmode] = (@options[:test] ? 1 : 0) unless parameters.has_key?(:testmode) seed = SecureRandom.hex(32).upcase - hash = Digest::SHA1.hexdigest("#{parameters[:command]}:#{@options[:password]}:#{parameters[:amount]}:#{parameters[:invoice]}:#{seed}") + hash = Digest::SHA1.hexdigest("#{parameters[:command]}:#{@options[:pin] || @options[:password]}:#{parameters[:amount]}:#{parameters[:invoice]}:#{seed}") parameters[:hash] = "s/#{seed}/#{hash}/n" parameters.collect { |key, value| "UM#{key}=#{CGI.escape(value.to_s)}" }.join('&') diff --git a/lib/active_merchant/billing/gateways/vanco.rb b/lib/active_merchant/billing/gateways/vanco.rb index 8f9925bb946..09d0bbe9519 100644 --- a/lib/active_merchant/billing/gateways/vanco.rb +++ b/lib/active_merchant/billing/gateways/vanco.rb @@ -10,24 +10,33 @@ class VancoGateway < Gateway self.supported_countries = ['US'] self.default_currency = 'USD' - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'http://vancopayments.com/' self.display_name = 'Vanco Payment Solutions' - def initialize(options={}) + SECONDS_PER_DAY = 3600 * 24 + BUFFER_TIME_IN_SECS = 60 * 3 + + def initialize(options = {}) requires!(options, :user_id, :password, :client_id) super end - def purchase(money, payment_method, options={}) - MultiResponse.run do |r| - r.process { login } - r.process { commit(purchase_request(money, payment_method, r.params['response_sessionid'], options), :response_transactionref) } + def purchase(money, payment_method, options = {}) + moment_less_than_24_hours_ago = Time.now - SECONDS_PER_DAY - BUFFER_TIME_IN_SECS + + if options[:session_id] && options[:session_id][:created_at] >= moment_less_than_24_hours_ago + commit(purchase_request(money, payment_method, options[:session_id][:id], options), :response_transactionref) + else + MultiResponse.run do |r| + r.process { login } + r.process { commit(purchase_request(money, payment_method, r.params['response_sessionid'], options), :response_transactionref) } + end end end - def refund(money, authorization, options={}) + def refund(money, authorization, options = {}) MultiResponse.run do |r| r.process { login } r.process { commit(refund_request(money, authorization, r.params['response_sessionid']), :response_creditrequestreceived) } @@ -52,7 +61,7 @@ def parse(xml) doc = Nokogiri::XML(xml) doc.root.xpath('*').each do |node| - if (node.elements.empty?) + if node.elements.empty? response[node.name.downcase.to_sym] = node.text else node.elements.each do |childnode| @@ -82,8 +91,8 @@ def add_errors_to_response(response, errors_xml) response[:error_message] = error['ErrorDescription'] response[:error_codes] = error['ErrorCode'] elsif error.kind_of?(Array) - error_str = error.map { |e| e['ErrorDescription']}.join('. ') - error_codes = error.map { |e| e['ErrorCode']}.join(', ') + error_str = error.map { |e| e['ErrorDescription'] }.join('. ') + error_codes = error.map { |e| e['ErrorCode'] }.join(', ') response[:error_message] = "#{error_str}." response[:error_codes] = error_codes end @@ -108,6 +117,7 @@ def success_from(response, success_field_name) def message_from(succeeded, response) return 'Success' if succeeded + response[:error_message] end @@ -271,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 @@ -288,7 +296,6 @@ def headers 'Content-Type' => 'text/xml' } end - end end 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..6ffc2e8f6a2 --- /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_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 b8c2142e621..46268c28d75 100644 --- a/lib/active_merchant/billing/gateways/verifi.rb +++ b/lib/active_merchant/billing/gateways/verifi.rb @@ -1,12 +1,12 @@ require 'rexml/document' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class VerifiGateway < Gateway class VerifiPostData < PostData # Fields that will be sent even if they are blank - self.required_fields = [:amount, :type, :ccnumber, :ccexp, :firstname, :lastname, - :company, :address1, :address2, :city, :state, :zip, :country, :phone] + self.required_fields = %i[amount type ccnumber ccexp firstname lastname + company address1 address2 city state zip country phone] end self.live_url = self.test_url = 'https://secure.verifi.com/gw/api/transact.php' @@ -50,16 +50,16 @@ class VerifiPostData < PostData SUCCESS = 1 TRANSACTIONS = { - :authorization => 'auth', - :purchase => 'sale', - :capture => 'capture', - :void => 'void', - :credit => 'credit', - :refund => 'refund' + authorization: 'auth', + purchase: 'sale', + capture: 'capture', + void: 'void', + credit: 'credit', + refund: 'refund' } self.supported_countries = ['US'] - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'http://www.verifi.com/' self.display_name = 'Verifi' @@ -180,10 +180,10 @@ def add_security_key_data(post, options, money) # MD5(username|password|orderid|amount|time) now = Time.now.to_i.to_s md5 = Digest::MD5.new - md5 << @options[:login].to_s + '|' - md5 << @options[:password].to_s + '|' - md5 << options[:order_id].to_s + '|' - md5 << amount(money).to_s + '|' + md5 << (@options[:login].to_s + '|') + md5 << (@options[:password].to_s + '|') + md5 << (options[:order_id].to_s + '|') + md5 << (amount(money).to_s + '|') md5 << now post[:key] = md5.hexdigest post[:time] = now @@ -192,13 +192,16 @@ def add_security_key_data(post, options, money) def commit(trx_type, money, post) post[:amount] = amount(money) - response = parse( ssl_post(self.live_url, post_data(trx_type, post)) ) + response = parse(ssl_post(self.live_url, post_data(trx_type, post))) - 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] + 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] ) end diff --git a/lib/active_merchant/billing/gateways/versa_pay.rb b/lib/active_merchant/billing/gateways/versa_pay.rb new file mode 100644 index 00000000000..74b942d0e60 --- /dev/null +++ b/lib/active_merchant/billing/gateways/versa_pay.rb @@ -0,0 +1,307 @@ +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class VersaPayGateway < Gateway + self.test_url = 'https://uat.versapay.com' + self.live_url = 'https://secure.versapay.com' + + self.supported_countries = ['US'] + self.default_currency = 'USD' + self.money_format = :cents + self.supported_cardtypes = %i[visa master american_express discover] + + self.homepage_url = 'https://www.versapay.com/' + self.display_name = 'VersaPay' + + def initialize(options = {}) + requires!(options, :api_token, :api_key) + @api_token = options[:api_token] + @api_key = options[:api_key] + super + end + + def purchase(money, payment, options = {}) + transact(money, payment, options) + end + + def authorize(money, payment, options = {}) + transact(money, payment, options, 'auth') + end + + def capture(money, authorization, options = {}) + post = { + amount_cents: money, + transaction: authorization + } + commit('capture', post) + end + + def verify(credit_card, options = {}) + transact(0, credit_card, options, 'verify') + end + + def void(authorization, options = {}) + commit('void', { transaction: authorization }) + end + + def refund(money, authorization, options = {}) + post = { + amount_cents: money, + transaction: authorization + } + commit('refund', post) + end + + def credit(money, payment_method, options = {}) + transact(money, payment_method, options, 'credit') + end + + def store(payment_method, options = {}) + post = { + contact: { email: options[:email] } + } + add_customer_data(post, options) + add_payment_method(post, payment_method, options) + commit('store', post) + end + + def unstore(authorization, options = {}) + wallet_token, fund_token = authorization.split('|') + commit('unstore', {}, :delete, { fund_token:, wallet_token: }) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). + gsub(%r(("card_number\\?":\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("cvv\\?":\\?")[^"]*)i, '\1[FILTERED]') + end + + private + + def transact(money, payment, options = {}, type = 'sale') + post = { + contact: { email: options[:email] } + } + add_invoice(post, money, options) + add_order(post, money, options) + add_payment_method(post, payment, options) + commit(type, post) + end + + def add_customer_data(post, options) + post[:customer_identifier] = options[:customer_identifier] if options[:customer_identifier] + end + + def add_invoice(post, money, options) + post[:amount_cents] = amount(money) + post[:currency] = options[:currency] || currency(money) + end + + def add_order(post, money, options = {}) + order = { + identifier: options[:order_id], + number: options[:order_number], + date: options[:order_date] || Time.now.strftime('%Y-%m-%d'), + draft: false, + settlement_token: options[:settlement_token] # A settlement token reference (see whoami response structure) representing the merchant/bank processor configuration that should be used for transaction settlement. + }.compact + + add_invoice(order, money, options) + add_address(order, options, 'shipping') + add_address(order, options) + post[:order] = order + end + + def add_address(post, options, address_key = 'billing', hash = 'order') + address = options["#{address_key}_address".to_sym] + return unless address + + address_data = { + address_1: address[:address1], + city: address[:city], + province: address[:state], + postal_code: address[:zip], + country: Country.find(address[:country]).code(:alpha3).value + } + + if hash == 'payment_method' + post[:address] = address_data + else + post.merge!({ + "#{address_key}_name": address[:company], + "#{address_key}_address": address[:address1], + "#{address_key}_address2": address[:address2], + "#{address_key}_city": address[:city], + "#{address_key}_country": address_data[:country], + "#{address_key}_email": options[:email], + "#{address_key}_telephone": address[:phone] || address[:phone_number], + "#{address_key}_postalcode": address[:zip], + "#{address_key}_state_province": address[:state] + }.compact) + end + end + + def add_payment_method(post, payment_method, options) + if payment_method.is_a?(CreditCard) + post[:credit_card] = { + name: payment_method.name, + expiry_month: format(payment_method.month, :two_digits), + expiry_year: payment_method.year, + card_number: payment_method.number, + cvv: payment_method.verification_value + } + add_address(post[:credit_card], options, 'billing', 'payment_method') + elsif payment_method.is_a?(String) + fund_token = payment_method.split('|').last + post[:fund_token] = fund_token + end + end + + def parse(body) + JSON.parse(body).with_indifferent_access + rescue JSON::ParserError => e + { + errors: body, + status: 'Unable to parse JSON response', + message: e.message + }.with_indifferent_access + end + + def commit(action, post, method = :post, options = {}) + raw_response = ssl_request(method, url(action, options), post.to_json, request_headers) + response = parse(raw_response) + first_transaction = response['transactions']&.first + + Response.new( + success_from(response, action), + message_from(response, action), + response, + authorization: authorization_from(response), + avs_result: AVSResult.new(code: dig_avs_code(first_transaction)), + cvv_result: CVVResult.new(dig_cvv_code(first_transaction)), + test: test?, + error_code: error_code_from(response, action) + ) + end + + def success_from(response, action) + case action + when 'store' + response['wallet_token'] || response['fund_token'] || false + when 'unstore' + response['fund_token'] || false + else + response['success'] || false + end + end + + def message_from(response, action) + return 'Succeeded' if success_from(response, action) + + first_transaction = response['transactions']&.first + gateway_response_errors = gateway_errors_message(response) + + response_message = { + error: response.dig('error') || response.dig('wallets', 'error'), + errors: response.dig('errors')&.join(', ').presence, + gateway_error_message: first_transaction&.dig('gateway_error_message').presence, + gateway_response_errors: gateway_response_errors.presence + }.compact + + response_message.map { |key, value| "#{key}: #{value}" }.join(' | ') + end + + def authorization_from(response) + transaction = response['transaction'] + wallet_token = response['wallet_token'] || response.dig('wallets', 0, 'token') + fund_token = response['fund_token'] || response.dig('wallets', 0, 'credit_cards', 0, 'token') + [transaction, wallet_token, fund_token].compact.join('|') + end + + def error_code_from(response, action) + return if success_from(response, action) + + response.dig('transactions', 0, 'gateway_error_code') + end + + def gateway_errors_message(response) + errors = response.dig('transactions', 0, 'gateway_response', 'errors') + return unless errors.is_a?(Hash) + + errors.flat_map do |field, error_details| + error_details.flat_map do |error| + if error.is_a?(Hash) + error.map { |key, messages| "[#{field} - #{key}: #{messages.join(', ')}]" } + else + "[#{field} - #{error}]" + end + end + end.join(' , ') + end + + def url(endpoint, options = {}) + case endpoint + when 'unstore' + parameters = "/#{options[:wallet_token]}/methods/#{options[:fund_token]}" + "#{test? ? test_url : live_url}/api/gateway/v1/wallets#{parameters}" + when 'store' + "#{test? ? test_url : live_url}/api/gateway/v1/wallets" + else + "#{test? ? test_url : live_url}/api/gateway/v1/orders/#{endpoint}" + end + end + + def basic_auth + Base64.strict_encode64("#{@api_token}:#{@api_key}") + end + + def request_headers + { + 'Content-Type' => 'application/json', + 'Authorization' => "Basic #{basic_auth}" + } + end + + def dig_cvv_code(first_transaction) + return unless first_transaction + + first_transaction.dig('cvv_response') || + first_transaction.dig('gateway_response', 'cvv_response') || + find_cvv_avs_code(first_transaction, 'cvvresponse') + end + + def dig_avs_code(first_transaction) + return unless first_transaction + + first_transaction.dig('avs_response') || + first_transaction.dig('gateway_response', 'avs_response') || + find_cvv_avs_code(first_transaction, 'avsresponse') + end + + def find_cvv_avs_code(first_transaction, to_find) + nested_response = first_transaction.dig( + 'gateway_response', + 'gateway_response', + 'response', 'content', + 'create' + ) + return nil unless nested_response.is_a?(Array) + + nested_response.find { |x| x.dig('transaction', to_find) }&.dig('transaction', to_find) + end + + def handle_response(response) + case response.code.to_i + when 200..412 + response.body + else + response.body || raise(ResponseError.new(response)) # some errors 500 has the error message + end + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/viaklix.rb b/lib/active_merchant/billing/gateways/viaklix.rb index c2e800b9b4c..4b9e81cb546 100644 --- a/lib/active_merchant/billing/gateways/viaklix.rb +++ b/lib/active_merchant/billing/gateways/viaklix.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class ViaklixGateway < Gateway class_attribute :test_url, :live_url, :delimiter, :actions @@ -8,13 +8,13 @@ class ViaklixGateway < Gateway self.delimiter = "\r\n" self.actions = { - :purchase => 'SALE', - :credit => 'CREDIT' + purchase: 'SALE', + credit: 'CREDIT' } APPROVED = '0' - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] self.supported_countries = ['US'] self.display_name = 'ViaKLIX' self.homepage_url = 'http://viaklix.com' @@ -49,9 +49,7 @@ def purchase(money, creditcard, options = {}) # Make a credit to a card (Void can only be done from the virtual terminal) # Viaklix does not support credits by reference. You must pass in the credit card def credit(money, creditcard, options = {}) - if creditcard.is_a?(String) - raise ArgumentError, 'Reference credits are not supported. Please supply the original credit card' - end + raise ArgumentError, 'Reference credits are not supported. Please supply the original credit card' if creditcard.is_a?(String) form = {} add_invoice(form, options) @@ -63,6 +61,7 @@ def credit(money, creditcard, options = {}) end private + def add_test_mode(form, options) form[:test_mode] = 'TRUE' if options[:test_mode] end @@ -72,12 +71,12 @@ def add_customer_data(form, options) form[:customer_code] = options[:customer].to_s.slice(0, 10) unless options[:customer].blank? end - def add_invoice(form,options) + def add_invoice(form, options) form[:invoice_number] = (options[:order_id] || options[:invoice]).to_s.slice(0, 10) form[:description] = options[:description].to_s.slice(0, 255) end - def add_address(form,options) + def add_address(form, options) billing_address = options[:billing_address] || options[:address] if billing_address @@ -108,9 +107,7 @@ def add_creditcard(form, creditcard) form[:card_number] = creditcard.number form[:exp_date] = expdate(creditcard) - if creditcard.verification_value? - add_verification_value(form, creditcard) - end + add_verification_value(form, creditcard) if creditcard.verification_value? form[:first_name] = creditcard.first_name.to_s.slice(0, 20) form[:last_name] = creditcard.last_name.to_s.slice(0, 30) @@ -137,13 +134,16 @@ def commit(action, money, parameters) parameters[:amount] = amount(money) parameters[:transaction_type] = self.actions[action] - response = parse( ssl_post(test? ? self.test_url : self.live_url, post_data(parameters)) ) + response = parse(ssl_post(test? ? self.test_url : self.live_url, post_data(parameters))) - 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'] + 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'] ) end @@ -164,7 +164,7 @@ def post_data(parameters) # Parse the response message def parse(msg) resp = {} - msg.split(self.delimiter).collect{|li| + msg.split(self.delimiter).collect { |li| key, value = li.split('=') resp[key.strip.gsub(/^ssl_/, '')] = value.to_s.strip } diff --git a/lib/active_merchant/billing/gateways/visanet_peru.rb b/lib/active_merchant/billing/gateways/visanet_peru.rb index cc84e95f9c1..a010b751949 100644 --- a/lib/active_merchant/billing/gateways/visanet_peru.rb +++ b/lib/active_merchant/billing/gateways/visanet_peru.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class VisanetPeruGateway < Gateway include Empty self.display_name = 'VisaNet Peru Gateway' @@ -8,24 +8,24 @@ class VisanetPeruGateway < Gateway self.test_url = 'https://devapi.vnforapps.com/api.tokenization/api/v2/merchant' self.live_url = 'https://api.vnforapps.com/api.tokenization/api/v2/merchant' - self.supported_countries = ['US', 'PE'] + self.supported_countries = %w[US PE] self.default_currency = 'PEN' self.money_format = :dollars - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_cardtypes = %i[visa master american_express discover] - def initialize(options={}) + def initialize(options = {}) requires!(options, :access_key_id, :secret_access_key, :merchant_id) super end - def purchase(amount, payment_method, options={}) + def purchase(amount, payment_method, options = {}) MultiResponse.run() do |r| r.process { authorize(amount, payment_method, options) } - r.process { capture(r.authorization, options) } + r.process { capture(amount, r.authorization, options) } end end - def authorize(amount, payment_method, options={}) + def authorize(amount, payment_method, options = {}) params = {} add_invoice(params, amount, options) @@ -37,32 +37,35 @@ def authorize(amount, payment_method, options={}) commit('authorize', params, options) end - def capture(authorization, options={}) + def capture(amount, authorization, options = {}) params = {} options[:id_unico] = split_authorization(authorization)[1] add_auth_order_id(params, authorization, options) commit('deposit', params, options) end - def void(authorization, options={}) + def void(authorization, options = {}) params = {} add_auth_order_id(params, authorization, options) commit('void', params, options) end - def refund(amount, authorization, options={}) + def refund(amount, authorization, options = {}) params = {} params[:amount] = amount(amount) if amount add_auth_order_id(params, authorization, options) response = commit('cancelDeposit', params, options) return response if response.success? || split_authorization(authorization).length == 1 || !options[:force_full_refund_if_unsettled] - # Attempt RefundSingleTransaction if unsettled + # Attempt RefundSingleTransaction if unsettled (and stash the original + # response message so it will be included it in the follow-up response + # message) + options[:error_message] = response.message prepare_refund_data(params, authorization, options) commit('refund', params, options) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -82,20 +85,20 @@ def scrub(transcript) private - CURRENCY_CODES = Hash.new{|h,k| raise ArgumentError.new("Unsupported currency: #{k}")} + CURRENCY_CODES = Hash.new { |_h, k| raise ArgumentError.new("Unsupported currency: #{k}") } CURRENCY_CODES['USD'] = 840 CURRENCY_CODES['PEN'] = 604 def add_invoice(params, money, options) - # Visanet Peru expects a 9-digit numeric purchaseNumber - params[:purchaseNumber] = (SecureRandom.random_number(900_000_000) + 100_000_000).to_s + # Visanet Peru expects a 12-digit alphanumeric purchaseNumber + params[:purchaseNumber] = generate_purchase_number_stamp params[:externalTransactionId] = options[:order_id] params[:amount] = amount(money) params[:currencyId] = CURRENCY_CODES[options[:currency] || currency(money)] end def add_auth_order_id(params, authorization, options) - purchase_number, _ = split_authorization(authorization) + purchase_number, = split_authorization(authorization) params[:purchaseNumber] = purchase_number params[:externalTransactionId] = options[:order_id] end @@ -131,7 +134,7 @@ def prepare_refund_data(params, authorization, options) params[:externalReferenceId] = params.delete(:externalTransactionId) _, transaction_id = split_authorization(authorization) - options.update(transaction_id: transaction_id) + options.update(transaction_id:) params[:ruc] = options[:ruc] end @@ -139,25 +142,27 @@ def split_authorization(authorization) authorization.split('|') end - def commit(action, params, options={}) - begin - raw_response = ssl_request(method(action), url(action, params, options), params.to_json, headers) - response = parse(raw_response) - rescue ResponseError => e - raw_response = e.response.body - response_error(raw_response, options, action) - rescue JSON::ParserError - unparsable_response(raw_response) - else - Response.new( - success_from(response), - message_from(response, options, action), - response, - :test => test?, - :authorization => authorization_from(params, response, options), - :error_code => response['errorCode'] - ) - end + def generate_purchase_number_stamp + 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).merge('purchaseNumber' => params[:purchaseNumber]) + rescue ResponseError => e + raw_response = e.response.body + response_error(raw_response, options, action) + rescue JSON::ParserError + unparsable_response(raw_response) + else + Response.new( + success_from(response), + message_from(response, options, action), + response, + test: test?, + authorization: authorization_from(params, response, options), + error_code: response['errorCode'] + ) end def headers @@ -167,10 +172,10 @@ def headers } end - def url(action, params, options={}) - if (action == 'authorize') + def url(action, params, options = {}) + if action == 'authorize' "#{base_url}/#{@options[:merchant_id]}" - elsif (action == 'refund') + elsif action == 'refund' "#{base_url}/#{@options[:merchant_id]}/#{action}/#{options[:transaction_id]}" else "#{base_url}/#{@options[:merchant_id]}/#{action}/#{params[:purchaseNumber]}" @@ -178,7 +183,7 @@ def url(action, params, options={}) end def method(action) - (%w(authorize refund).include? action) ? :post : :put + %w(authorize refund).include?(action) ? :post : :put end def authorization_from(params, response, options) @@ -199,32 +204,40 @@ def success_from(response) end def message_from(response, options, action) - if empty?(response['errorMessage']) || response['errorMessage'] == '[ ]' - action == 'refund' ? "#{response['data']['DSC_COD_ACCION']}, #{options[:error_message]}" : response['data']['DSC_COD_ACCION'] - elsif action == 'refund' - message = "#{response['errorMessage']}, #{options[:error_message]}" - options[:error_message] = response['errorMessage'] - message - else - response['errorMessage'] - end + message_from_messages( + response['errorMessage'], + action_code_description(response), + options[:error_message] + ) + end + + def message_from_messages(*args) + args.reject { |m| error_message_empty?(m) }.join(' | ') + end + + def action_code_description(response) + return nil unless response['data'] + + response['data']['DSC_COD_ACCION'] + end + + def error_message_empty?(error_message) + empty?(error_message) || error_message == '[ ]' end def response_error(raw_response, options, action) - begin - response = parse(raw_response) - rescue JSON::ParserError - unparsable_response(raw_response) - else - return Response.new( - false, - message_from(response, options, action), - response, - :test => test?, - :authorization => response['transactionUUID'], - :error_code => response['errorCode'] - ) - end + response = parse(raw_response) + rescue JSON::ParserError + unparsable_response(raw_response) + else + return Response.new( + false, + message_from(response, options, action), + response, + test: test?, + authorization: response['transactionUUID'], + error_code: response['errorCode'] + ) end def unparsable_response(raw_response) diff --git a/lib/active_merchant/billing/gateways/vpos.rb b/lib/active_merchant/billing/gateways/vpos.rb new file mode 100644 index 00000000000..d76475cf605 --- /dev/null +++ b/lib/active_merchant/billing/gateways/vpos.rb @@ -0,0 +1,223 @@ +require 'digest' +require 'jwe' + +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class VposGateway < Gateway + self.test_url = 'https://vpos.infonet.com.py:8888' + self.live_url = 'https://vpos.infonet.com.py' + + self.supported_countries = ['PY'] + self.default_currency = 'PYG' + self.supported_cardtypes = %i[visa master panal] + + self.homepage_url = 'https://comercios.bancard.com.py' + self.display_name = 'vPOS' + + self.money_format = :dollars + + ENDPOINTS = { + pci_encryption_key: '/vpos/api/0.3/application/encryption-key', + pay_pci_buy_encrypted: '/vpos/api/0.3/pci/encrypted', + pci_buy_rollback: '/vpos/api/0.3/pci_buy/rollback', + refund: '/vpos/api/0.3/refunds' + } + + def initialize(options = {}) + requires!(options, :private_key, :public_key) + @private_key = options[:private_key] + @public_key = options[:public_key] + @encryption_key = OpenSSL::PKey::RSA.new(options[:encryption_key]) if options[:encryption_key] + @shop_process_id = options[:shop_process_id] || SecureRandom.random_number(10**15) + super + end + + def purchase(money, payment, options = {}) + commerce = options[:commerce] || @options[:commerce] + commerce_branch = options[:commerce_branch] || @options[:commerce_branch] + shop_process_id = options[:shop_process_id] || @shop_process_id + + token = generate_token(shop_process_id, 'pay_pci', commerce, commerce_branch, amount(money), currency(money)) + + post = {} + post[:token] = token + post[:commerce] = commerce.to_s + post[:commerce_branch] = commerce_branch.to_s + post[:shop_process_id] = shop_process_id + post[:number_of_payments] = options[:number_of_payments] || 1 + post[:recursive] = options[:recursive] || false + + add_invoice(post, money, options) + add_card_data(post, payment) + add_customer_data(post, options) + + commit(:pay_pci_buy_encrypted, post) + end + + def void(authorization, options = {}) + _, shop_process_id = authorization.to_s.split('#') + token = generate_token(shop_process_id, 'rollback', '0.00') + post = { + token:, + shop_process_id: + } + commit(:pci_buy_rollback, post) + end + + def credit(money, payment, options = {}) + # Not permitted for foreign cards. + commerce = options[:commerce] || @options[:commerce] + commerce_branch = options[:commerce_branch] || @options[:commerce_branch] + + token = generate_token(@shop_process_id, 'refund', commerce, commerce_branch, amount(money), currency(money)) + post = {} + post[:token] = token + post[:commerce] = commerce.to_i + post[:commerce_branch] = commerce_branch.to_i + post[:shop_process_id] = @shop_process_id + add_invoice(post, money, options) + add_card_data(post, payment) + add_customer_data(post, options) + post[:origin_shop_process_id] = options[:original_shop_process_id] if options[:original_shop_process_id] + commit(:refund, post) + end + + def refund(money, authorization, options = {}) + commerce = options[:commerce] || @options[:commerce] + commerce_branch = options[:commerce_branch] || @options[:commerce_branch] + shop_process_id = options[:shop_process_id] || @shop_process_id + _, original_shop_process_id = authorization.to_s.split('#') + + token = generate_token(shop_process_id, 'refund', commerce, commerce_branch, amount(money), currency(money)) + post = {} + post[:token] = token + post[:commerce] = commerce.to_i + post[:commerce_branch] = commerce_branch.to_i + post[:shop_process_id] = shop_process_id + add_invoice(post, money, options) + add_customer_data(post, options) + post[:origin_shop_process_id] = original_shop_process_id || options[:original_shop_process_id] + commit(:refund, post) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + clean_transcript = remove_invalid_utf_8_byte_sequences(transcript) + clean_transcript. + gsub(/(token\\":\\")[.\-\w]+/, '\1[FILTERED]'). + gsub(/(card_encrypted_data\\":\\")[.\-\w]+/, '\1[FILTERED]') + end + + def remove_invalid_utf_8_byte_sequences(transcript) + transcript.encode('UTF-8', 'binary', undef: :replace, replace: '') + end + + # Required to encrypt PAN data. + def one_time_public_key + token = generate_token('get_encription_public_key', @public_key) + response = commit(:pci_encryption_key, token:) + response.params['encryption_key'] + end + + private + + def generate_token(*elements) + Digest::MD5.hexdigest(@private_key + elements.join) + end + + def add_invoice(post, money, options) + post[:amount] = amount(money) + post[:currency] = options[:currency] || currency(money) + end + + def add_card_data(post, payment) + card_number = payment.number + cvv = payment.verification_value + + payload = { card_number:, cvv: }.to_json + + encryption_key = @encryption_key || OpenSSL::PKey::RSA.new(one_time_public_key) + + post[:card_encrypted_data] = JWE.encrypt(payload, encryption_key) + post[:card_month_expiration] = format(payment.month, :two_digits) + post[:card_year_expiration] = format(payment.year, :two_digits) + end + + def add_customer_data(post, options) + post[:additional_data] = options[:additional_data] || '' # must be passed even if empty + end + + def parse(body) + JSON.parse(body) + end + + def commit(action, parameters) + url = build_request_url(action) + begin + response = parse(ssl_post(url, post_data(parameters))) + rescue ResponseError => e + # Errors are returned with helpful data, + # but get filtered out by `ssl_post` because of their HTTP status. + response = parse(e.response.body) + end + + Response.new( + success_from(response), + message_from(response), + response, + authorization: authorization_from(response), + avs_result: nil, + cvv_result: nil, + test: test?, + error_code: error_code_from(response) + ) + end + + def success_from(response) + if code = response.dig('confirmation', 'response_code') + code == '00' + else + response['status'] == 'success' + end + end + + def message_from(response) + %w(confirmation refund).each do |m| + message = + response.dig(m, 'extended_response_description') || + response.dig(m, 'response_description') || + response.dig(m, 'response_details') + return message if message + end + [response.dig('messages', 0, 'key'), response.dig('messages', 0, 'dsc')].join(':') + end + + def authorization_from(response) + response_body = response.dig('confirmation') || response.dig('refund') + return unless response_body + + authorization_number = response_body.dig('authorization_number') || response_body.dig('authorization_code') + shop_process_id = response_body.dig('shop_process_id') + + "#{authorization_number}##{shop_process_id}" + end + + def error_code_from(response) + response.dig('confirmation', 'response_code') unless success_from(response) + end + + def build_request_url(action) + base_url = (test? ? test_url : live_url) + base_url + ENDPOINTS[action] + end + + def post_data(data) + { public_key: @public_key, + operation: data }.compact.to_json + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/webpay.rb b/lib/active_merchant/billing/gateways/webpay.rb index 29636897892..1b9fc12562c 100644 --- a/lib/active_merchant/billing/gateways/webpay.rb +++ b/lib/active_merchant/billing/gateways/webpay.rb @@ -1,14 +1,14 @@ require 'json' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class WebpayGateway < StripeGateway self.live_url = 'https://api.webpay.jp/v1/' self.supported_countries = ['JP'] self.default_currency = 'JPY' self.money_format = :cents - self.supported_cardtypes = [:visa, :master, :american_express, :jcb, :diners_club] + self.supported_cardtypes = %i[visa master american_express jcb diners_club] self.homepage_url = 'https://webpay.jp/' self.display_name = 'WebPay' @@ -51,9 +51,9 @@ def store(creditcard, options = {}) MultiResponse.run(:first) do |r| r.process { commit(:post, "customers/#{CGI.escape(options[:customer])}/", post, options) } - return r unless options[:set_default] and r.success? and !r.params['id'].blank? + return r unless options[:set_default] && r.success? && !r.params['id'].blank? - r.process { update_customer(options[:customer], :default_card => r.params['id']) } + r.process { update_customer(options[:customer], default_card: r.params['id']) } end else commit(:post, 'customers', post, options) @@ -84,12 +84,12 @@ def json_error(raw_response) } end - def headers(options = {}) + def headers(method = :post, options = {}) { 'Authorization' => 'Basic ' + Base64.encode64(@api_key.to_s + ':').strip, 'User-Agent' => "Webpay/v1 ActiveMerchantBindings/#{ActiveMerchant::VERSION}", 'X-Webpay-Client-User-Agent' => user_agent, - 'X-Webpay-Client-User-Metadata' => {:ip => options[:ip]}.to_json + 'X-Webpay-Client-User-Metadata' => { ip: options[:ip] }.to_json } end end diff --git a/lib/active_merchant/billing/gateways/wepay.rb b/lib/active_merchant/billing/gateways/wepay.rb index 4ae60420421..c300ca20d6b 100644 --- a/lib/active_merchant/billing/gateways/wepay.rb +++ b/lib/active_merchant/billing/gateways/wepay.rb @@ -1,11 +1,11 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class WepayGateway < Gateway self.test_url = 'https://stage.wepayapi.com/v2' self.live_url = 'https://wepayapi.com/v2' - self.supported_countries = ['US', 'CA'] - self.supported_cardtypes = [:visa, :master, :american_express, :discover] + self.supported_countries = %w[US CA] + self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'https://www.wepay.com/' self.default_currency = 'USD' self.display_name = 'WePay' @@ -48,9 +48,7 @@ def capture(money, identifier, options = {}) post = {} post[:checkout_id] = checkout_id - if(money && (original_amount != amount(money))) - post[:amount] = amount(money) - end + post[:amount] = amount(money) if money && (original_amount != amount(money)) commit('/checkout/capture', post, options) end @@ -66,9 +64,7 @@ def refund(money, identifier, options = {}) post = {} post[:checkout_id] = checkout_id - if(money && (original_amount != amount(money))) - post[:amount] = amount(money) - end + post[:amount] = amount(money) if money && (original_amount != amount(money)) post[:refund_reason] = (options[:description] || 'Refund') post[:payer_email_message] = options[:payer_email_message] if options[:payer_email_message] post[:payee_email_message] = options[:payee_email_message] if options[:payee_email_message] @@ -85,12 +81,12 @@ def store(creditcard, options = {}) post[:expiration_month] = creditcard.month post[:expiration_year] = creditcard.year - if(billing_address = (options[:billing_address] || options[:address])) + if (billing_address = (options[:billing_address] || options[:address])) post[:address] = {} post[:address]['address1'] = billing_address[:address1] if billing_address[:address1] post[:address]['city'] = billing_address[:city] if billing_address[:city] post[:address]['country'] = billing_address[:country] if billing_address[:country] - post[:address]['region'] = billing_address[:state] if billing_address[:state] + post[:address]['region'] = billing_address[:state] if billing_address[:state] post[:address]['postal_code'] = billing_address[:zip] end @@ -172,13 +168,15 @@ def parse(response) JSON.parse(response) end - def commit(action, params, options={}) + def commit(action, params, options = {}) begin - response = parse(ssl_post( - ((test? ? test_url : live_url) + action), - params.to_json, - headers(options) - )) + response = parse( + ssl_post( + ((test? ? test_url : live_url) + action), + params.to_json, + headers(options) + ) + ) rescue ResponseError => e response = parse(e.response.body) end @@ -190,7 +188,6 @@ def commit(action, params, options={}) authorization: authorization_from(response, params), test: test? ) - rescue JSON::ParserError return unparsable_response(response) end diff --git a/lib/active_merchant/billing/gateways/wirecard.rb b/lib/active_merchant/billing/gateways/wirecard.rb index 321cd379565..2c55f496737 100644 --- a/lib/active_merchant/billing/gateways/wirecard.rb +++ b/lib/active_merchant/billing/gateways/wirecard.rb @@ -1,7 +1,7 @@ require 'base64' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class WirecardGateway < Gateway self.test_url = 'https://c3-test.wirecard.com/secure/ssl-gateway' self.live_url = 'https://c3.wirecard.com/secure/ssl-gateway' @@ -13,9 +13,9 @@ class WirecardGateway < Gateway 'xsi:noNamespaceSchemaLocation' => 'wirecard.xsd' } - PERMITTED_TRANSACTIONS = %w[ PREAUTHORIZATION CAPTURE PURCHASE ] + PERMITTED_TRANSACTIONS = %w[PREAUTHORIZATION CAPTURE PURCHASE] - RETURN_CODES = %w[ ACK NOK ] + RETURN_CODES = %w[ACK NOK] # Wirecard only allows phone numbers with a format like this: +xxx(yyy)zzz-zzzz-ppp, where: # xxx = Country code @@ -26,7 +26,7 @@ class WirecardGateway < Gateway # number 5551234 within area code 202 (country code 1). VALID_PHONE_FORMAT = /\+\d{1,3}(\(?\d{3}\)?)?\d{3}-\d{4}-\d{3}/ - self.supported_cardtypes = [ :visa, :master, :american_express, :diners_club, :jcb, :switch ] + self.supported_cardtypes = %i[visa master american_express diners_club jcb] self.supported_countries = %w(AD CY GI IM MT RO CH AT DK GR IT MC SM TR BE EE HU LV NL SK GB BG FI IS LI NO SI VA FR IL LT PL ES CZ DE IE LU PT SE) self.homepage_url = 'http://www.wirecard.com' self.display_name = 'Wirecard' @@ -138,8 +138,9 @@ def scrub(transcript) end private + def clean_description(description) - description.to_s.slice(0,32).encode('US-ASCII', invalid: :replace, undef: :replace, replace: '?') + description.to_s.slice(0, 32).encode('US-ASCII', invalid: :replace, undef: :replace, replace: '?') end def prepare_options_hash(options) @@ -178,11 +179,14 @@ def commit(action, money, options) message = response[:Message] authorization = response[:GuWID] - Response.new(success, message, response, - :test => test?, - :authorization => authorization, - :avs_result => { :code => avs_code(response, options) }, - :cvv_result => response[:CVCResponseCode] + Response.new( + success, + message, + response, + test: test?, + authorization:, + avs_result: { code: avs_code(response, options) }, + cvv_result: response[:CVCResponseCode] ) rescue ResponseError => e if e.response.code == '401' @@ -196,16 +200,16 @@ def commit(action, money, options) def build_request(action, money, options) options = prepare_options_hash(options) options[:action] = action - xml = Builder::XmlMarkup.new :indent => 2 + xml = Builder::XmlMarkup.new indent: 2 xml.instruct! xml.tag! 'WIRECARD_BXML' do xml.tag! 'W_REQUEST' do xml.tag! 'W_JOB' do - xml.tag! 'JobID', '' - # UserID for this transaction - xml.tag! 'BusinessCaseSignature', options[:signature] || options[:login] - # Create the whole rest of the message - add_transaction_data(xml, money, options) + xml.tag! 'JobID', '' + # UserID for this transaction + xml.tag! 'BusinessCaseSignature', options[:signature] || options[:login] + # Create the whole rest of the message + add_transaction_data(xml, money, options) end end end @@ -262,6 +266,7 @@ def add_amount(xml, money, options) # Includes the credit-card data to the transaction-xml def add_creditcard(xml, creditcard) raise 'Creditcard must be supplied!' if creditcard.nil? + xml.tag! 'CREDIT_CARD_DATA' do xml.tag! 'CreditCardNumber', creditcard.number xml.tag! 'CVC2', creditcard.verification_value @@ -274,6 +279,7 @@ def add_creditcard(xml, creditcard) # Includes the IP address of the customer to the transaction-xml def add_customer_data(xml, options) return unless options[:ip] + xml.tag! 'CONTACT_DATA' do xml.tag! 'IPAddress', options[:ip] end @@ -282,6 +288,7 @@ def add_customer_data(xml, options) # Includes the address to the transaction-xml def add_address(xml, address) return if address.nil? + xml.tag! 'CORPTRUSTCENTER_DATA' do xml.tag! 'ADDRESS' do xml.tag! 'Address1', address[:address1] @@ -289,9 +296,7 @@ def add_address(xml, address) xml.tag! 'City', address[:city] xml.tag! 'ZipCode', address[:zip] - if address[:state] =~ /[A-Za-z]{2}/ && address[:country] =~ /^(us|ca)$/i - xml.tag! 'State', address[:state].upcase - end + xml.tag! 'State', address[:state].upcase if address[:state] =~ /[A-Za-z]{2}/ && address[:country] =~ /^(us|ca)$/i xml.tag! 'Country', address[:country] xml.tag! 'Phone', address[:phone] if address[:phone] =~ VALID_PHONE_FORMAT @@ -330,9 +335,7 @@ def parse_response(response, root) status = nil root.elements.to_a.each do |node| - if node.name =~ /FNC_CC_/ - status = REXML::XPath.first(node, 'CC_TRANSACTION/PROCESSING_STATUS') - end + status = REXML::XPath.first(node, 'CC_TRANSACTION/PROCESSING_STATUS') if node.name =~ /FNC_CC_/ end message = '' @@ -342,7 +345,7 @@ def parse_response(response, root) end status.elements.to_a.each do |node| - if (node.elements.size == 0) + if node.elements.size == 0 response[node.name.to_sym] = (node.text || '').strip else node.elements.each do |childnode| @@ -391,7 +394,7 @@ def errors_to_string(root) string << error[:Message] if error[:Message] error[:Advice].each_with_index do |advice, index| string << ' (' if index == 0 - string << "#{index+1}. #{advice}" + string << "#{index + 1}. #{advice}" string << ' and ' if index < error[:Advice].size - 1 string << ')' if index == error[:Advice].size - 1 end @@ -406,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 new file mode 100644 index 00000000000..1ac55b63253 --- /dev/null +++ b/lib/active_merchant/billing/gateways/wompi.rb @@ -0,0 +1,202 @@ +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class WompiGateway < Gateway + self.test_url = 'https://sync.sandbox.wompi.co/v1' + self.live_url = 'https://sync.production.wompi.co/v1' + + self.supported_countries = ['CO'] + self.default_currency = 'COP' + self.supported_cardtypes = %i[visa master american_express] + + self.homepage_url = 'https://wompi.co/' + self.display_name = 'Wompi' + + self.money_format = :cents + + def initialize(options = {}) + ## Sandbox keys have prefix pub_test_ and prv_test_ + ## Production keys have prefix pub_prod_ and prv_prod_ + begin + requires!(options, :prod_private_key, :prod_public_key) + rescue ArgumentError + begin + requires!(options, :test_private_key, :test_public_key) + rescue ArgumentError + raise ArgumentError, 'Gateway requires both test_private_key and test_public_key, or both prod_private_key and prod_public_key' + end + end + super + end + + def purchase(money, payment, options = {}) + post = { + reference: options[:reference] || generate_reference, + public_key: + } + add_invoice(post, money, options) + add_tip_in_cents(post, options) + add_card(post, payment, options) + + commit('sale', post, '/transactions_sync') + end + + def authorize(money, payment, options = {}) + post = { + public_key:, + type: 'CARD', + financial_operation: 'PREAUTHORIZATION' + } + add_auth_params(post, money, payment, options) + + commit('authorize', post, '/payment_sources_sync') + end + + def capture(money, authorization, options = {}) + post = { + reference: options[:reference] || generate_reference, + public_key:, + payment_source_id: authorization.to_i + } + add_invoice(post, money, options) + commit('capture', post, '/transactions_sync') + end + + def refund(money, authorization, options = {}) + # post = { amount_in_cents: amount(money).to_i, transaction_id: authorization.to_s } + # commit('refund', post, '/refunds_sync') + + # All refunds will instead be voided. This is temporary. + void(authorization, options, money) + end + + def void(authorization, options = {}, money = nil) + post = money ? { amount_in_cents: amount(money).to_i } : {} + commit('void', post, "/transactions/#{authorization}/void_sync") + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript.gsub(/(Bearer )\w+/, '\1[REDACTED]'). + gsub(/(\\\"number\\\":\\\")\d+/, '\1[REDACTED]'). + gsub(/(\\\"cvc\\\":\\\")\d+/, '\1[REDACTED]'). + gsub(/(\\\"phone_number\\\":\\\")\+?\d+/, '\1[REDACTED]'). + gsub(/(\\\"email\\\":\\\")\S+\\\",/, '\1[REDACTED]\",'). + gsub(/(\\\"legal_id\\\":\\\")\d+/, '\1[REDACTED]') + end + + private + + def headers + { + 'Authorization' => "Bearer #{private_key}", + 'Content-Type' => 'application/json' + } + end + + def generate_reference + SecureRandom.alphanumeric(12) + end + + def private_key + test? ? options[:test_private_key] : options[:prod_private_key] + end + + def public_key + test? ? options[:test_public_key] : options[:prod_public_key] + end + + def add_invoice(post, money, options) + post[:amount_in_cents] = amount(money).to_i + post[:currency] = (options[:currency] || currency(money)) + end + + def add_card(post, card, options) + payment_method = { + type: 'CARD' + } + add_basic_card_info(payment_method, card, options) + post[:payment_method] = payment_method + end + + def add_auth_params(post, money, card, options) + data = { + amount_in_cents: amount(money).to_i, + currency: (options[:currency] || currency(money)) + } + add_basic_card_info(data, card, options) + post[:data] = data + end + + def add_basic_card_info(post, card, options) + installments = options[:installments] ? options[:installments].to_i : 1 + cvc = card.verification_value || nil + + post[:number] = card.number + post[:exp_month] = card.month.to_s.rjust(2, '0') + post[:exp_year] = card.year.to_s[2..3] + post[:installments] = installments + post[:card_holder] = card.name + 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 + + def commit(action, parameters, endpoint) + url = (test? ? test_url : live_url) + endpoint + response = parse(ssl_post(url, post_data(action, parameters), headers)) + Response.new( + success_from(response), + message_from(response), + response, + authorization: authorization_from(response), + avs_result: nil, + cvv_result: nil, + test: test?, + error_code: error_code_from(response) + ) + end + + def handle_response(response) + case response.code.to_i + when 200...300, 401, 404, 422 + response.body + else + raise ResponseError.new(response) + end + end + + def success_from(response) + success_statuses.include? response.dig('data', 'status') + end + + def success_statuses + %w(APPROVED AVAILABLE) + end + + def message_from(response) + response.dig('data', 'status_message') || response.dig('error', 'reason') || response.dig('error', 'messages').to_json + end + + def authorization_from(response) + response.dig('data', 'transaction_id') || response.dig('data', 'id') || response.dig('data', 'transaction', 'id') + end + + def post_data(action, parameters = {}) + parameters.to_json + end + + def error_code_from(response) + response.dig('error', 'type') unless success_from(response) + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/world_net.rb b/lib/active_merchant/billing/gateways/world_net.rb index fc7eb38f9fc..ff15798459b 100644 --- a/lib/active_merchant/billing/gateways/world_net.rb +++ b/lib/active_merchant/billing/gateways/world_net.rb @@ -1,7 +1,7 @@ require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # See https://helpdesk.worldnettps.com/support/solutions/articles/1000167298-integrator-guide class WorldNetGateway < Gateway self.test_url = 'https://testpayments.worldnettps.com/merchant/xmlpayment' @@ -113,9 +113,9 @@ def supports_scrubbing? end def scrub(transcript) - transcript - .gsub(%r{(\d{6})\d+(\d{4})}, '\1...\2') - .gsub(%r{()\d+(\d{6})\d+(\d{4})}, '\1...\2'). + gsub(%r{()\d+( 'VISA-SSL', - 'master' => 'ECMC-SSL', - 'discover' => 'DISCOVER-SSL', - 'american_express' => 'AMEX-SSL', - 'jcb' => 'JCB-SSL', - 'maestro' => 'MAESTRO-SSL', - 'laser' => 'LASER-SSL', - 'diners_club' => 'DINERS-SSL', - 'switch' => 'MAESTRO-SSL' + NETWORK_TOKEN_TYPE = { + apple_pay: 'APPLEPAY', + google_pay: 'GOOGLEPAY', + network_token: 'NETWORKTOKEN' + } + + AVS_CODE_MAP = { + 'A' => 'M', # Match + 'B' => 'P', # Postcode matches, address not verified + 'C' => 'Z', # Postcode matches, address does not match + 'D' => 'B', # Address matched; postcode not checked + 'E' => 'I', # Address and postal code not checked + 'F' => 'A', # Address matches, postcode does not match + '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 + } + + CVC_CODE_MAP = { + 'A' => 'M', # CVV matches + 'B' => 'P', # Not provided + 'C' => 'P', # Not checked + 'D' => 'N' # Does not match } def initialize(options = {}) @@ -32,44 +55,56 @@ def initialize(options = {}) def purchase(money, payment_method, options = {}) MultiResponse.run do |r| - r.process{authorize(money, payment_method, options)} - r.process{capture(money, r.authorization, options.merge(:authorization_validated => true))} + r.process { authorize(money, payment_method, options) } + r.process { capture(money, r.authorization, options.merge(authorization_validated: true)) } unless options[:skip_capture] end end def authorize(money, payment_method, options = {}) requires!(options, :order_id) - authorize_request(money, payment_method, options) + payment_details = payment_details(payment_method, options) + if options[:account_funding_transaction] + aft_request(money, payment_method, payment_details.merge(**options)) + else + authorize_request(money, payment_method, payment_details.merge(options)) + end end def capture(money, authorization, options = {}) + authorization = order_id_from_authorization(authorization.to_s) MultiResponse.run do |r| - r.process{inquire_request(authorization, options, 'AUTHORISED')} unless options[:authorization_validated] + r.process { inquire_request(authorization, options, 'AUTHORISED', 'CAPTURED') } unless options[:authorization_validated] if r.params authorization_currency = r.params['amount_currency_code'] - options = options.merge(:currency => authorization_currency) if authorization_currency.present? + options = options.merge(currency: authorization_currency) if authorization_currency.present? end - r.process{capture_request(money, authorization, options)} + r.process { capture_request(money, authorization, options) } end end def void(authorization, options = {}) + authorization = order_id_from_authorization(authorization.to_s) MultiResponse.run do |r| - r.process{inquire_request(authorization, options, 'AUTHORISED')} unless options[:authorization_validated] - r.process{cancel_request(authorization, options)} + r.process { inquire_request(authorization, options, 'AUTHORISED') } unless options[:authorization_validated] + r.process { cancel_request(authorization, options) } end end def refund(money, authorization, options = {}) + authorization = order_id_from_authorization(authorization.to_s) + success_criteria = %w(CAPTURED SETTLED SETTLED_BY_MERCHANT SENT_FOR_REFUND) + success_criteria.push('AUTHORIZED') if options[:cancel_or_refund] response = MultiResponse.run do |r| - r.process { inquire_request(authorization, options, 'CAPTURED', 'SETTLED', 'SETTLED_BY_MERCHANT') } + r.process { inquire_request(authorization, options, *success_criteria) } unless options[:authorization_validated] r.process { refund_request(money, authorization, options) } end - return response if response.success? - return response unless options[:force_full_refund_if_unsettled] - - void(authorization, options ) if response.params['last_event'] == 'AUTHORISED' + if !response.success? && options[:force_full_refund_if_unsettled] && + response.params['last_event'] == 'AUTHORISED' + void(authorization, options) + else + response + end end # Credits only function on a Merchant ID/login/profile flagged for Payouts @@ -77,35 +112,68 @@ def refund(money, authorization, options = {}) # and other transactions should be performed on a normal eCom-flagged # merchant ID. def credit(money, payment_method, options = {}) - credit_request(money, payment_method, options.merge(:credit => true)) + 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)) + elsif options[:account_funding_transaction] + aft_request(money, payment_method, payment_details.merge(**options)) + else + credit_request(money, payment_method, payment_details.merge(credit: true, **options)) + end end - def verify(credit_card, options={}) + def verify(payment_method, options = {}) + amount = (eligible_for_0_auth?(payment_method, options) ? 0 : 100) MultiResponse.run(:use_first_response) do |r| - r.process { authorize(100, credit_card, options) } - r.process(:ignore_result) { void(r.authorization, options.merge(:authorization_validated => true)) } + r.process { authorize(amount, payment_method, options) } + r.process(:ignore_result) { void(r.authorization, options.merge(authorization_validated: true)) } end end + def store(credit_card, options = {}) + requires!(options, :customer) + store_request(credit_card, options) + end + + def inquire(authorization, options = {}) + order_id = order_id_from_authorization(authorization.to_s) || options[:order_id] + commit('direct_inquiry', build_order_inquiry_request(order_id, options), :ok, options) + end + def supports_scrubbing true end + def supports_network_tokenization? + true + end + def scrub(transcript) transcript. gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). gsub(%r(()\d+()), '\1[FILTERED]\2'). - gsub(%r(()[^<]+()), '\1[FILTERED]\2') + gsub(%r(()[^<]+()), '\1[FILTERED]\2'). + gsub(%r(()\d+()), '\1[FILTERED]\2'). + gsub(%r(()[^<]+()), '\1[FILTERED]\2'). + gsub(%r(()\d+(<\/accountReference>)), '\1[FILTERED]\2') end 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', options) + commit('authorize', build_authorization_request(money, payment_method, options), 'AUTHORISED', 'CAPTURED', options) end def capture_request(money, authorization, options) - commit('capture', build_capture_request(money, authorization, options), :ok, options) + commit('capture', build_capture_request(money, authorization, options), 'CAPTURED', :ok, options) end def cancel_request(authorization, options) @@ -117,18 +185,30 @@ def inquire_request(authorization, options, *success_criteria) end def refund_request(money, authorization, options) - commit('refund', build_refund_request(money, authorization, options), :ok, options) + commit('refund', build_refund_request(money, authorization, options), :ok, 'SENT_FOR_REFUND', options) end def credit_request(money, payment_method, options) - commit('credit', build_authorization_request(money, payment_method, options), :ok, options) + commit('credit', build_authorization_request(money, payment_method, options), :ok, 'SENT_FOR_REFUND', options) + end + + def fast_fund_credit_request(money, payment_method, options) + commit('fast_credit', build_fast_fund_credit_request(money, payment_method, options), :ok, 'PUSH_APPROVED', options) + end + + def aft_request(money, payment_method, options) + commit('funding_transfer_transaction', build_aft_request(money, payment_method, options), :ok, 'AUTHORISED', options) + end + + def store_request(credit_card, options) + commit('store', build_store_request(credit_card, options), options) end def build_request - xml = Builder::XmlMarkup.new :indent => 2 - xml.instruct! :xml, :encoding => 'UTF-8' + xml = Builder::XmlMarkup.new indent: 2 + xml.instruct! :xml, encoding: 'UTF-8' xml.declare! :DOCTYPE, :paymentService, :PUBLIC, '-//WorldPay//DTD WorldPay PaymentService v1//EN', 'http://dtd.worldpay.com/paymentService_v1.dtd' - xml.tag! 'paymentService', 'version' => '1.4', 'merchantCode' => @options[:login] do + xml.paymentService 'version' => '1.4', 'merchantCode' => @options[:login] do yield xml end xml.target! @@ -136,8 +216,8 @@ def build_request def build_order_modify_request(authorization) build_request do |xml| - xml.tag! 'modify' do - xml.tag! 'orderModification', 'orderCode' => authorization do + xml.modify do + xml.orderModification 'orderCode' => authorization do yield xml end end @@ -146,152 +226,682 @@ def build_order_modify_request(authorization) def build_order_inquiry_request(authorization, options) build_request do |xml| - xml.tag! 'inquiry' do - xml.tag! 'orderInquiry', 'orderCode' => authorization + xml.inquiry do + xml.orderInquiry 'orderCode' => authorization end end end def build_authorization_request(money, payment_method, options) build_request do |xml| - xml.tag! 'submit' do - xml.tag! 'order', order_tag_attributes(options) do + xml.submit do + xml.order order_tag_attributes(options) do xml.description(options[:description].blank? ? 'Purchase' : options[:description]) add_amount(xml, money, options) - if options[:order_content] - xml.tag! 'orderContent' do - xml.cdata! options[:order_content] - end - end + add_order_content(xml, options) add_payment_method(xml, money, payment_method, options) - add_email(xml, options) - if options[:hcg_additional_data] - add_hcg_additional_data(xml, options) - end + add_shopper(xml, options) + add_fraud_sight_data(xml, options) + add_statement_narrative(xml, options) + add_risk_data(xml, options[:risk_data]) if options[:risk_data] + add_sub_merchant_data(xml, options[:sub_merchant_data]) if options[:sub_merchant_data] + add_hcg_additional_data(xml, options) if options[:hcg_additional_data] + add_instalments_data(xml, options) if options[:instalments] + add_additional_data(xml, money, options) if options[:level_2_data] || options[:level_3_data] + add_moto_flag(xml, options) if options.dig(:metadata, :manual_entry) + add_additional_3ds_data(xml, options) if options[:execute_threed] && options[:three_ds_version] && options[:three_ds_version] =~ /^2/ + add_3ds_exemption(xml, options) if options[:exemption_type] end end end end + def add_additional_data(xml, amount, options) + level_two_data = options[:level_2_data] || {} + level_three_data = options[:level_3_data] || {} + level_two_and_three_data = level_two_data.merge(level_three_data).symbolize_keys + + xml.branchSpecificExtension do + xml.purchase do + add_level_two_and_three_data(xml, amount, level_two_and_three_data) + end + end + end + + def add_level_two_and_three_data(xml, amount, data) + xml.invoiceReferenceNumber data[:invoice_reference_number] if data.include?(:invoice_reference_number) + xml.customerReference data[:customer_reference] if data.include?(:customer_reference) + xml.cardAcceptorTaxId data[:card_acceptor_tax_id] if data.include?(:card_acceptor_tax_id) + { + tax_amount: 'salesTax', + discount_amount: 'discountAmount', + shipping_amount: 'shippingAmount', + duty_amount: 'dutyAmount' + }.each do |key, tag| + next unless data.include?(key) + + xml.tag! tag do + add_amount(xml, data[key].to_i, data) + end + end + + add_optional_data_level_two_and_three(xml, data) + + 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_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] + 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 + + def add_optional_data_level_two_and_three(xml, data) + xml.shipFromPostalCode data[:ship_from_postal_code] if data.include?(:ship_from_postal_code) + xml.destinationPostalCode data[:destination_postal_code] if data.include?(:destination_postal_code) + xml.destinationCountryCode data[:destination_country_code] if data.include?(:destination_country_code) + add_date_element(xml, 'orderDate', data[:order_date].symbolize_keys) if data.include?(:order_date) + xml.taxExempt data[:tax_amount].to_i > 0 ? 'false' : 'true' + end + def order_tag_attributes(options) - { 'orderCode' => options[:order_id], 'installationId' => options[:inst_id] || @options[:inst_id] }.reject{|_,v| !v} + { 'orderCode' => clean_order_id(options[:order_id]), 'installationId' => options[:inst_id] || @options[:inst_id] }.reject { |_, v| !v.present? } + end + + def clean_order_id(order_id) + order_id.to_s.gsub(/(\s|\||<|>|'|")/, '')[0..64] + end + + def add_order_content(xml, options) + return unless options[:order_content] + + xml.orderContent do + xml.cdata! options[:order_content] + end end def build_capture_request(money, authorization, options) build_order_modify_request(authorization) do |xml| - xml.tag! 'capture' do + xml.capture do time = Time.now - xml.tag! 'date', 'dayOfMonth' => time.day, 'month' => time.month, 'year'=> time.year + xml.date 'dayOfMonth' => time.day, 'month' => time.month, 'year' => time.year add_amount(xml, money, options) end end end def build_void_request(authorization, options) - build_order_modify_request(authorization) do |xml| - xml.tag! 'cancel' + if options[:cancel_or_refund] + build_order_modify_request(authorization, &:cancelOrRefund) + else + build_order_modify_request(authorization, &:cancel) end end def build_refund_request(money, authorization, options) build_order_modify_request(authorization) do |xml| - xml.tag! 'refund' do - add_amount(xml, money, options.merge(:debit_credit_indicator => 'credit')) + if options[:cancel_or_refund] + # Worldpay docs claim amount must be passed. This causes an error. + xml.cancelOrRefund # { add_amount(xml, money, options.merge(debit_credit_indicator: 'credit')) } + else + xml.refund do + add_amount(xml, money, options.merge(debit_credit_indicator: 'credit')) + end + end + end + end + + def build_store_request(credit_card, options) + build_request do |xml| + xml.submit do + xml.paymentTokenCreate do + add_authenticated_shopper_id(xml, options) + xml.createToken + xml.paymentInstrument do + xml.cardDetails do + add_card(xml, credit_card, options) + end + end + add_transaction_identifier(xml, options) if network_transaction_id(options) + end + end + end + end + + def network_transaction_id(options) + options[:stored_credential_transaction_id] || options.dig(:stored_credential, :network_transaction_id) + end + + def add_transaction_identifier(xml, options) + xml.storedCredentials 'usage' => 'FIRST' do + xml.schemeTransactionIdentifier network_transaction_id(options) + end + end + + def build_fast_fund_credit_request(money, payment_method, options) + build_request do |xml| + xml.submit do + xml.order order_tag_attributes(options) do + xml.description(options[:description].blank? ? 'Fast Fund Credit' : options[:description]) + add_amount(xml, money, options) + add_order_content(xml, options) + add_payment_details_for_ff_credit(xml, payment_method, options) + + if options[:email] + xml.shopper do + xml.shopperEmailAddress options[:email] + end + end + end + end + end + end + + def build_aft_request(money, payment_method, options) + build_request do |xml| + xml.submit do + xml.order order_tag_attributes(options) do + xml.description(options[:description].blank? ? 'Account Funding Transaction' : options[:description]) + add_amount(xml, money, options) + add_order_content(xml, options) + add_payment_method(xml, money, payment_method, options) + add_shopper(xml, options) + add_sub_merchant_data(xml, options[:sub_merchant_data]) if options[:sub_merchant_data] + add_aft_data(xml, payment_method, options) + end + end + end + end + + def add_aft_data(xml, payment_method, options) + xml.fundingTransfer 'type' => options[:aft_type], 'category' => 'PULL_FROM_CARD' do + xml.paymentPurpose options[:aft_payment_purpose] # Must be included for the recipient for following countries, otherwise optional: Argentina, Bangladesh, Chile, Columbia, Jordan, Mexico, Thailand, UAE, India cross-border + xml.fundingParty 'type' => 'sender' do + xml.accountReference options[:aft_sender_account_reference], 'accountType' => options[:aft_sender_account_type] + xml.fullName do + xml.first options.dig(:aft_sender_full_name, :first) + xml.middle options.dig(:aft_sender_full_name, :middle) if options.dig(:aft_sender_full_name, :middle) + xml.last options.dig(:aft_sender_full_name, :last) + end + xml.fundingAddress do + xml.address1 options.dig(:aft_sender_funding_address, :address1) + xml.address2 options.dig(:aft_sender_funding_address, :address2) if options.dig(:aft_sender_funding_address, :address2) + xml.postalCode options.dig(:aft_sender_funding_address, :postal_code) + xml.city options.dig(:aft_sender_funding_address, :city) + xml.state options.dig(:aft_sender_funding_address, :state) + xml.countryCode options.dig(:aft_sender_funding_address, :country_code) + end + end + xml.fundingParty 'type' => 'recipient' do + xml.accountReference options[:aft_recipient_account_reference], 'accountType' => options[:aft_recipient_account_type] + xml.fullName do + xml.first options.dig(:aft_recipient_full_name, :first) + xml.middle options.dig(:aft_recipient_full_name, :middle) if options.dig(:aft_recipient_full_name, :middle) + xml.last options.dig(:aft_recipient_full_name, :last) + end + xml.fundingAddress do + xml.address1 options.dig(:aft_recipient_funding_address, :address1) + xml.address2 options.dig(:aft_recipient_funding_address, :address2) if options.dig(:aft_recipient_funding_address, :address2) + xml.postalCode options.dig(:aft_recipient_funding_address, :postal_code) + xml.city options.dig(:aft_recipient_funding_address, :city) + xml.state options.dig(:aft_recipient_funding_address, :state) + xml.countryCode options.dig(:aft_recipient_funding_address, :country_code) + end + if options[:aft_recipient_funding_data] + xml.fundingData do + add_date_element(xml, 'birthDate', options[:aft_recipient_funding_data][:birth_date]) + xml.telephoneNumber options.dig(:aft_recipient_funding_data, :telephone_number) + end + end + end + end + end + + def add_payment_details_for_ff_credit(xml, payment_method, options) + xml.paymentDetails do + xml.tag! 'FF_DISBURSE-SSL' do + if payment_method.is_a?(CreditCard) + add_card_for_ff_credit(xml, payment_method, options) + else + add_token_for_ff_credit(xml, payment_method, options) + end + end + add_shopper_id(xml, options) + end + end + + def add_card_for_ff_credit(xml, payment_method, options) + xml.recipient do + xml.paymentInstrument do + xml.cardDetails do + add_card(xml, payment_method, options) + end + end + end + end + + def add_token_for_ff_credit(xml, payment_method, options) + return unless payment_method.is_a?(String) + + token_details = token_details_from_authorization(payment_method) + + xml.tag! 'recipient', 'tokenScope' => token_details[:token_scope] do + xml.paymentTokenID token_details[:token_id] + add_authenticated_shopper_id(xml, token_details) + end + end + + def add_additional_3ds_data(xml, options) + additional_data = { 'dfReferenceId' => options[:df_reference_id] } + additional_data['challengeWindowSize'] = options[:browser_size] if options[:browser_size] + + xml.additional3DSData additional_data + end + + def add_3ds_exemption(xml, options) + xml.exemption 'type' => options[:exemption_type], 'placement' => options[:exemption_placement] || 'AUTHORISATION' + end + + def add_risk_data(xml, risk_data) + xml.riskData do + add_authentication_risk_data(xml, risk_data[:authentication_risk_data]) + add_shopper_account_risk_data(xml, risk_data[:shopper_account_risk_data]) + add_transaction_risk_data(xml, risk_data[:transaction_risk_data]) + end + end + + def add_authentication_risk_data(xml, authentication_risk_data) + return unless authentication_risk_data + + timestamp = authentication_risk_data.fetch(:authentication_date, {}) + + xml.authenticationRiskData('authenticationMethod' => authentication_risk_data[:authentication_method]) do + xml.authenticationTimestamp do + xml.date( + 'dayOfMonth' => timestamp[:day_of_month], + 'month' => timestamp[:month], + 'year' => timestamp[:year], + 'hour' => timestamp[:hour], + 'minute' => timestamp[:minute], + 'second' => timestamp[:second] + ) + end + end + end + + def add_sub_merchant_data(xml, options) + xml.subMerchantData do + xml.pfId options[:pf_id] if options[:pf_id] + xml.subName options[:sub_name] if options[:sub_name] + xml.subId options[:sub_id] if options[:sub_id] + xml.subStreet options[:sub_street] if options[:sub_street] + xml.subCity options[:sub_city] if options[:sub_city] + xml.subState options[:sub_state] if options[:sub_state] + xml.subCountryCode options[:sub_country_code] if options[:sub_country_code] + xml.subPostalCode options[:sub_postal_code] if options[:sub_postal_code] + xml.subTaxId options[:sub_tax_id] if options[:sub_tax_id] + end + end + + def add_shopper_account_risk_data(xml, shopper_account_risk_data) + return unless shopper_account_risk_data + + data = { + 'transactionsAttemptedLastDay' => shopper_account_risk_data[:transactions_attempted_last_day], + 'transactionsAttemptedLastYear' => shopper_account_risk_data[:transactions_attempted_last_year], + 'purchasesCompletedLastSixMonths' => shopper_account_risk_data[:purchases_completed_last_six_months], + 'addCardAttemptsLastDay' => shopper_account_risk_data[:add_card_attempts_last_day], + 'previousSuspiciousActivity' => shopper_account_risk_data[:previous_suspicious_activity], + 'shippingNameMatchesAccountName' => shopper_account_risk_data[:shipping_name_matches_account_name], + 'shopperAccountAgeIndicator' => shopper_account_risk_data[:shopper_account_age_indicator], + 'shopperAccountChangeIndicator' => shopper_account_risk_data[:shopper_account_change_indicator], + 'shopperAccountPasswordChangeIndicator' => shopper_account_risk_data[:shopper_account_password_change_indicator], + 'shopperAccountShippingAddressUsageIndicator' => shopper_account_risk_data[:shopper_account_shipping_address_usage_indicator], + 'shopperAccountPaymentAccountIndicator' => shopper_account_risk_data[:shopper_account_payment_account_indicator] + }.reject { |_k, v| v.nil? } + + xml.shopperAccountRiskData(data) do + add_date_element(xml, 'shopperAccountCreationDate', shopper_account_risk_data[:shopper_account_creation_date]) + add_date_element(xml, 'shopperAccountModificationDate', shopper_account_risk_data[:shopper_account_modification_date]) + add_date_element(xml, 'shopperAccountPasswordChangeDate', shopper_account_risk_data[:shopper_account_password_change_date]) + add_date_element(xml, 'shopperAccountShippingAddressFirstUseDate', shopper_account_risk_data[:shopper_account_shipping_address_first_use_date]) + add_date_element(xml, 'shopperAccountPaymentAccountFirstUseDate', shopper_account_risk_data[:shopper_account_payment_account_first_use_date]) + end + end + + def add_transaction_risk_data(xml, transaction_risk_data) + return unless transaction_risk_data + + data = { + 'shippingMethod' => transaction_risk_data[:shipping_method], + 'deliveryTimeframe' => transaction_risk_data[:delivery_timeframe], + 'deliveryEmailAddress' => transaction_risk_data[:delivery_email_address], + 'reorderingPreviousPurchases' => transaction_risk_data[:reordering_previous_purchases], + 'preOrderPurchase' => transaction_risk_data[:pre_order_purchase], + 'giftCardCount' => transaction_risk_data[:gift_card_count] + }.reject { |_k, v| v.nil? } + + xml.transactionRiskData(data) do + xml.transactionRiskDataGiftCardAmount do + amount_hash = { + 'value' => transaction_risk_data.dig(:transaction_risk_data_gift_card_amount, :value), + 'currencyCode' => transaction_risk_data.dig(:transaction_risk_data_gift_card_amount, :currency), + 'exponent' => transaction_risk_data.dig(:transaction_risk_data_gift_card_amount, :exponent) + } + debit_credit_indicator = transaction_risk_data.dig(:transaction_risk_data_gift_card_amount, :debit_credit_indicator) + amount_hash['debitCreditIndicator'] = debit_credit_indicator if debit_credit_indicator + xml.amount(amount_hash) end + add_date_element(xml, 'transactionRiskDataPreOrderDate', transaction_risk_data[:transaction_risk_data_pre_order_date]) + end + end + + def add_date_element(xml, name, date) + xml.tag! name do + xml.date('dayOfMonth' => date[:day_of_month], 'month' => date[:month], 'year' => date[:year]) end 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) } - if options[:debit_credit_indicator] - amount_hash.merge!('debitCreditIndicator' => options[:debit_credit_indicator]) - end + amount_hash['debitCreditIndicator'] = options[:debit_credit_indicator] if options[:debit_credit_indicator] - xml.tag! 'amount', amount_hash + xml.amount amount_hash end def add_payment_method(xml, amount, payment_method, options) - if payment_method.is_a?(String) - if options[:merchant_code] - xml.tag! 'payAsOrder', 'orderCode' => payment_method, 'merchantCode' => options[:merchant_code] do - add_amount(xml, amount, options) - end - else - xml.tag! 'payAsOrder', 'orderCode' => payment_method do - add_amount(xml, amount, options) - end + case options[:payment_type] + when :pay_as_order + add_amount_for_pay_as_order(xml, amount, payment_method, options) + when :encrypted_wallet + add_encrypted_wallet(xml, payment_method) + when :network_token + add_network_tokenization_card(xml, payment_method, options) + else + add_card_or_token(xml, payment_method, options) + end + end + + def add_amount_for_pay_as_order(xml, amount, payment_method, options) + if options[:merchant_code] + xml.payAsOrder 'orderCode' => payment_method, 'merchantCode' => options[:merchant_code] do + add_amount(xml, amount, options) end else - xml.tag! 'paymentDetails', credit_fund_transfer_attribute(options) do - xml.tag! CARD_CODES[card_brand(payment_method)] do - xml.tag! 'cardNumber', payment_method.number - xml.tag! 'expiryDate' do - xml.tag! 'date', 'month' => format(payment_method.month, :two_digits), 'year' => format(payment_method.year, :four_digits) - end + xml.payAsOrder 'orderCode' => payment_method do + add_amount(xml, amount, options) + end + end + end - xml.tag! 'cardHolderName', payment_method.name - xml.tag! 'cvc', payment_method.verification_value + 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') - add_address(xml, (options[:billing_address] || options[:address])) + xml.paymentDetails do + xml.tag! 'EMVCO_TOKEN-SSL', 'type' => token_type do + xml.tokenNumber payment_method.number + xml.expiryDate do + xml.date( + 'month' => format(payment_method.month, :two_digits), + 'year' => format(payment_method.year, :four_digits_year) + ) end - if options[:ip] && options[:session_id] - xml.tag! 'session', 'shopperIPAddress' => options[:ip], 'id' => options[:session_id] + name = card_holder_name(payment_method, options) + xml.cardHolderName name if name.present? + xml.cryptogram payment_method.payment_cryptogram unless should_send_payment_cryptogram?(options, payment_method) + eci = eci_value(payment_method, options) + xml.eciIndicator eci if eci.present? + end + add_stored_credential_options(xml, options) + add_shopper_id(xml, options, false) + add_three_d_secure(xml, options) + end + end + + def should_send_payment_cryptogram?(options, payment_method) + wallet_type_google_pay?(options) || + (payment_method_apple_pay?(payment_method) && + merchant_initiated?(options)) + end + + def merchant_initiated?(options) + options.dig(:stored_credential, :initiator) == 'merchant' + end + + def add_encrypted_wallet(xml, payment_method) + source = encrypted_wallet_source(payment_method.source) + + xml.paymentDetails do + xml.tag! "#{source}-SSL" do + if source == 'APPLEPAY' + add_encrypted_apple_pay(xml, payment_method) else - xml.tag! 'session', 'shopperIPAddress' => options[:ip] if options[:ip] - xml.tag! 'session', 'id' => options[:session_id] if options[:session_id] + add_encrypted_google_pay(xml, payment_method) end end end end - def add_email(xml, options) - return unless options[:execute_threed] || options[:email] - xml.tag! 'shopper' do - xml.tag! 'shopperEmailAddress', options[:email] if options[:email] - xml.tag! 'browser' do - xml.tag! 'acceptHeader', options[:accept_header] - xml.tag! 'userAgentHeader', options[:user_agent] + def add_encrypted_apple_pay(xml, payment_method) + xml.header do + xml.ephemeralPublicKey payment_method.payment_data.dig(:header, :ephemeralPublicKey) + xml.publicKeyHash payment_method.payment_data.dig(:header, :publicKeyHash) + xml.transactionId payment_method.payment_data.dig(:header, :transactionId) + end + xml.signature payment_method.payment_data[:signature] + xml.version payment_method.payment_data[:version] + xml.data payment_method.payment_data[:data] + end + + def add_encrypted_google_pay(xml, payment_method) + xml.protocolVersion payment_method.payment_data[:version] + xml.signature payment_method.payment_data[:signature] + xml.signedMessage payment_method.payment_data[:signed_message] + end + + def add_card_or_token(xml, payment_method, options) + xml.paymentDetails credit_fund_transfer_attribute(options) do + if options[:payment_type] == :token + add_token_details(xml, options) + else + add_card_details(xml, payment_method, options) + end + add_stored_credential_options(xml, options) + add_shopper_id(xml, options) + add_three_d_secure(xml, options) + end + end + + def add_token_details(xml, options) + xml.tag! 'TOKEN-SSL', 'tokenScope' => options[:token_scope] do + xml.paymentTokenID options[:token_id] + end + end + + def add_card_details(xml, payment_method, options) + xml.tag! 'CARD-SSL' do + add_card(xml, payment_method, options) + end + end + + def add_shopper_id(xml, options, with_session_id = true) + session_params = { + 'shopperIPAddress' => options[:ip], + 'id' => with_session_id ? options[:session_id] : nil + }.compact + + xml.session session_params if session_params.present? + end + + def add_three_d_secure(xml, options) + return unless three_d_secure = options[:three_d_secure] + + xml.info3DSecure do + xml.threeDSVersion three_d_secure[:version] + if three_d_secure[:version] && three_d_secure[:ds_transaction_id] + xml.dsTransactionId three_d_secure[:ds_transaction_id] + else + xml.xid three_d_secure[:xid] + end + xml.cavv three_d_secure[:cavv] + xml.eci three_d_secure[:eci] + end + end + + def add_card(xml, payment_method, options) + xml.cardNumber payment_method.number + xml.expiryDate do + xml.date( + 'month' => format(payment_method.month, :two_digits), + 'year' => format(payment_method.year, :four_digits_year) + ) + end + 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) + end + + def add_stored_credential_options(xml, options = {}) + if options[:stored_credential] + add_stored_credential_using_normalized_fields(xml, options) + elsif options[:stored_credential_usage] + add_stored_credential_using_gateway_specific_fields(xml, options) + end + end + + def add_stored_credential_using_normalized_fields(xml, options) + 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, options[:stored_credential][:initiator]) + + xml.storedCredentials stored_credential_params do + xml.schemeTransactionIdentifier network_transaction_id(options) if send_network_transaction_id?(options) + end + end + + def add_stored_credential_using_gateway_specific_fields(xml, options) + 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 + + def send_network_transaction_id?(options) + network_transaction_id(options) && !options.dig(:stored_credential, :initial_transaction) && options.dig(:stored_credential, :initiator) != 'cardholder' + end + + def add_shopper(xml, options) + return unless options[:execute_threed] || options[:email] || options[:customer] + + xml.shopper do + xml.shopperEmailAddress options[:email] if options[:email] + add_authenticated_shopper_id(xml, options) + xml.browser do + xml.acceptHeader options[:accept_header] + xml.userAgentHeader options[:user_agent] + end + end + end + + def add_fraud_sight_data(xml, options) + return unless options[:custom_string_fields].is_a?(Hash) + + xml.tag! 'FraudSightData' do + xml.tag! 'customStringFields' do + options[:custom_string_fields].each do |key, value| + # transform custom_string_field_1 into customStringField1, etc. + formatted_key = key.to_s.camelize(:lower).to_sym + xml.tag! formatted_key, value + end end end end - def add_address(xml, address) + def add_statement_narrative(xml, options) + xml.statementNarrative truncate(options[:statement_narrative], 50) if options[:statement_narrative] + end + + def add_authenticated_shopper_id(xml, options) + xml.authenticatedShopperID options[:customer] if options[:customer] + end + + def add_address(xml, address, options) return unless address address = address_with_defaults(address) - xml.tag! 'cardAddress' do - xml.tag! 'address' do + xml.cardAddress do + xml.address do if m = /^\s*([^\s]+)\s+(.+)$/.match(address[:name]) - xml.tag! 'firstName', m[1] - xml.tag! 'lastName', m[2] + xml.firstName m[1] + xml.lastName m[2] end - xml.tag! 'address1', address[:address1] - xml.tag! 'address2', address[:address2] if address[:address2] - xml.tag! 'postalCode', address[:zip] - xml.tag! 'city', address[:city] - xml.tag! 'state', address[:state] - xml.tag! 'countryCode', address[:country] - xml.tag! 'telephoneNumber', address[:phone] if address[:phone] + xml.address1 address[:address1] + xml.address2 address[:address2] if address[:address2] + xml.postalCode address[:zip] + xml.city address[:city] + xml.state address[:state] unless address[:country] != 'US' && options[:execute_threed] + xml.countryCode address[:country] + xml.telephoneNumber address[:phone] if address[:phone] end end end def add_hcg_additional_data(xml, options) - xml.tag! 'hcgAdditionalData' do + xml.hcgAdditionalData do options[:hcg_additional_data].each do |k, v| - xml.tag! 'param', {name: k.to_s}, v + xml.param({ name: k.to_s }, v) end end end + def add_instalments_data(xml, options) + xml.thirdPartyData do + xml.instalments options[:instalments] + xml.cpf options[:cpf] if options[:cpf] + end + end + + def add_moto_flag(xml, options) + xml.dynamicInteractionType 'type' => 'MOTO' + end + def address_with_defaults(address) address ||= {} address.delete_if { |_, v| v.blank? } @@ -300,38 +910,66 @@ def address_with_defaults(address) def default_address { - address1: 'N/A', zip: '0000', + country: 'US', city: 'N/A', - state: 'N/A', - country: 'US' + address1: 'N/A' } end def parse(action, xml) - parse_element({:action => action}, REXML::Document.new(xml)) + xml = xml.strip.gsub(/\&/, '&') + doc = Nokogiri::XML(xml, &:strict) + doc.remove_namespaces! + resp_params = { action: } + + parse_elements(doc.root, resp_params) + extract_issuer_response(doc.root, resp_params) + + resp_params end - def parse_element(raw, node) + def extract_issuer_response(doc, response) + return unless issuer_response = doc.at_xpath('//paymentService//reply//orderStatus//payment//IssuerResponseCode') + + response[:issuer_response_code] = issuer_response['code'] + response[:issuer_response_description] = issuer_response['description'] + end + + def parse_elements(node, response) + node_name = node.name.underscore node.attributes.each do |k, v| - raw["#{node.name.underscore}_#{k.underscore}".to_sym] = v + response["#{node_name}_#{k.underscore}".to_sym] = v.value end - if node.has_elements? - raw[node.name.underscore.to_sym] = true unless node.name.blank? - node.elements.each{|e| parse_element(raw, e) } + if node.elements.empty? + response[node_name.to_sym] = node.text unless node.text.blank? else - raw[node.name.underscore.to_sym] = node.text unless node.text.nil? + response[node_name.to_sym] = true unless node.name.blank? + node.elements.each do |childnode| + parse_elements(childnode, response) + end end - raw end def headers(options) + @idempotency_key ||= options[:idempotency_key] + headers = { 'Content-Type' => 'text/xml', 'Authorization' => encoded_credentials } - if options[:cookie] - headers.merge!('Set-Cookie' => options[:cookie]) if options[:cookie] + + # ensure cookie included on follow-up '3ds' and 'capture_request' calls, using the cookie saved from the preceding response + # cookie should be present in options on the 3ds and capture calls, but also still saved in the instance var in case + cookie = defined?(@cookie) ? @cookie : nil + cookie = options[:cookie] || cookie + headers['Cookie'] = cookie if cookie + + # Required because Worldpay does not accept duplicate idempotency keys + # for different transactions, such as in the case of an authorize => capture flow. + if @idempotency_key + headers['Idempotency-Key'] = @idempotency_key + @idempotency_key = SecureRandom.uuid end headers end @@ -339,23 +977,30 @@ def headers(options) def commit(action, request, *success_criteria, options) xml = ssl_post(url, request, headers(options)) raw = parse(action, xml) + if options[:execute_threed] - raw[:cookie] = @cookie + raw[:cookie] = @cookie if defined?(@cookie) raw[:session_id] = options[:session_id] + raw[:is3DSOrder] = true end - success, message = success_and_message_from(raw, success_criteria) + success = success_from(action, raw, success_criteria) + message = message_from(success, raw, success_criteria, action) Response.new( success, message, raw, - :authorization => authorization_from(raw), - :error_code => error_code_from(success, raw), - :test => test?) - + authorization: authorization_from(action, raw, options), + error_code: error_code_from(success, raw), + test: test?, + avs_result: AVSResult.new(code: AVS_CODE_MAP[raw[:avs_result_code_description]]), + cvv_result: CVVResult.new(CVC_CODE_MAP[raw[:cvc_result_code_description]]) + ) + rescue Nokogiri::SyntaxError + unparsable_response(xml) rescue ActiveMerchant::ResponseError => e if e.response.code.to_s == '401' - return Response.new(false, 'Invalid credentials', {}, :test => test?) + return Response.new(false, 'Invalid credentials', {}, test: test?) else raise e end @@ -365,53 +1010,147 @@ def url test? ? self.test_url : self.live_url end + def unparsable_response(raw_response) + message = 'Unparsable response received from Worldpay. Please contact Worldpay if you continue to receive this message.' + message += " (The raw response returned by the API was: #{raw_response.inspect})" + return Response.new(false, message) + end + # Override the regular handle response so we can access the headers # Set-Cookie value is needed for 3DS transactions def handle_response(response) case response.code.to_i when 200...300 - @cookie = response.response['Set-Cookie'] + cookie = response.header['Set-Cookie']&.match('^[^;]*') + @cookie = cookie[0] if cookie response.body else raise ResponseError.new(response) end end + def success_from(action, raw, success_criteria) + success_criteria_success?(raw, success_criteria) || action_success?(action, raw) + end + + def message_from(success, raw, success_criteria, action) + return 'SUCCESS' if success + + raw[:iso8583_return_code_description] || raw[:error] || required_status_message(raw, success_criteria, action) || raw[:issuer_response_description] + end + # success_criteria can be: # - a string or an array of strings (if one of many responses) # - An array of strings if one of many responses could be considered a # success. - def success_and_message_from(raw, success_criteria) - success = (success_criteria.include?(raw[:last_event]) || raw[:ok].present?) - if success - message = 'SUCCESS' + def success_criteria_success?(raw, success_criteria) + return if raw[:error] + + raw[:ok].present? || (success_criteria.include?(raw[:last_event]) if raw[:last_event]) + end + + def action_success?(action, raw) + case action + when 'store' + raw[:token].present? + when 'direct_inquiry' + raw[:last_event].present? else - message = (raw[:iso8583_return_code_description] || raw[:error] || required_status_message(raw, success_criteria)) + false end - - [ success, message ] end def error_code_from(success, raw) - unless success == 'SUCCESS' - raw[:iso8583_return_code_code] || raw[:error_code] || nil - end + raw[:iso8583_return_code_code] || raw[:error_code] || nil unless success == 'SUCCESS' end - def required_status_message(raw, success_criteria) - if(!success_criteria.include?(raw[:last_event])) - "A transaction status of #{success_criteria.collect{|c| "'#{c}'"}.join(" or ")} is required." + def required_status_message(raw, success_criteria, action) + return if success_criteria.include?(raw[:last_event]) + return unless %w[cancel refund inquiry credit fast_credit].include?(action) + + "A transaction status of #{success_criteria.collect { |c| "'#{c}'" }.join(' or ')} is required." + end + + def authorization_from(action, raw, options) + order_id = order_id_from(raw) + + case action + when 'store' + authorization_from_token_details( + order_id:, + token_id: raw[:payment_token_id], + token_scope: 'shopper', + customer: options[:customer] + ) + else + order_id end end - def authorization_from(raw) - pair = raw.detect{|k,v| k.to_s =~ /_order_code$/} + def order_id_from(raw) + pair = raw.detect { |k, _v| k.to_s =~ /_order_code$/ } (pair ? pair.last : nil) end + def authorization_from_token_details(options = {}) + [options[:order_id], options[:token_id], options[:token_scope], options[:customer]].join('|') + end + + def order_id_from_authorization(authorization) + token_details_from_authorization(authorization)[:order_id] + end + + def token_details_from_authorization(authorization) + order_id, token_id, token_scope, customer = authorization.split('|') + + token_details = {} + token_details[:order_id] = order_id if order_id.present? + token_details[:token_id] = token_id if token_id.present? + token_details[:token_scope] = token_scope if token_scope.present? + token_details[:customer] = customer if customer.present? + + token_details + end + + def payment_details(payment_method, options = {}) + case payment_method + when String + token_type_and_details(payment_method) + else + payment_method_type(payment_method, options) + end + end + + def payment_method_type(payment_method, options) + type = if payment_method.is_a?(NetworkTokenizationCreditCard) + payment_method.encrypted_wallet? ? :encrypted_wallet : :network_token + else + wallet_type_google_pay?(options) ? :network_token : :credit + end + { payment_type: type } + end + + def payment_method_apple_pay?(payment_method) + return false unless payment_method.is_a?(NetworkTokenizationCreditCard) + + payment_method.source == :apple_pay + end + + def wallet_type_google_pay?(options) + options[:wallet_type] == :google_pay + 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 + + token_details + end + def credit_fund_transfer_attribute(options) return unless options[:credit] - {'action' => 'REFUND'} + + { 'action' => 'REFUND' } end def encoded_credentials @@ -422,8 +1161,40 @@ def encoded_credentials def currency_exponent(currency) return 0 if non_fractional_currency?(currency) return 3 if three_decimal_currency?(currency) + return 2 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 + + 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, initiator = nil) + customer_or_merchant = initiator == 'cardholder' ? 'customerInitiatedReason' : 'merchantInitiatedReason' + + stored_credential_params = {} + stored_credential_params['usage'] = is_initial_transaction ? 'FIRST' : 'USED' + + return stored_credential_params if customer_or_merchant == 'customerInitiatedReason' && stored_credential_params['usage'] == 'USED' + + stored_credential_params[customer_or_merchant] = reason if reason + stored_credential_params + end + + def encrypted_wallet_source(source) + case source + when :apple_pay + 'APPLEPAY' + when :google_pay + 'PAYWITHGOOGLE' + else + raise ArgumentError, 'Invalid encrypted wallet source' + end + 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 4d4490efb9e..6b20f4dcfdf 100644 --- a/lib/active_merchant/billing/gateways/worldpay_online_payments.rb +++ b/lib/active_merchant/billing/gateways/worldpay_online_payments.rb @@ -1,27 +1,27 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class WorldpayOnlinePaymentsGateway < Gateway - self.live_url = 'https://api.worldpay.com/v1/' + self.live_url = 'https://api.worldpay.com/v1/' self.default_currency = 'GBP' self.money_format = :cents self.supported_countries = %w(HK US GB BE CH CZ DE DK ES FI FR GR HU IE IT LU MT NL NO PL PT SE SG TR) - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :jcb, :maestro, :laser, :switch] + self.supported_cardtypes = %i[visa master american_express discover jcb maestro] self.homepage_url = 'http://online.worldpay.com' self.display_name = 'Worldpay Online Payments' - def initialize(options={}) + def initialize(options = {}) requires!(options, :client_key, :service_key) @client_key = options[:client_key] @service_key = options[:service_key] super end - def authorize(money, credit_card, options={}) - response = create_token(true, credit_card.first_name+' '+credit_card.last_name, credit_card.month, credit_card.year, credit_card.number, credit_card.verification_value) + def authorize(money, credit_card, options = {}) + response = create_token(true, credit_card.first_name + ' ' + credit_card.last_name, credit_card.month, credit_card.year, credit_card.number, credit_card.verification_value) if response.success? options[:authorizeOnly] = true post = create_post_for_auth_or_purchase(response.authorization, money, options) @@ -30,24 +30,25 @@ def authorize(money, credit_card, options={}) response end - def capture(money, authorization, options={}) + def capture(money, authorization, options = {}) if authorization - commit(:post, "orders/#{CGI.escape(authorization)}/capture", {'captureAmount'=>money}, options, 'capture') + 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 + test: test?, + authorization: false, + avs_result: {}, + cvv_result: {}, + error_code: false ) end end - def purchase(money, credit_card, options={}) - response = create_token(true, credit_card.first_name+' '+credit_card.last_name, credit_card.month, credit_card.year, credit_card.number, credit_card.verification_value) + def purchase(money, credit_card, options = {}) + response = create_token(true, credit_card.first_name + ' ' + credit_card.last_name, credit_card.month, credit_card.year, credit_card.number, credit_card.verification_value) if response.success? post = create_post_for_auth_or_purchase(response.authorization, money, options) response = commit(:post, 'orders', post, options, 'purchase') @@ -55,20 +56,18 @@ def purchase(money, credit_card, options={}) response end - def refund(money, orderCode, options={}) - obj = money ? {'refundAmount' => money} : {} + def refund(money, orderCode, options = {}) + obj = money ? { 'refundAmount' => money } : {} commit(:post, "orders/#{CGI.escape(orderCode)}/refund", obj, options, 'refund') end - def void(orderCode, options={}) + def void(orderCode, options = {}) response = commit(:delete, "orders/#{CGI.escape(orderCode)}", nil, options, 'void') - if !response.success? && (response.params && response.params['customCode'] != 'ORDER_NOT_FOUND') - response = refund(nil, orderCode) - end + response = refund(nil, orderCode) if !response.success? && (response.params && response.params['customCode'] != 'ORDER_NOT_FOUND') response end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) authorize(0, credit_card, options) end @@ -76,36 +75,35 @@ def verify(credit_card, options={}) def create_token(reusable, name, exp_month, exp_year, number, cvc) obj = { - 'reusable'=> reusable, - 'paymentMethod'=> { - 'type'=> 'Card', - 'name'=> name, - 'expiryMonth'=> exp_month, - 'expiryYear'=> exp_year, - 'cardNumber'=> number, - 'cvc'=> cvc + 'reusable' => reusable, + 'paymentMethod' => { + 'type' => 'Card', + 'name' => name, + 'expiryMonth' => exp_month, + 'expiryYear' => exp_year, + 'cardNumber' => number, + 'cvc' => cvc }, - 'clientKey'=> @client_key + '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) - { - 'token' => token, - 'orderDescription' => options[:description] || 'Worldpay Order', - 'amount' => money, - 'currencyCode' => options[:currency] || default_currency, - 'name' => options[:billing_address]&&options[:billing_address][:name] ? options[:billing_address][:name] : '', - 'billingAddress' => { - 'address1'=>options[:billing_address]&&options[:billing_address][:address1] ? options[:billing_address][:address1] : '', - 'address2'=>options[:billing_address]&&options[:billing_address][:address2] ? options[:billing_address][:address2] : '', - 'address3'=>'', - 'postalCode'=>options[:billing_address]&&options[:billing_address][:zip] ? options[:billing_address][:zip] : '', - 'city'=>options[:billing_address]&&options[:billing_address][:city] ? options[:billing_address][:city] : '', - 'state'=>options[:billing_address]&&options[:billing_address][:state] ? options[:billing_address][:state] : '', - 'countryCode'=>options[:billing_address]&&options[:billing_address][:country] ? options[:billing_address][:country] : '' + { + 'token' => token, + 'orderDescription' => options[:description] || 'Worldpay Order', + 'amount' => money, + 'currencyCode' => options[:currency] || default_currency, + 'name' => options[:billing_address] && options[:billing_address][:name] ? options[:billing_address][:name] : '', + 'billingAddress' => { + 'address1' => options[:billing_address] && options[:billing_address][:address1] ? options[:billing_address][:address1] : '', + 'address2' => options[:billing_address] && options[:billing_address][:address2] ? options[:billing_address][:address2] : '', + 'address3' => '', + 'postalCode' => options[:billing_address] && options[:billing_address][:zip] ? options[:billing_address][:zip] : '', + 'city' => options[:billing_address] && options[:billing_address][:city] ? options[:billing_address][:city] : '', + 'state' => options[:billing_address] && options[:billing_address][:state] ? options[:billing_address][:state] : '', + 'countryCode' => options[:billing_address] && options[:billing_address][:country] ? options[:billing_address][:country] : '' }, 'customerOrderCode' => options[:order_id], 'orderType' => 'ECOM', @@ -123,15 +121,13 @@ def headers(options = {}) 'Content-Type' => 'application/json', 'User-Agent' => "Worldpay/v1 ActiveMerchantBindings/#{ActiveMerchant::VERSION}", 'X-Worldpay-Client-User-Agent' => user_agent, - 'X-Worldpay-Client-User-Metadata' => {:ip => options[:ip]}.to_json + 'X-Worldpay-Client-User-Metadata' => { ip: options[:ip] }.to_json } - if options['Authorization'] - headers['Authorization'] = options['Authorization'] - end + headers['Authorization'] = options['Authorization'] if options['Authorization'] headers end - def commit(method, url, parameters=nil, options = {}, type = false) + def commit(method, url, parameters = nil, options = {}, type = false) raw_response = response = nil success = false begin @@ -139,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,20 +150,16 @@ def commit(method, url, parameters=nil, options = {}, type = false) success = true elsif type == 'purchase' && response['paymentStatus'] == 'SUCCESS' success = true - elsif type == 'capture' || type=='refund' || type=='void' + elsif type == 'capture' || type == 'refund' || type == 'void' success = true end end end - else - success = true - response = {} end - rescue ResponseError => e raw_response = e.response.body response = response_error(raw_response) - rescue JSON::ParserError => e + rescue JSON::ParserError response = json_error(raw_response) end @@ -176,27 +171,26 @@ 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'] + test: test?, + authorization:, + avs_result: {}, + cvv_result: {}, + error_code: success ? nil : response['customCode'] ) end def test? - @service_key[0]=='T' ? true : false + @service_key[0] == 'T' end def response_error(raw_response) - begin - parse(raw_response) - rescue JSON::ParserError - json_error(raw_response) - end + parse(raw_response) + rescue JSON::ParserError + json_error(raw_response) end def json_error(raw_response) @@ -212,7 +206,6 @@ def json_error(raw_response) def handle_response(response) response.body end - end end end diff --git a/lib/active_merchant/billing/gateways/worldpay_us.rb b/lib/active_merchant/billing/gateways/worldpay_us.rb index 303b5b1767e..2bf4e676c12 100644 --- a/lib/active_merchant/billing/gateways/worldpay_us.rb +++ b/lib/active_merchant/billing/gateways/worldpay_us.rb @@ -1,7 +1,7 @@ require 'nokogiri' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class WorldpayUsGateway < Gateway class_attribute :backup_url @@ -15,14 +15,14 @@ class WorldpayUsGateway < Gateway self.supported_countries = ['US'] self.default_currency = 'USD' self.money_format = :dollars - self.supported_cardtypes = [:visa, :master, :american_express, :discover, :jcb] + self.supported_cardtypes = %i[visa master american_express discover jcb] - def initialize(options={}) + def initialize(options = {}) requires!(options, :acctid, :subid, :merchantpin) super end - def purchase(money, payment_method, options={}) + def purchase(money, payment_method, options = {}) post = {} add_invoice(post, money, options) add_payment_method(post, payment_method) @@ -31,7 +31,7 @@ def purchase(money, payment_method, options={}) commit('purchase', options, post) end - def authorize(money, payment, options={}) + def authorize(money, payment, options = {}) post = {} add_invoice(post, money, options) add_credit_card(post, payment) @@ -40,7 +40,7 @@ def authorize(money, payment, options={}) commit('authorize', options, post) end - def capture(amount, authorization, options={}) + def capture(amount, authorization, options = {}) post = {} add_invoice(post, amount, options) add_reference(post, authorization) @@ -49,7 +49,7 @@ def capture(amount, authorization, options={}) commit('capture', options, post) end - def refund(amount, authorization, options={}) + def refund(amount, authorization, options = {}) post = {} add_invoice(post, amount, options) add_reference(post, authorization) @@ -58,14 +58,14 @@ def refund(amount, authorization, options={}) commit('refund', options, post) end - def void(authorization, options={}) + def void(authorization, options = {}) post = {} add_reference(post, authorization) commit('void', options, post) end - def verify(credit_card, options={}) + def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, credit_card, options) } r.process(:ignore_result) { void(r.authorization, options) } @@ -91,7 +91,7 @@ def url(options) end def add_customer_data(post, options) - if(billing_address = (options[:billing_address] || options[:address])) + if (billing_address = (options[:billing_address] || options[:address])) post[:ci_companyname] = billing_address[:company] post[:ci_billaddr1] = billing_address[:address1] post[:ci_billaddr2] = billing_address[:address2] @@ -105,13 +105,13 @@ def add_customer_data(post, options) post[:ci_ipaddress] = billing_address[:ip] end - if(shipping_address = options[:shipping_address]) + if (shipping_address = options[:shipping_address]) post[:ci_shipaddr1] = shipping_address[:address1] post[:ci_shipaddr2] = shipping_address[:address2] post[:ci_shipcity] = shipping_address[:city] post[:ci_shipstate] = shipping_address[:state] - post[:ci_shipzip] = shipping_address[:zip] - post[:ci_shipcountry] = shipping_address[:country] + post[:ci_shipzip] = shipping_address[:zip] + post[:ci_shipcountry] = shipping_address[:country] end end @@ -139,7 +139,7 @@ def add_credit_card(post, payment_method) ACCOUNT_TYPES = { 'checking' => '1', - 'savings' => '2', + 'savings' => '2' } def add_check(post, payment_method) @@ -178,7 +178,7 @@ def parse(xml) 'refund' => 'ns_credit', 'authorize' => 'ns_quicksale_cc', 'capture' => 'ns_quicksale_cc', - 'void' => 'ns_void', + 'void' => 'ns_void' } def commit(action, options, post) @@ -196,8 +196,8 @@ def commit(action, options, post) succeeded, message_from(succeeded, raw), raw, - :authorization => authorization_from(raw), - :test => test? + authorization: authorization_from(raw), + test: test? ) end diff --git a/lib/active_merchant/billing/gateways/xpay.rb b/lib/active_merchant/billing/gateways/xpay.rb new file mode 100644 index 00000000000..6aaae022d04 --- /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:, 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: + }.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 0f358948ab8..a3c800e08da 100644 --- a/lib/active_merchant/billing/network_tokenization_credit_card.rb +++ b/lib/active_merchant/billing/network_tokenization_credit_card.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: class NetworkTokenizationCreditCard < CreditCard # A +NetworkTokenizationCreditCard+ object represents a tokenized credit card # using the EMV Network Tokenization specification, http://www.emvco.com/specifications.aspx?id=263. @@ -14,10 +14,10 @@ class NetworkTokenizationCreditCard < CreditCard self.require_verification_value = false self.require_name = false - attr_accessor :payment_cryptogram, :eci, :transaction_id + attr_accessor :payment_cryptogram, :eci, :transaction_id, :metadata, :payment_data attr_writer :source - SOURCES = %i(apple_pay android_pay google_pay) + SOURCES = %i(apple_pay android_pay google_pay network_token) def source if defined?(@source) && SOURCES.include?(@source) @@ -31,6 +31,18 @@ def credit_card? true end + def network_token? + source == :network_token + end + + def mobile_wallet? + %i[apple_pay android_pay google_pay].include?(source) + end + + def encrypted_wallet? + payment_data.present? + end + def type 'network_tokenization' end diff --git a/lib/active_merchant/billing/payment_token.rb b/lib/active_merchant/billing/payment_token.rb index aea70ff6f66..4484e2acd9a 100644 --- a/lib/active_merchant/billing/payment_token.rb +++ b/lib/active_merchant/billing/payment_token.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: # Base class representation of cryptographic payment data tokens that may be used for EMV-style transactions # like Apple Pay. Payment data may be transmitted via any data type, and may also be padded # with metadata specific to the cryptographer. This metadata should be parsed and interpreted in concrete diff --git a/lib/active_merchant/billing/response.rb b/lib/active_merchant/billing/response.rb index a59174ceb7c..5b9a5308920 100644 --- a/lib/active_merchant/billing/response.rb +++ b/lib/active_merchant/billing/response.rb @@ -1,15 +1,19 @@ -module ActiveMerchant #:nodoc: - module Billing #:nodoc: - class Error < ActiveMerchantError #:nodoc: +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class Error < ActiveMerchantError # :nodoc: end class Response - attr_reader :params, :message, :test, :authorization, :avs_result, :cvv_result, :error_code, :emv_authorization + attr_reader :params, :message, :test, :authorization, :avs_result, :cvv_result, :error_code, :emv_authorization, :network_transaction_id, :pending def success? @success end + def failure? + !success? + end + def test? @test end @@ -25,18 +29,20 @@ def initialize(success, message, params = {}, options = {}) @fraud_review = options[:fraud_review] @error_code = options[:error_code] @emv_authorization = options[:emv_authorization] + @network_transaction_id = options[:network_transaction_id] + @pending = options[:pending] || false @avs_result = if options[:avs_result].kind_of?(AVSResult) - options[:avs_result].to_hash - else - AVSResult.new(options[:avs_result]).to_hash - end + options[:avs_result].to_hash + else + AVSResult.new(options[:avs_result]).to_hash + end @cvv_result = if options[:cvv_result].kind_of?(CVVResult) - options[:cvv_result].to_hash - else - CVVResult.new(options[:cvv_result]).to_hash - end + options[:cvv_result].to_hash + else + CVVResult.new(options[:cvv_result]).to_hash + end end end @@ -53,14 +59,14 @@ def initialize(use_first_response = false) @primary_response = nil end - def process(ignore_result=false) + def process(ignore_result = false) return unless success? response = yield self << response unless ignore_result - if(@use_first_response && response.success?) + if @use_first_response && response.success? @primary_response ||= response else @primary_response = response @@ -70,7 +76,7 @@ def process(ignore_result=false) def <<(response) if response.is_a?(MultiResponse) - response.responses.each{|r| @responses << r} + response.responses.each { |r| @responses << r } else @responses << response end @@ -80,12 +86,26 @@ def success? (primary_response ? primary_response.success? : true) end - %w(params message test authorization avs_result cvv_result error_code emv_authorization test? fraud_review?).each do |m| - class_eval %( + def avs_result + return @primary_response.try(:avs_result) if @use_first_response + + result = responses.reverse.find { |r| r.avs_result['code'].present? } + result.try(:avs_result) || responses.last.try(:avs_result) + end + + def cvv_result + return @primary_response.try(:cvv_result) if @use_first_response + + result = responses.reverse.find { |r| r.cvv_result['code'].present? } + result.try(:cvv_result) || responses.last.try(:cvv_result) + end + + %w(params message test authorization error_code emv_authorization test? fraud_review?).each do |m| + class_eval <<~RUBY, __FILE__, __LINE__ + 1 def #{m} (@responses.empty? ? nil : primary_response.#{m}) end - ) + RUBY end end end diff --git a/lib/active_merchant/billing/three_d_secure_eci_mapper.rb b/lib/active_merchant/billing/three_d_secure_eci_mapper.rb new file mode 100644 index 00000000000..b5e1776d642 --- /dev/null +++ b/lib/active_merchant/billing/three_d_secure_eci_mapper.rb @@ -0,0 +1,27 @@ +module ActiveMerchant + module Billing + module ThreeDSecureEciMapper + NON_THREE_D_SECURE_TRANSACTION = :non_three_d_secure_transaction + ATTEMPTED_AUTHENTICATION_TRANSACTION = :attempted_authentication_transaction + FULLY_AUTHENTICATED_TRANSACTION = :fully_authenticated_transaction + + ECI_00_01_02_MAP = { '00' => NON_THREE_D_SECURE_TRANSACTION, '01' => ATTEMPTED_AUTHENTICATION_TRANSACTION, '02' => FULLY_AUTHENTICATED_TRANSACTION }.freeze + ECI_05_06_07_MAP = { '05' => FULLY_AUTHENTICATED_TRANSACTION, '06' => ATTEMPTED_AUTHENTICATION_TRANSACTION, '07' => NON_THREE_D_SECURE_TRANSACTION }.freeze + BRAND_TO_ECI_MAP = { + american_express: ECI_05_06_07_MAP, + dankort: ECI_05_06_07_MAP, + diners_club: ECI_05_06_07_MAP, + discover: ECI_05_06_07_MAP, + elo: ECI_05_06_07_MAP, + jcb: ECI_05_06_07_MAP, + maestro: ECI_00_01_02_MAP, + master: ECI_00_01_02_MAP, + visa: ECI_05_06_07_MAP + }.freeze + + def self.map(brand, eci) + BRAND_TO_ECI_MAP.dig(brand, eci) + end + end + end +end diff --git a/lib/active_merchant/connection.rb b/lib/active_merchant/connection.rb index a289bcdd82f..024f833a84f 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, :proxy_user, :proxy_password + 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) @@ -58,10 +44,13 @@ def initialize(endpoint) @ssl_connection = {} @proxy_address = :ENV @proxy_port = nil + @proxy_user = nil + @proxy_password = nil end def wiredump_device=(device) - raise ArgumentError, "can't wiredump to frozen #{device.class}" if device && device.frozen? + raise ArgumentError, "can't wiredump to frozen #{device.class}" if device&.frozen? + @wiredump_device = device end @@ -71,24 +60,23 @@ def request(method, body, headers = {}) headers = headers.dup headers['connection'] ||= 'close' - retry_exceptions(:max_retries => max_retries, :logger => logger, :tag => tag) do - begin - info "connection_http_method=#{method.to_s.upcase} connection_uri=#{endpoint}", tag + retry_exceptions(max_retries:, logger:, tag:) do + info "connection_http_method=#{method.to_s.upcase} connection_uri=#{endpoint}", tag - result = nil + result = nil - realtime = Benchmark.realtime do - http.start unless http.started? - @ssl_connection = http.ssl_connection - info "connection_ssl_version=#{ssl_connection[:version]} connection_ssl_cipher=#{ssl_connection[:cipher]}", tag + realtime = Benchmark.realtime do + http.start unless http.started? + @ssl_connection = http.ssl_connection + info "connection_ssl_version=#{ssl_connection[:version]} connection_ssl_cipher=#{ssl_connection[:cipher]}", tag - result = case method + 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 - http.post(endpoint.request_uri, body, RUBY_184_POST_HEADERS.merge(headers)) + http.post(endpoint.request_uri, body, headers.reverse_merge!(RUBY_184_POST_HEADERS)) when :put debug body http.put(endpoint.request_uri, body, headers) @@ -110,14 +98,12 @@ def request(method, body, headers = {}) else raise ArgumentError, "Unsupported request method #{method.to_s.upcase}" end - end - - info '--> %d %s (%d %.4fs)' % [result.code, result.message, result.body ? result.body.length : 0, realtime], tag - debug result.body - result end - end + info '--> %d %s (%d %.4fs)' % [result.code, result.message, result.body ? result.body.length : 0, realtime], tag + debug result.body + result + end ensure info 'connection_request_total_time=%.4fs' % [Process.clock_gettime(Process::CLOCK_MONOTONIC) - request_start], tag http.finish if http.started? @@ -127,7 +113,7 @@ def request(method, body, headers = {}) def http @http ||= begin - http = Net::HTTP.new(endpoint.host, endpoint.port, proxy_address, proxy_port) + http = Net::HTTP.new(endpoint.host, endpoint.port, proxy_address, proxy_port, proxy_user, proxy_password) configure_debugging(http) configure_timeouts(http) configure_ssl(http) @@ -162,7 +148,6 @@ def configure_ssl(http) else http.verify_mode = OpenSSL::SSL::VERIFY_NONE end - end def configure_cert(http) @@ -177,19 +162,6 @@ def configure_cert(http) end end - def handle_response(response) - if @ignore_http_status then - return response.body - else - case response.code.to_i - when 200...300 - response.body - else - raise ResponseError.new(response) - end - end - end - def debug(message, tag = nil) log(:debug, message, tag) end @@ -204,7 +176,7 @@ def error(message, tag = nil) def log(level, message, tag) message = "[#{tag}] #{message}" if tag - logger.send(level, message) if logger + logger&.send(level, message) end end end diff --git a/lib/active_merchant/country.rb b/lib/active_merchant/country.rb index 46349e63123..3f91b1dad47 100644 --- a/lib/active_merchant/country.rb +++ b/lib/active_merchant/country.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -module ActiveMerchant #:nodoc: +module ActiveMerchant # :nodoc: class InvalidCountryCodeError < StandardError end @@ -9,6 +9,7 @@ class CountryCodeFormatError < StandardError class CountryCode attr_reader :value, :format + def initialize(value) @value = value.to_s.upcase detect_format @@ -39,11 +40,11 @@ class Country def initialize(options = {}) @name = options.delete(:name) - @codes = options.collect{|k,v| CountryCode.new(v)} + @codes = options.collect { |_k, v| CountryCode.new(v) } end def code(format) - @codes.detect{|c| c.format == format} + @codes.detect { |c| c.format == format } end def ==(other) @@ -183,6 +184,8 @@ def to_s { alpha2: 'KI', name: 'Kiribati', alpha3: 'KIR', numeric: '296' }, { alpha2: 'KP', name: 'Korea, Democratic People\'s Republic of', alpha3: 'PRK', numeric: '408' }, { alpha2: 'KR', name: 'Korea, Republic of', alpha3: 'KOR', numeric: '410' }, + { alpha2: 'XK', name: 'Kosovo', alpha3: 'XKX', numeric: '900' }, + { alpha2: 'QZ', name: 'Kosovo', alpha3: 'XKX', numeric: '900' }, { alpha2: 'KW', name: 'Kuwait', alpha3: 'KWT', numeric: '414' }, { alpha2: 'KG', name: 'Kyrgyzstan', alpha3: 'KGZ', numeric: '417' }, { alpha2: 'LA', name: 'Lao People\'s Democratic Republic', alpha3: 'LAO', numeric: '418' }, @@ -245,6 +248,7 @@ def to_s { alpha2: 'PR', name: 'Puerto Rico', alpha3: 'PRI', numeric: '630' }, { alpha2: 'QA', name: 'Qatar', alpha3: 'QAT', numeric: '634' }, { alpha2: 'RE', name: 'Reunion', alpha3: 'REU', numeric: '638' }, + { alpha2: 'RO', name: 'Romania', alpha3: 'ROU', numeric: '642' }, { alpha2: 'RO', name: 'Romania', alpha3: 'ROM', numeric: '642' }, { alpha2: 'RU', name: 'Russian Federation', alpha3: 'RUS', numeric: '643' }, { alpha2: 'RW', name: 'Rwanda', alpha3: 'RWA', numeric: '646' }, @@ -306,6 +310,7 @@ def to_s { alpha2: 'VU', name: 'Vanuatu', alpha3: 'VUT', numeric: '548' }, { alpha2: 'VE', name: 'Venezuela', alpha3: 'VEN', numeric: '862' }, { alpha2: 'VN', name: 'Viet Nam', alpha3: 'VNM', numeric: '704' }, + { alpha2: 'VN', name: 'Vietnam', alpha3: 'VNM', numeric: '704' }, { alpha2: 'VG', name: 'Virgin Islands, British', alpha3: 'VGB', numeric: '092' }, { alpha2: 'VI', name: 'Virgin Islands, U.S.', alpha3: 'VIR', numeric: '850' }, { alpha2: 'WF', name: 'Wallis and Futuna', alpha3: 'WLF', numeric: '876' }, @@ -323,11 +328,12 @@ def self.find(name) when 2, 3 upcase_name = name.upcase country_code = CountryCode.new(name) - country = COUNTRIES.detect{|c| c[country_code.format] == upcase_name } + country = COUNTRIES.detect { |c| c[country_code.format] == upcase_name } else - country = COUNTRIES.detect{|c| c[:name].upcase == name.upcase } + country = COUNTRIES.detect { |c| c[:name].casecmp(name).zero? } end raise InvalidCountryCodeError, "No country could be found for the country #{name}" if country.nil? + Country.new(country.dup) end end diff --git a/lib/active_merchant/errors.rb b/lib/active_merchant/errors.rb index af4bcb8b1be..c88f3a43709 100644 --- a/lib/active_merchant/errors.rb +++ b/lib/active_merchant/errors.rb @@ -1,5 +1,5 @@ -module ActiveMerchant #:nodoc: - class ActiveMerchantError < StandardError #:nodoc: +module ActiveMerchant # :nodoc: + class ActiveMerchantError < StandardError # :nodoc: end class ConnectionError < ActiveMerchantError # :nodoc: @@ -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/net_http_ssl_connection.rb b/lib/active_merchant/net_http_ssl_connection.rb index d0764cbc615..00f8df73699 100644 --- a/lib/active_merchant/net_http_ssl_connection.rb +++ b/lib/active_merchant/net_http_ssl_connection.rb @@ -1,10 +1,46 @@ require 'net/http' module NetHttpSslConnection + module InnocuousCapitalize + def capitalize(name) = name + private :capitalize + end + + class CaseSensitivePost < Net::HTTP::Post; prepend InnocuousCapitalize; end + + class CaseSensitivePatch < Net::HTTP::Patch; prepend InnocuousCapitalize; end + + class CaseSensitivePut < Net::HTTP::Put; prepend InnocuousCapitalize; end + + class CaseSensitiveDelete < Net::HTTP::Delete; prepend InnocuousCapitalize; end + refine Net::HTTP do def ssl_connection - return {} unless @socket.present? + return {} unless use_ssl? && @socket.present? + { version: @socket.io.ssl_version, cipher: @socket.io.cipher[0] } end + + unless ENV['RUNNING_UNIT_TESTS'] + def post(path, data, initheader = nil, dest = nil, &block) + send_entity(path, data, initheader, dest, request_type(CaseSensitivePost, Net::HTTP::Post, initheader), &block) + end + + def patch(path, data, initheader = nil, dest = nil, &block) + send_entity(path, data, initheader, dest, request_type(CaseSensitivePatch, Net::HTTP::Patch, initheader), &block) + end + + def put(path, data, initheader = nil) + request(request_type(CaseSensitivePut, Net::HTTP::Put, initheader).new(path, initheader), data) + end + + def delete(path, initheader = { 'Depth' => 'Infinity' }) + request(request_type(CaseSensitiveDelete, Net::HTTP::Delete, initheader).new(path, initheader)) + end + + def request_type(replace, default, initheader) + initheader.is_a?(ActiveMerchant::PostsData::CaseSensitiveHeaders) ? replace : default + end + end end end diff --git a/lib/active_merchant/network_connection_retries.rb b/lib/active_merchant/network_connection_retries.rb index 772e8617936..267524b27cd 100644 --- a/lib/active_merchant/network_connection_retries.rb +++ b/lib/active_merchant/network_connection_retries.rb @@ -17,25 +17,29 @@ def self.included(base) base.send(:attr_accessor, :retry_safe) end - def retry_exceptions(options={}) + def retry_exceptions(options = {}) connection_errors = DEFAULT_CONNECTION_ERRORS.merge(options[:connection_exceptions] || {}) retry_network_exceptions(options) do - begin - yield - rescue Errno::ECONNREFUSED => e - raise ActiveMerchant::RetriableConnectionError.new('The remote server refused the connection', e) - rescue OpenSSL::X509::CertificateError => e - NetworkConnectionRetries.log(options[:logger], :error, e.message, options[:tag]) - raise ActiveMerchant::ClientCertificateError, 'The remote server did not accept the provided SSL certificate' - rescue Zlib::BufError => e - raise ActiveMerchant::InvalidResponseError, 'The remote server replied with an invalid response' - rescue *connection_errors.keys => e - raise ActiveMerchant::ConnectionError.new(derived_error_message(connection_errors, e.class), e) - end + yield + rescue Errno::ECONNREFUSED => e + raise ActiveMerchant::RetriableConnectionError.new('The remote server refused the connection', e) + rescue OpenSSL::X509::CertificateError => e + NetworkConnectionRetries.log(options[:logger], :error, e.message, options[:tag]) + raise ActiveMerchant::ClientCertificateError, 'The remote server did not accept the provided SSL certificate' + rescue Zlib::BufError + raise ActiveMerchant::InvalidResponseError, 'The remote server replied with an invalid response' + rescue *connection_errors.keys => e + raise ActiveMerchant::ConnectionError.new(derived_error_message(connection_errors, e.class), e) end end + def self.log(logger, level, message, tag = nil) + tag ||= self.class.to_s + message = "[#{tag}] #{message}" + logger&.send(level, message) + end + private def retry_network_exceptions(options = {}) @@ -46,29 +50,22 @@ def retry_network_exceptions(options = {}) begin request_start = Process.clock_gettime(Process::CLOCK_MONOTONIC) result = yield - log_with_retry_details(options[:logger], initial_retries-retries + 1, Process.clock_gettime(Process::CLOCK_MONOTONIC) - request_start, 'success', options[:tag]) + log_with_retry_details(options[:logger], initial_retries - retries + 1, Process.clock_gettime(Process::CLOCK_MONOTONIC) - request_start, 'success', options[:tag]) result rescue ActiveMerchant::RetriableConnectionError => e retries -= 1 - log_with_retry_details(options[:logger], initial_retries-retries, Process.clock_gettime(Process::CLOCK_MONOTONIC) - request_start, e.message, options[:tag]) + log_with_retry_details(options[:logger], initial_retries - retries, Process.clock_gettime(Process::CLOCK_MONOTONIC) - request_start, e.message, options[:tag]) retry unless retries.zero? raise ActiveMerchant::ConnectionError.new(e.message, e) rescue ActiveMerchant::ConnectionError, ActiveMerchant::InvalidResponseError => e retries -= 1 - log_with_retry_details(options[:logger], initial_retries-retries, Process.clock_gettime(Process::CLOCK_MONOTONIC) - request_start, e.message, options[:tag]) + log_with_retry_details(options[:logger], initial_retries - retries, Process.clock_gettime(Process::CLOCK_MONOTONIC) - request_start, e.message, options[:tag]) retry if (options[:retry_safe] || retry_safe) && !retries.zero? raise end end - def self.log(logger, level, message, tag=nil) - tag ||= self.class.to_s - message = "[#{tag}] #{message}" - logger.send(level, message) if logger - end - - private def log_with_retry_details(logger, attempts, time, message, tag) NetworkConnectionRetries.log(logger, :info, 'connection_attempt=%d connection_request_time=%.4fs connection_msg="%s"' % [attempts, time, message], tag) end diff --git a/lib/active_merchant/post_data.rb b/lib/active_merchant/post_data.rb index b1c715b0108..2ad6a15fdb1 100644 --- a/lib/active_merchant/post_data.rb +++ b/lib/active_merchant/post_data.rb @@ -2,11 +2,12 @@ module ActiveMerchant class PostData < Hash - class_attribute :required_fields, :instance_writer => false + class_attribute :required_fields, instance_writer: false self.required_fields = [] def []=(key, value) return if value.blank? && !required?(key) + super end @@ -14,9 +15,10 @@ def to_post_data collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&') end - alias_method :to_s, :to_post_data + alias to_s to_post_data private + def required?(key) required_fields.include?(key) end diff --git a/lib/active_merchant/posts_data.rb b/lib/active_merchant/posts_data.rb index 9930f2635e4..581a4317003 100644 --- a/lib/active_merchant/posts_data.rb +++ b/lib/active_merchant/posts_data.rb @@ -1,6 +1,5 @@ -module ActiveMerchant #:nodoc: - module PostsData #:nodoc: - +module ActiveMerchant # :nodoc: + module PostsData # :nodoc: def self.included(base) base.class_attribute :ssl_strict base.ssl_strict = true @@ -31,9 +30,11 @@ def self.included(base) base.class_attribute :proxy_address base.class_attribute :proxy_port + base.class_attribute :proxy_user + base.class_attribute :proxy_password end - def ssl_get(endpoint, headers={}) + def ssl_get(endpoint, headers = {}) ssl_request(:get, endpoint, nil, headers) end @@ -46,8 +47,8 @@ def ssl_request(method, endpoint, data, headers) end def raw_ssl_request(method, endpoint, data, headers = {}) - logger.warn "#{self.class} using ssl_strict=false, which is insecure" if logger unless ssl_strict - logger.warn "#{self.class} posting to plaintext endpoint, which is insecure" if logger unless endpoint.to_s =~ /^https:/ + logger&.warn "#{self.class} using ssl_strict=false, which is insecure" unless ssl_strict + logger&.warn "#{self.class} posting to plaintext endpoint, which is insecure" unless endpoint.to_s =~ /^https:/ connection = new_connection(endpoint) connection.open_timeout = open_timeout @@ -69,8 +70,10 @@ def raw_ssl_request(method, endpoint, data, headers = {}) connection.ignore_http_status = @options[:ignore_http_status] if @options - connection.proxy_address = proxy_address - connection.proxy_port = proxy_port + connection.proxy_address = proxy_address + connection.proxy_port = proxy_port + connection.proxy_user = proxy_user + connection.proxy_password = proxy_password connection.request(method, data, headers) end @@ -90,5 +93,15 @@ def handle_response(response) end end + # This class is needed to play along with the Refinement done for Net::HTTP + # class so it can have a way to detect if the hash that represent the headers + # should use the case sensitive version of the headers or not. + class CaseSensitiveHeaders < Hash + def dup + case_sensitive_dup = self.class.new + each { |key, value| case_sensitive_dup[key] = value } + case_sensitive_dup + end + end end end diff --git a/lib/active_merchant/version.rb b/lib/active_merchant/version.rb index bac8d31747e..aa54e52ddea 100644 --- a/lib/active_merchant/version.rb +++ b/lib/active_merchant/version.rb @@ -1,3 +1,3 @@ module ActiveMerchant - VERSION = '1.79.3' + VERSION = '1.130.0' end diff --git a/lib/active_merchant/versionable.rb b/lib/active_merchant/versionable.rb new file mode 100644 index 00000000000..12f7b759b16 --- /dev/null +++ b/lib/active_merchant/versionable.rb @@ -0,0 +1,29 @@ +module ActiveMerchant + module Versionable + def self.included(base) + if base.respond_to?(:class_attribute) + base.class_attribute :versions, default: {} + base.extend(ClassMethods) + end + end + + module ClassMethods + def inherited(subclass) + super + subclass.versions = {} + end + + def version(version, feature = :default_api) + versions[feature] = version + end + + def fetch_version(feature = :default_api) + versions[feature] + end + end + + def fetch_version(feature = :default_api) + versions[feature] + end + end +end diff --git a/lib/activemerchant.rb b/lib/activemerchant.rb index 0a3f08fee3f..118568f06b3 100644 --- a/lib/activemerchant.rb +++ b/lib/activemerchant.rb @@ -1 +1 @@ -require 'active_merchant' \ No newline at end of file +require 'active_merchant' diff --git a/lib/certs/cacert.pem b/lib/certs/cacert.pem index 45654c0b9c4..bcd7060e5be 100644 --- a/lib/certs/cacert.pem +++ b/lib/certs/cacert.pem @@ -3335,4 +3335,4 @@ BBYEFFvKXuXe0oGqzagtZFG22XKbl+ZPMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe 5d7SgarNqC1kUbbZcpuX5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJ N+vp1RPZytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZgh5Mm m7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== ------END CERTIFICATE----- +-----END CERTIFICATE----- \ No newline at end of file diff --git a/lib/support/gateway_support.rb b/lib/support/gateway_support.rb index 29b3fc05a8e..894d58c906f 100644 --- a/lib/support/gateway_support.rb +++ b/lib/support/gateway_support.rb @@ -2,9 +2,8 @@ require 'active_support' require 'active_merchant' - -class GatewaySupport #:nodoc: - ACTIONS = [:purchase, :authorize, :capture, :void, :credit, :recurring] +class GatewaySupport # :nodoc: + ACTIONS = %i[purchase authorize capture void credit recurring] include ActiveMerchant::Billing @@ -15,7 +14,7 @@ def initialize filename = File.basename(f, '.rb') gateway_name = filename + '_gateway' begin - gateway_class = ('ActiveMerchant::Billing::' + gateway_name.camelize).constantize + ('ActiveMerchant::Billing::' + gateway_name.camelize).constantize rescue NameError puts 'Could not load gateway ' + gateway_name.camelize + ' from ' + f + '.' end @@ -24,21 +23,21 @@ 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 width = 15 print 'Name'.center(width + 20) - ACTIONS.each{|f| print "#{f.to_s.capitalize.center(width)}" } + ACTIONS.each { |f| print f.to_s.capitalize.center(width) } puts each_gateway do |g| - print "#{g.display_name.ljust(width + 20)}" + print g.display_name.ljust(width + 20) ACTIONS.each do |f| - print "#{(g.instance_methods.include?(f.to_s) ? "Y" : "N").center(width)}" + print((g.instance_methods.include?(f.to_s) ? 'Y' : 'N').center(width)) end puts end @@ -68,4 +67,3 @@ def to_s end end end - diff --git a/lib/support/ssl_verify.rb b/lib/support/ssl_verify.rb index 94dbf618695..d3b27f1756a 100644 --- a/lib/support/ssl_verify.rb +++ b/lib/support/ssl_verify.rb @@ -2,7 +2,6 @@ require 'support/gateway_support' class SSLVerify - def initialize @gateways = GatewaySupport.new.gateways end @@ -18,22 +17,20 @@ def test_gateways next end - if !g.ssl_strict - disabled << g - end + disabled << g if !g.ssl_strict uri = URI.parse(g.live_url) - result,message = ssl_verify_peer?(uri) + result, message = ssl_verify_peer?(uri) case result when :success print '.' success << g when :fail print 'F' - failed << {:gateway => g, :message => message} + failed << { gateway: g, message: } when :error print 'E' - errored << {:gateway => g, :message => message} + errored << { gateway: g, message: } end end @@ -60,7 +57,6 @@ def test_gateways puts d.name end end - end def try_host(http, path) @@ -84,10 +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 ed7c716c9c0..cebcefe3229 100644 --- a/lib/support/ssl_version.rb +++ b/lib/support/ssl_version.rb @@ -29,10 +29,10 @@ def test_gateways(min_version = :TLS1_1) success << g when :fail print 'F' - failed << {:gateway => g, :message => message} + failed << { gateway: g, message: } when :error print 'E' - errored << {:gateway => g, :message => message} + errored << { gateway: g, message: } end end @@ -75,13 +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/comm_stub.rb b/test/comm_stub.rb index 2d4c95013b4..23aa23f8aa6 100644 --- a/test/comm_stub.rb +++ b/test/comm_stub.rb @@ -8,18 +8,30 @@ def initialize(gateway, method_to_stub, action) @check = nil end - def check_request(&block) - @check = block - self + def check_request(skip_response: false, &block) + if skip_response + @complete = true + overwrite_gateway_method do |*args| + block&.call(*args) || '' + end + @action.call + else + @check = block + self + end + end + + def overwrite_gateway_method(&block) + singleton_class = (class << @gateway; self; end) + singleton_class.send(:undef_method, @method_to_stub) + singleton_class.send(:define_method, @method_to_stub, &block) end def respond_with(*responses) @complete = true check = @check - singleton_class = (class << @gateway; self; end) - singleton_class.send(:undef_method, @method_to_stub) - singleton_class.send(:define_method, @method_to_stub) do |*args| - check.call(*args) if check + overwrite_gateway_method do |*args| + check&.call(*args) (responses.size == 1 ? responses.last : responses.shift) end @action.call @@ -40,7 +52,7 @@ def last_comm_stub @last_comm_stub ||= Stub::Complete.new end - def stub_comms(gateway=@gateway, method_to_stub=:ssl_post, &action) + def stub_comms(gateway = @gateway, method_to_stub = :ssl_post, &action) assert last_comm_stub.complete?, "Tried to stub communications when there's a stub already in progress." @last_comm_stub = Stub.new(gateway, method_to_stub, action) end diff --git a/test/fixtures.yml b/test/fixtures.yml index c14a40480aa..0183c360730 100644 --- a/test/fixtures.yml +++ b/test/fixtures.yml @@ -16,6 +16,14 @@ adyen: password: '' merchant_account: '' +airwallex: + client_id: SOMECREDENTIAL + client_api_key: ANOTHERCREDENTIAL + +alelo: + client_id: xxxxxxx + client_secret: xxxxxxx + allied_wallet: site_id: site_id merchant_id: merchant_id @@ -36,6 +44,10 @@ axcessms: balanced: login: 'e1c5ad38d1c711e1b36c026ba7e239a9' +bambora_apac: + username: nmi.api + password: qwerty123 + # Bank Frick doesn't provide public testing data bank_frick: sender: sender-uuid @@ -67,6 +79,7 @@ beanstream: password: password secure_profile_api_key: API Access Passcode recurring_api_key: API Access Passcode + api_key: API KEY beanstream_interac: login: merchant id @@ -103,6 +116,13 @@ braintree_blue: private_key: Z merchant_account_id: A +braintree_blue_with_ach_enabled: + merchant_id: X + public_key: Y + private_key: Z + merchant_account_id: A + venmo_profile_id: B + braintree_blue_with_processing_rules: merchant_id: X public_key: Y @@ -158,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 @@ -174,7 +195,13 @@ checkout: password: Password1! checkout_v2: - secret_key: secret_key + secret_key: SECRET_KEY_FOR_BASIC_TRANSACTIONS + client_id: CLIENT_ID_FOR_OAUTH_TRANSACTIONS + client_secret: CLIENT_SECRET_FOR_OAUTH_TRANSACTIONS + +checkout_v2_token: + secret_key: sk_sbox_xxxxxxxxxxxxxxxxx + public_key: pk_sbox_xxxxxxxxxxxxxxxxx citrus_pay: userid: CPF00001 @@ -198,6 +225,11 @@ clearhaus_secure: InviQqJd1KTGRDmWIGrE5YACVmW2JSszD9t5VKxkAA== -----END RSA PRIVATE KEY----- +commerce_hub: + api_key: API KEY + api_secret: API SECRET + merchant_id: MERCHANT ID + terminal_id: TERMINAL ID # Contact Support at it_support@commercegate.com for credentials and offer/site commercegate: @@ -215,10 +247,18 @@ creditcall: terminal_id: '99961426' transaction_key: '9drdRU9wJ65SNRw3' +# NOTE: the IP address you run the remote tests from will need to be +# whitelisted by Credorax; contact support@credorax.com as necessary to request +# your IP address be added to the whitelist for your test account. credorax: merchant_id: 'merchant_id' cipher_key: 'cipher_key' +ct_payment: + api_key: SOMECREDENTIAL + company_number: '12345' + merchant_number: '12345678' + # Culqi does not provide public testing data culqi: merchant_id: MERCHANT @@ -233,10 +273,51 @@ cyber_source: login: X password: Y +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 + trans_key: 9de3769b7e + secret_key: ae132899f56162a669b38dab5927862f3 + 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 + preauth_mode: true + +decidir_plus: + public_key: SOMECREDENTIAL + private_key: SOMECREDENTIAL + +decidir_plus_preauth: + public_key: SOMECREDENTIAL + private_key: SOMECREDENTIAL + +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 @@ -259,10 +340,17 @@ efsnet: password: Y # Provided for url update test + elavon: - login: "000127" - user: ssltest - password: "IERAOBEE5V0D6Q3Q6R51TG89XAIVGEQ3LGLKMKCKCVQBGGGAU7FN627GPA54P5HR" + login: "009005" + user: "devportal" + password: "BDDZY5KOUDCNPV4L3821K7PETO4Z7TPYOJB06TYBI1CW771IDHXBVBP51HZ6ZANJ" + +elavon_multi_currency: + login: "009006" + user: "devportal" + password: "XWJS3QTFCH40HW0QGHJKXAYADCTDH0TXXAKXAEZCGCCJ29CFNPCZT4KA9D5KQMDA" + multi_currency: true element: account_id: "1013963" @@ -327,10 +415,27 @@ 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 +firstdata_e4_v27: + login: ALOGIN + password: APASSWORD + 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 @@ -347,6 +452,12 @@ forte: api_key: "f087a90f00f0ae57050c937ed3815c9f" secret: "d793d64064e3113a74fa72035cfc3a1d" +fortis: + user_id: 'USER_ID' + user_api_key: 'USER_API_KEY' + developer_id: 'DEVELOPER_ID' + location_id: 'LOCATION_ID' + garanti: login: "PROVAUT" terminal_id: 30691300 @@ -355,8 +466,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" @@ -367,10 +483,17 @@ hdfc: login: LOGIN password: PASSWORD +hi_pay: + username: "USERNAME" + password: "PASSWORD" + # Working credentials, no need to replace hps: secret_api_key: "skapi_cert_MYl2AQAowiQAbLp5JesGKh7QFkcizOP2jcX9BrEMqQ" +hps_echeck: + secret_api_key: + iats_payments: agent_code: TEST88 password: TEST88 @@ -384,6 +507,20 @@ instapay: login: TEST0 password: +ipg: + store_id: "YOUR STORE ID" + user_id: "YOUR USER ID" + password: "YOUR PASSWORD" + 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 @@ -403,6 +540,12 @@ iveri: cert_id: CB69E68D-C7E7-46B9-9B7A-025DCABAD6EF app_id: d10a603d-4ade-405b-93f1-826dfc0181e8 +ixopay: + username: USERNAME + api_key: API_KEY + password: PASSWORD + secret: SHARED_SECRET + jetpay: login: TESTTERMINAL @@ -472,7 +615,7 @@ merchant_warrior: # Working credentials, no need to replace mercury: - login: '089716741701445' + login: '62589006=TEST' password: 'xyz' mercury_no_tokenization: @@ -495,32 +638,40 @@ migs: advanced_login: activemerchant advanced_password: test12345 +# Working credentials, no need to replace +mit: + commerce_id: '147' + user: IVCA33721 + api_key: IGECPJ0QOJJCEHUI + key_session: CB0DC4887DD1D5CEA205E66EE934E430 + test: true + modern_payments: login: login password: password +moka: + dealer_code: DEALER_CODE + username: USERNAME + password: PASSWORD + # Working credentials, no need to replace monei: - sender_id: 8a829417488d34c401489a5cd1350977 - channel_id: 8a829417488d34c401489a5de5dd097b - login: 8a829417488d34c401489a5cd1360979 - pwd: GyNSEAp2 + api_key: pk_test_3cb2d54b7ee145fa92d683c01816ad15 # Working credentials, no need to replace moneris: - login: store1 + login: store3 password: yesguy -moneris_us: - login: monusqa002 - password: qatoken - money_movers: login: demo password: password mundipagg: api_key: api_key + gateway_affiliation_id: gateway_affiliation_id + # left for backward-compatibility gateway_id: gateway_id # Working credentials, no need to replace @@ -568,6 +719,14 @@ nmi: login: demo password: password +nmi_secure: + security_key: '6457Thfj624V5r7WUwc5v6a68Zsd6YEm' + +nuvei: + merchant_id: 'merchantId' + merchant_site_id: 'siteId' + secret_key: 'secretKey' + ogone: login: LOGIN user: USER @@ -586,8 +745,7 @@ openpay: merchant_id: 'mzdtln0bmtms6o3kck8f' opp: - user_id: '8a8294174b7ecb28014b9699220015cc' - password: 'sy6KJsT8' + access_token: 'OGE4Mjk0MTc0YjdlY2IyODAxNGI5Njk5MjIwMDE1Y2N8c3k2S0pzVDg=' entity_id: '8a8294174d0a8edd014d242337942575' optimal_payment: @@ -600,6 +758,16 @@ orbital_gateway: password: PASSWORD merchant_id: MERCHANTID +orbital_tandem_gateway: + login: LOGIN + 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' @@ -610,6 +778,10 @@ pago_facil: merchant_id: 62ad6f592ecf2faa87ef2437ed85a4d175e73c58 service_id: 3 +# Working credentials, no need to replace +pay_arc: + api_key: APIKEY + # Working credentials, no need to replace pay_conex: account_id: '220614968961' @@ -642,10 +814,20 @@ pay_secure: login: LOGIN password: PASSWORD +pay_trace: + username: USERNAME + password: PASSWORD + integrator_id: INTEGRATOR_ID + paybox_direct: - login: 199988863 + login: 1999888 password: 1999888I - rang: 85 + rang: 222 + credit_card_ok_3ds: 4012001037141112 + credit_card_nok_3ds: 4012001037141113 + credit_card_ok_3ds_not_enrolled: 4012001038443335 + credit_card_ok: 1111222233334444 + credit_card_nok: 1111222233334445 # Working credentials, no need to replace payeezy: @@ -677,6 +859,10 @@ paymentez: application_code: APPCODE app_key: APPKEY +paymentez_ecuador: + application_code: APPCODE + app_key: APPKEY + paymill: private_key: a9580be4a7b9d0151a3da88c6c935ce0 public_key: 57313835619696ac361dc591bc973626 @@ -724,6 +910,11 @@ paypal_signature: password: HBC6A84QLRWC923A signature: AFcWxV21C7fd0v3bYYYRCpSSRl31AC-11AKBL8FFO9tjImL311y8a0hx +paysafe: + login: SOMECREDENTIAL + secret_key: ANOTHERCREDENTIAL + account_id: CREDENTIAL + payscout: username: demo password: password @@ -748,13 +939,30 @@ payway: password: pem: +# Working credentials, no need to replace +payway_dot_com: + login: "sprerestwsdev" + password: "sprerestwsdev1!" + company_id: "3" + source_id: "67" + pin: api_key: "I_mo9BUUUXIwXF-avcs3LA" +plexo: + client_id: YOUR_CLIENT_ID + api_key: YOUR_API_KEY + merchant_id: YOUR_MERCHANT_ID + plugnpay: login: LOGIN password: PASSWORD +priority: + api_key: SANDBOX_KEY + secret: SECRET + merchant_id: MERCHANT_ID + # Working credentials, no need to replace pro_pay: cert_str: "5ab9cddef2e4911b77e0c4ffb70f03" @@ -833,8 +1041,17 @@ quantum: password: Y # You will need to create a developer sandbox at https://developer.intuit.com/ and -# successfully generate an OAuth 1.0a access token and token secret. +# successfully generate an OAuth 2.0 access token and refresh_token. Access token +# expires every 60 minutes quickbooks: + client_id: + client_secret: + refresh_token: + access_token: + +# You will need to create a developer sandbox at https://developer.intuit.com/ and +# successfully generate an OAuth 1.0a access token and token secret. +quickbooks_oauth_1: consumer_key: consumer_secret: access_token: @@ -858,6 +1075,11 @@ quickpay_with_api_key: apikey: "fB46983ZwR5dzy46A3r6ngDx7P37N5YTu1F4S9W2JKCs9v4t5L9m2Q8Mlbjpa2I1" version: 7 +# To get this credential, log into: https://quickstream.support.qvalent.com/->click Administration->Facility Settings-> +# Authentication and Credentials->update "Technology" to SOAP and save->create cert and download .pfx file-> +# use the following command to get the Bag Attributes: +# $ openssl pkcs12 -in filename.pfx -out cert.pem -nodes +# open cert.pem file in a text editor (e.g. TextEdit) and you'll see the Bag Attributes to add below under "pem:" field qvalent: username: "QRSL" password: "QRSLTEST" @@ -956,11 +1178,19 @@ qvalent: RFjxWKtn9pXbM1PLmUXCkKQnSJSeD1K0NjV+g8KFChTEgmhnLogyF/7YYw/amfc= -----END CERTIFICATE----- +rapyd: + access_key: SOMECREDENTIAL + secret_key: ANOTHERCREDENTIAL + raven_pac_net: user: ernest secret: "all good men die young" prn: 987743 +reach: + merchant_id: 'xxxxxxx' + secret: 'xxxxxxx' + realex: login: X password: Y @@ -1009,6 +1239,12 @@ realex_visa: year: '2020' verification_value: '123' +realex_visa_3ds_enrolled: + number: '4012001037141112' + month: '9' + year: '2021' + verification_value: '123' + realex_visa_coms_error: number: '4009830000001985' month: '6' @@ -1042,6 +1278,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 @@ -1093,6 +1335,18 @@ secure_pay_tech: securion_pay: secret_key: pr_test_qZN4VVIKCySfCeXCBoHO9DBe +shift4: + client_guid: YOUR_CLIENT_ID + auth_token: YOUR_AUTH_TOKEN + +shift4_v2: + secret_key: pr_test_xxxxxxxxx + +# Working credentials, no need to replace +simetrik: + client_id: 'wNhJBdrKDk3vTmkQMAWi5zWN7y21adO3' + client_secret: 'fq2riPpiDJaAwS4_UMAXZy1_nU1jNGz0F6gAFWOJFNmm_TfC8EFiHwMmGKAEDkwY' + # Replace with your serial numbers for the skipjack test environment skipjack: login: X @@ -1114,13 +1368,21 @@ stripe: # Working credentials, no need to replace stripe_destination: - stripe_user_id: "acct_17FRNfIPBJTitsen" + stripe_user_id: "acct_1K5HlrPT5iqVqrJn" # Externally verified bank account for testing 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 @@ -1136,10 +1398,15 @@ tns: userid: TESTSPREEDLY01 password: 3f34fe50334fbe6cbe04c283411a5860 +# This account seem to have expired tns_ap: userid: TESTUNISOLMAA01 password: b7f8119fda3bd27c17656badb52c95bb +tns_pay_mode: + userid: USERID + password: password + trans_first: login: 45567 password: TNYYKYMFZ59HSN7Q @@ -1171,6 +1438,7 @@ trexle: trust_commerce: login: 'TestMerchant' password: 'password' + aggregator_id: 'abc123' # Working credentials, no need to replace usa_epay: @@ -1197,6 +1465,10 @@ verify: login: 'demo' password: 'password' +versa_pay: + api_token: API_TOKEN + api_key: API_KEY + viaklix: login: LOGIN password: PASSWORD @@ -1209,6 +1481,13 @@ visanet_peru: merchant_id: "543025501" ruc: '20341198217' +vpos: + public_key: SOMECREDENTIAL + private_key: ANOTHERCREDENTIAL + encryption_key: "-----BEGIN PUBLIC KEY-----\n ..." + commerce: 123 + commerce_branch: 45 + webpay: login: "test_secret_eHn4TTgsGguBcW764a2KA8Yd" @@ -1230,6 +1509,10 @@ wirecard_checkout_page: shop_id: '' paymenttype: IDL +wompi: + test_public_key: SOMECREDENTIAL + test_private_key: ANOTHERCREDENTIAL + # Working credentials, no need to replace world_net: terminal_id: '6001' @@ -1251,3 +1534,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 a5d646f182b..31810c54271 100644 --- a/test/remote/gateways/remote_adyen_test.rb +++ b/test/remote/gateways/remote_adyen_test.rb @@ -6,33 +6,186 @@ def setup @amount = 100 - @credit_card = credit_card('4111111111111111', - :month => 8, - :year => 2018, - :first_name => 'John', - :last_name => 'Smith', - :verification_value => '737', - :brand => 'visa' + @bank_account = check(account_number: '123456789', routing_number: '121000358') + + @adyen_bank_account = check(account_number: '9876543210', routing_number: '021000021') + + @declined_bank_account = check(account_number: '123456789', routing_number: '121000348') + + @general_bank_account = check(name: 'A. Klaassen', account_number: '123456789', routing_number: 'NL13TEST0123456789') + + @credit_card = credit_card( + '4111111111111111', + month: 3, + year: 2030, + first_name: 'John', + last_name: 'Smith', + verification_value: '737', + brand: 'visa' + ) + + @avs_credit_card = credit_card( + '4400000000000008', + month: 3, + year: 2030, + first_name: 'John', + last_name: 'Smith', + verification_value: '737', + brand: 'visa' + ) + + @elo_credit_card = credit_card( + '5066 9911 1111 1118', + month: 3, + year: 2030, + first_name: 'John', + last_name: 'Smith', + verification_value: '737', + brand: 'elo' + ) + + @three_ds_enrolled_card = credit_card( + '4917610000000000', + month: 3, + year: 2030, + verification_value: '737', + brand: :visa + ) + + @cabal_credit_card = credit_card( + '6035 2277 1642 7021', + month: 3, + year: 2030, + first_name: 'John', + last_name: 'Smith', + verification_value: '737', + brand: 'cabal' + ) + + @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' + ) + + @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' + ) + + @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' ) @declined_card = credit_card('4000300011112220') - @apple_pay_card = network_tokenization_credit_card('4111111111111111', - :payment_cryptogram => 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', - :month => '08', - :year => '2018', - :source => :apple_pay, - :verification_value => nil + @improperly_branded_maestro = credit_card( + '5500000000000004', + month: 8, + year: 2018, + first_name: 'John', + last_name: 'Smith', + verification_value: '737', + brand: 'mastercard' ) + @apple_pay_card = network_tokenization_credit_card( + '4761209980011439', + payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', + month: '11', + year: '2022', + source: :apple_pay, + verification_value: 569 + ) + + @google_pay_card = network_tokenization_credit_card( + '4761209980011439', + payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', + month: '11', + year: '2022', + source: :google_pay, + verification_value: nil + ) + + @nt_credit_card = network_tokenization_credit_card( + '4895370015293175', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + eci: '07', + source: :network_token, + 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', - shopper_email: 'john.smith@test.com', - shopper_ip: '77.110.174.153', + email: 'john.smith@test.com', + ip: '77.110.174.153', + shopper_reference: 'John Smith', + billing_address: @us_address, + order_id: '123', + stored_credential: { reason_type: 'unscheduled' } + } + + @normalized_3ds_2_options = { + reference: '345123', + email: 'john.smith@test.com', + ip: '77.110.174.153', shopper_reference: 'John Smith', billing_address: address(), - order_id: '123' + order_id: '123', + stored_credential: { reason_type: 'unscheduled' }, + three_ds_2: { + channel: 'browser', + notification_url: 'https://example.com/notification', + browser_info: { + accept_header: 'unknown', + depth: 48, + java: false, + language: 'US', + height: 1000, + width: 500, + timezone: '-120', + user_agent: 'unknown' + } + } } + + @long_order_id = 'asdfjkl;asdfjkl;asdfj;aiwyutinvpoaieryutnmv;203987528752098375j3q-p489756ijmfpvbijpq348nmdf;vbjp3845' end def test_successful_authorize @@ -41,10 +194,302 @@ def test_successful_authorize assert_equal 'Authorised', response.message end + def test_successful_authorize_with_bank_account + response = @gateway.authorize(@amount, @bank_account, @options) + assert_success response + assert_equal 'Authorised', response.message + end + + def test_successful_authorize_avs + # Account configuration may need to be done: https://docs.adyen.com/developers/api-reference/payments-api#paymentresultadditionaldata + options = @options.update({ + billing_address: { + address1: 'Infinite Loop', + address2: 1, + country: 'US', + city: 'Cupertino', + state: 'CA', + zip: '95014' + } + }) + response = @gateway.authorize(@amount, @avs_credit_card, options) + assert_success response + assert_equal 'Authorised', response.message + assert_equal 'D', response.avs_result['code'] + end + + def test_successful_authorize_with_idempotency_key + options = @options.merge(idempotency_key: SecureRandom.hex) + response = @gateway.authorize(@amount, @credit_card, options) + assert_success response + assert_equal 'Authorised', response.message + first_auth = response.authorization + + response = @gateway.authorize(@amount, @credit_card, options) + assert_success response + assert_equal response.authorization, first_auth + end + + def test_successful_authorize_with_3ds + assert response = @gateway.authorize(@amount, @three_ds_enrolled_card, @options.merge(execute_threed: true)) + assert response.test? + refute response.authorization.blank? + assert_equal response.params['resultCode'], 'RedirectShopper' + refute response.params['issuerUrl'].blank? + refute response.params['md'].blank? + refute response.params['paRequest'].blank? + end + + def test_successful_authorize_with_execute_threed_false + assert response = @gateway.authorize(@amount, @three_ds_enrolled_card, @options.merge(execute_threed: false, sca_exemption: 'lowValue')) + assert response.test? + refute response.authorization.blank? + assert_equal response.params['resultCode'], 'Authorised' + end + + def test_successful_authorize_with_3ds_with_idempotency_key + options = @options.merge(idempotency_key: SecureRandom.hex, execute_threed: true) + assert response = @gateway.authorize(@amount, @three_ds_enrolled_card, options) + assert response.test? + refute response.authorization.blank? + assert_equal response.params['resultCode'], 'RedirectShopper' + refute response.params['issuerUrl'].blank? + refute response.params['md'].blank? + refute response.params['paRequest'].blank? + + assert response2 = @gateway.authorize(@amount, @three_ds_enrolled_card, options) + assert_success response2 + refute response2.authorization.blank? + assert_equal response2.params['resultCode'], 'RedirectShopper' + refute response2.params['issuerUrl'].blank? + refute response2.params['md'].blank? + refute response2.params['paRequest'].blank? + assert_equal response.authorization, response2.authorization + end + + def test_successful_authorize_with_3ds_dynamic + assert response = @gateway.authorize(@amount, @three_ds_enrolled_card, @options.merge(threed_dynamic: true)) + assert response.test? + refute response.authorization.blank? + assert_equal response.params['resultCode'], 'RedirectShopper' + refute response.params['issuerUrl'].blank? + refute response.params['md'].blank? + refute response.params['paRequest'].blank? + end + + def test_successful_authorize_with_3ds2_browser_client_data + assert response = @gateway.authorize(@amount, @three_ds_enrolled_card, @normalized_3ds_2_options) + assert response.test? + refute response.authorization.blank? + + assert_equal response.params['resultCode'], 'IdentifyShopper' + refute response.params['additionalData']['threeds2.threeDS2Token'].blank? + refute response.params['additionalData']['threeds2.threeDSServerTransID'].blank? + refute response.params['additionalData']['threeds2.threeDSMethodURL'].blank? + end + + def test_successful_authorize_with_network_token + response = @gateway.authorize(@amount, @nt_credit_card, @options) + assert_success response + assert_equal 'Authorised', response.message + end + + def test_successful_purchase_with_3ds2_exemption_requested_and_execute_threed_false + assert response = @gateway.authorize(@amount, @three_ds_enrolled_card, @normalized_3ds_2_options.merge(execute_threed: false, sca_exemption: 'lowValue')) + assert response.test? + refute response.authorization.blank? + + assert_equal response.params['resultCode'], 'Authorised' + end + + # According to Adyen documentation, if execute_threed is set to true and an exemption provided + # the gateway will apply and request for the specified exemption in the authentication request, + # after the device fingerprint is submitted to the issuer. + def test_successful_purchase_with_3ds2_exemption_requested_and_execute_threed_true + assert response = @gateway.authorize(@amount, @three_ds_enrolled_card, @normalized_3ds_2_options.merge(execute_threed: true, sca_exemption: 'lowValue')) + assert response.test? + refute response.authorization.blank? + + assert_equal response.params['resultCode'], 'IdentifyShopper' + refute response.params['additionalData']['threeds2.threeDS2Token'].blank? + refute response.params['additionalData']['threeds2.threeDSServerTransID'].blank? + refute response.params['additionalData']['threeds2.threeDSMethodURL'].blank? + end + + def test_successful_authorize_with_3ds2_app_based_request + three_ds_app_based_options = { + reference: '345123', + email: 'john.smith@test.com', + ip: '77.110.174.153', + shopper_reference: 'John Smith', + billing_address: address(), + order_id: '123', + stored_credential: { reason_type: 'unscheduled' }, + three_ds_2: { + channel: 'app' + } + } + + assert response = @gateway.authorize(@amount, @three_ds_enrolled_card, three_ds_app_based_options) + assert response.test? + refute response.authorization.blank? + assert_equal response.params['resultCode'], 'IdentifyShopper' + refute response.params['additionalData']['threeds2.threeDS2Token'].blank? + refute response.params['additionalData']['threeds2.threeDSServerTransID'].blank? + refute response.params['additionalData']['threeds2.threeDS2DirectoryServerInformation.algorithm'].blank? + refute response.params['additionalData']['threeds2.threeDS2DirectoryServerInformation.directoryServerId'].blank? + refute response.params['additionalData']['threeds2.threeDS2DirectoryServerInformation.publicKey'].blank? + end + + # 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', + month: 3, + year: 2030, + first_name: 'John', + last_name: 'Smith', + verification_value: '737', + brand: 'mastercard' + ) + assert response = @gateway.authorize(@amount, mastercard_threed, @options.merge(threed_dynamic: true)) + assert response.test? + refute response.authorization.blank? + assert_equal response.params['resultCode'], 'Authorised' + end + + # Fail in situations where neither execute_threed nor dynamic_threed is + # present, but the account is set to dynamic 3ds and it is triggered. This + # test assumes a Dynamic 3DS rule set for the Adyen test account to always + # perform 3ds auth for an amount of 8484 + def test_purchase_fails_on_unexpected_3ds_initiation + response = @gateway.purchase(8484, @three_ds_enrolled_card, @options) + assert_failure response + assert_match 'Received unexpected 3DS authentication response, but a 3DS initiation flag was not included in the request.', response.message + end + + def test_successful_purchase_with_auth_data_via_threeds1_standalone + eci = '05' + cavv = '3q2+78r+ur7erb7vyv66vv\/\/\/\/8=' + cavv_algorithm = '1' + xid = 'ODUzNTYzOTcwODU5NzY3Qw==' + enrolled = 'Y' + authentication_response_status = 'Y' + options = @options.merge( + three_d_secure: { + eci:, + cavv:, + cavv_algorithm:, + xid:, + enrolled:, + authentication_response_status: + } + ) + + auth = @gateway.authorize(@amount, @credit_card, options) + assert_success auth + assert_equal 'Authorised', auth.message + # The assertion below requires the "3D Secure Result" data activated for the test account + assert_equal 'true', auth.params['additionalData']['liabilityShift'] + + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal '[capture-received]', response.message + end + + def test_successful_purchase_with_auth_data_via_threeds2_standalone + version = '2.1.0' + eci = '02' + cavv = 'jJ81HADVRtXfCBATEp01CJUAAAA=' + ds_transaction_id = '97267598-FAE6-48F2-8083-C23433990FBC' + directory_response_status = 'C' + authentication_response_status = 'Y' + + options = @options.merge( + three_d_secure: { + version:, + eci:, + cavv:, + ds_transaction_id:, + directory_response_status:, + authentication_response_status: + } + ) + + auth = @gateway.authorize(@amount, @credit_card, options) + assert_success auth + assert_equal 'Authorised', auth.message + # The assertion below requires the "3D Secure Result" data activated for the test account + assert_equal 'true', auth.params['additionalData']['liabilityShift'] + + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + end + + def test_successful_authorize_with_no_address + options = { + reference: '345123', + email: 'john.smith@test.com', + ip: '77.110.174.153', + shopper_reference: 'John Smith', + order_id: '123', + recurring_processing_model: 'CardOnFile' + } + response = @gateway.authorize(@amount, @credit_card, options) + assert_success response + assert_equal 'Authorised', response.message + end + + def test_successful_authorize_with_fund_source_and_fund_destination + fund_options = { + fund_source: { + additional_data: { fundingSource: 'Debit' }, + first_name: 'Payer', + last_name: 'Name', + billing_address: @us_address, + shopper_email: 'john.smith@test.com' + }, + fund_destination: { + additional_data: { walletIdentifier: '12345' } + } + } + + response = @gateway.authorize(@amount, @credit_card, @options.merge!(fund_options)) + assert_success response + assert_equal 'Authorised', response.message + end + + def test_successful_authorize_with_credit_card_no_name + credit_card_no_name = ActiveMerchant::Billing::CreditCard.new({ + number: '4111111111111111', + month: 3, + year: 2030, + verification_value: '737', + brand: 'visa' + }) + + response = @gateway.authorize(@amount, credit_card_no_name, @options) + assert_success response + assert_equal 'Authorised', response.message + end + 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 + response = @gateway.authorize(@amount, @declined_bank_account, @options) + assert_failure response + assert_equal 'Bank Account or Bank Location Id not valid or missing', response.message + end + + def test_failed_authorize_with_bank_account_missing_country_code + response = @gateway.authorize(@amount, @bank_account, @options.except(:billing_address)) + assert_failure response + assert_equal 'BankDetails missing', response.message end def test_successful_purchase @@ -53,6 +498,12 @@ def test_successful_purchase assert_equal '[capture-received]', response.message end + def test_successful_purchase_with_bank_account + response = @gateway.purchase(@amount, @bank_account, @options) + assert_success response + assert_equal '[capture-received]', response.message + end + def test_successful_purchase_no_cvv credit_card = @credit_card credit_card.verification_value = nil @@ -62,10 +513,73 @@ def test_successful_purchase_no_cvv end def test_successful_purchase_with_more_options - options = @options.merge!(fraudOffset: '1', installments: 2) + options = @options.merge!( + fraudOffset: '1', + installments: 2, + shopper_statement: 'statement note', + device_fingerprint: 'm7Cmrf++0cW4P6XfF7m/rA', + capture_delay_hours: 4 + ) + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal '[capture-received]', response.message + end + + def test_successful_purchase_with_risk_data + options = @options.merge( + risk_data: + { + 'operatingSystem' => 'HAL9000', + 'destinationLatitude' => '77.641423', + 'destinationLongitude' => '12.9503376' + } + ) + + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal '[capture-received]', response.message + end + + def test_successful_purchase_with_idempotency_key + options = @options.merge(idempotency_key: SecureRandom.hex) response = @gateway.purchase(@amount, @credit_card, options) assert_success response assert_equal '[capture-received]', response.message + first_auth = response.authorization + + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + 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 @@ -74,12 +588,114 @@ def test_successful_purchase_with_apple_pay assert_equal '[capture-received]', response.message end + def test_succesful_authorize_with_manual_capture + response = @gateway.authorize(@amount, @credit_card, @options.merge(manual_capture: 'true')) + assert_success response + assert_equal 'Authorised', response.message + end + + def test_succesful_purchase_with_brand_override + response = @gateway.purchase(@amount, @improperly_branded_maestro, @options.merge({ overwrite_brand: true, selected_brand: 'maestro' })) + assert_success response + assert_equal '[capture-received]', response.message + end + + def test_succesful_purchase_with_brand_override_with_execute_threed_false + response = @gateway.purchase(@amount, @improperly_branded_maestro, @options.merge({ execute_threed: false, overwrite_brand: true, selected_brand: 'maestro' })) + assert_success response + assert_equal '[capture-received]', response.message + end + + def test_successful_purchase_with_google_pay + response = @gateway.purchase(@amount, @google_pay_card, @options) + assert_success response + assert_equal '[capture-received]', response.message + end + + def test_successful_purchase_with_google_pay_pan_only + response = @gateway.purchase(@amount, @credit_card, @options.merge(wallet_type: :google_pay)) + assert_success response + assert_equal '[capture-received]', response.message + end + + def test_successful_purchase_with_google_pay_without_billing_address_and_address_override + options = { + reference: '345123', + email: 'john.smith@test.com', + ip: '77.110.174.153', + shopper_reference: 'John Smith', + billing_address: { + address1: '', + address2: '', + country: 'US', + city: 'Beverly Hills', + state: 'CA', + zip: '90210' + }, + order_id: '123', + stored_credential: { reason_type: 'unscheduled' }, + address_override: true + } + + response = @gateway.purchase(@amount, @google_pay_card, options) + assert_success response + assert_equal '[capture-received]', response.message + end + + def test_successful_purchase_with_google_pay_and_truncate_order_id + response = @gateway.purchase(@amount, @google_pay_card, @options.merge(order_id: @long_order_id)) + assert_success response + assert_equal '[capture-received]', response.message + end + + def test_successful_purchase_with_elo_card + response = @gateway.purchase(@amount, @elo_credit_card, @options.merge(currency: 'BRL')) + assert_success response + assert_equal '[capture-received]', response.message + end + + def test_successful_purchase_with_cabal_card + response = @gateway.purchase(@amount, @cabal_credit_card, @options.merge(currency: 'ARS')) + assert_success response + assert_equal '[capture-received]', response.message + end + + def test_successful_purchase_with_unionpay_card + response = @gateway.purchase(@amount, @unionpay_credit_card, @options.merge(currency: 'CNY')) + assert_success response + assert_equal '[capture-received]', response.message + end + + def test_successful_purchase_with_network_token + response = @gateway.purchase(@amount, @nt_credit_card, @options) + assert_success response + assert_equal '[capture-received]', response.message + end + def test_failed_purchase response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response assert_equal 'Refused', response.message end + def test_failed_purchase_with_bank_account + response = @gateway.purchase(@amount, @declined_bank_account, @options) + assert_failure response + assert_equal 'Bank Account or Bank Location Id not valid or missing', response.message + end + + def test_failed_purchase_with_invalid_cabal_card + response = @gateway.purchase(@amount, @invalid_cabal_credit_card, @options) + assert_failure response + assert_equal 'Invalid card number', response.message + end + + def test_failed_purchase_with_invalid_unionpay_card + response = @gateway.purchase(@amount, @invalid_unionpay_credit_card, @options) + assert_failure response + assert_equal 'Invalid card number', response.message + end + def test_successful_authorize_and_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth @@ -89,11 +705,56 @@ def test_successful_authorize_and_capture assert_equal '[capture-received]', capture.message end + def test_successful_authorize_and_capture_with_elo_card + auth = @gateway.authorize(@amount, @elo_credit_card, @options) + assert_success auth + + assert capture = @gateway.capture(@amount, auth.authorization) + assert_success capture + assert_equal '[capture-received]', capture.message + end + + def test_successful_authorize_and_capture_with_cabal_card + auth = @gateway.authorize(@amount, @cabal_credit_card, @options) + assert_success auth + + assert capture = @gateway.capture(@amount, auth.authorization) + assert_success capture + assert_equal '[capture-received]', capture.message + end + + def test_successful_authorize_and_capture_with_unionpay_card + auth = @gateway.authorize(@amount, @unionpay_credit_card, @options) + assert_success auth + + assert capture = @gateway.capture(@amount, auth.authorization) + assert_success capture + assert_equal '[capture-received]', capture.message + end + + def test_error_code_render_from_response + options = { + order_id: '123', + email: 'shopper@sky.uk', + billing_address: { + address2: 'address2', + zip: '31331', + city: 'Wanaque', + state: 'NJ', + country: 'IE' + }, + delivery_date: 'invalid' + } + response = @gateway.authorize(@amount, @credit_card, options) + assert_failure response + assert_equal '702', response.error_code + end + def test_partial_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture end @@ -112,11 +773,65 @@ def test_successful_refund assert_equal '[refund-received]', refund.message end + def test_successful_refund_with_bank_account + purchase = @gateway.purchase(@amount, @bank_account, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization) + assert_success refund + assert_equal '[refund-received]', refund.message + end + + def test_successful_refund_with_auth_original_reference + auth_response = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth_response + assert_equal 'Authorised', auth_response.message + + refund_resp = @gateway.refund(@amount, auth_response.authorization) + assert_success refund_resp + assert_equal '[refund-received]', refund_resp.message + end + + def test_successful_refund_with_elo_card + purchase = @gateway.purchase(@amount, @elo_credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization) + assert_success refund + assert_equal '[refund-received]', refund.message + end + + def test_successful_refund_with_cabal_card + purchase = @gateway.purchase(@amount, @cabal_credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization) + assert_success refund + assert_equal '[refund-received]', refund.message + end + + def test_successful_refund_with_unionpay_card + purchase = @gateway.purchase(@amount, @unionpay_credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization) + assert_success refund + assert_equal '[refund-received]', refund.message + end + def test_partial_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount-1, purchase.authorization) + assert refund = @gateway.refund(@amount - 1, purchase.authorization) + assert_success refund + end + + def test_partial_refund_with_bank_account + purchase = @gateway.purchase(@amount, @bank_account, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount - 1, purchase.authorization) assert_success refund end @@ -126,23 +841,304 @@ def test_failed_refund assert_equal 'Original pspReference required for this operation', response.message end + def test_successful_credit + response = @gateway.credit(@amount, @credit_card, @options) + + assert_success response + assert_equal 'Received', response.message + end + + def test_successful_credit_with_bank_account + @options[:currency] = 'EUR' + @options[:billing_address][:country] = 'NL' + response = @gateway.credit(1500, @general_bank_account, @options) + + assert_success response + assert_equal 'Received', response.message + end + + def test_failed_credit + response = @gateway.credit(@amount, '') + assert_failure response + 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 - assert void = @gateway.void(auth.authorization) - assert_success void - assert_equal '[cancel-received]', void.message + assert void = @gateway.void(auth.authorization) + assert_success void + assert_equal '[cancel-received]', void.message + end + + def test_successful_void_with_bank_account + auth = @gateway.authorize(@amount, @bank_account, @options) + assert_success auth + + assert void = @gateway.void(auth.authorization) + assert_success void + assert_equal '[cancel-received]', void.message + end + + def test_successful_void_with_elo_card + auth = @gateway.authorize(@amount, @elo_credit_card, @options) + assert_success auth + + assert void = @gateway.void(auth.authorization) + assert_success void + assert_equal '[cancel-received]', void.message + end + + def test_successful_void_with_cabal_card + auth = @gateway.authorize(@amount, @cabal_credit_card, @options) + assert_success auth + + assert void = @gateway.void(auth.authorization) + assert_success void + assert_equal '[cancel-received]', void.message + end + + def test_successful_void_with_unionpay_card + auth = @gateway.authorize(@amount, @unionpay_credit_card, @options) + assert_success auth + + assert void = @gateway.void(auth.authorization) + assert_success void + assert_equal '[cancel-received]', void.message + end + + def test_successul_void_of_pending_3ds_authorization + assert auth = @gateway.authorize(@amount, @three_ds_enrolled_card, @options.merge(execute_threed: true)) + assert auth.test? + refute auth.authorization.blank? + assert_equal auth.params['resultCode'], 'RedirectShopper' + refute auth.params['issuerUrl'].blank? + refute auth.params['md'].blank? + refute auth.params['paRequest'].blank? + + assert void = @gateway.void(auth.authorization) + assert_success void + assert_equal '[cancel-received]', void.message + end + + def test_successful_void_requires_unique_idempotency_key + idempotency_key = SecureRandom.hex + options = @options.merge(idempotency_key:) + auth = @gateway.authorize(@amount, @credit_card, options) + assert_success auth + + assert void = @gateway.void(auth.authorization, idempotency_key:) + assert_failure void + + assert void = @gateway.void(auth.authorization, idempotency_key: "#{idempotency_key}-auto-void") + assert_success void + assert_equal '[cancel-received]', void.message + end + + def test_failed_void + response = @gateway.void('') + assert_failure response + assert_equal 'Original pspReference required for this operation', response.message + end + + def test_successful_asynchronous_adjust + authorize = @gateway.authorize(@amount, @credit_card, @options.merge(authorisation_type: 'PreAuth')) + assert_success authorize + + assert adjust = @gateway.adjust(200, authorize.authorization, @options) + assert_success adjust + assert_equal '[adjustAuthorisation-received]', adjust.message + end + + def test_successful_asynchronous_adjust_and_capture + authorize = @gateway.authorize(@amount, @credit_card, @options.merge(authorisation_type: 'PreAuth')) + assert_success authorize + + assert adjust = @gateway.adjust(200, authorize.authorization, @options) + assert_success adjust + assert_equal '[adjustAuthorisation-received]', adjust.message + + assert capture = @gateway.capture(200, authorize.authorization) + assert_success capture + end + + def test_failed_asynchronous_adjust + authorize = @gateway.authorize(@amount, @credit_card, @options.merge(authorisation_type: 'PreAuth')) + assert_success authorize + + assert response = @gateway.adjust(200, '', @options) + assert_failure response + assert_equal 'Original pspReference required for this operation', response.message + end + + # Requires Adyen to set your test account to Synchronous Adjust mode. + def test_successful_synchronous_adjust_using_adjust_data + authorize = @gateway.authorize(@amount, @credit_card, @options.merge(authorisation_type: 'PreAuth', shopper_statement: 'statement note')) + assert_success authorize + + options = @options.merge(adjust_authorisation_data: authorize.params['additionalData']['adjustAuthorisationData'], update_shopper_statement: 'new statement note', industry_usage: 'DelayedCharge') + assert adjust = @gateway.adjust(200, authorize.authorization, options) + assert_success adjust + assert_equal 'Authorised', adjust.message + end + + # Requires Adyen to set your test account to Synchronous Adjust mode. + def test_successful_synchronous_adjust_and_capture + authorize = @gateway.authorize(@amount, @credit_card, @options.merge(authorisation_type: 'PreAuth')) + assert_success authorize + + options = @options.merge(adjust_authorisation_data: authorize.params['additionalData']['adjustAuthorisationData']) + assert adjust = @gateway.adjust(200, authorize.authorization, options) + assert_success adjust + assert_equal 'Authorised', adjust.message + + assert capture = @gateway.capture(200, authorize.authorization) + assert_success capture + end + + # Requires Adyen to set your test account to Synchronous Adjust mode. + def test_failed_synchronous_adjust_using_adjust_data + authorize = @gateway.authorize(@amount, @credit_card, @options.merge(authorisation_type: 'PreAuth')) + assert_success authorize + + options = @options.merge(adjust_authorisation_data: authorize.params['additionalData']['adjustAuthorisationData'], + requested_test_acquirer_response_code: '2') + assert adjust = @gateway.adjust(200, authorize.authorization, options) + assert_failure adjust + assert_equal 'Refused', adjust.message + end + + def test_successful_store + assert response = @gateway.store(@credit_card, @options) + + assert_success response + assert !response.authorization.split('#')[2].nil? + assert_equal 'Authorised', response.message + end + + def test_successful_store_with_bank_account + assert response = @gateway.store(@bank_account, @options) + + assert_success response + assert !response.authorization.split('#')[2].nil? + assert_equal 'Authorised', response.message + end + + def test_successful_unstore + assert response = @gateway.store(@credit_card, @options) + assert !response.authorization.split('#')[2].nil? + assert_equal 'Authorised', response.message + + shopper_reference = response.params['additionalData']['recurring.shopperReference'] + recurring_detail_reference = response.params['additionalData']['recurring.recurringDetailReference'] + + assert response = @gateway.unstore(shopper_reference:, + recurring_detail_reference:) + + assert_success response + assert_equal '[detail-successfully-disabled]', response.message + end + + def test_successful_unstore_with_bank_account + assert response = @gateway.store(@adyen_bank_account, @options) + + assert !response.authorization.split('#')[2].nil? + assert_equal 'Authorised', response.message + + shopper_reference = response.params['additionalData']['recurring.shopperReference'] + recurring_detail_reference = response.params['additionalData']['recurring.recurringDetailReference'] + + assert response = @gateway.unstore(shopper_reference:, + recurring_detail_reference:) + + assert_success response + assert_equal '[detail-successfully-disabled]', response.message + end + + def test_failed_unstore + assert response = @gateway.store(@credit_card, @options) + + assert !response.authorization.split('#')[2].nil? + assert_equal 'Authorised', response.message + + shopper_reference = response.params['additionalData']['recurring.shopperReference'] + recurring_detail_reference = response.params['additionalData']['recurring.recurringDetailReference'] + + assert response = @gateway.unstore(shopper_reference: 'random_reference', + recurring_detail_reference:) + + assert_failure response + assert_equal 'Contract not found', response.message + + assert response = @gateway.unstore(shopper_reference:, + recurring_detail_reference: 'random_reference') + + assert_failure response + assert_equal 'PaymentDetail not found', response.message + end + + def test_successful_tokenize_only_store + assert response = @gateway.store(@credit_card, @options.merge({ tokenize_only: true })) + + assert_success response + assert !response.authorization.split('#')[2].nil? + assert_equal 'Success', response.message + end + + def test_successful_tokenize_only_store_with_ntid + assert response = @gateway.store(@credit_card, @options.merge({ tokenize_only: true, network_transaction_id: '858435661128555' })) + + assert_success response + assert !response.authorization.split('#')[2].nil? + assert_equal 'Success', response.message + end + + def test_successful_store_with_elo_card + assert response = @gateway.store(@elo_credit_card, @options) + + assert_success response + assert !response.authorization.split('#')[2].nil? + assert_equal 'Authorised', response.message end - def test_failed_void - response = @gateway.void('') - assert_failure response - assert_equal 'Original pspReference required for this operation', response.message + def test_successful_store_with_cabal_card + assert response = @gateway.store(@cabal_credit_card, @options) + assert_success response end - def test_successful_store - assert response = @gateway.store(@credit_card, @options) + def test_successful_store_with_unionpay_card + assert response = @gateway.store(@unionpay_credit_card, @options) assert_success response assert !response.authorization.split('#')[2].nil? @@ -165,6 +1161,15 @@ def test_successful_purchase_using_stored_card assert_equal '[capture-received]', response.message end + def test_successful_purchase_using_stored_elo_card + assert store_response = @gateway.store(@elo_credit_card, @options) + assert_success store_response + + response = @gateway.purchase(@amount, store_response.authorization, @options) + assert_success response + assert_equal '[capture-received]', response.message + end + def test_successful_authorize_using_stored_card assert store_response = @gateway.store(@credit_card, @options) assert_success store_response @@ -180,12 +1185,39 @@ def test_successful_verify assert_match 'Authorised', response.message end + def test_successful_verify_with_custom_amount + response = @gateway.verify(@credit_card, @options.merge({ verify_amount: '500' })) + assert_success response + assert_match 'Authorised', response.message + end + + def test_successful_verify_with_bank_account + response = @gateway.verify(@bank_account, @options) + assert_success response + assert_match 'Authorised', response.message + end + def test_failed_verify response = @gateway.verify(@declined_card, @options) assert_failure response assert_match 'Refused', response.message end + def test_verify_with_idempotency_key + options = @options.merge(idempotency_key: SecureRandom.hex) + response = @gateway.authorize(0, @credit_card, options) + assert_success response + assert_equal 'Authorised', response.message + first_auth = response.authorization + + response = @gateway.verify(@credit_card, options) + assert_success response + assert_equal response.authorization, first_auth + + response = @gateway.void(first_auth, @options) + assert_success response + end + def test_invalid_login gateway = AdyenGateway.new(username: '', password: '', merchant_account: '') @@ -204,6 +1236,17 @@ def test_transcript_scrubbing assert_scrubbed(@gateway.options[:password], transcript) end + def test_transcript_scrubbing_with_bank_account + 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) + assert_scrubbed(@gateway.options[:password], transcript) + end + def test_transcript_scrubbing_network_tokenization_card transcript = capture_transcript(@gateway) do @gateway.purchase(@amount, @apple_pay_card, @options) @@ -233,7 +1276,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 '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 @@ -269,17 +1312,569 @@ def test_missing_house_number_or_name_for_purchase assert_success response end - def test_invalid_country_for_purchase + def test_missing_state_for_purchase + @options[:billing_address].delete(:state) + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + end + + 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 + @options[:billing_address][:state] = nil + response = @gateway.authorize(@amount, @credit_card, @options) + 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_invalid_state_for_purchase + def test_blank_state_for_purchase @options[:billing_address][:state] = '' response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + end + + def test_missing_phone_for_purchase + @options[:billing_address].delete(:phone) + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + end + + def test_successful_auth_application_info + options = @options.merge!( + externalPlatform: { + name: 'Acme', + version: '1', + integrator: 'abc' + }, + merchantApplication: { + name: 'Acme Inc.', + version: '2' + } + ) + response = @gateway.authorize(@amount, @credit_card, options) + assert_success response + end + + def test_successful_authorize_phone + @options[:billing_address][:phone] = '1234567890' + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + end + + def test_successful_authorize_phone_number + @options[:billing_address].delete(:phone) + @options[:billing_address][:phone_number] = '0987654321' + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + end + + def test_purchase_using_stored_credential_recurring_cit + initial_options = stored_credential_options(:cardholder, :recurring, :initial) + assert auth = @gateway.authorize(@amount, @credit_card, initial_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 = stored_credential_options(:recurring, :cardholder, ntid: auth.network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + end + + def test_purchase_using_stored_credential_recurring_mit + initial_options = stored_credential_options(:merchant, :recurring, :initial) + assert auth = @gateway.authorize(@amount, @credit_card, initial_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 = stored_credential_options(:recurring, :cardholder, ntid: auth.network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + end + + def test_purchase_using_stored_credential_unscheduled_cit + initial_options = stored_credential_options(:cardholder, :unscheduled, :initial) + assert auth = @gateway.authorize(@amount, @credit_card, initial_options) + assert_success auth + assert_equal 'CardOnFile', auth.params['additionalData']['recurringProcessingModel'] + assert capture = @gateway.capture(@amount, auth.authorization) + assert_success capture + assert_equal '[capture-received]', capture.message + + used_options = stored_credential_options(:unscheduled, :cardholder, ntid: auth.network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + end + + def test_purchase_using_stored_credential_unscheduled_mit + initial_options = stored_credential_options(:merchant, :unscheduled, :initial) + assert auth = @gateway.authorize(@amount, @credit_card, initial_options) + assert_success auth + assert_equal 'UnscheduledCardOnFile', auth.params['additionalData']['recurringProcessingModel'] + assert capture = @gateway.capture(@amount, auth.authorization) + assert_success capture + assert_equal '[capture-received]', capture.message + + used_options = stored_credential_options(:unscheduled, :cardholder, ntid: auth.network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + end + + def test_auth_and_capture_with_network_txn_id + initial_options = stored_credential_options(:merchant, :recurring, :initial) + assert auth = @gateway.authorize(@amount, @credit_card, initial_options) + assert auth.network_transaction_id + + assert capture = @gateway.capture(@amount, auth.authorization, @options.merge(network_transaction_id: auth.network_transaction_id)) + assert_success capture + end + + def test_auth_and_capture_with_network_txn_id_from_stored_cred_hash + initial_options = stored_credential_options(:merchant, :recurring, :initial) + assert auth = @gateway.authorize(@amount, @credit_card, initial_options) + assert auth.network_transaction_id + + assert capture = @gateway.capture(@amount, auth.authorization, @options.merge(stored_credential: { network_transaction_id: auth.network_transaction_id })) + assert_success capture + end + + def test_auth_capture_refund_with_network_txn_id + initial_options = stored_credential_options(:merchant, :recurring, :initial) + assert auth = @gateway.authorize(@amount, @credit_card, initial_options) + assert auth.network_transaction_id + + assert capture = @gateway.capture(@amount, auth.authorization, @options.merge(network_transaction_id: auth.network_transaction_id)) + assert_success capture + + assert refund = @gateway.refund(@amount, auth.authorization, @options.merge(network_transaction_id: auth.network_transaction_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_successful_capture_with_localized_shopper_statement + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert capture = @gateway.capture(@amount, auth.authorization, @options.merge(localized_shopper_statement: { 'ja-Kana' => 'ADYEN - セラーA' })) + assert_success capture + end + + def test_successful_refund_with_localized_shopper_statement + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization, @options.merge(localized_shopper_statement: { 'ja-Kana' => 'ADYEN - セラーA' })) + assert_success refund + 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, + 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', + sub_merchant_name: 'Wildsea', + sub_merchant_street: '1234 Street St', + sub_merchant_city: 'Night City', + sub_merchant_state: 'East Block', + sub_merchant_postal_code: '112233', + sub_merchant_country: 'EUR', + sub_merchant_tax_id: '12345abcde67', + sub_merchant_mcc: '1234' + } + options = @options.update({ + installments: 2, + billing_address: { + address1: 'Infinite Loop', + address2: 1, + country: 'US', + city: 'Cupertino', + state: 'CA', + zip: '95014' + } + }) + assert response = @gateway.authorize(@amount, @avs_credit_card, options.merge(sub_merchant_data)) + assert response.test? + refute response.authorization.blank? + assert_success response + 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', + customer_reference: '101' + } + assert response = @gateway.authorize(@amount, @avs_credit_card, @options.merge(level_2_data:)) + assert response.test? + refute response.authorization.blank? + assert_success response + end + + def test_successful_purchase_with_level_2_data + level_2_data = { + total_tax_amount: '160', + customer_reference: '101' + } + response = @gateway.purchase(@amount, @credit_card, @options.merge(level_2_data:)) + assert_success response + assert_equal '[capture-received]', response.message + end + + def test_successful_response_with_recurring_detail_reference + response = @gateway.purchase(@amount, @credit_card, @options.merge(recurring_detail_reference: '12345')) + + assert_success response + assert_equal '[capture-received]', response.message + end + + def test_successful_authorize_with_level_3_data + level_3_data = { + total_tax_amount: '12800', + customer_reference: '101', + freight_amount: '300', + destination_state_province_code: 'NYC', + ship_from_postal_code: '1082GM', + order_date: '101216', + destination_postal_code: '1082GM', + destination_country_code: 'NLD', + duty_amount: '500', + items: [ + { + description: 'T16 Test products 1', + product_code: 'TEST120', + commodity_code: 'COMMCODE1', + quantity: '5', + unit_of_measure: 'm', + unit_price: '1000', + discount_amount: '60', + total_amount: '4940' + } + ] + } + assert response = @gateway.authorize(@amount, @avs_credit_card, @options.merge(level_3_data:)) + assert response.test? + assert_success response + end + + def test_successful_purchase_with_level_3_data + level_3_data = { + total_tax_amount: '12800', + customer_reference: '101', + freight_amount: '300', + destination_state_province_code: 'NYC', + ship_from_postal_code: '1082GM', + order_date: '101216', + destination_postal_code: '1082GM', + destination_country_code: 'NLD', + duty_amount: '500', + items: [ + { + description: 'T16 Test products 1', + product_code: 'TEST120', + commodity_code: 'COMMCODE1', + quantity: '5', + unit_of_measure: 'm', + unit_price: '1000', + discount_amount: '60', + total_amount: '4940' + } + ] + } + response = @gateway.purchase(@amount, @credit_card, @options.merge(level_3_data:)) + assert_success response + assert_equal '[capture-received]', response.message + end + + def test_succesful_purchase_with_airline_data + airline_data = { + agency_invoice_number: 'BAC123', + agency_plan_name: 'plan name', + airline_code: '434234', + airline_designator_code: '1234', + boarding_fee: '100', + computerized_reservation_system: 'abcd', + customer_reference_number: 'asdf1234', + document_type: 'cc', + flight_date: '2023-09-08', + ticket_issue_address: 'abcqwer', + ticket_number: 'ABCASDF', + travel_agency_code: 'ASDF', + travel_agency_name: 'hopper', + passenger_name: 'Joe Doe', + leg: { + carrier_code: 'KL', + class_of_travel: 'F' + }, + passenger: { + first_name: 'Joe', + last_name: 'Doe', + telephone_number: '432211111' + } + } + + response = @gateway.purchase(@amount, @credit_card, @options.merge(additional_data_airline: airline_data)) + assert_success response + assert_equal '[capture-received]', response.message + end + + def test_succesful_purchase_with_airline_data_with_legs + 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', + legs: [{ + 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 + + assert cancel = @gateway.void(auth.authorization) + assert_success cancel + assert_equal '[cancel-received]', cancel.message + + capture = @gateway.capture(@amount, auth.authorization) + assert_success capture + + @options[:cancel_or_refund] = true + assert void = @gateway.void(auth.authorization, @options) + assert_success void + assert_equal '[cancelOrRefund-received]', void.message + assert_void_references_original_authorization(void, auth) + end + + def test_successful_cancel_or_refund_passing_purchase + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + @options[:cancel_or_refund] = true + assert void = @gateway.void(purchase.authorization, @options) + assert_success void + assert_equal '[cancelOrRefund-received]', void.message + assert_void_references_original_authorization(void, purchase.responses.first) + end + + def test_successful_cancel_or_refund_passing_capture + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + capture = @gateway.capture(@amount, auth.authorization) + assert_success capture + + @options[:cancel_or_refund] = true + assert void = @gateway.void(capture.authorization, @options) + assert_success void + assert_equal '[cancelOrRefund-received]', void.message + assert_void_references_original_authorization(void, auth) + end + + def test_successful_authorize_with_alternate_kosovo_code + @options[:billing_address][:country] = 'XK' + response = @gateway.authorize(@amount, @credit_card, @options) assert_failure response - assert_match Gateway::STANDARD_ERROR_CODE[:incorrect_address], response.error_code + assert_equal 'Billing address problem (Country XK invalid)', response.message + + @options[:billing_address][:country] = 'QZ' + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + end + + def test_successful_authorize_with_localized_shopper_statement + response = @gateway.authorize(@amount, @credit_card, @options.merge(localized_shopper_statement: { 'ja-Kana' => 'ADYEN - セラーA' })) + 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:)) + assert_success response + assert_equal '[capture-received]', response.message + end + + private + + def stored_credential_options(*args, ntid: nil) + @options.merge(order_id: generate_unique_id, + stored_credential: stored_credential(*args, network_transaction_id: ntid)) + end + + def assert_void_references_original_authorization(void, auth) + assert_equal void.authorization.split('#').first, auth.params['pspReference'] end end diff --git a/test/remote/gateways/remote_airwallex_test.rb b/test/remote/gateways/remote_airwallex_test.rb new file mode 100644 index 00000000000..8498f05ae96 --- /dev/null +++ b/test/remote/gateways/remote_airwallex_test.rb @@ -0,0 +1,268 @@ +require 'test_helper' + +class RemoteAirwallexTest < Test::Unit::TestCase + def setup + @gateway = AirwallexGateway.new(fixtures(:airwallex)) + + # https://www.airwallex.com/docs/online-payments__test-card-numbers + @amount = 100 + @declined_amount = 8014 + @credit_card = credit_card('4012 0003 0000 1003') + @declined_card = credit_card('2223 0000 1018 1375') + @options = { return_url: 'https://example.com', description: 'a test transaction' } + @stored_credential_cit_options = { initial_transaction: true, initiator: 'cardholder', reason_type: 'recurring', network_transaction_id: nil } + @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 + assert_equal 'AUTHORIZED', response.message + assert_not_nil response.authorization + end + + def test_successful_purchase_with_shipping_address + response = @gateway.purchase(@amount, @credit_card, @options.merge(shipping_address: address)) + assert_success response + assert_equal 'AUTHORIZED', response.message + end + + def test_successful_purchase_with_address + response = @gateway.purchase(@amount, @credit_card, @options.merge(address)) + assert_success response + assert_equal 'AUTHORIZED', response.message + end + + def test_successful_purchase_with_specified_ids + request_id = SecureRandom.uuid + merchant_order_id = SecureRandom.uuid + response = @gateway.purchase(@amount, @credit_card, @options.merge(request_id:, merchant_order_id:)) + assert_success response + assert_match(request_id, response.params.dig('request_id')) + assert_match(merchant_order_id, response.params.dig('merchant_order_id')) + end + + def test_successful_purchase_with_skip_3ds + response = @gateway.purchase(@amount, @credit_card, @options.merge({ skip_3ds: 'true' })) + assert_success response + assert_equal 'AUTHORIZED', response.message + end + + def test_failed_purchase + response = @gateway.purchase(@declined_amount, @declined_card, @options) + assert_failure response + assert_equal 'The card issuer declined this transaction. Please refer to the original response code.', response.message + assert_equal '14', response.error_code + end + + def test_purchase_with_reused_id_raises_error + assert_raise ArgumentError do + @gateway.purchase(@amount, @credit_card, @options.merge(request_id: '1234')) + end + end + + def test_successful_authorize_and_capture + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert capture = @gateway.capture(@amount, auth.authorization, @options) + assert_success capture + assert_equal 'CAPTURE_REQUESTED', capture.message + end + + def test_failed_authorize + response = @gateway.authorize(@declined_amount, @declined_card, @options) + assert_failure response + assert_equal 'The card issuer declined this transaction. Please refer to the original response code.', 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, @options) + assert_success capture + end + + def test_failed_capture + response = @gateway.capture(@declined_amount, '12345', @options) + assert_failure response + assert_match(/The requested endpoint does not exist/, response.message) + end + + def test_successful_refund + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization, @options) + assert_success refund + assert_equal 'RECEIVED', refund.message + assert_not_nil refund.authorization + 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(@declined_amount, '12345', @options) + assert_failure response + assert_match(/The PaymentIntent with ID 12345 cannot be found./, response.message) + end + + def test_successful_void + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert void = @gateway.void(auth.authorization, @options) + assert_success void + assert_equal 'CANCELLED', void.message + end + + def test_failed_void + response = @gateway.void('12345', @options) + assert_failure response + assert_match(/The requested endpoint does not exist/, response.message) + end + + def test_successful_verify + response = @gateway.verify(@credit_card, @options) + assert_success response + assert_match %r{AUTHORIZED}, response.message + end + + def test_failed_verify + response = @gateway.verify(credit_card('1111111111111111'), @options) + assert_failure response + assert_match %r{Invalid card number}, response.message + end + + def test_descriptor_is_truncated_to_max_length + response = @gateway.verify(@credit_card, @options.merge(description: 'This description is longer than 32 characters.')) + assert_success response + assert_match %r{AUTHORIZED}, response.message + end + + def test_successful_cit_with_recurring_stored_credential + auth = @gateway.authorize(@amount, @credit_card, @options.merge(stored_credential: @stored_credential_cit_options)) + assert_success auth + end + + def test_successful_mit_with_recurring_stored_credential + auth = @gateway.authorize(@amount, @credit_card, @options.merge(stored_credential: @stored_credential_cit_options)) + assert_success auth + + add_cit_network_transaction_id_to_stored_credential(auth) + + purchase = @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential: @stored_credential_mit_options)) + assert_success purchase + end + + def test_successful_mit_with_unscheduled_stored_credential + @stored_credential_cit_options[:reason_type] = 'unscheduled' + @stored_credential_mit_options[:reason_type] = 'unscheduled' + + auth = @gateway.authorize(@amount, @credit_card, @options.merge(stored_credential: @stored_credential_cit_options)) + assert_success auth + + add_cit_network_transaction_id_to_stored_credential(auth) + + purchase = @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential: @stored_credential_mit_options)) + assert_success purchase + end + + def test_successful_mit_with_installment_stored_credential + @stored_credential_cit_options[:reason_type] = 'installment' + @stored_credential_mit_options[:reason_type] = 'installment' + + auth = @gateway.authorize(@amount, @credit_card, @options.merge(stored_credential: @stored_credential_cit_options)) + assert_success auth + + add_cit_network_transaction_id_to_stored_credential(auth) + + purchase = @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential: @stored_credential_mit_options)) + assert_success purchase + end + + def test_successful_network_transaction_id_override_with_mastercard + mastercard = credit_card('2223 0000 1018 1375', { brand: 'master' }) + + auth = @gateway.authorize(@amount, mastercard, @options.merge(stored_credential: @stored_credential_cit_options)) + assert_success auth + + add_cit_network_transaction_id_to_stored_credential(auth) + + purchase = @gateway.purchase(@amount, mastercard, @options.merge(stored_credential: @stored_credential_mit_options)) + assert_success purchase + 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[:client_api_key], transcript) + end + + def test_successful_authorize_with_3ds_v1_options + @options[:three_d_secure] = { + version: '1', + cavv: 'VGhpcyBpcyBhIHRlc3QgYmFzZTY=', + eci: '02', + xid: 'b2h3aDZrd3BJWXVCWEFMbzJqSGQ=' + } + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + assert_match 'AUTHORIZED', response.message + end + + def test_successful_authorize_with_3ds_v2_options + @options[:three_d_secure] = { + version: '2.2.0', + cavv: 'MTIzNDU2Nzg5MDA5ODc2NTQzMjE=', + ds_transaction_id: 'f25084f0-5b16-4c0a-ae5d-b24808a95e4b', + eci: '02', + three_ds_server_trans_id: 'df8b9557-e41b-4e17-87e9-2328694a2ea0' + } + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + assert_match 'AUTHORIZED', response.message + end + + def test_successful_purchase_with_3ds_v2_options + @options[:three_d_secure] = { + version: '2.0', + cavv: 'MTIzNDU2Nzg5MDA5ODc2NTQzMjE=', + ds_transaction_id: 'f25084f0-5b16-4c0a-ae5d-b24808a95e4b', + eci: '02', + three_ds_server_trans_id: 'df8b9557-e41b-4e17-87e9-2328694a2ea0' + } + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_match 'AUTHORIZED', response.message + end + + private + + def add_cit_network_transaction_id_to_stored_credential(auth) + @stored_credential_mit_options[:network_transaction_id] = auth.params['latest_payment_attempt']['provider_transaction_id'] + end +end diff --git a/test/remote/gateways/remote_alelo_test.rb b/test/remote/gateways/remote_alelo_test.rb new file mode 100644 index 00000000000..8a4fef24e7b --- /dev/null +++ b/test/remote/gateways/remote_alelo_test.rb @@ -0,0 +1,204 @@ +require 'test_helper' +require 'singleton' + +class RemoteAleloTest < Test::Unit::TestCase + def setup + @gateway = AleloGateway.new(fixtures(:alelo)) + + @amount = 100 + @credit_card = credit_card('4000100011112224') + @declined_card = credit_card('4000300011112220') + @options = { + order_id: '1', + establishment_code: '000002007690360', + sub_merchant_mcc: '5499', + player_identification: '1', + description: 'Store Purchase', + external_trace_number: '123456' + } + end + + def test_access_token_success + resp = @gateway.send :fetch_access_token + + assert_kind_of Response, resp + refute_nil resp.message + end + + def test_failure_access_token_with_invalid_keys + error = assert_raises(ActiveMerchant::OAuthResponseError) do + gateway = AleloGateway.new({ client_id: 'abc123', client_secret: 'abc456' }) + gateway.send :fetch_access_token + end + + assert_match(/401/, error.message) + end + + def test_successful_remote_encryption_key_with_provided_access_token + access_token = @gateway.send :fetch_access_token + resp = @gateway.send(:remote_encryption_key, access_token.message) + + assert_kind_of Response, resp + refute_nil resp.message + end + + def test_ensure_credentials_with_no_provided_access_token_key_are_generated + credentials = @gateway.send :ensure_credentials, {} + + refute_nil credentials[:key] + refute_nil credentials[:access_token] + assert_kind_of Response, credentials[:multiresp] + assert_equal 2, credentials[:multiresp].responses.size + end + + def test_sucessful_encryption_key_requested_when_access_token_provided + access_token = @gateway.send :fetch_access_token + @gateway.options[:access_token] = access_token.message + credentials = @gateway.send :ensure_credentials + + refute_nil credentials[:key] + refute_nil credentials[:access_token] + assert_equal access_token.message, credentials[:access_token] + assert_kind_of Response, credentials[:multiresp] + assert_equal 1, credentials[:multiresp].responses.size + end + + def test_successful_fallback_with_expired_access_token + @gateway.options[:access_token] = 'abc123' + credentials = @gateway.send :ensure_credentials + + refute_nil credentials[:key] + refute_nil credentials[:access_token] + refute_equal 'abc123', credentials[:access_token] + assert_kind_of Response, credentials[:multiresp] + assert_equal 2, credentials[:multiresp].responses.size + end + + def test_successful_purchase + set_credentials! + @gateway.options[:encryption_uuid] = '53141521-afc8-4a08-af0c-f0382aef43c1' + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + assert_match %r{confirmada}i, response.message + end + + def test_successful_purchase_with_no_predefined_credentials + @gateway.options[:encryption_uuid] = '53141521-afc8-4a08-af0c-f0382aef43c1' + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + assert_match %r{confirmada}i, response.message + refute_nil response.params['access_token'] + refute_nil response.params['encryption_key'] + end + + def test_unsuccessful_purchase_with_merchant_discredited + set_credentials! + @gateway.options[:encryption_uuid] = '7c82f46e-64f7-4745-9c60-335a689b8e90' + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_failure response + assert_match %r(contato), response.message + end + + def test_unsuccessful_purchase_with_insuffieicent_funds + set_credentials! + @gateway.options[:encryption_uuid] = 'a36aa740-d505-4d47-8aa6-6c31c7526a68' + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_failure response + assert_match %r(insuficiente), response.message + end + + def test_unsuccessful_purchase_with_invalid_fields + set_credentials! + @gateway.options[:encryption_uuid] = 'd7aff4a6-1ea1-4e74-b81a-934589385958' + response = @gateway.purchase(@amount, @declined_card, @options) + + assert_failure response + assert_match %r{Erro}, response.message + end + + def test_unsuccessful_purchase_with_blocked_card + set_credentials! + @gateway.options[:encryption_uuid] = 'd2a0350d-e872-47bf-a543-2d36c2ad693e' + response = @gateway.purchase(@amount, @declined_card, @options) + + assert_failure response + assert_match %r(Bloqueado), response.message + end + + def test_successful_purchase_with_geolocalitation + set_credentials! + options = { + geo_longitude: '10.451526', + geo_latitude: '51.165691', + uuid: '53141521-afc8-4a08-af0c-f0382aef43c1' + } + + response = @gateway.purchase(@amount, @credit_card, @options.merge(options)) + assert_success response + assert_match %r(Confirmada), response.message + end + + def test_invalid_login + gateway = AleloGateway.new(client_id: 'asdfghj', client_secret: '1234rtytre') + + error = assert_raises(ActiveMerchant::OAuthResponseError) do + gateway.purchase(@amount, @credit_card, @options) + end + + assert_match(/401/, error.message) + end + + def test_transcript_scrubbing + set_credentials! + transcript = capture_transcript(@gateway) do + @gateway.options[:encryption_uuid] = '53141521-afc8-4a08-af0c-f0382aef43c1' + @gateway.purchase(@amount, @credit_card, @options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@gateway.options[:client_id], transcript) + assert_scrubbed(@gateway.options[:client_secret], transcript) + end + + def test_successful_refund + set_credentials! + response = @gateway.refund(@amount, '990a39dd-3df2-46a5-89ac-012cca00ef0b#def456', {}) + + assert_success response + assert_match %r{Estornada}, response.message + end + + def test_failure_refund_with_invalid_uuid + set_credentials! + response = @gateway.refund(@amount, '7f723387-d449-4c6c-aca3-9a583689dc34', {}) + + assert_failure response + end + + private + + def set_credentials! + if AleloCredentials.instance.access_token.nil? + credentials = @gateway.send :ensure_credentials, {} + AleloCredentials.instance.access_token = credentials[:access_token] + AleloCredentials.instance.key = credentials[:key] + AleloCredentials.instance.uuid = credentials[:uuid] + end + + @gateway.options[:access_token] = AleloCredentials.instance.access_token + @gateway.options[:encryption_key] = AleloCredentials.instance.key + @gateway.options[:encryption_uuid] = AleloCredentials.instance.uuid + end +end + +# A simple singleton so an access token and key can +# be shared among several tests +class AleloCredentials + include Singleton + + attr_accessor :access_token, :key, :uuid +end diff --git a/test/remote/gateways/remote_alelo_test_certification.rb b/test/remote/gateways/remote_alelo_test_certification.rb new file mode 100644 index 00000000000..f6a2cb0d771 --- /dev/null +++ b/test/remote/gateways/remote_alelo_test_certification.rb @@ -0,0 +1,136 @@ +require 'test_helper' +require 'singleton' + +class RemoteAleloTestCertification < Test::Unit::TestCase + def setup + @gateway = AleloGateway.new(fixtures(:alelo_certification)) + @amount = 1000 + @cc_alimentacion = credit_card('5098870005467012', { + month: 8, + year: 2027, + first_name: 'Longbob', + last_name: 'Longsen', + verification_value: 747, + brand: 'mc' + }) + @cc_snack = credit_card('5067580024660011', { + month: 8, + year: 2027, + first_name: 'Longbob', + last_name: 'Longsen', + verification_value: 576, + brand: 'mc' + }) + @options = { + order_id: SecureRandom.uuid, + establishment_code: '1040471819', + sub_merchant_mcc: '5499', + player_identification: '7', + description: 'Store Purchase', + external_trace_number: '123456' + } + end + + def test_failure_purchase_with_wrong_cvv_ct05 + set_credentials! + @cc_snack.verification_value = 999 + response = @gateway.purchase(@amount, @cc_snack, @options) + + assert_failure response + assert_match %r{incorreto}i, response.message + end + + def test_failure_with_incomplete_required_options_ct06 + set_credentials! + @options.delete(:establishment_code) + response = @gateway.purchase(@amount, @cc_alimentacion, @options) + + assert_failure response + assert_match %r{Erro ao validar dados}i, response.message + end + + def test_failure_refund_with_non_existent_uuid_ct07 + set_credentials! + response = @gateway.refund(@amount, SecureRandom.uuid, {}) + + assert_failure response + assert_match %r{RequestId informado, não encontrado!}, response.message + end + + def test_successful_purchase_with_alimentazao_cc_ct08 + response = @gateway.purchase(@amount, @cc_alimentacion, @options) + + assert_success response + assert_match %r{confirmada}i, response.message + end + + # Testing High value transaction disables the test credit card + # + # def test_successful_purchase_with_alimentazao_cc_ct08_B_high_value + # response = @gateway.purchase(10_000_000, @cc_alimentacion, @options) + # assert_failure response + # end + + def test_successful_refund_ct09 + set_credentials! + purchase = @gateway.purchase(@amount, @cc_alimentacion, @options) + response = @gateway.refund(@amount, purchase.authorization, {}) + + assert_success response + assert_match %r{Transação Estornada com sucesso}, response.message + end + + def test_failure_with_non_exitent_establishment_code_ct10 + @options[:establishment_code] = '0987654321' + @options[:sub_merchant_mcc] = '5411' + + response = @gateway.purchase(@amount, @cc_alimentacion, @options) + + assert_failure response + assert_match %r{no adquirente}i, response.message + end + + def test_failure_when_cc_expired_ct11 + @cc_alimentacion.year = 2020 + set_credentials! + + response = @gateway.purchase(@amount, @cc_alimentacion, @options) + + assert_failure response + end + + def test_failure_refund_with_purchase_already_refunded_ct12 + set_credentials! + purchase = @gateway.purchase(@amount, @cc_alimentacion, @options) + assert_success purchase + + response = @gateway.refund(@amount, purchase.authorization, {}) + assert_success response + + response = @gateway.refund(@amount, purchase.authorization, {}) + assert_failure response + end + + private + + def set_credentials! + if AleloCredentials.instance.access_token.nil? + credentials = @gateway.send :ensure_credentials, {} + AleloCredentials.instance.access_token = credentials[:access_token] + AleloCredentials.instance.key = credentials[:key] + AleloCredentials.instance.uuid = credentials[:uuid] + end + + @gateway.options[:access_token] = AleloCredentials.instance.access_token + @gateway.options[:encryption_key] = AleloCredentials.instance.key + @gateway.options[:encryption_uuid] = AleloCredentials.instance.uuid + end +end + +# A simple singleton so an access token and key can +# be shared among several tests +class AleloCredentials + include Singleton + + attr_accessor :access_token, :key, :uuid +end diff --git a/test/remote/gateways/remote_allied_wallet_test.rb b/test/remote/gateways/remote_allied_wallet_test.rb index 8ab761e508b..7511b5f9644 100644 --- a/test/remote/gateways/remote_allied_wallet_test.rb +++ b/test/remote/gateways/remote_allied_wallet_test.rb @@ -9,7 +9,7 @@ def setup @declined_card = credit_card('4242424242424242', verification_value: '555') @options = { - billing_address: address, + billing_address: address } end @@ -32,11 +32,12 @@ def test_failed_purchase_no_address end def test_successful_purchase_with_more_options - response = @gateway.purchase(@amount, @credit_card, @options.merge( + options = @options.merge( order_id: generate_unique_id, ip: '127.0.0.1', email: 'jim_smith@example.com' - )) + ) + response = @gateway.purchase(@amount, @credit_card, options) assert_success response assert_equal 'Succeeded', response.message end diff --git a/test/remote/gateways/remote_authorize_net_apple_pay_test.rb b/test/remote/gateways/remote_authorize_net_apple_pay_test.rb index 6b8a632743f..836af912cb3 100644 --- a/test/remote/gateways/remote_authorize_net_apple_pay_test.rb +++ b/test/remote/gateways/remote_authorize_net_apple_pay_test.rb @@ -47,20 +47,19 @@ def test_successful_apple_pay_authorization_and_void end def test_failed_apple_pay_authorization - response = @gateway.authorize(@amount, apple_pay_payment_token(payment_data: {data: 'garbage'}), @options) + response = @gateway.authorize(@amount, apple_pay_payment_token(payment_data: { data: 'garbage' }), @options) assert_failure response assert_equal 'There was an error processing the payment data', response.message assert_equal 'processing_error', response.error_code end def test_failed_apple_pay_purchase - response = @gateway.purchase(@amount, apple_pay_payment_token(payment_data: {data: 'garbage'}), @options) + response = @gateway.purchase(@amount, apple_pay_payment_token(payment_data: { data: 'garbage' }), @options) assert_failure response assert_equal 'There was an error processing the payment data', response.message assert_equal 'processing_error', response.error_code end - private def apple_pay_payment_token(options = {}) @@ -83,11 +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] ) end - end diff --git a/test/remote/gateways/remote_authorize_net_arb_test.rb b/test/remote/gateways/remote_authorize_net_arb_test.rb index 5a04f9de419..b48c9bf24a6 100644 --- a/test/remote/gateways/remote_authorize_net_arb_test.rb +++ b/test/remote/gateways/remote_authorize_net_arb_test.rb @@ -8,17 +8,17 @@ def setup @check = check @options = { - :amount => 100, - :subscription_name => 'Test Subscription 1', - :credit_card => @credit_card, - :billing_address => address.merge(:first_name => 'Jim', :last_name => 'Smith'), - :interval => { - :length => 1, - :unit => :months + amount: 100, + subscription_name: 'Test Subscription 1', + credit_card: @credit_card, + billing_address: address.merge(first_name: 'Jim', last_name: 'Smith'), + interval: { + length: 1, + unit: :months }, - :duration => { - :start_date => Date.today, - :occurrences => 1 + duration: { + start_date: Date.today, + occurrences: 1 } } end @@ -30,7 +30,7 @@ def test_successful_recurring subscription_id = response.authorization - assert response = @gateway.update_recurring(:subscription_id => subscription_id, :amount => @amount * 2) + assert response = @gateway.update_recurring(subscription_id:, amount: @amount * 2) assert_success response assert response = @gateway.status_recurring(subscription_id) @@ -50,8 +50,8 @@ def test_recurring_should_fail_expired_credit_card def test_bad_login gateway = AuthorizeNetArbGateway.new( - :login => 'X', - :password => 'Y' + login: 'X', + password: 'Y' ) assert response = gateway.recurring(@amount, @credit_card, @options) diff --git a/test/remote/gateways/remote_authorize_net_cim_test.rb b/test/remote/gateways/remote_authorize_net_cim_test.rb index 8f7f000b54a..b767cd6ded9 100644 --- a/test/remote/gateways/remote_authorize_net_cim_test.rb +++ b/test/remote/gateways/remote_authorize_net_cim_test.rb @@ -9,39 +9,39 @@ def setup @amount = 100 @credit_card = credit_card('4242424242424242') @payment = { - :credit_card => @credit_card + credit_card: @credit_card } @profile = { - :merchant_customer_id => 'Up to 20 chars', # Optional - :description => 'Up to 255 Characters', # Optional - :email => 'Up to 255 Characters', # Optional - :payment_profiles => { # Optional - :customer_type => 'individual', # Optional - :bill_to => address, - :payment => @payment + merchant_customer_id: 'Up to 20 chars', # Optional + description: 'Up to 255 Characters', # Optional + email: 'Up to 255 Characters', # Optional + payment_profiles: { # Optional + customer_type: 'individual', # Optional + bill_to: address, + payment: @payment }, - :ship_to_list => { - :first_name => 'John', - :last_name => 'Doe', - :company => 'Widgets, Inc', - :address1 => '1234 Fake Street', - :city => 'Anytown', - :state => 'MD', - :zip => '12345', - :country => 'USA', - :phone_number => '(123)123-1234', # Optional - Up to 25 digits (no letters) - :fax_number => '(123)123-1234' # Optional - Up to 25 digits (no letters) + ship_to_list: { + first_name: 'John', + last_name: 'Doe', + company: 'Widgets, Inc', + address1: '1234 Fake Street', + city: 'Anytown', + state: 'MD', + zip: '12345', + country: 'USA', + phone_number: '(123)123-1234', # Optional - Up to 25 digits (no letters) + fax_number: '(123)123-1234' # Optional - Up to 25 digits (no letters) } } @options = { - :ref_id => '1234', # Optional - :profile => @profile + ref_id: '1234', # Optional + profile: @profile } end def teardown if @customer_profile_id - assert response = @gateway.delete_customer_profile(:customer_profile_id => @customer_profile_id) + assert response = @gateway.delete_customer_profile(customer_profile_id: @customer_profile_id) assert_success response @customer_profile_id = nil end @@ -54,7 +54,7 @@ def test_successful_profile_create_get_update_and_delete assert_success response assert response.test? - assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert response = @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) assert response.test? assert_success response assert_equal @customer_profile_id, response.authorization @@ -68,16 +68,35 @@ def test_successful_profile_create_get_update_and_delete assert_equal @profile[:ship_to_list][:phone_number], response.params['profile']['ship_to_list']['phone_number'] assert_equal @profile[:ship_to_list][:company], response.params['profile']['ship_to_list']['company'] - assert response = @gateway.update_customer_profile(:profile => {:customer_profile_id => @customer_profile_id, :email => 'new email address'}) + assert response = @gateway.update_customer_profile(profile: { customer_profile_id: @customer_profile_id, email: 'new email address' }) assert response.test? assert_success response assert_nil response.authorization - assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) - assert_nil response.params['profile']['merchant_customer_id'] - assert_nil response.params['profile']['description'] + assert response = @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) assert_equal 'new email address', response.params['profile']['email'] end + def test_get_customer_profile_with_unmasked_exp_date_and_issuer_info + assert response = @gateway.create_customer_profile(@options) + @customer_profile_id = response.authorization + + assert_success response + assert response.test? + + assert response = @gateway.get_customer_profile( + customer_profile_id: @customer_profile_id, + unmask_expiration_date: true, + include_issuer_info: true + ) + assert response.test? + assert_success response + assert_equal @customer_profile_id, response.authorization + assert_equal 'Successful.', response.message + assert_equal "XXXX#{@credit_card.last_digits}", response.params['profile']['payment_profiles']['payment']['credit_card']['card_number'], "The card number should contain the last 4 digits of the card we passed in #{@credit_card.last_digits}" + assert_equal formatted_expiration_date(@credit_card), response.params['profile']['payment_profiles']['payment']['credit_card']['expiration_date'] + assert_equal @credit_card.first_digits, response.params['profile']['payment_profiles']['payment']['credit_card']['issuer_number'] + end + # NOTE - prior_auth_capture should be used to complete an auth_only request # (not capture_only as that will leak the authorization), so don't use this # test as a template. @@ -85,15 +104,15 @@ def test_successful_create_customer_profile_transaction_auth_only_and_then_captu assert response = @gateway.create_customer_profile(@options) @customer_profile_id = response.authorization - assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert response = @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) @customer_payment_profile_id = response.params['profile']['payment_profiles']['customer_payment_profile_id'] assert response = @gateway.create_customer_profile_transaction( - :transaction => { - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => @customer_payment_profile_id, - :type => :auth_only, - :amount => @amount + transaction: { + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: @customer_payment_profile_id, + type: :auth_only, + amount: @amount } ) @@ -111,12 +130,12 @@ def test_successful_create_customer_profile_transaction_auth_only_and_then_captu # Capture the previously authorized funds assert response = @gateway.create_customer_profile_transaction( - :transaction => { - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => @customer_payment_profile_id, - :type => :capture_only, - :amount => @amount, - :approval_code => approval_code + transaction: { + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: @customer_payment_profile_id, + type: :capture_only, + amount: @amount, + approval_code: } ) @@ -133,22 +152,22 @@ def test_successful_create_customer_profile_transaction_auth_capture_request assert response = @gateway.create_customer_profile(@options) @customer_profile_id = response.authorization - assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert response = @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) @customer_payment_profile_id = response.params['profile']['payment_profiles']['customer_payment_profile_id'] assert response = @gateway.create_customer_profile_transaction( - :transaction => { - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => @customer_payment_profile_id, - :type => :auth_capture, - :order => { - :invoice_number => '1234', - :description => 'Test Order Description', - :purchase_order_number => '4321' + transaction: { + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: @customer_payment_profile_id, + type: :auth_capture, + order: { + invoice_number: '1234', + description: 'Test Order Description', + purchase_order_number: '4321' }, - :recurring_billing => true, - :card_code => '900', # authorize.net says this is a matching CVV - :amount => @amount + recurring_billing: true, + card_code: '900', # authorize.net says this is a matching CVV + amount: @amount } ) @@ -169,56 +188,56 @@ def test_successful_create_customer_payment_profile_request assert response = @gateway.create_customer_profile(@options) @customer_profile_id = response.authorization - assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert response = @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) assert_nil response.params['profile']['payment_profiles'] assert response = @gateway.create_customer_payment_profile( - :customer_profile_id => @customer_profile_id, - :payment_profile => payment_profile + customer_profile_id: @customer_profile_id, + payment_profile: ) assert response.test? assert_success response - assert_nil response.authorization + assert_equal @customer_profile_id, response.authorization assert customer_payment_profile_id = response.params['customer_payment_profile_id'] assert customer_payment_profile_id =~ /\d+/, "The customerPaymentProfileId should be numeric. It was #{customer_payment_profile_id}" end def test_successful_create_customer_payment_profile_request_with_bank_account - payment_profile = @options[:profile].delete(:payment_profiles) + @options[:profile].delete(:payment_profiles) assert response = @gateway.create_customer_profile(@options) @customer_profile_id = response.authorization - assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert response = @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) assert_nil response.params['profile']['payment_profiles'] assert response = @gateway.create_customer_payment_profile( - :customer_profile_id => @customer_profile_id, - :payment_profile => { - :customer_type => 'individual', # Optional - :bill_to => @address, - :payment => { - :bank_account => { - :account_type => :checking, - :name_on_account => 'John Doe', - :echeck_type => :ccd, - :bank_name => 'Bank of America', - :routing_number => '123456789', - :account_number => '12345' + customer_profile_id: @customer_profile_id, + payment_profile: { + customer_type: 'individual', # Optional + bill_to: @address, + payment: { + bank_account: { + account_type: :checking, + name_on_account: 'John Doe', + echeck_type: :ccd, + bank_name: 'Bank of America', + routing_number: '123456789', + account_number: '12345' } }, - :drivers_license => { - :state => 'MD', - :number => '12345', - :date_of_birth => '1981-3-31' + drivers_license: { + state: 'MD', + number: '12345', + date_of_birth: '1981-3-31' }, - :tax_id => '123456789' + tax_id: '123456789' } ) assert response.test? assert_success response - assert_nil response.authorization + assert_equal @customer_profile_id, response.authorization assert customer_payment_profile_id = response.params['customer_payment_profile_id'] assert customer_payment_profile_id =~ /\d+/, "The customerPaymentProfileId should be numeric. It was #{customer_payment_profile_id}" end @@ -228,68 +247,68 @@ def test_successful_create_customer_shipping_address_request assert response = @gateway.create_customer_profile(@options) @customer_profile_id = response.authorization - assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert response = @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) assert_nil response.params['profile']['ship_to_list'] assert response = @gateway.create_customer_shipping_address( - :customer_profile_id => @customer_profile_id, - :address => shipping_address + customer_profile_id: @customer_profile_id, + address: shipping_address ) assert response.test? assert_success response - assert_nil response.authorization + assert_equal @customer_profile_id, response.authorization assert customer_address_id = response.params['customer_address_id'] assert customer_address_id =~ /\d+/, "The customerAddressId should be numeric. It was #{customer_address_id}" end def test_successful_get_customer_profile_with_multiple_payment_profiles second_payment_profile = { - :customer_type => 'individual', - :bill_to => @address, - :payment => { - :credit_card => credit_card('1234123412341234') + customer_type: 'individual', + bill_to: @address, + payment: { + credit_card: credit_card('4111111111111111') } } assert response = @gateway.create_customer_profile(@options) @customer_profile_id = response.authorization - assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) assert response = @gateway.create_customer_payment_profile( - :customer_profile_id => @customer_profile_id, - :payment_profile => second_payment_profile + customer_profile_id: @customer_profile_id, + payment_profile: second_payment_profile ) assert response.test? assert_success response - assert_nil response.authorization + assert_equal @customer_profile_id, response.authorization assert customer_payment_profile_id = response.params['customer_payment_profile_id'] assert customer_payment_profile_id =~ /\d+/, "The customerPaymentProfileId should be numeric. It was #{customer_payment_profile_id}" - assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert response = @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) assert_equal 2, response.params['profile']['payment_profiles'].size - assert_equal 'XXXX4242', response.params['profile']['payment_profiles'][0]['payment']['credit_card']['card_number'] - assert_equal 'XXXX1234', response.params['profile']['payment_profiles'][1]['payment']['credit_card']['card_number'] + assert(response.params['profile']['payment_profiles'].one? { |payment| payment['payment']['credit_card']['card_number'] == 'XXXX4242' }) + assert(response.params['profile']['payment_profiles'].one? { |payment| payment['payment']['credit_card']['card_number'] == 'XXXX1111' }) end def test_successful_delete_customer_payment_profile_request assert response = @gateway.create_customer_profile(@options) @customer_profile_id = response.authorization - assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert response = @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) assert customer_payment_profile_id = response.params['profile']['payment_profiles']['customer_payment_profile_id'] assert response = @gateway.delete_customer_payment_profile( - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => customer_payment_profile_id + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: ) assert response.test? assert_success response assert_nil response.authorization - assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert response = @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) assert_nil response.params['profile']['payment_profiles'] end @@ -297,19 +316,19 @@ def test_successful_delete_customer_shipping_address_request assert response = @gateway.create_customer_profile(@options) @customer_profile_id = response.authorization - assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert response = @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) assert customer_address_id = response.params['profile']['ship_to_list']['customer_address_id'] assert response = @gateway.delete_customer_shipping_address( - :customer_profile_id => @customer_profile_id, - :customer_address_id => customer_address_id + customer_profile_id: @customer_profile_id, + customer_address_id: ) assert response.test? assert_success response assert_nil response.authorization - assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert response = @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) assert_nil response.params['profile']['ship_to_list'] end @@ -317,12 +336,12 @@ def test_successful_get_customer_payment_profile_request assert response = @gateway.create_customer_profile(@options) @customer_profile_id = response.authorization - assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert response = @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) assert customer_payment_profile_id = response.params['profile']['payment_profiles']['customer_payment_profile_id'] assert response = @gateway.get_customer_payment_profile( - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => customer_payment_profile_id + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: ) assert response.test? @@ -338,34 +357,37 @@ def test_successful_get_customer_payment_profile_unmasked_request assert response = @gateway.create_customer_profile(@options) @customer_profile_id = response.authorization - assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert response = @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) assert customer_payment_profile_id = response.params['profile']['payment_profiles']['customer_payment_profile_id'] assert response = @gateway.get_customer_payment_profile( - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => customer_payment_profile_id, - :unmask_expiration_date => true + customer_profile_id: @customer_profile_id, + customer_payment_profile_id:, + unmask_expiration_date: true, + include_issuer_info: true ) assert response.test? assert_success response assert_nil response.authorization + assert response.params['payment_profile']['customer_payment_profile_id'] =~ /\d+/, 'The customer_payment_profile_id should be a number' assert_equal "XXXX#{@credit_card.last_digits}", response.params['payment_profile']['payment']['credit_card']['card_number'], "The card number should contain the last 4 digits of the card we passed in #{@credit_card.last_digits}" assert_equal @profile[:payment_profiles][:customer_type], response.params['payment_profile']['customer_type'] assert_equal formatted_expiration_date(@credit_card), response.params['payment_profile']['payment']['credit_card']['expiration_date'] + assert_equal @credit_card.first_digits, response.params['payment_profile']['payment']['credit_card']['issuer_number'] end def test_successful_get_customer_shipping_address_request assert response = @gateway.create_customer_profile(@options) @customer_profile_id = response.authorization - assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert response = @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) assert customer_address_id = response.params['profile']['ship_to_list']['customer_address_id'] assert response = @gateway.get_customer_shipping_address( - :customer_profile_id => @customer_profile_id, - :customer_address_id => customer_address_id + customer_profile_id: @customer_profile_id, + customer_address_id: ) assert response.test? @@ -381,13 +403,13 @@ def test_successful_update_customer_payment_profile_request @customer_profile_id = response.authorization # Get the customerPaymentProfileId - assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert response = @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) assert customer_payment_profile_id = response.params['profile']['payment_profiles']['customer_payment_profile_id'] # Get the customerPaymentProfile assert response = @gateway.get_customer_payment_profile( - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => customer_payment_profile_id + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: ) # The value before updating @@ -395,11 +417,11 @@ def test_successful_update_customer_payment_profile_request # Update the payment profile assert response = @gateway.update_customer_payment_profile( - :customer_profile_id => @customer_profile_id, - :payment_profile => { - :customer_payment_profile_id => customer_payment_profile_id, - :payment => { - :credit_card => credit_card('1234123412341234') + customer_profile_id: @customer_profile_id, + payment_profile: { + customer_payment_profile_id:, + payment: { + credit_card: credit_card('1234123412341234') } } ) @@ -409,8 +431,8 @@ def test_successful_update_customer_payment_profile_request # Get the updated payment profile assert response = @gateway.get_customer_payment_profile( - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => customer_payment_profile_id + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: ) # Show that the payment profile was updated @@ -419,25 +441,25 @@ def test_successful_update_customer_payment_profile_request assert_nil response.params['payment_profile']['customer_type'] new_billing_address = response.params['payment_profile']['bill_to'] - new_billing_address.update(:first_name => 'Frank', :last_name => 'Brown') - masked_credit_card = ActiveMerchant::Billing::CreditCard.new(:number => response.params['payment_profile']['payment']['credit_card']['card_number']) + new_billing_address.update(first_name: 'Frank', last_name: 'Brown') + masked_credit_card = ActiveMerchant::Billing::CreditCard.new(number: response.params['payment_profile']['payment']['credit_card']['card_number']) # Update only the billing address with a masked card and expiration date - assert response = @gateway.update_customer_payment_profile( - :customer_profile_id => @customer_profile_id, - :payment_profile => { - :customer_payment_profile_id => customer_payment_profile_id, - :bill_to => new_billing_address, - :payment => { - :credit_card => masked_credit_card + assert @gateway.update_customer_payment_profile( + customer_profile_id: @customer_profile_id, + payment_profile: { + customer_payment_profile_id:, + bill_to: new_billing_address, + payment: { + credit_card: masked_credit_card } } ) # Get the updated payment profile assert response = @gateway.get_customer_payment_profile( - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => customer_payment_profile_id + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: ) # Show that the billing address on the payment profile was updated @@ -450,40 +472,40 @@ def test_successful_update_customer_payment_profile_request_with_credit_card_las @customer_profile_id = response.authorization # Get the customerPaymentProfileId - assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert response = @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) assert customer_payment_profile_id = response.params['profile']['payment_profiles']['customer_payment_profile_id'] # Get the customerPaymentProfile assert response = @gateway.get_customer_payment_profile( - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => customer_payment_profile_id + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: ) # Card number last 4 digits is 4242 assert_equal 'XXXX4242', response.params['payment_profile']['payment']['credit_card']['card_number'], 'The card number should contain the last 4 digits of the card we passed in 4242' new_billing_address = response.params['payment_profile']['bill_to'] - new_billing_address.update(:first_name => 'Frank', :last_name => 'Brown') + new_billing_address.update(first_name: 'Frank', last_name: 'Brown') # Initialize credit card with only last 4 digits as the number - last_four_credit_card = ActiveMerchant::Billing::CreditCard.new(:number => '4242') #Credit card with only last four digits + last_four_credit_card = ActiveMerchant::Billing::CreditCard.new(number: '4242') # Credit card with only last four digits # Update only the billing address with a card with the last 4 digits and expiration date - assert response = @gateway.update_customer_payment_profile( - :customer_profile_id => @customer_profile_id, - :payment_profile => { - :customer_payment_profile_id => customer_payment_profile_id, - :bill_to => new_billing_address, - :payment => { - :credit_card => last_four_credit_card + assert @gateway.update_customer_payment_profile( + customer_profile_id: @customer_profile_id, + payment_profile: { + customer_payment_profile_id:, + bill_to: new_billing_address, + payment: { + credit_card: last_four_credit_card } } ) # Get the updated payment profile assert response = @gateway.get_customer_payment_profile( - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => customer_payment_profile_id + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: ) # Show that the billing address on the payment profile was updated @@ -496,13 +518,13 @@ def test_successful_update_customer_shipping_address_request @customer_profile_id = response.authorization # Get the customerAddressId - assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert response = @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) assert customer_address_id = response.params['profile']['ship_to_list']['customer_address_id'] # Get the customerShippingAddress assert response = @gateway.get_customer_shipping_address( - :customer_profile_id => @customer_profile_id, - :customer_address_id => customer_address_id + customer_profile_id: @customer_profile_id, + customer_address_id: ) assert address = response.params['address'] @@ -511,14 +533,14 @@ def test_successful_update_customer_shipping_address_request # Update the address and remove the phone_number new_address = address.symbolize_keys.merge!( - :address => '5678 Fake Street' + address: '5678 Fake Street' ) new_address.delete(:phone_number) - #Update the shipping address + # Update the shipping address assert response = @gateway.update_customer_shipping_address( - :customer_profile_id => @customer_profile_id, - :address => new_address + customer_profile_id: @customer_profile_id, + address: new_address ) assert response.test? assert_success response @@ -526,8 +548,8 @@ def test_successful_update_customer_shipping_address_request # Get the updated shipping address assert response = @gateway.get_customer_shipping_address( - :customer_profile_id => @customer_profile_id, - :customer_address_id => customer_address_id + customer_profile_id: @customer_profile_id, + customer_address_id: ) # Show that the shipping address was updated @@ -540,15 +562,15 @@ def test_successful_validate_customer_payment_profile_request_live assert response = @gateway.create_customer_profile(@options) @customer_profile_id = response.authorization - assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert response = @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) assert @customer_payment_profile_id = response.params['profile']['payment_profiles']['customer_payment_profile_id'] assert @customer_address_id = response.params['profile']['ship_to_list']['customer_address_id'] assert response = @gateway.validate_customer_payment_profile( - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => @customer_payment_profile_id, - :customer_address_id => @customer_address_id, - :validation_mode => :live + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: @customer_payment_profile_id, + customer_address_id: @customer_address_id, + validation_mode: :live ) assert response.test? @@ -562,15 +584,15 @@ def test_validate_customer_payment_profile_request_live_requires_billing_address assert response = @gateway.create_customer_profile(@options) @customer_profile_id = response.authorization - assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert response = @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) assert @customer_payment_profile_id = response.params['profile']['payment_profiles']['customer_payment_profile_id'] assert @customer_address_id = response.params['profile']['ship_to_list']['customer_address_id'] assert response = @gateway.validate_customer_payment_profile( - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => @customer_payment_profile_id, - :customer_address_id => @customer_address_id, - :validation_mode => :live + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: @customer_payment_profile_id, + customer_address_id: @customer_address_id, + validation_mode: :live ) assert response.test? @@ -583,15 +605,15 @@ def test_validate_customer_payment_profile_request_old_does_not_require_billing_ assert response = @gateway.create_customer_profile(@options) @customer_profile_id = response.authorization - assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert response = @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) assert @customer_payment_profile_id = response.params['profile']['payment_profiles']['customer_payment_profile_id'] assert @customer_address_id = response.params['profile']['ship_to_list']['customer_address_id'] assert response = @gateway.validate_customer_payment_profile( - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => @customer_payment_profile_id, - :customer_address_id => @customer_address_id, - :validation_mode => :old + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: @customer_payment_profile_id, + customer_address_id: @customer_address_id, + validation_mode: :old ) assert response.test? @@ -603,24 +625,24 @@ def test_should_create_duplicate_customer_profile_transactions_with_duplicate_wi assert response = @gateway.create_customer_profile(@options) @customer_profile_id = response.authorization - assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert response = @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) assert @customer_payment_profile_id = response.params['profile']['payment_profiles']['customer_payment_profile_id'] key = (Time.now.to_f * 1000000).to_i.to_s customer_profile_transaction = { - :transaction => { - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => @customer_payment_profile_id, - :type => :auth_capture, - :order => { - :invoice_number => key.to_s, - :description => "Test Order Description #{key.to_s}", - :purchase_order_number => key.to_s + transaction: { + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: @customer_payment_profile_id, + type: :auth_capture, + order: { + invoice_number: key.to_s, + description: "Test Order Description #{key}", + purchase_order_number: key.to_s }, - :amount => @amount + amount: @amount }, - :extra_options => { 'x_duplicate_window' => 1 } + extra_options: { 'x_duplicate_window' => 1 } } assert response = @gateway.create_customer_profile_transaction(customer_profile_transaction) @@ -639,22 +661,22 @@ def test_should_not_create_duplicate_customer_profile_transactions_without_dupli assert response = @gateway.create_customer_profile(@options) @customer_profile_id = response.authorization - assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert response = @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) assert @customer_payment_profile_id = response.params['profile']['payment_profiles']['customer_payment_profile_id'] key = (Time.now.to_f * 1000000).to_i.to_s customer_profile_transaction = { - :transaction => { - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => @customer_payment_profile_id, - :type => :auth_capture, - :order => { - :invoice_number => key.to_s, - :description => "Test Order Description #{key.to_s}", - :purchase_order_number => key.to_s + transaction: { + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: @customer_payment_profile_id, + type: :auth_capture, + order: { + invoice_number: key.to_s, + description: "Test Order Description #{key}", + purchase_order_number: key.to_s }, - :amount => @amount + amount: @amount } } @@ -674,9 +696,9 @@ def test_should_create_customer_profile_transaction_auth_capture_and_then_void_r response = get_and_validate_auth_capture_response assert response = @gateway.create_customer_profile_transaction_for_void( - :transaction => { - :type => :void, - :trans_id => response.params['direct_response']['transaction_id'] + transaction: { + type: :void, + trans_id: response.params['direct_response']['transaction_id'] } ) assert_instance_of Response, response @@ -689,12 +711,12 @@ def test_should_create_customer_profile_transaction_auth_capture_and_then_refund response = get_and_validate_auth_capture_response assert response = @gateway.create_customer_profile_transaction( - :transaction => { - :type => :refund, - :amount => 1, - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => @customer_payment_profile_id, - :trans_id => response.params['direct_response']['transaction_id'] + transaction: { + type: :refund, + amount: 1, + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: @customer_payment_profile_id, + trans_id: response.params['direct_response']['transaction_id'] } ) assert_instance_of Response, response @@ -710,13 +732,13 @@ def test_should_create_customer_profile_transaction_auth_capture_and_then_refund response = get_and_validate_auth_capture_response assert response = @gateway.create_customer_profile_transaction( - :transaction => { - :type => :refund, - :amount => 1, - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => @customer_payment_profile_id, - :trans_id => response.params['direct_response']['transaction_id'], - :order => {} + transaction: { + type: :refund, + amount: 1, + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: @customer_payment_profile_id, + trans_id: response.params['direct_response']['transaction_id'], + order: {} } ) assert_instance_of Response, response @@ -732,11 +754,11 @@ def test_should_create_customer_profile_transaction_auth_capture_and_then_refund response = get_and_validate_auth_capture_response assert response = @gateway.create_customer_profile_transaction( - :transaction => { - :type => :refund, - :amount => 1, - :credit_card_number_masked => 'XXXX4242', - :trans_id => response.params['direct_response']['transaction_id'] + transaction: { + type: :refund, + amount: 1, + credit_card_number_masked: 'XXXX4242', + trans_id: response.params['direct_response']['transaction_id'] } ) assert_instance_of Response, response @@ -752,10 +774,10 @@ def test_should_create_customer_profile_transaction_auth_only_and_then_prior_aut response = get_and_validate_auth_only_response assert response = @gateway.create_customer_profile_transaction( - :transaction => { - :type => :prior_auth_capture, - :trans_id => response.params['direct_response']['transaction_id'], - :amount => response.params['direct_response']['amount'] + transaction: { + type: :prior_auth_capture, + trans_id: response.params['direct_response']['transaction_id'], + amount: response.params['direct_response']['amount'] } ) assert_instance_of Response, response @@ -766,40 +788,40 @@ def test_should_create_customer_profile_transaction_auth_only_and_then_prior_aut end def get_and_validate_customer_payment_profile_request_with_bank_account_response - payment_profile = @options[:profile].delete(:payment_profiles) + @options[:profile].delete(:payment_profiles) assert response = @gateway.create_customer_profile(@options) @customer_profile_id = response.authorization - assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert response = @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) assert_nil response.params['profile']['payment_profiles'] assert response = @gateway.create_customer_payment_profile( - :customer_profile_id => @customer_profile_id, - :payment_profile => { - :customer_type => 'individual', # Optional - :bill_to => @address, - :payment => { - :bank_account => { - :account_type => :checking, - :name_on_account => 'John Doe', - :echeck_type => :ccd, - :bank_name => 'Bank of America', - :routing_number => '123456789', - :account_number => '12345678' + customer_profile_id: @customer_profile_id, + payment_profile: { + customer_type: 'individual', # Optional + bill_to: @address, + payment: { + bank_account: { + account_type: :checking, + name_on_account: 'John Doe', + echeck_type: :ccd, + bank_name: 'Bank of America', + routing_number: '123456789', + account_number: '12345678' } }, - :drivers_license => { - :state => 'MD', - :number => '12345', - :date_of_birth => '1981-3-31' + drivers_license: { + state: 'MD', + number: '12345', + date_of_birth: '1981-3-31' }, - :tax_id => '123456789' + tax_id: '123456789' } ) assert response.test? assert_success response - assert_nil response.authorization + assert_equal @customer_profile_id, response.authorization assert @customer_payment_profile_id = response.params['customer_payment_profile_id'] assert @customer_payment_profile_id =~ /\d+/, "The customerPaymentProfileId should be numeric. It was #{@customer_payment_profile_id}" return response @@ -809,22 +831,22 @@ def get_and_validate_auth_capture_response assert response = @gateway.create_customer_profile(@options) @customer_profile_id = response.authorization - assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert response = @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) @customer_payment_profile_id = response.params['profile']['payment_profiles']['customer_payment_profile_id'] key = (Time.now.to_f * 1000000).to_i.to_s assert response = @gateway.create_customer_profile_transaction( - :transaction => { - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => @customer_payment_profile_id, - :type => :auth_capture, - :order => { - :invoice_number => key.to_s, - :description => "Test Order Description #{key.to_s}", - :purchase_order_number => key.to_s + transaction: { + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: @customer_payment_profile_id, + type: :auth_capture, + order: { + invoice_number: key.to_s, + description: "Test Order Description #{key}", + purchase_order_number: key.to_s }, - :amount => @amount + amount: @amount } ) @@ -836,7 +858,7 @@ def get_and_validate_auth_capture_response assert_equal 'auth_capture', response.params['direct_response']['transaction_type'] assert_equal '100.00', response.params['direct_response']['amount'] assert_equal response.params['direct_response']['invoice_number'], key.to_s - assert_equal response.params['direct_response']['order_description'], "Test Order Description #{key.to_s}" + assert_equal response.params['direct_response']['order_description'], "Test Order Description #{key}" assert_equal response.params['direct_response']['purchase_order_number'], key.to_s return response end @@ -847,20 +869,20 @@ def get_and_validate_auth_only_response key = (Time.now.to_f * 1000000).to_i.to_s - assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert response = @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) @customer_payment_profile_id = response.params['profile']['payment_profiles']['customer_payment_profile_id'] assert response = @gateway.create_customer_profile_transaction( - :transaction => { - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => @customer_payment_profile_id, - :type => :auth_only, - :order => { - :invoice_number => key.to_s, - :description => "Test Order Description #{key.to_s}", - :purchase_order_number => key.to_s + transaction: { + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: @customer_payment_profile_id, + type: :auth_only, + order: { + invoice_number: key.to_s, + description: "Test Order Description #{key}", + purchase_order_number: key.to_s }, - :amount => @amount - } + amount: @amount + } ) assert response.test? @@ -872,6 +894,4 @@ def get_and_validate_auth_only_response return response end - - end diff --git a/test/remote/gateways/remote_authorize_net_test.rb b/test/remote/gateways/remote_authorize_net_test.rb index af48e65a4b5..2bd9075690c 100644 --- a/test/remote/gateways/remote_authorize_net_test.rb +++ b/test/remote/gateways/remote_authorize_net_test.rb @@ -9,6 +9,17 @@ def setup @check = check @declined_card = credit_card('400030001111222') + @payment_token = network_tokenization_credit_card( + '4242424242424242', + payment_cryptogram: 'dGVzdGNyeXB0b2dyYW1YWFhYWFhYWFhYWFg9PQ==', + brand: 'visa', + eci: '05', + month: '09', + year: '2030', + first_name: 'Longbob', + last_name: 'Longsen' + ) + @options = { order_id: '1', email: 'anet@example.com', @@ -19,23 +30,33 @@ def setup @level_2_options = { tax: { - amount: '100', - name: 'tax name', - description: 'tax description' - }, + amount: '100', + name: 'tax name', + description: 'tax description' + }, duty: { - amount: '200', - name: 'duty name', - description: 'duty description' - }, + amount: '200', + name: 'duty name', + description: 'duty description' + }, shipping: { amount: '300', name: 'shipping name', - description: 'shipping description', + description: 'shipping description' }, tax_exempt: 'false', po_number: '123' } + + @level_3_options = { + ship_from_address: { + zip: '27701', + country: 'US' + }, + summary_commodity_code: 'CODE' + } + + @level_2_and_3_options = @level_2_options.merge(@level_3_options) end def test_successful_purchase @@ -46,6 +67,26 @@ def test_successful_purchase assert response.authorization end + def test_successful_purchase_with_google_pay + @payment_token.source = :google_pay + response = @gateway.purchase(@amount, @payment_token, @options) + + assert_success response + assert response.test? + assert_equal 'This transaction has been approved', response.message + assert response.authorization + end + + def test_successful_purchase_with_apple_pay + @payment_token.source = :apple_pay + response = @gateway.purchase(@amount, @payment_token, @options) + + assert_success response + assert response.test? + assert_equal 'This transaction has been approved', response.message + assert response.authorization + end + def test_successful_purchase_with_minimal_options response = @gateway.purchase(@amount, @credit_card, duplicate_window: 0, email: 'anet@example.com', billing_address: address) assert_success response @@ -105,8 +146,41 @@ def test_successful_purchase_with_line_items assert response.authorization end - def test_successful_purchase_with_level_2_data - response = @gateway.purchase(@amount, @credit_card, @options.merge(@level_2_options)) + def test_successful_purchase_with_level_3_line_item_data + additional_options = { + email: 'anet@example.com', + line_items: [ + { + item_id: '1', + name: 'mug', + description: 'coffee', + quantity: '100', + unit_price: '10', + unit_of_measure: 'yards', + total_amount: '1000', + product_code: 'coupon' + } + ] + } + response = @gateway.purchase(@amount, @credit_card, @options.merge(additional_options)) + assert_success response + assert response.test? + assert_equal 'This transaction has been approved', response.message + assert response.authorization + end + + def test_successful_purchase_with_level_2_and_3_data + response = @gateway.purchase(@amount, @credit_card, @options.merge(@level_2_and_3_options)) + assert_success response + 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 @@ -131,7 +205,7 @@ def test_successful_purchase_with_utf_character assert_match %r{This transaction has been approved}, response.message end - def test_successful_echeck_purchase + def test_successful_echeck_purchase_with_checking_account_type response = @gateway.purchase(@amount, @check, @options) assert_success response assert response.test? @@ -139,6 +213,15 @@ def test_successful_echeck_purchase assert response.authorization end + def test_successful_echeck_purchase_with_savings_account_type + savings_account = check(account_type: 'savings') + response = @gateway.purchase(@amount, savings_account, @options) + assert_success response + assert response.test? + assert_equal 'This transaction has been approved', response.message + assert response.authorization + end + def test_card_present_purchase_with_no_data no_data_credit_card = ActiveMerchant::Billing::CreditCard.new response = @gateway.purchase(@amount, no_data_credit_card, @options) @@ -169,8 +252,92 @@ def test_successful_purchase_with_disable_partial_authorize assert_success purchase end + def test_successful_auth_and_capture_with_recurring_stored_credential + stored_credential_params = { + initial_transaction: true, + reason_type: 'recurring', + initiator: 'cardholder', + network_transaction_id: nil + } + assert auth = @gateway.authorize(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_params })) + assert_success auth + assert auth.authorization + + assert capture = @gateway.capture(@amount, auth.authorization, authorization_validated: true) + assert_success capture + + @options[:stored_credential] = { + initial_transaction: false, + reason_type: 'recurring', + initiator: 'merchant', + network_transaction_id: auth.params['network_trans_id'] + } + + assert next_auth = @gateway.authorize(@amount, @credit_card, @options) + assert next_auth.authorization + + assert capture = @gateway.capture(@amount, next_auth.authorization, authorization_validated: true) + assert_success capture + end + + def test_successful_auth_and_capture_with_unscheduled_stored_credential + stored_credential_params = { + initial_transaction: true, + reason_type: 'unscheduled', + initiator: 'cardholder', + network_transaction_id: nil + } + assert auth = @gateway.authorize(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_params })) + assert_success auth + assert auth.authorization + + assert capture = @gateway.capture(@amount, auth.authorization, authorization_validated: true) + assert_success capture + + @options[:stored_credential] = { + initial_transaction: false, + reason_type: 'unscheduled', + initiator: 'merchant', + network_transaction_id: auth.params['network_trans_id'] + } + + assert next_auth = @gateway.authorize(@amount, @credit_card, @options) + assert next_auth.authorization + + assert capture = @gateway.capture(@amount, next_auth.authorization, authorization_validated: true) + assert_success capture + end + + def test_successful_auth_and_capture_with_installment_stored_credential + stored_credential_params = { + initial_transaction: true, + reason_type: 'installment', + initiator: 'cardholder', + network_transaction_id: nil + } + assert auth = @gateway.authorize(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_params })) + assert_success auth + assert auth.authorization + + assert capture = @gateway.capture(@amount, auth.authorization, authorization_validated: true) + assert_success capture + + @options[:stored_credential] = { + initial_transaction: false, + reason_type: 'installment', + initiator: 'merchant', + network_transaction_id: auth.params['network_trans_id'] + } + + assert next_auth = @gateway.authorize(@amount, @credit_card, @options) + assert next_auth.authorization + + assert capture = @gateway.capture(@amount, next_auth.authorization, authorization_validated: true) + assert_success capture + end + def test_successful_authorize_with_email_and_ip - options = @options.merge({email: 'hello@example.com', ip: '127.0.0.1'}) + options = @options.merge({ email: 'hello@example.com', ip: '127.0.0.1' }) auth = @gateway.authorize(@amount, @credit_card, options) assert_success auth @@ -187,7 +354,7 @@ def test_failed_authorize end def test_card_present_authorize_and_capture_with_track_data_only - track_credit_card = ActiveMerchant::Billing::CreditCard.new(:track_data => '%B378282246310005^LONGSON/LONGBOB^1705101130504392?') + track_credit_card = ActiveMerchant::Billing::CreditCard.new(track_data: '%B378282246310005^LONGSON/LONGBOB^1705101130504392?') assert authorization = @gateway.authorize(@amount, track_credit_card, @options) assert_success authorization @@ -212,7 +379,7 @@ def test_failed_echeck_authorization end def test_card_present_purchase_with_track_data_only - track_credit_card = ActiveMerchant::Billing::CreditCard.new(:track_data => '%B378282246310005^LONGSON/LONGBOB^1705101130504392?') + track_credit_card = ActiveMerchant::Billing::CreditCard.new(track_data: '%B378282246310005^LONGSON/LONGBOB^1705101130504392?') response = @gateway.purchase(@amount, track_credit_card, @options) assert response.test? assert_equal 'This transaction has been approved', response.message @@ -257,11 +424,76 @@ def test_successful_authorization_with_moto_retail_type assert response.authorization end + def test_successful_purchase_with_phone_number + @options[:billing_address][:phone] = nil + @options[:billing_address][:phone_number] = '5554443210' + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert response.test? + assert_equal 'This transaction has been approved', response.message + assert response.authorization + end + def test_successful_verify response = @gateway.verify(@credit_card, @options) assert_success response assert_equal 'This transaction has been approved', response.message - assert_success response.responses.last, 'The void should succeed' + assert_equal response.responses.count, 2 + end + + def test_successful_verify_with_no_address + @options[:billing_address] = nil + response = @gateway.verify(@credit_card, @options) + + assert_success response + assert_equal 'This transaction has been approved', response.message + assert_equal response.responses.count, 2 + end + + def test_successful_verify_with_verify_amount_and_billing_address + @options[:verify_amount] = 1 + response = @gateway.verify(@credit_card, @options) + assert_success response + assert_equal 'This transaction has been approved', response.message + assert_equal response.responses.count, 2 + end + + def test_successful_verify_after_store_with_custom_verify_amount + @options[:verify_amount] = 1 + assert store = @gateway.store(@credit_card, @options) + assert_success store + response = @gateway.verify(store.authorization, @options) + assert_success response + assert_equal response.responses.count, 2 + end + + def test_successful_verify_with_apple_pay + credit_card = network_tokenization_credit_card('4242424242424242', payment_cryptogram: '111111111100cryptogram') + response = @gateway.verify(credit_card, @options) + assert_success response + assert_equal 'This transaction has been approved', response.message + end + + def test_successful_verify_with_check + response = @gateway.verify(@check, @options) + assert_success response + assert_equal 'This transaction has been approved', response.message + end + + def test_successful_verify_with_nil_custom_verify_amount + @options[:verify_amount] = nil + response = @gateway.verify(@credit_card, @options) + assert_success response + assert_equal 'This transaction has been approved', response.message + end + + def test_verify_tpt_with_custom_verify_amount_and_no_address + @options[:verify_amount] = 100 + assert store = @gateway.store(@credit_card, @options) + assert_success store + @options[:billing_address] = nil + response = @gateway.verify(store.authorization, @options) + assert_success response end def test_failed_verify @@ -285,9 +517,9 @@ def test_successful_store_new_payment_profile assert store.authorization new_card = credit_card('4424222222222222') - customer_profile_id, _, _ = store.authorization.split('#') + customer_profile_id, = store.authorization.split('#') - assert response = @gateway.store(new_card, customer_profile_id: customer_profile_id) + assert response = @gateway.store(new_card, customer_profile_id:) assert_success response assert_equal 'Successful', response.message assert_equal '1', response.params['message_code'] @@ -299,9 +531,9 @@ def test_failed_store_new_payment_profile assert store.authorization new_card = credit_card('141241') - customer_profile_id, _, _ = store.authorization.split('#') + customer_profile_id, = store.authorization.split('#') - assert response = @gateway.store(new_card, customer_profile_id: customer_profile_id) + assert response = @gateway.store(new_card, customer_profile_id:) assert_failure response assert_equal 'The field length is invalid for Card Number', response.message end @@ -322,6 +554,16 @@ def test_successful_purchase_using_stored_card assert_equal 'This transaction has been approved.', response.message end + def test_successful_purchase_using_stored_card_with_delimiter + response = @gateway.store(@credit_card, @options.merge(delimiter: '|')) + assert_success response + + response = @gateway.purchase(@amount, response.authorization, @options.merge(delimiter: '|', description: 'description, with, commas')) + assert_success response + assert_equal 'This transaction has been approved.', response.message + assert_equal 'description, with, commas', response.params['order_description'] + end + def test_failed_purchase_using_stored_card response = @gateway.store(@declined_card) assert_success response @@ -341,9 +583,9 @@ def test_successful_purchase_using_stored_card_new_payment_profile assert store.authorization new_card = credit_card('4007000000027') - customer_profile_id, _, _ = store.authorization.split('#') + customer_profile_id, = store.authorization.split('#') - assert response = @gateway.store(new_card, customer_profile_id: customer_profile_id, email: 'anet@example.com', billing_address: address) + assert response = @gateway.store(new_card, customer_profile_id:, email: 'anet@example.com', billing_address: address) assert_success response response = @gateway.purchase(@amount, response.authorization, @options) @@ -351,11 +593,11 @@ def test_successful_purchase_using_stored_card_new_payment_profile assert_equal 'This transaction has been approved.', response.message end - def test_successful_purchase_with_stored_card_and_level_2_data + def test_successful_purchase_with_stored_card_and_level_2_and_3_data store_response = @gateway.store(@credit_card, @options) assert_success store_response - response = @gateway.purchase(@amount, store_response.authorization, @options.merge(@level_2_options)) + response = @gateway.purchase(@amount, store_response.authorization, @options.merge(@level_2_and_3_options)) assert_success response assert_equal 'This transaction has been approved.', response.message end @@ -373,15 +615,15 @@ def test_successful_authorize_and_capture_using_stored_card assert_equal 'This transaction has been approved.', capture.message end - def test_successful_authorize_and_capture_using_stored_card_with_level_2_data + def test_successful_authorize_and_capture_using_stored_card_with_level_2_and_3_data store = @gateway.store(@credit_card, @options) assert_success store - auth = @gateway.authorize(@amount, store.authorization, @options.merge(@level_2_options)) + auth = @gateway.authorize(@amount, store.authorization, @options.merge(@level_2_and_3_options)) assert_success auth assert_equal 'This transaction has been approved.', auth.message - capture = @gateway.capture(@amount, auth.authorization, @options.merge(@level_2_options)) + capture = @gateway.capture(@amount, auth.authorization, @options.merge(@level_2_and_3_options)) assert_success capture assert_equal 'This transaction has been approved.', capture.message end @@ -457,14 +699,14 @@ def test_faux_successful_refund_using_stored_card assert_match %r{does not meet the criteria for issuing a credit}, refund.message, 'Only allowed to refund transactions that have settled. This is the best we can do for now testing wise.' end - def test_faux_successful_refund_using_stored_card_and_level_2_data + def test_faux_successful_refund_using_stored_card_and_level_2_and_3_data store = @gateway.store(@credit_card, @options) assert_success store - purchase = @gateway.purchase(@amount, store.authorization, @options.merge(@level_2_options)) + purchase = @gateway.purchase(@amount, store.authorization, @options.merge(@level_2_and_3_options)) assert_success purchase - refund = @gateway.refund(@amount, purchase.authorization, @options.merge(@level_2_options)) + refund = @gateway.refund(@amount, purchase.authorization, @options.merge(@level_2_and_3_options)) assert_failure refund assert_match %r{does not meet the criteria for issuing a credit}, refund.message, 'Only allowed to refund transactions that have settled. This is the best we can do for now testing wise.' end @@ -511,8 +753,8 @@ def test_failed_void_using_stored_card def test_bad_login gateway = AuthorizeNetGateway.new( - :login => 'X', - :password => 'Y' + login: 'X', + password: 'Y' ) response = gateway.purchase(@amount, @credit_card) @@ -525,6 +767,8 @@ def test_bad_login avs_result_code card_code cardholder_authentication_code + full_response_code + network_trans_id response_code response_reason_code response_reason_text @@ -539,7 +783,7 @@ def test_partial_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - capture = @gateway.capture(@amount-1, auth.authorization) + capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture end @@ -594,12 +838,24 @@ def test_successful_echeck_refund purchase = @gateway.purchase(@amount, @check, @options) assert_success purchase - @options.update(transaction_id: purchase.params['transaction_id'], test_request: true) + @options.update(transaction_id: purchase.params['transaction_id'], test_request: true) refund = @gateway.credit(@amount, @check, @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_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 @@ -625,7 +881,8 @@ 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 ) @@ -638,7 +895,8 @@ 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 ) @@ -654,8 +912,9 @@ 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=', + credit_card = network_tokenization_credit_card( + '5424000000000015', + payment_cryptogram: 'EjRWeJASNFZ4kBI0VniQEjRWeJA=', verification_value: nil ) @@ -666,10 +925,11 @@ def test_successful_credit_with_network_tokenization end def test_network_tokenization_transcript_scrubbing - credit_card = network_tokenization_credit_card('4111111111111111', - :brand => 'visa', - :eci => '05', - :payment_cryptogram => 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + credit_card = network_tokenization_credit_card( + '4111111111111111', + brand: 'visa', + eci: '05', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' ) transcript = capture_transcript(@gateway) do @@ -693,11 +953,19 @@ def test_purchase_scrubbing assert_scrubbed(@gateway.options[:password], transcript) end + def test_account_number_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @check, @options) + end + clean_transcript = @gateway.scrub(transcript) + + assert_scrubbed(@check.account_number, clean_transcript) + end + def test_verify_credentials assert @gateway.verify_credentials gateway = AuthorizeNetGateway.new(login: 'unknown_login', password: 'not_right') assert !gateway.verify_credentials end - end diff --git a/test/remote/gateways/remote_axcessms_test.rb b/test/remote/gateways/remote_axcessms_test.rb index 54b1c82213a..225064740f9 100644 --- a/test/remote/gateways/remote_axcessms_test.rb +++ b/test/remote/gateways/remote_axcessms_test.rb @@ -30,7 +30,7 @@ def test_successful_authorize_and_capture assert_success auth, 'Authorize failed' assert_match %r{Successful Processing - Request successfully processed}, auth.message - assert capture = @gateway.capture(@amount, auth.authorization, {mode: @mode}) + assert capture = @gateway.capture(@amount, auth.authorization, { mode: @mode }) assert_success capture, 'Capture failed' assert_match %r{Successful Processing - Request successfully processed}, capture.message end @@ -40,7 +40,7 @@ def test_successful_authorize_and_partial_capture assert_success auth, 'Authorize failed' assert_match %r{Successful Processing - Request successfully processed}, auth.message - assert capture = @gateway.capture(@amount-30, auth.authorization, {mode: @mode}) + assert capture = @gateway.capture(@amount - 30, auth.authorization, { mode: @mode }) assert_success capture, 'Capture failed' assert_match %r{Successful Processing - Request successfully processed}, capture.message end @@ -50,7 +50,7 @@ def test_successful_authorize_and_void assert_success auth, 'Authorize failed' assert_match %r{Successful Processing - Request successfully processed}, auth.message - assert void = @gateway.void(auth.authorization, {mode: @mode}) + assert void = @gateway.void(auth.authorization, { mode: @mode }) assert_success void, 'Void failed' assert_match %r{Successful Processing - Request successfully processed}, void.message end @@ -82,7 +82,7 @@ def test_successful_purchase_and_refund assert_success purchase, 'Purchase failed' assert_match %r{Successful Processing - Request successfully processed}, purchase.message - assert refund = @gateway.refund(@amount, purchase.authorization, {mode: @mode}) + assert refund = @gateway.refund(@amount, purchase.authorization, { mode: @mode }) assert_success refund, 'Refund failed' assert_match %r{Successful Processing - Request successfully processed}, refund.message end @@ -92,7 +92,7 @@ def test_successful_purchase_and_partial_refund assert_success purchase, 'Purchase failed' assert_match %r{Successful Processing - Request successfully processed}, purchase.message - assert refund = @gateway.refund(@amount-50, purchase.authorization, {mode: @mode}) + assert refund = @gateway.refund(@amount - 50, purchase.authorization, { mode: @mode }) assert_success refund, 'Refund failed' assert_match %r{Successful Processing - Request successfully processed}, refund.message end @@ -115,7 +115,7 @@ def test_failed_bigger_capture_then_authorised auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth, 'Authorize failed' - assert capture = @gateway.capture(@amount+30, auth.authorization, {mode: @mode}) + assert capture = @gateway.capture(@amount + 30, auth.authorization, { mode: @mode }) assert_failure capture, 'Capture failed' assert_match %r{PA value exceeded}, capture.message end @@ -127,13 +127,13 @@ def test_failed_authorize end def test_failed_refund - assert refund = @gateway.refund(@amount, 'invalid authorization', {mode: @mode}) + assert refund = @gateway.refund(@amount, 'invalid authorization', { mode: @mode }) assert_failure refund assert_match %r{Configuration Validation - Invalid payment data}, refund.message end def test_failed_void - void = @gateway.void('invalid authorization', {mode: @mode}) + void = @gateway.void('invalid authorization', { mode: @mode }) assert_failure void assert_match %r{Reference Error - reversal}, void.message end diff --git a/test/remote/gateways/remote_balanced_test.rb b/test/remote/gateways/remote_balanced_test.rb index d5930116419..9613e557f06 100644 --- a/test/remote/gateways/remote_balanced_test.rb +++ b/test/remote/gateways/remote_balanced_test.rb @@ -144,7 +144,7 @@ def test_refund_partial_purchase def test_store new_email_address = '%d@example.org' % Time.now store = @gateway.store(@credit_card, { - email: new_email_address + email: new_email_address }) assert_instance_of String, store.authorization end diff --git a/test/remote/gateways/remote_bambora_apac_test.rb b/test/remote/gateways/remote_bambora_apac_test.rb new file mode 100644 index 00000000000..43f92c7be3b --- /dev/null +++ b/test/remote/gateways/remote_bambora_apac_test.rb @@ -0,0 +1,122 @@ +require 'test_helper' + +class RemoteBamboraApacTest < Test::Unit::TestCase + def setup + @gateway = BamboraApacGateway.new(fixtures(:bambora_apac)) + + @credit_card = credit_card('4005550000000001') + + @options = { + order_id: '1', + billing_address: address, + description: 'Store Purchase' + } + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(200, @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[:password], transcript) + end + + def test_successful_purchase + response = @gateway.purchase(200, @credit_card, @options) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_failed_purchase + response = @gateway.purchase(105, @credit_card, @options) + assert_failure response + end + + def test_successful_authorize_and_capture + response = @gateway.authorize(200, @credit_card, @options) + assert_success response + response = @gateway.capture(200, response.authorization) + assert_success response + end + + def test_failed_authorize + response = @gateway.authorize(105, @credit_card, @options) + assert_failure response + end + + def test_failed_capture + response = @gateway.capture(200, '') + assert_failure response + end + + def test_successful_refund + response = @gateway.purchase(200, @credit_card, @options) + response = @gateway.refund(200, response.authorization, @options) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_failed_refund + response = @gateway.purchase(200, @credit_card, @options) + response = @gateway.refund(105, response.authorization, @options) + assert_failure response + assert_equal 'Do Not Honour', response.message + end + + def test_successful_void + response = @gateway.purchase(200, @credit_card, @options) + assert_success response + response = @gateway.void(response.authorization, amount: 200) + assert_success response + end + + def test_failed_void + response = @gateway.purchase(200, @credit_card, @options) + assert_success response + response = @gateway.void(123, amount: 200) + assert_failure response + assert_equal 'Cannot find matching transaction to VOID', response.message + end + + def test_successful_store + response = @gateway.store(@credit_card, @options) + assert_success response + end + + def test_failed_store + bad_credit_card = credit_card(nil) + + response = @gateway.store(bad_credit_card, @options) + assert_failure response + end + + def test_successful_purchase_using_stored_card + assert store_response = @gateway.store(@credit_card, @options) + assert_success store_response + + response = @gateway.purchase(500, store_response.authorization, @options) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_authorize_using_stored_card + assert store_response = @gateway.store(@credit_card, @options) + assert_success store_response + + response = @gateway.authorize(500, store_response.authorization, @options) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_invalid_login + gateway = BamboraApacGateway.new( + username: '', + password: '' + ) + response = gateway.purchase(200, @credit_card, @options) + assert_failure response + end +end diff --git a/test/remote/gateways/remote_bank_frick_test.rb b/test/remote/gateways/remote_bank_frick_test.rb index a7202c677d4..68408158ba7 100644 --- a/test/remote/gateways/remote_bank_frick_test.rb +++ b/test/remote/gateways/remote_bank_frick_test.rb @@ -24,7 +24,7 @@ def test_successful_purchase end def test_successful_purchase_with_minimal_options - assert response = @gateway.purchase(@amount, @credit_card, {address: address}) + assert response = @gateway.purchase(@amount, @credit_card, { address: }) assert_success response assert response.test? assert_match %r{Transaction succeeded}, response.message @@ -63,7 +63,7 @@ def test_partial_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture end @@ -84,7 +84,7 @@ def test_partial_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount-1, purchase.authorization) + assert refund = @gateway.refund(@amount - 1, purchase.authorization) assert_success refund end @@ -123,7 +123,7 @@ def test_invalid_login sender: '', channel: '', userid: '', - userpwd: '', + userpwd: '' ) response = gateway.purchase(@amount, @credit_card, @options) assert_failure response diff --git a/test/remote/gateways/remote_banwire_test.rb b/test/remote/gateways/remote_banwire_test.rb index 2f908d3ea9f..37004591279 100644 --- a/test/remote/gateways/remote_banwire_test.rb +++ b/test/remote/gateways/remote_banwire_test.rb @@ -1,4 +1,5 @@ # encoding: utf-8 + require 'test_helper' class RemoteBanwireTest < Test::Unit::TestCase @@ -6,12 +7,12 @@ def setup @gateway = BanwireGateway.new(fixtures(:banwire)) @amount = 100 - @credit_card = credit_card('5204164299999999', :verification_value => '999', :brand => 'mastercard') - @visa_credit_card = credit_card('4485814063899108', :verification_value => '434') + @credit_card = credit_card('5204164299999999', verification_value: '999', brand: 'mastercard') + @visa_credit_card = credit_card('4485814063899108', verification_value: '434') @declined_card = credit_card('4000300011112220') @options = { - billing_address: address, + billing_address: address } end @@ -36,7 +37,6 @@ def test_successful_purchase_with_extra_options assert_success response end - def test_unsuccessful_purchase assert response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response @@ -45,22 +45,21 @@ def test_unsuccessful_purchase def test_invalid_login gateway = BanwireGateway.new( - :login => 'fakeuser', - :currency => 'MXN' - ) + login: 'fakeuser', + currency: 'MXN' + ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal 'ID de cuenta invalido', 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(@credit_card.number, clean_transcript) - assert_scrubbed(@credit_card.verification_value.to_s, clean_transcript) -end + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @credit_card, @options) + end + clean_transcript = @gateway.scrub(transcript) + assert_scrubbed(@credit_card.number, clean_transcript) + assert_scrubbed(@credit_card.verification_value.to_s, clean_transcript) + end end diff --git a/test/remote/gateways/remote_barclaycard_smartpay_test.rb b/test/remote/gateways/remote_barclaycard_smartpay_test.rb index 104808183d1..1839dfe8ea9 100644 --- a/test/remote/gateways/remote_barclaycard_smartpay_test.rb +++ b/test/remote/gateways/remote_barclaycard_smartpay_test.rb @@ -6,83 +6,87 @@ def setup BarclaycardSmartpayGateway.ssl_strict = false @amount = 100 - @credit_card = credit_card('4111111111111111', :month => 8, :year => 2018, :verification_value => 737) - @declined_card = credit_card('4000300011112220', :month => 8, :year => 2018, :verification_value => 737) + @error_amount = 1_000_000_000_000_000_000_000 + @credit_card = credit_card('4111111111111111', month: 10, year: 2020, verification_value: 737) + @declined_card = credit_card('4000300011112220', month: 3, year: 2030, verification_value: 737) @three_ds_enrolled_card = credit_card('4212345678901237', brand: :visa) + @three_ds_2_enrolled_card = credit_card('4917610000000000', month: 10, year: 2020, verification_value: '737', brand: :visa) @options = { order_id: '1', - billing_address: { - name: 'Jim Smith', - address1: '100 Street', - company: 'Widgets Inc', - city: 'Ottawa', - state: 'ON', - zip: 'K1C2N6', - country: 'CA', - phone: '(555)555-5555', - fax: '(555)555-6666'}, + billing_address: { + name: 'Jim Smith', + address1: '100 Street', + company: 'Widgets Inc', + city: 'Ottawa', + state: 'ON', + zip: 'K1C2N6', + country: 'CA', + phone: '(555)555-5555', + fax: '(555)555-6666' + }, email: 'long@bob.com', customer: 'Longbob Longsen', description: 'Store Purchase' } @options_with_alternate_address = { - order_id: '1', - billing_address: { - name: 'PU JOI SO', - address1: '新北市店溪路3579號139樓', - company: 'Widgets Inc', - city: '新北市', - zip: '231509', - country: 'TW', - phone: '(555)555-5555', - fax: '(555)555-6666' - }, - email: 'pujoi@so.com', - customer: 'PU JOI SO', - description: 'Store Purchase' + order_id: '1', + billing_address: { + name: 'PU JOI SO', + address1: '新北市店溪路3579號139樓', + company: 'Widgets Inc', + city: '新北市', + zip: '231509', + country: 'TW', + phone: '(555)555-5555', + fax: '(555)555-6666' + }, + email: 'pujoi@so.com', + customer: 'PU JOI SO', + description: 'Store Purchase' } @options_with_house_number_and_street = { - order_id: '1', - house_number: '100', - street: 'Top Level Drive', - billing_address: { - name: 'Jim Smith', - address1: '100 Top Level Dr', - company: 'Widgets Inc', - city: 'Ottawa', - state: 'ON', - zip: 'K1C2N6', - country: 'CA', - phone: '(555)555-5555', - fax: '(555)555-6666' - }, - email: 'long@deb.com', - customer: 'Longdeb Longsen', - description: 'Store Purchase' + order_id: '1', + house_number: '100', + street: 'Top Level Drive', + billing_address: { + name: 'Jim Smith', + address1: '100 Top Level Dr', + company: 'Widgets Inc', + city: 'Ottawa', + state: 'ON', + zip: 'K1C2N6', + country: 'CA', + phone: '(555)555-5555', + fax: '(555)555-6666' + }, + email: 'long@deb.com', + customer: 'Longdeb Longsen', + description: 'Store Purchase' } @options_with_no_address = { - order_id: '1', - email: 'long@bob.com', - customer: 'Longbob Longsen', - description: 'Store Purchase' + order_id: '1', + email: 'long@bob.com', + customer: 'Longbob Longsen', + description: 'Store Purchase' } @options_with_credit_fields = { order_id: '1', - billing_address: { - name: 'Jim Smith', - address1: '100 Street', - company: 'Widgets Inc', - city: 'Ottawa', - state: 'ON', - zip: 'K1C2N6', - country: 'CA', - phone: '(555)555-5555', - fax: '(555)555-6666'}, + billing_address: { + name: 'Jim Smith', + address1: '100 Street', + company: 'Widgets Inc', + city: 'Ottawa', + state: 'ON', + zip: 'K1C2N6', + country: 'CA', + phone: '(555)555-5555', + fax: '(555)555-6666' + }, email: 'long@bob.com', customer: 'Longbob Longsen', description: 'Store Purchase', @@ -96,21 +100,47 @@ def setup } } - @avs_credit_card = credit_card('4400000000000008', - :month => 8, - :year => 2018, - :verification_value => 737) + @avs_credit_card = credit_card( + '4400000000000008', + month: 8, + year: 2018, + verification_value: 737 + ) @avs_address = @options.clone @avs_address.update(billing_address: { - name: 'Jim Smith', - street: 'Test AVS result', - houseNumberOrName: '2', - city: 'Cupertino', - state: 'CA', - zip: '95014', - country: 'US' - }) + name: 'Jim Smith', + street: 'Test AVS result', + houseNumberOrName: '2', + city: 'Cupertino', + state: 'CA', + zip: '95014', + country: 'US' + }) + + @normalized_3ds_2_options = { + reference: '345123', + shopper_email: 'john.smith@test.com', + shopper_ip: '77.110.174.153', + shopper_reference: 'John Smith', + billing_address: address(), + order_id: '123', + stored_credential: { reason_type: 'unscheduled' }, + three_ds_2: { + channel: 'browser', + browser_info: { + accept_header: 'unknown', + depth: 100, + java: false, + language: 'US', + height: 1000, + width: 500, + timezone: '-120', + user_agent: 'unknown' + }, + notification_url: 'https://example.com/notification' + } + } end def teardown @@ -130,25 +160,31 @@ def test_failed_purchase end def test_successful_purchase_with_unusual_address - response = @gateway.purchase(@amount, - @credit_card, - @options_with_alternate_address) + response = @gateway.purchase( + @amount, + @credit_card, + @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, - @credit_card, - @options.merge(street: 'Top Level Drive', house_number: '100')) + response = @gateway.purchase( + @amount, + @credit_card, + @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, - @credit_card, - @options_with_no_address) + response = @gateway.purchase( + @amount, + @credit_card, + @options_with_no_address + ) assert_success response assert_equal '[capture-received]', response.message end @@ -159,6 +195,23 @@ def test_successful_purchase_with_shopper_interaction assert_equal '[capture-received]', response.message end + def test_successful_purchase_with_device_fingerprint + response = @gateway.purchase(@amount, @credit_card, @options.merge(device_fingerprint: 'abcde1123')) + assert_success response + assert_equal '[capture-received]', response.message + end + + def test_successful_purchase_with_shopper_statement + response = @gateway.purchase( + @amount, + @credit_card, + @options.merge(shopper_statement: 'One-year premium subscription') + ) + + assert_success response + assert_equal '[capture-received]', response.message + end + def test_successful_authorize_with_3ds assert response = @gateway.authorize(@amount, @three_ds_enrolled_card, @options.merge(execute_threed: true)) assert_equal 'RedirectShopper', response.message @@ -169,6 +222,63 @@ def test_successful_authorize_with_3ds refute response.params['paRequest'].blank? end + def test_successful_authorize_with_3ds2_browser_client_data + assert response = @gateway.authorize(@amount, @three_ds_2_enrolled_card, @normalized_3ds_2_options) + assert response.test? + refute response.authorization.blank? + assert_equal response.params['resultCode'], 'IdentifyShopper' + refute response.params['additionalData']['threeds2.threeDS2Token'].blank? + refute response.params['additionalData']['threeds2.threeDSServerTransID'].blank? + refute response.params['additionalData']['threeds2.threeDSMethodURL'].blank? + end + + def test_successful_purchase_with_3ds2_exemption_requested_and_execute_threed_false + assert response = @gateway.authorize(@amount, @three_ds_2_enrolled_card, @normalized_3ds_2_options.merge(execute_threed: false, sca_exemption: 'lowValue')) + assert response.test? + refute response.authorization.blank? + + assert_equal response.params['resultCode'], 'Authorised' + end + + # According to Adyen documentation, if execute_threed is set to true and an exemption provided + # the gateway will apply and request for the specified exemption in the authentication request, + # after the device fingerprint is submitted to the issuer. + def test_successful_purchase_with_3ds2_exemption_requested_and_execute_threed_true + assert response = @gateway.authorize(@amount, @three_ds_2_enrolled_card, @normalized_3ds_2_options.merge(execute_threed: true, sca_exemption: 'lowValue')) + assert response.test? + refute response.authorization.blank? + + assert_equal response.params['resultCode'], 'IdentifyShopper' + refute response.params['additionalData']['threeds2.threeDS2Token'].blank? + refute response.params['additionalData']['threeds2.threeDSServerTransID'].blank? + refute response.params['additionalData']['threeds2.threeDSMethodURL'].blank? + end + + def test_successful_authorize_with_3ds2_app_based_request + three_ds_app_based_options = { + reference: '345123', + shopper_email: 'john.smith@test.com', + shopper_ip: '77.110.174.153', + shopper_reference: 'John Smith', + billing_address: address(), + order_id: '123', + stored_credential: { reason_type: 'unscheduled' }, + three_ds_2: { + channel: 'app' + } + } + + assert response = @gateway.authorize(@amount, @three_ds_2_enrolled_card, three_ds_app_based_options) + assert response.test? + refute response.authorization.blank? + assert_equal response.params['resultCode'], 'IdentifyShopper' + refute response.params['additionalData']['threeds2.threeDS2Token'].blank? + refute response.params['additionalData']['threeds2.threeDSServerTransID'].blank? + refute response.params['additionalData']['threeds2.threeDS2DirectoryServerInformation.algorithm'].blank? + refute response.params['additionalData']['threeds2.threeDS2DirectoryServerInformation.directoryServerId'].blank? + refute response.params['additionalData']['threeds2.threeDS2DirectoryServerInformation.publicKey'].blank? + end + def test_successful_authorize_and_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth @@ -190,9 +300,16 @@ def test_partial_capture assert_success capture end - def test_failed_capture + def test_failed_capture_with_bad_auth + response = @gateway.capture(100, '0000000000000000', @options) + assert_failure response + assert_equal('167: Original pspReference required for this operation', response.message) + end + + def test_failed_capture_with_bad_amount response = @gateway.capture(nil, '', @options) assert_failure response + assert_equal('137: Invalid amount specified', response.message) end def test_successful_refund @@ -232,6 +349,11 @@ def test_failed_credit_insufficient_validation # assert_failure response end + def test_successful_third_party_payout + response = @gateway.credit(@amount, @credit_card, @options_with_credit_fields.merge({ third_party_payout: true })) + assert_success response + end + def test_successful_void auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth @@ -261,9 +383,9 @@ def test_unsuccessful_verify def test_invalid_login gateway = BarclaycardSmartpayGateway.new( - company: '', - merchant: '', - password: '' + company: '', + merchant: '', + password: '' ) response = gateway.purchase(@amount, @credit_card, @options) assert_failure response @@ -276,9 +398,9 @@ def test_successful_store end def test_failed_store - response = @gateway.store(credit_card('', :month => '', :year => '', :verification_value => ''), @options) + response = @gateway.store(credit_card('4111111111111111', month: '', year: '', verification_value: ''), @options) assert_failure response - assert_equal 'Unprocessable Entity', response.message + assert_equal '129: Expiry Date Invalid', response.message end # AVS must be enabled on the gateway's end for the test account used @@ -295,17 +417,17 @@ def test_avs_no_with_house_number end def test_nonfractional_currency - response = @gateway.authorize(1234, @credit_card, @options.merge(:currency => 'JPY')) + response = @gateway.authorize(1234, @credit_card, @options.merge(currency: 'JPY')) assert_success response - response = @gateway.purchase(1234, @credit_card, @options.merge(:currency => 'JPY')) + response = @gateway.purchase(1234, @credit_card, @options.merge(currency: 'JPY')) assert_success response end def test_three_decimal_currency - response = @gateway.authorize(1234, @credit_card, @options.merge(:currency => 'OMR')) + response = @gateway.authorize(1234, @credit_card, @options.merge(currency: 'OMR')) assert_success response - response = @gateway.purchase(1234, @credit_card, @options.merge(:currency => 'OMR')) + response = @gateway.purchase(1234, @credit_card, @options.merge(currency: 'OMR')) assert_success response end @@ -319,4 +441,10 @@ def test_transcript_scrubbing assert_scrubbed(@credit_card.verification_value.to_s, clean_transcript) assert_scrubbed(@gateway.options[:password], clean_transcript) end + + def test_proper_error_response_handling + response = @gateway.purchase(@error_amount, @credit_card, @options) + assert_equal('702: Internal error', response.message) + assert_not_equal(response.message, 'Unable to communicate with the payment system.') + end end diff --git a/test/remote/gateways/remote_barclays_epdq_extra_plus_test.rb b/test/remote/gateways/remote_barclays_epdq_extra_plus_test.rb index 5996436a96a..69a37d7bf1c 100644 --- a/test/remote/gateways/remote_barclays_epdq_extra_plus_test.rb +++ b/test/remote/gateways/remote_barclays_epdq_extra_plus_test.rb @@ -1,19 +1,20 @@ # coding: utf-8 + require 'test_helper' class RemoteBarclaysEpdqExtraPlusTest < Test::Unit::TestCase def setup @gateway = BarclaysEpdqExtraPlusGateway.new(fixtures(:barclays_epdq_extra_plus)) @amount = 100 - @credit_card = credit_card('4000100011112224', :verification_value => '987') - @mastercard = credit_card('5399999999999999', :brand => 'mastercard') + @credit_card = credit_card('4000100011112224', verification_value: '987') + @mastercard = credit_card('5399999999999999', brand: 'mastercard') @declined_card = credit_card('1111111111111111') - @credit_card_d3d = credit_card('4000000000000002', :verification_value => '111') + @credit_card_d3d = credit_card('4000000000000002', verification_value: '111') @options = { - :order_id => generate_unique_id[0...30], - :billing_address => address, - :description => 'Store Purchase', - :currency => fixtures(:barclays_epdq_extra_plus)[:currency] || 'GBP' + order_id: generate_unique_id[0...30], + billing_address: address, + description: 'Store Purchase', + currency: fixtures(:barclays_epdq_extra_plus)[:currency] || 'GBP' } end @@ -36,13 +37,13 @@ def test_successful_purchase_with_minimal_info 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.purchase(@amount, credit_card('4000100011112224', first_name: 'Rémy', last_name: 'Fröåïør'), @options) assert_success response assert_equal BarclaysEpdqExtraPlusGateway::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.purchase(@amount, credit_card('4000100011112224', first_name: 'ワタシ', last_name: 'ёжзийклмнопрсуфхцч'), @options) assert_success response assert_equal BarclaysEpdqExtraPlusGateway::SUCCESS_MESSAGE, response.message end @@ -50,7 +51,7 @@ def test_successful_purchase_with_utf8_encoding_2 # NOTE: You have to set the "Hash algorithm" to "SHA-1" in the "Technical information"->"Global security parameters" # section of your account admin before running this test def test_successful_purchase_with_signature_encryptor_to_sha1 - gateway = BarclaysEpdqExtraPlusGateway.new(fixtures(:barclays_epdq_extra_plus).merge(:signature_encryptor => 'sha1')) + gateway = BarclaysEpdqExtraPlusGateway.new(fixtures(:barclays_epdq_extra_plus).merge(signature_encryptor: 'sha1')) assert response = gateway.purchase(@amount, @credit_card, @options) assert_success response assert_equal BarclaysEpdqExtraPlusGateway::SUCCESS_MESSAGE, response.message @@ -59,7 +60,7 @@ def test_successful_purchase_with_signature_encryptor_to_sha1 # NOTE: You have to set the "Hash algorithm" to "SHA-256" in the "Technical information"->"Global security parameters" # section of your account admin before running this test def test_successful_purchase_with_signature_encryptor_to_sha256 - gateway = BarclaysEpdqExtraPlusGateway.new(fixtures(:barclays_epdq_extra_plus).merge(:signature_encryptor => 'sha256')) + gateway = BarclaysEpdqExtraPlusGateway.new(fixtures(:barclays_epdq_extra_plus).merge(signature_encryptor: 'sha256')) assert response = gateway.purchase(@amount, @credit_card, @options) assert_success response assert_equal BarclaysEpdqExtraPlusGateway::SUCCESS_MESSAGE, response.message @@ -68,12 +69,21 @@ def test_successful_purchase_with_signature_encryptor_to_sha256 # NOTE: You have to set the "Hash algorithm" to "SHA-512" in the "Technical information"->"Global security parameters" # section of your account admin before running this test def test_successful_purchase_with_signature_encryptor_to_sha512 - gateway = BarclaysEpdqExtraPlusGateway.new(fixtures(:barclays_epdq_extra_plus).merge(:signature_encryptor => 'sha512')) + gateway = BarclaysEpdqExtraPlusGateway.new(fixtures(:barclays_epdq_extra_plus).merge(signature_encryptor: 'sha512')) assert response = gateway.purchase(@amount, @credit_card, @options) assert_success response assert_equal BarclaysEpdqExtraPlusGateway::SUCCESS_MESSAGE, response.message end + def test_successful_purchase_with_custom_eci + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(eci: 1)) + assert_success response + assert_equal BarclaysEpdqExtraPlusGateway::SUCCESS_MESSAGE, response.message + assert_equal '1', response.params['ECI'] + assert_equal @options[:currency], response.params['currency'] + assert_equal @options[:order_id], response.order_id + end + def test_successful_with_non_numeric_order_id @options[:order_id] = "##{@options[:order_id][0...26]}.12" assert response = @gateway.purchase(@amount, @credit_card, @options) @@ -91,7 +101,7 @@ def test_successful_purchase_without_explicit_order_id def test_unsuccessful_purchase assert response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response - assert_equal 'No brand', response.message + assert_equal 'No brand or invalid card number', response.message end def test_successful_authorize_with_mastercard @@ -112,7 +122,7 @@ def test_authorize_and_capture def test_unsuccessful_capture assert response = @gateway.capture(@amount, '') assert_failure response - assert_equal 'No card no, no exp date, no brand', response.message + assert_equal 'No card no, no exp date, no brand or invalid card number', response.message end def test_successful_void @@ -124,76 +134,75 @@ def test_successful_void assert_success void end -=begin Enable if/when fully enabled account is available to test - 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_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_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_success response - end - - def test_successful_store - assert response = @gateway.store(@credit_card, :billing_id => 'test_alias') - assert_success response - assert purchase = @gateway.purchase(@amount, 'test_alias') - assert_success purchase - end - - def test_successful_store_generated_alias - assert response = @gateway.store(@credit_card) - assert_success response - assert purchase = @gateway.purchase(@amount, response.billing_id) - assert_success purchase - end - - def test_successful_store - assert response = @gateway.store(@credit_card, :billing_id => 'test_alias') - assert_success response - assert purchase = @gateway.purchase(@amount, 'test_alias') - assert_success purchase - end - - def test_successful_unreferenced_credit - assert credit = @gateway.credit(@amount, @credit_card, @options) - assert_success credit - assert credit.authorization - assert_equal BarclaysEpdqExtraPlusGateway::SUCCESS_MESSAGE, credit.message - end - - # NOTE: You have to allow USD as a supported currency in the "Account"->"Currencies" - # section of your account admin before running this test - def test_successful_purchase_with_custom_currency_at_the_gateway_level - gateway = BarclaysEpdqExtraPlusGateway.new(fixtures(:barclays_epdq_extra_plus).merge(:currency => 'USD')) - assert response = gateway.purchase(@amount, @credit_card) - assert_success response - assert_equal BarclaysEpdqExtraPlusGateway::SUCCESS_MESSAGE, response.message - assert_equal "USD", response.params["currency"] - end - - # NOTE: You have to allow USD as a supported currency in the "Account"->"Currencies" - # section of your account admin before running this test - def test_successful_purchase_with_custom_currency - gateway = BarclaysEpdqExtraPlusGateway.new(fixtures(:barclays_epdq_extra_plus).merge(:currency => 'EUR')) - assert response = gateway.purchase(@amount, @credit_card, @options.merge(:currency => 'USD')) - assert_success response - assert_equal BarclaysEpdqExtraPlusGateway::SUCCESS_MESSAGE, response.message - assert_equal "USD", response.params["currency"] - end - - # NOTE: You have to contact Barclays to make sure your test account allow 3D Secure transactions before running this test - def test_successful_purchase_with_3d_secure - assert response = @gateway.purchase(@amount, @credit_card_d3d, @options.merge(:d3d => true)) - assert_success response - assert_equal '46', response.params["STATUS"] - assert_equal BarclaysEpdqExtraPlusGateway::SUCCESS_MESSAGE, response.message - assert response.params["HTML_ANSWER"] - end -=end + # Enable if/when fully enabled account is available to test + # 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_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_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_success response + # end + # + # def test_successful_store + # assert response = @gateway.store(@credit_card, :billing_id => 'test_alias') + # assert_success response + # assert purchase = @gateway.purchase(@amount, 'test_alias') + # assert_success purchase + # end + # + # def test_successful_store_generated_alias + # assert response = @gateway.store(@credit_card) + # assert_success response + # assert purchase = @gateway.purchase(@amount, response.billing_id) + # assert_success purchase + # end + # + # def test_successful_store + # assert response = @gateway.store(@credit_card, :billing_id => 'test_alias') + # assert_success response + # assert purchase = @gateway.purchase(@amount, 'test_alias') + # assert_success purchase + # end + # + # def test_successful_unreferenced_credit + # assert credit = @gateway.credit(@amount, @credit_card, @options) + # assert_success credit + # assert credit.authorization + # assert_equal BarclaysEpdqExtraPlusGateway::SUCCESS_MESSAGE, credit.message + # end + # + # # NOTE: You have to allow USD as a supported currency in the "Account"->"Currencies" + # # section of your account admin before running this test + # def test_successful_purchase_with_custom_currency_at_the_gateway_level + # gateway = BarclaysEpdqExtraPlusGateway.new(fixtures(:barclays_epdq_extra_plus).merge(:currency => 'USD')) + # assert response = gateway.purchase(@amount, @credit_card) + # assert_success response + # assert_equal BarclaysEpdqExtraPlusGateway::SUCCESS_MESSAGE, response.message + # assert_equal "USD", response.params["currency"] + # end + # + # # NOTE: You have to allow USD as a supported currency in the "Account"->"Currencies" + # # section of your account admin before running this test + # def test_successful_purchase_with_custom_currency + # gateway = BarclaysEpdqExtraPlusGateway.new(fixtures(:barclays_epdq_extra_plus).merge(:currency => 'EUR')) + # assert response = gateway.purchase(@amount, @credit_card, @options.merge(:currency => 'USD')) + # assert_success response + # assert_equal BarclaysEpdqExtraPlusGateway::SUCCESS_MESSAGE, response.message + # assert_equal "USD", response.params["currency"] + # end + # + # # NOTE: You have to contact Barclays to make sure your test account allow 3D Secure transactions before running this test + # def test_successful_purchase_with_3d_secure + # assert response = @gateway.purchase(@amount, @credit_card_d3d, @options.merge(:d3d => true)) + # assert_success response + # assert_equal '46', response.params["STATUS"] + # assert_equal BarclaysEpdqExtraPlusGateway::SUCCESS_MESSAGE, response.message + # assert response.params["HTML_ANSWER"] + # end def test_successful_refund assert purchase = @gateway.purchase(@amount, @credit_card, @options) @@ -207,7 +216,7 @@ def test_successful_refund def test_unsuccessful_refund assert purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount+1, purchase.authorization, @options) # too much refund requested + assert refund = @gateway.refund(@amount + 1, purchase.authorization, @options) # too much refund requested assert_failure refund assert refund.authorization assert_equal 'Overflow in refunds requests', refund.message @@ -215,11 +224,11 @@ def test_unsuccessful_refund def test_invalid_login gateway = BarclaysEpdqExtraPlusGateway.new( - :login => '', - :user => '', - :password => '', - :signature_encryptor => 'none' - ) + login: '', + user: '', + password: '', + signature_encryptor: 'none' + ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal 'Some of the data entered is incorrect. please retry.', response.message diff --git a/test/remote/gateways/remote_be2bill_test.rb b/test/remote/gateways/remote_be2bill_test.rb index 6e48bdcba5e..0c0b1da2761 100644 --- a/test/remote/gateways/remote_be2bill_test.rb +++ b/test/remote/gateways/remote_be2bill_test.rb @@ -9,13 +9,13 @@ def setup @declined_card = credit_card('5555557376384001') @options = { - :order_id => '1', - :description => 'Store Purchase', - :client_id => '1', - :referrer => 'google.com', - :user_agent => 'Firefox 25', - :ip => '127.0.0.1', - :email => 'customer@yopmail.com' + order_id: '1', + description: 'Store Purchase', + client_id: '1', + referrer: 'google.com', + user_agent: 'Firefox 25', + ip: '127.0.0.1', + email: 'customer@yopmail.com' } end @@ -49,8 +49,8 @@ def test_failed_capture def test_invalid_login gateway = Be2billGateway.new( - :login => '', - :password => '' + login: '', + password: '' ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response diff --git a/test/remote/gateways/remote_beanstream_interac_test.rb b/test/remote/gateways/remote_beanstream_interac_test.rb index 497b0bb744f..4b716d8be38 100644 --- a/test/remote/gateways/remote_beanstream_interac_test.rb +++ b/test/remote/gateways/remote_beanstream_interac_test.rb @@ -1,51 +1,50 @@ require 'test_helper' class RemoteBeanstreamInteracTest < Test::Unit::TestCase - def setup @gateway = BeanstreamInteracGateway.new(fixtures(:beanstream_interac)) - + @amount = 100 - - @options = { - :order_id => generate_unique_id, - :billing_address => { - :name => 'xiaobo zzz', - :phone => '555-555-5555', - :address1 => '1234 Levesque St.', - :address2 => 'Apt B', - :city => 'Montreal', - :state => 'QC', - :country => 'CA', - :zip => 'H2C1X8' + + @options = { + order_id: generate_unique_id, + billing_address: { + name: 'xiaobo zzz', + phone: '555-555-5555', + address1: '1234 Levesque St.', + address2: 'Apt B', + city: 'Montreal', + state: 'QC', + country: 'CA', + zip: 'H2C1X8' }, - :email => 'xiaobozzz@example.com', - :subtotal => 800, - :shipping => 100, - :tax1 => 100, - :tax2 => 100, - :custom => 'reference one' + email: 'xiaobozzz@example.com', + subtotal: 800, + shipping: 100, + tax1: 100, + tax2: 100, + custom: 'reference one' } end - + def test_successful_purchase assert response = @gateway.purchase(@amount, @options) assert_success response assert_equal 'R', response.params['responseType'] assert_false response.redirect.blank? end - + def test_failed_confirmation assert response = @gateway.confirm('') assert_failure response end - + def test_invalid_login gateway = BeanstreamInteracGateway.new( - :merchant_id => '', - :login => '', - :password => '' - ) + merchant_id: '', + login: '', + password: '' + ) assert response = gateway.purchase(@amount, @options) assert_failure response assert_equal 'Invalid merchant id (merchant_id = 0)', response.message diff --git a/test/remote/gateways/remote_beanstream_test.rb b/test/remote/gateways/remote_beanstream_test.rb index 0002e292bc8..cc950418571 100644 --- a/test/remote/gateways/remote_beanstream_test.rb +++ b/test/remote/gateways/remote_beanstream_test.rb @@ -6,7 +6,6 @@ # only work the first time you run them since the profile, if created again, becomes a duplicate. There is a setting in order settings which, when unchecked will allow the tests to be run any number # of times without needing the manual deletion step between test runs. The setting is: Do not allow profile to be created with card data duplicated from an existing profile. class RemoteBeanstreamTest < Test::Unit::TestCase - def setup @gateway = BeanstreamGateway.new(fixtures(:beanstream)) @@ -18,50 +17,51 @@ def setup @mastercard = credit_card('5100000010001004') @declined_mastercard = credit_card('5100000020002000') - @amex = credit_card('371100001000131', {:verification_value => 1234}) - @declined_amex = credit_card('342400001000180', {:verification_value => 1234}) + @amex = credit_card('371100001000131', { verification_value: 1234 }) + @declined_amex = credit_card('342400001000180', { verification_value: 1234 }) # Canadian EFT - @check = check( - :institution_number => '001', - :transit_number => '26729' - ) + @check = check( + institution_number: '001', + transit_number: '26729' + ) @amount = 1500 @options = { - :order_id => generate_unique_id, - :billing_address => { - :name => 'xiaobo zzz', - :phone => '555-555-5555', - :address1 => '4444 Levesque St.', - :address2 => 'Apt B', - :city => 'Montreal', - :state => 'Quebec', - :country => 'CA', - :zip => 'H2C1X8' + order_id: generate_unique_id, + billing_address: { + name: 'xiaobo zzz', + phone: '555-555-5555', + address1: '4444 Levesque St.', + address2: 'Apt B', + city: 'Montreal', + state: 'Quebec', + country: 'CA', + zip: 'H2C1X8' }, - :shipping_address => { - :name => 'shippy', - :phone => '888-888-8888', - :address1 => '777 Foster Street', - :address2 => 'Ste #100', - :city => 'Durham', - :state => 'North Carolina', - :country => 'US', - :zip => '27701' + shipping_address: { + name: 'shippy', + phone: '888-888-8888', + address1: '777 Foster Street', + address2: 'Ste #100', + city: 'Durham', + state: 'North Carolina', + country: 'US', + zip: '27701' }, - :email => 'xiaobozzz@example.com', - :subtotal => 800, - :shipping => 100, - :tax1 => 100, - :tax2 => 100, - :custom => 'reference one' + email: 'xiaobozzz@example.com', + subtotal: 800, + shipping: 100, + tax1: 100, + tax2: 100, + custom: 'reference one' } @recurring_options = @options.merge( - :interval => { :unit => :months, :length => 1 }, - :occurences => 5) + interval: { unit: :months, length: 1 }, + occurences: 5 + ) end def test_successful_visa_purchase @@ -79,12 +79,18 @@ def test_successful_visa_purchase_with_recurring end def test_successful_visa_purchase_no_cvv - assert response = @gateway.purchase(@amount, @visa_no_cvv, @options) + assert response = @gateway.purchase(@amount, @visa_no_cvv, @options.merge(recurring: true)) assert_success response assert_false response.authorization.blank? assert_equal 'Approved', response.message end + def test_unsuccessful_visa_purchase_with_no_cvv + assert response = @gateway.purchase(@amount, @visa_no_cvv, @options) + assert_failure response + assert_equal 'Card CVD is invalid.', response.message + end + def test_unsuccessful_visa_purchase assert response = @gateway.purchase(@amount, @declined_visa, @options) assert_failure response @@ -140,9 +146,9 @@ def test_successful_purchase_with_state_in_iso_format def test_successful_purchase_with_only_email options = { - :order_id => generate_unique_id, - :email => 'xiaobozzz@example.com', - :shipping_email => 'ship@mail.com' + order_id: generate_unique_id, + email: 'xiaobozzz@example.com', + shipping_email: 'ship@mail.com' } assert response = @gateway.purchase(@amount, @visa, options) @@ -181,6 +187,16 @@ def test_failed_purchase_due_to_missing_country_with_state assert_match %r{Invalid shipping country id}, response.message end + def test_authorize_and_void + assert auth = @gateway.authorize(@amount, @visa, @options) + assert_success auth + assert_equal 'Approved', auth.message + assert_false auth.authorization.blank? + + assert void = @gateway.void(auth.authorization) + assert_success void + end + def test_authorize_and_capture assert auth = @gateway.authorize(@amount, @visa, @options) assert_success auth @@ -261,7 +277,7 @@ def test_successful_check_purchase_and_refund assert_success purchase assert refund = @gateway.refund(@amount, purchase.authorization) - assert_success credit + assert_success refund end def test_successful_recurring @@ -277,7 +293,7 @@ def test_successful_update_recurring assert response.test? assert_false response.authorization.blank? - assert response = @gateway.update_recurring(@amount + 500, @visa, @recurring_options.merge(:account_id => response.params['rbAccountId'])) + assert response = @gateway.update_recurring(@amount + 500, @visa, @recurring_options.merge(account_id: response.params['rbAccountId'])) assert_success response end @@ -287,30 +303,30 @@ def test_successful_cancel_recurring assert response.test? assert_false response.authorization.blank? - assert response = @gateway.cancel_recurring(:account_id => response.params['rbAccountId']) + assert response = @gateway.cancel_recurring(account_id: response.params['rbAccountId']) assert_success response end def test_invalid_login gateway = BeanstreamGateway.new( - :merchant_id => '', - :login => '', - :password => '' - ) + merchant_id: '', + login: '', + password: '' + ) assert response = gateway.purchase(@amount, @visa, @options) assert_failure response assert_equal 'merchantid=Invalid merchant id (merchant_id = )', response.message end def test_successful_add_to_vault_with_store_method - assert response = @gateway.store(@visa,@options) + assert response = @gateway.store(@visa, @options) assert_equal 'Operation Successful', response.message assert_success response assert_not_nil response.params['customer_vault_id'] end def test_add_to_vault_with_custom_vault_id_with_store_method - @options[:vault_id] = rand(100000)+10001 + @options[:vault_id] = rand(10001..110000) assert response = @gateway.store(@visa, @options.dup) assert_equal 'Operation Successful', response.message assert_success response @@ -354,7 +370,7 @@ def test_delete_from_vault_with_unstore_method def test_successful_add_to_vault_and_use test_add_to_vault_with_custom_vault_id_with_store_method - assert second_response = @gateway.purchase(@amount*2, @options[:vault_id], @options) + assert second_response = @gateway.purchase(@amount * 2, @options[:vault_id], @options) assert_equal 'Approved', second_response.message assert second_response.success? end @@ -364,13 +380,13 @@ def test_unsuccessful_visa_with_vault assert response = @gateway.update(@options[:vault_id], @declined_visa) assert_success response - assert second_response = @gateway.purchase(@amount*2, @options[:vault_id], @options) + assert second_response = @gateway.purchase(@amount * 2, @options[:vault_id], @options) assert_equal 'DECLINE', second_response.message end def test_unsuccessful_closed_profile_charge test_delete_from_vault - assert second_response = @gateway.purchase(@amount*2, @options[:vault_id], @options) + assert second_response = @gateway.purchase(@amount * 2, @options[:vault_id], @options) assert_failure second_response assert_match %r{Invalid customer code\.}, second_response.message end @@ -384,6 +400,36 @@ def test_transcript_scrubbing assert_scrubbed(@visa.number, clean_transcript) assert_scrubbed(@visa.verification_value.to_s, clean_transcript) assert_scrubbed(@gateway.options[:password], clean_transcript) + assert_scrubbed(@gateway.options[:api_key], clean_transcript) + end + + def test_successful_authorize_with_3ds_v1_options + @options[:three_d_secure] = { + version: '1.0', + cavv: '3q2+78r+ur7erb7vyv66vv\/\/\/\/8=', + eci: '05', + xid: 'ODUzNTYzOTcwODU5NzY3Qw==', + enrolled: 'true', + authentication_response_status: 'Y' + } + assert response = @gateway.purchase(@amount, @visa, @options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_authorize_with_3ds_v2_options + @options[:three_d_secure] = { + version: '2.2.0', + cavv: '3q2+78r+ur7erb7vyv66vv\/\/\/\/8=', + eci: '05', + ds_transaction_id: 'ODUzNTYzOTcwODU5NzY3Qw==', + enrolled: 'Y', + authentication_response_status: 'Y' + } + + assert response = @gateway.purchase(@amount, @visa, @options) + assert_success response + assert_equal 'Approved', response.message end private @@ -400,7 +446,7 @@ def generate_single_use_token(credit_card) 'number' => credit_card.number, 'expiry_month' => '01', 'expiry_year' => (Time.now.year + 1) % 100, - 'cvd' => credit_card.verification_value, + 'cvd' => credit_card.verification_value }.to_json response = http.request(request) diff --git a/test/remote/gateways/remote_blue_pay_test.rb b/test/remote/gateways/remote_blue_pay_test.rb index ee6b779e1b9..da99b51d043 100644 --- a/test/remote/gateways/remote_blue_pay_test.rb +++ b/test/remote/gateways/remote_blue_pay_test.rb @@ -8,18 +8,19 @@ def setup @amount = 100 @credit_card = credit_card('4242424242424242') @options = { - :order_id => generate_unique_id, - :billing_address => address, - :description => 'Store purchase' + order_id: generate_unique_id, + billing_address: address, + description: 'Store purchase', + ip: '192.168.0.1' } @recurring_options = { - :rebill_amount => 100, - :rebill_start_date => Date.today, - :rebill_expression => '1 DAY', - :rebill_cycles => '4', - :billing_address => address.merge(:first_name => 'Jim', :last_name => 'Smith'), - :duplicate_override => 1 + rebill_amount: 100, + rebill_start_date: Date.today, + rebill_expression: '1 DAY', + rebill_cycles: '4', + billing_address: address.merge(first_name: 'Jim', last_name: 'Smith'), + duplicate_override: 1 } end @@ -33,7 +34,16 @@ def test_successful_purchase # The included test account credentials do not support ACH processor. def test_successful_purchase_with_check - assert response = @gateway.purchase(@amount, check, @options.merge(:email=>'foo@example.com')) + assert response = @gateway.purchase(@amount, check, @options.merge(email: 'foo@example.com')) + assert_success response + assert response.test? + assert_equal 'ACH Accepted', response.message + assert response.authorization + end + + def test_successful_purchase_with_stored_credential + options = @options.merge(stored_credential: { initiator: 'cardholder', reason_type: 'recurring' }) + assert response = @gateway.purchase(@amount, @credit_card, options) assert_success response assert response.test? assert_equal 'This transaction has been approved', response.message @@ -49,7 +59,7 @@ def test_expired_credit_card end def test_forced_test_mode_purchase - gateway = BluePayGateway.new(fixtures(:blue_pay).update(:test => true)) + gateway = BluePayGateway.new(fixtures(:blue_pay).update(test: true)) assert response = gateway.purchase(@amount, @credit_card, @options) assert_success response assert response.test? @@ -80,7 +90,7 @@ def test_that_we_understand_and_parse_all_keys_in_rebilling_response assert response = @gateway.recurring(@amount, @credit_card, @recurring_options) assert_success response rebill_id = response.params['rebid'] - assert response = @gateway.update_recurring(:rebill_id => rebill_id, :rebill_amount => @amount * 2) + assert response = @gateway.update_recurring(rebill_id:, rebill_amount: @amount * 2) assert_success response response_keys = response.params.keys.map(&:to_sym) @@ -110,8 +120,8 @@ def test_authorization_and_void def test_bad_login gateway = BluePayGateway.new( - :login => 'X', - :password => 'Y' + login: 'X', + password: 'Y' ) assert response = gateway.purchase(@amount, @credit_card) @@ -122,8 +132,8 @@ def test_bad_login def test_using_test_request gateway = BluePayGateway.new( - :login => 'X', - :password => 'Y' + login: 'X', + password: 'Y' ) assert response = gateway.purchase(@amount, @credit_card) assert_equal Response, response.class @@ -139,7 +149,7 @@ def test_successful_recurring rebill_id = response.params['rebid'] - assert response = @gateway.update_recurring(:rebill_id => rebill_id, :rebill_amount => @amount * 2) + assert response = @gateway.update_recurring(rebill_id:, rebill_amount: @amount * 2) assert_success response assert response = @gateway.status_recurring(rebill_id) @@ -170,6 +180,24 @@ def test_successful_purchase_with_solution_id ActiveMerchant::Billing::BluePayGateway.application_id = nil end + def test_successful_refund_with_check + assert response = @gateway.purchase(@amount, check, @options.merge(email: 'foo@example.com')) + assert_success response + assert response.test? + assert_equal 'ACH Accepted', response.message + assert response.authorization + + assert refund = @gateway.refund(@amount, response.authorization, @options.merge(doc_type: 'PPD')) + assert_success refund + assert_equal 'ACH VOIDED', refund.message + end + + def test_successful_credit_with_check + assert credit = @gateway.credit(@amount, check, @options.merge(doc_type: 'PPD')) + assert_success credit + assert_equal 'ACH Accepted', credit.message + end + def test_transcript_scrubbing transcript = capture_transcript(@gateway) do @gateway.purchase(@amount, @credit_card, @options) @@ -179,4 +207,13 @@ def test_transcript_scrubbing assert_scrubbed(@credit_card.number, clean_transcript) assert_scrubbed(@credit_card.verification_value.to_s, clean_transcript) end + + def test_account_number_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, check, @options) + end + clean_transcript = @gateway.scrub(transcript) + + assert_scrubbed(check.account_number, clean_transcript) + end end diff --git a/test/remote/gateways/remote_blue_snap_test.rb b/test/remote/gateways/remote_blue_snap_test.rb index 8530b92dba5..752cfe3d732 100644 --- a/test/remote/gateways/remote_blue_snap_test.rb +++ b/test/remote/gateways/remote_blue_snap_test.rb @@ -6,8 +6,63 @@ def setup @amount = 100 @credit_card = credit_card('4263982640269299') - @declined_card = credit_card('4917484589897107', 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: + # Order Total Amount Decline Threshold = 3728 + # Payment Country Decline List = Brazil + @fraudulent_amount = 3729 + @fraudulent_card = credit_card('4007702835532454') + @options = { billing_address: address } + @options_3ds2 = @options.merge( + three_d_secure: { + eci: '05', + cavv: 'AAABAWFlmQAAAABjRWWZEEFgFz+A', + xid: 'MGpHWm5ZWVpKclo0aUk0VmltVDA=', + ds_transaction_id: 'jhg34-sdgds87-sdg87-sdfg7', + version: '2.2.0' + } + ) + @refund_options = { + reason: 'Refund for order #1992', + cancel_subscription: 'false', + tax_amount: 0.05, + transaction_meta_data: [ + { + meta_key: 'refundedItems', + meta_value: '1552,8832', + meta_description: 'Refunded Items', + meta_is_visible: 'false' + }, + { + meta_key: 'Number2', + meta_value: 'KTD', + meta_description: 'Metadata 2', + meta_is_visible: 'true' + } + ] + } + + @check = check + @invalid_check = check(routing_number: '123456', account_number: '123456789') + @valid_check_options = { + billing_address: { + address1: '123 Street', + address2: 'Apt 1', + city: 'Happy City', + state: 'CA', + zip: '94901' + }, + authorized_by_shopper: true + } end def test_successful_purchase @@ -16,6 +71,38 @@ def test_successful_purchase assert_equal 'Success', response.message end + def test_successful_fractionless_currency_purchase + options = @options.merge(currency: 'JPY') + response = @gateway.purchase(12300, @credit_card, options) + assert_success response + assert_equal 'Success', response.message + end + + def test_successful_three_decimal_currency_purchase + options = @options.merge(currency: 'BHD') + response = @gateway.purchase(1234, @credit_card, options) + assert_success response + assert_equal 'Success', response.message + end + + def test_successful_purchase_with_cabal_card + options = @options.merge({ + email: 'joe@example.com' + }) + response = @gateway.purchase(@amount, @cabal_card, options) + assert_success response + assert_equal 'Success', response.message + end + + def test_successful_purchase_with_naranja_card + options = @options.merge({ + email: 'joe@example.com' + }) + response = @gateway.purchase(@amount, @naranja_card, options) + assert_success response + assert_equal 'Success', response.message + end + def test_successful_purchase_sans_options response = @gateway.purchase(@amount, @credit_card) assert_success response @@ -28,7 +115,127 @@ def test_successful_purchase_with_more_options ip: '127.0.0.1', email: 'joe@example.com', description: 'Product Description', - soft_descriptor: 'OnCardStatement' + soft_descriptor: 'OnCardStatement', + personal_identification_number: 'CNPJ' + }) + + response = @gateway.purchase(@amount, @credit_card, more_options) + assert_success response + assert_equal 'Success', response.message + + # description SHOULD BE set as a meta-data field + assert_not_empty response.params['transaction-meta-data'] + meta = response.params['transaction-meta-data'] + + assert_equal 1, meta.length + assert_equal 'description', meta[0]['meta-key'] + assert_equal 'Product Description', meta[0]['meta-value'] + assert_equal 'Description', meta[0]['meta-description'] + end + + def test_successful_purchase_with_meta_data + more_options = @options.merge({ + order_id: '1', + ip: '127.0.0.1', + email: 'joe@example.com', + transaction_meta_data: [ + { + meta_key: 'stateTaxAmount', + meta_value: '20.00', + meta_description: 'State Tax Amount' + }, + { + meta_key: 'cityTaxAmount', + meta_value: '10.00', + meta_description: 'City Tax Amount' + }, + { + meta_key: 'description', + meta_value: 'Product ABC', + meta_description: 'Product Description' + } + ], + soft_descriptor: 'OnCardStatement', + personal_identification_number: 'CNPJ' + }) + + response = @gateway.purchase(@amount, @credit_card, more_options) + assert_success response + assert_equal 'Success', response.message + + # description SHOULD BE set as a meta-data field + assert_not_empty response.params['transaction-meta-data'] + meta = response.params['transaction-meta-data'] + + assert_equal 3, meta.length + + meta.each { |m| + assert_true m['meta-key'].length > 0 + assert_true m['meta-value'].length > 0 + assert_true m['meta-description'].length > 0 + + case m['meta-key'] + when 'description' + assert_equal 'Product ABC', m['meta-value'] + assert_equal 'Product Description', m['meta-description'] + when 'cityTaxAmount' + assert_equal '10.00', m['meta-value'] + assert_equal 'City Tax Amount', m['meta-description'] + when 'stateTaxAmount' + assert_equal '20.00', m['meta-value'] + assert_equal 'State Tax Amount', m['meta-description'] + end + } + end + + def test_successful_purchase_with_metadata_empty + more_options = @options.merge({ + order_id: '1', + ip: '127.0.0.1', + email: 'joe@example.com', + soft_descriptor: 'OnCardStatement', + personal_identification_number: 'CNPJ' + }) + + response = @gateway.purchase(@amount, @credit_card, more_options) + assert_success response + assert_equal 'Success', response.message + + assert_nil response.params['transaction-meta-data'] + end + + def test_successful_purchase_with_card_holder_info + more_options = @options.merge({ + order_id: '1', + ip: '127.0.0.1', + email: 'joe@example.com', + soft_descriptor: 'OnCardStatement', + personal_identification_number: 'CNPJ', + billing_address: { + address1: '123 Street', + address2: 'Apt 1', + city: 'Happy City', + state: 'CA', + zip: '94901' + }, + phone_number: '555 888 0000' + }) + + response = @gateway.purchase(@amount, @credit_card, more_options) + assert_success response + assert_equal 'Success', response.message + end + + def test_successful_purchase_with_shipping_contact_info + more_options = @options.merge({ + shipping_address: { + address1: '123 Main St', + address2: 'Apt B', + city: 'Springfield', + state: 'NC', + country: 'US', + zip: '27701' + } }) response = @gateway.purchase(@amount, @credit_card, more_options) @@ -36,6 +243,46 @@ def test_successful_purchase_with_more_options assert_equal 'Success', response.message end + def test_successful_purchase_with_3ds2_auth + response = @gateway.purchase(@amount, @three_ds_visa_card, @options_3ds2) + assert_success response + assert_equal 'Success', response.message + end + + def test_successful_purchase_for_stored_credentials_with_cit + cit_stored_credentials = { + initiator: 'cardholder' + } + response = @gateway.purchase(@amount, @three_ds_visa_card, @options_3ds2.merge({ stored_credential: cit_stored_credentials })) + assert_success response + assert_equal 'Success', response.message + + cit_stored_credentials = { + initiator: 'cardholder', + network_transaction_id: response.params['original-network-transaction-id'] + } + response = @gateway.purchase(@amount, @three_ds_visa_card, @options_3ds2.merge({ stored_credential: cit_stored_credentials })) + assert_success response + assert_equal 'Success', response.message + end + + def test_successful_purchase_for_stored_credentials_with_mit + mit_stored_credentials = { + initiator: 'merchant' + } + response = @gateway.purchase(@amount, @three_ds_visa_card, @options_3ds2.merge({ stored_credential: mit_stored_credentials })) + assert_success response + assert_equal 'Success', response.message + + mit_stored_credentials = { + initiator: 'merchant', + network_transaction_id: response.params['original-network-transaction-id'] + } + response = @gateway.purchase(@amount, @three_ds_visa_card, @options_3ds2.merge({ stored_credential: mit_stored_credentials })) + assert_success response + assert_equal 'Success', response.message + end + def test_successful_purchase_with_currency response = @gateway.purchase(@amount, @credit_card, @options.merge(currency: 'CAD')) assert_success response @@ -44,13 +291,116 @@ def test_successful_purchase_with_currency assert_equal 'CAD', response.params['currency'] end + def test_successful_purchase_with_level3_data + l_three_visa = credit_card('4111111111111111') + options = @options.merge({ + customer_reference_number: '1234A', + sales_tax_amount: 0.6, + freight_amount: 0, + duty_amount: 0, + destination_zip_code: 12345, + destination_country_code: 'us', + ship_from_zip_code: 12345, + discount_amount: 0, + tax_amount: 0.6, + tax_rate: 6.0, + level_3_data_items: [ + { + line_item_total: 9.00, + description: 'test_desc', + product_code: 'test_code', + item_quantity: 1.0, + tax_rate: 6.0, + tax_amount: 0.60, + unit_of_measure: 'lb', + commodity_code: 123, + discount_indicator: 'Y', + gross_net_indicator: 'Y', + tax_type: 'test', + unit_cost: 10.00 + }, + { + line_item_total: 9.00, + description: 'test_2', + product_code: 'test_2', + item_quantity: 1.0, + tax_rate: 7.0, + tax_amount: 0.70, + unit_of_measure: 'lb', + commodity_code: 123, + discount_indicator: 'Y', + gross_net_indicator: 'Y', + tax_type: 'test', + unit_cost: 14.00 + } + ] + }) + response = @gateway.purchase(@amount, l_three_visa, options) + + assert_success response + assert_equal 'Success', response.message + assert_equal '1234A', response.params['customer-reference-number'] + assert_equal '9', response.params['line-item-total'] + end + + def test_successful_purchase_with_unused_state_code + unrecognized_state_code_options = { + billing_address: { + city: 'Dresden', + state: 'Sachsen', + country: 'DE', + zip: '01069' + } + } + + response = @gateway.purchase(@amount, @credit_card, unrecognized_state_code_options) + assert_success response + assert_equal 'Success', response.message + end + + def test_successful_purchase_with_transaction_fraud_info + fraud_info_options = @options.merge({ + ip: '123.12.134.1', + transaction_fraud_info: { + fraud_session_id: 'fbcc094208f54c0e974d56875c73af7a' + } + }) + + response = @gateway.purchase(@amount, @credit_card, fraud_info_options) + assert_success response + assert_equal 'Success', response.message + end + + def test_successful_echeck_purchase + response = @gateway.purchase(@amount, @check, @options.merge(@valid_check_options)) + assert_success response + assert_equal 'Success', response.message + end + + def test_fraudulent_purchase + # Reflects specific settings on Bluesnap sandbox account. + response = @gateway.purchase(@fraudulent_amount, @fraudulent_card, @options) + assert_failure response + assert_match(/fraud-reference-id/, response.message) + assert_match(/fraud-event/, response.message) + assert_match(/blacklistPaymentCountryDecline/, response.message) + assert_match(/orderTotalDecline/, response.message) + end + def test_failed_purchase response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response - assert_match /Authorization has failed for this transaction/, response.message + assert_match(/Authorization has failed for this transaction/, response.message) assert_equal '14002', response.error_code end + def test_failed_purchase_with_invalid_cabal_card + response = @gateway.purchase(@amount, @invalid_cabal_card, @options) + assert_failure response + assert_match(/'Card Number' should be a valid Credit Card/, response.message) + assert_equal '10001', response.error_code + end + def test_cvv_result response = @gateway.purchase(@amount, @credit_card, @options) assert_success response @@ -65,6 +415,20 @@ def test_avs_result assert_equal 'I', response.avs_result['code'] end + def test_failed_echeck_purchase + response = @gateway.purchase(@amount, @invalid_check, @options.merge(@valid_check_options)) + assert_failure response + assert_match(/ECP data validity check failed/, response.message) + assert_equal '10001', response.error_code + end + + def test_failed_unauthorized_echeck_purchase + response = @gateway.purchase(@amount, @check, @options.merge({ authorized_by_shopper: false })) + assert_failure response + assert_match(/The payment was not authorized by shopper/, response.message) + assert_equal '16004', response.error_code + end + def test_successful_authorize_and_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth @@ -74,31 +438,67 @@ def test_successful_authorize_and_capture assert_equal 'Success', capture.message end + def test_successful_authorize_and_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 '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 + + assert capture = @gateway.capture(@amount, auth.authorization) + assert_success capture + assert_equal 'Success', capture.message + end + def test_failed_authorize response = @gateway.authorize(@amount, @declined_card, @options) assert_failure response - assert_match /Authorization has failed for this transaction/, response.message + assert_match(/Authorization has failed for this transaction/, response.message) end def test_partial_capture_succeeds_even_though_amount_is_ignored_by_gateway auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture end def test_failed_capture response = @gateway.capture(@amount, '') assert_failure response - assert_match /due to missing transaction ID/, response.message + assert_match(/due to missing transaction ID/, response.message) end def test_successful_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount, purchase.authorization, @options) + assert refund = @gateway.refund(@amount, purchase.authorization, @refund_options) + assert_success refund + assert_equal 'Success', refund.message + assert_not_nil refund.authorization + end + + def test_successful_refund_with_merchant_id + order_id = generate_unique_id + purchase = @gateway.purchase(@amount, @credit_card, @options.merge({ order_id: })) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization, @refund_options.merge({ merchant_transaction_id: order_id })) assert_success refund assert_equal 'Success', refund.message end @@ -107,14 +507,13 @@ def test_partial_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount-1, purchase.authorization) + assert refund = @gateway.refund(@amount - 1, purchase.authorization, @refund_options) assert_success refund end def test_failed_refund response = @gateway.refund(@amount, '') assert_failure response - assert_match /cannot be completed due to missing transaction ID/, response.message end def test_successful_void @@ -129,7 +528,7 @@ def test_successful_void def test_failed_void response = @gateway.void('') assert_failure response - assert_match /cannot be completed due to missing transaction ID/, response.message + assert_match(/cannot be completed due to missing transaction ID/, response.message) end def test_successful_verify @@ -141,7 +540,7 @@ def test_successful_verify def test_failed_verify response = @gateway.verify(@declined_card, @options) assert_failure response - assert_match /Authorization has failed for this transaction/, response.message + assert_match(/Transaction failed because of payment processing failure/, response.message) end def test_successful_store @@ -152,15 +551,32 @@ def test_successful_store assert response.authorization assert_equal 'I', response.avs_result['code'] assert_equal 'P', response.cvv_result['code'] - assert_match /services\/2\/vaulted-shoppers/, response.params['content-location-header'] + assert_match(/services\/2\/vaulted-shoppers/, response.params['content-location-header']) + end + + def test_successful_echeck_store + assert response = @gateway.store(@check, @options.merge(@valid_check_options)) + + assert_success response + assert_equal 'Success', response.message + assert response.authorization + assert_match(/services\/2\/vaulted-shoppers/, response.params['content-location-header']) end def test_failed_store - assert response = @gateway.store(@declined_card, @options) + assert response = @gateway.store(@invalid_card, @options) assert_failure response - assert_match /Transaction failed because of payment processing failure/, response.message - assert_equal '14002', response.error_code + assert_match(/'Card Number' should be a valid Credit Card/, response.message) + assert_equal '10001', response.error_code + end + + def test_failed_echeck_store + assert response = @gateway.store(@invalid_check, @options) + + assert_failure response + assert_match(/ECP data validity check failed/, response.message) + assert_equal '10001', response.error_code end def test_successful_purchase_using_stored_card @@ -172,6 +588,16 @@ def test_successful_purchase_using_stored_card assert_equal 'Success', response.message end + def test_successful_purchase_using_stored_echeck + assert store_response = @gateway.store(@check, @options.merge(@valid_check_options)) + assert_success store_response + assert_match(/check/, store_response.authorization) + + response = @gateway.purchase(@amount, store_response.authorization, @options.merge({ authorized_by_shopper: true })) + assert_success response + assert_equal 'Success', response.message + end + def test_successful_authorize_using_stored_card assert store_response = @gateway.store(@credit_card, @options) assert_success store_response @@ -196,7 +622,6 @@ def test_verify_credentials assert !gateway.verify_credentials end - def test_transcript_scrubbing transcript = capture_transcript(@gateway) do @gateway.purchase(@amount, @credit_card, @options) @@ -208,4 +633,20 @@ def test_transcript_scrubbing assert_scrubbed(@gateway.options[:api_password], transcript) end + def test_transcript_scrubbing_with_echeck + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @check, @valid_check_options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@check.account_number, transcript) + assert_scrubbed(@check.routing_number, transcript) + assert_scrubbed(@gateway.options[:api_password], transcript) + end + + def test_successful_purchase_with_idempotency_key + response = @gateway.purchase(@amount, @credit_card, @options.merge(idempotency_key: 'test123')) + assert_success response + assert_equal 'Success', response.message + end end diff --git a/test/remote/gateways/remote_borgun_test.rb b/test/remote/gateways/remote_borgun_test.rb index 32f7b1b6fc7..6f05a691f4b 100644 --- a/test/remote/gateways/remote_borgun_test.rb +++ b/test/remote/gateways/remote_borgun_test.rb @@ -8,7 +8,8 @@ def setup @gateway = BorgunGateway.new(fixtures(:borgun)) @amount = 100 - @credit_card = credit_card('5587402000012011', year: 2018, month: 9, verification_value: 415) + @credit_card = credit_card('5587402000012011', year: 2027, month: 9, verification_value: 415) + @frictionless_3ds_card = credit_card('5455330200000016', verification_value: 415, month: 9, year: 2027) @declined_card = credit_card('4155520000000002') @options = { @@ -28,6 +29,21 @@ def test_successful_purchase assert_equal 'Succeeded', response.message end + def test_successful_preauth_3ds + response = @gateway.purchase(@amount, @credit_card, @options.merge({ redirect_url: 'http://localhost/index.html', apply_3d_secure: '1' })) + assert_success response + assert_equal 'Succeeded', response.message + assert_not_nil response.params['redirecttoacsform'] + end + + def test_successful_preauth_frictionless_3ds + response = @gateway.purchase(@amount, @frictionless_3ds_card, @options.merge({ redirect_url: 'http://localhost/index.html', apply_3d_secure: '1' })) + assert_success response + assert_equal 'Succeeded', response.message + assert_nil response.params['redirecttoacsform'] + assert_equal response.params['threedsfrictionless'], 'A' + end + def test_successful_purchase_usd response = @gateway.purchase(@amount, @credit_card, @options.merge(currency: 'USD')) assert_success response @@ -54,6 +70,27 @@ def test_successful_authorize_and_capture assert_success capture end + def test_successful_authorize_airline_data + passenger_itinerary_data = { + 'MessageNumber' => '1111111', + 'TrDate' => '20120222', + 'TrTime' => '151515', + 'PassengerName' => 'Jane Doe', + 'ServiceClassCode_1' => '100', + 'FlightNumber_1' => '111111', + 'TravelDate_1' => '20120222', + 'DepartureAirport_1' => 'KEF', + 'CarrierCode_1' => 'CC', + 'TravelAgencyCode' => 'A7654321', + 'TravelAgencyName' => 'Spreedly Inc', + 'TicketNumber' => '900.123.222' + } + + options = @options.merge(passenger_itinerary_data:) + auth = @gateway.authorize(@amount, @credit_card, options) + assert_success auth + end + def test_successful_authorize_and_capture_usd auth = @gateway.authorize(@amount, @credit_card, @options.merge(currency: 'USD')) assert_success auth @@ -71,7 +108,7 @@ def test_partial_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture end @@ -100,7 +137,7 @@ def test_partial_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount-1, purchase.authorization) + assert refund = @gateway.refund(@amount - 1, purchase.authorization) assert_success refund end @@ -150,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_bpoint_test.rb b/test/remote/gateways/remote_bpoint_test.rb index 32f0124e787..e176bc4c648 100644 --- a/test/remote/gateways/remote_bpoint_test.rb +++ b/test/remote/gateways/remote_bpoint_test.rb @@ -40,6 +40,12 @@ def test_successful_purchase assert_equal 'Approved', response.message end + def test_successful_purchase_with_more_options + response = @gateway.purchase(@amount, @credit_card, @options.merge({ crn1: 'ref' })) + assert_success response + assert_equal 'Approved', response.message + end + def test_failed_purchase response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response @@ -63,7 +69,7 @@ def test_partial_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture end @@ -84,7 +90,7 @@ def test_partial_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount-1, purchase.authorization) + assert refund = @gateway.refund(@amount - 1, purchase.authorization) assert_success refund end @@ -97,12 +103,12 @@ def test_successful_void auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert void = @gateway.void(@amount, auth.authorization) + assert void = @gateway.void(auth.authorization, amount: @amount) assert_success void end def test_failed_void - response = @gateway.void(@amount, '') + response = @gateway.void('', amount: @amount) assert_failure response end diff --git a/test/remote/gateways/remote_braintree_blue_test.rb b/test/remote/gateways/remote_braintree_blue_test.rb index 82ad314494b..5255e4c5dc1 100644 --- a/test/remote/gateways/remote_braintree_blue_test.rb +++ b/test/remote/gateways/remote_braintree_blue_test.rb @@ -2,8 +2,9 @@ class RemoteBraintreeBlueTest < Test::Unit::TestCase def setup - @gateway = BraintreeGateway.new(fixtures(:braintree_blue)) - @braintree_backend = @gateway.instance_eval{@braintree_gateway} + fixture_key = method_name.match?(/bank_account/i) ? :braintree_blue_with_ach_enabled : :braintree_blue + @gateway = BraintreeGateway.new(fixtures(fixture_key)) + @braintree_backend = @gateway.instance_eval { @braintree_gateway } @amount = 100 @declined_amount = 2000_00 @@ -11,10 +12,31 @@ def setup @declined_card = credit_card('4000300011112220') @options = { - :order_id => '1', - :billing_address => address(:country_name => 'United States of America'), - :description => 'Store Purchase' + order_id: '1', + billing_address: address(country_name: 'Canada'), + description: 'Store Purchase' } + + ach_mandate = 'By clicking "Checkout", I authorize Braintree, a service of PayPal, ' \ + 'on behalf of My Company (i) to verify my bank account information ' \ + 'using bank information and consumer reports and (ii) to debit my bank account.' + + @check_required_options = { + billing_address: { + address1: '1670', + address2: '1670 NW 82ND AVE', + city: 'Miami', + state: 'FL', + zip: '32191' + }, + 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 @@ -38,6 +60,34 @@ 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 = { + billing_address: { + name: 'John Smith', + phone: '123-456-7890', + company: nil, + address1: nil, + address2: '', + city: nil, + state: nil, + zip: nil, + country: '' + } + } + assert response = @gateway.authorize(@amount, credit_card, options) + assert_success response + assert_equal '1000 Approved', response.message + assert_equal 'authorized', response.params['braintree_transaction']['status'] + end + def test_masked_card_number assert response = @gateway.authorize(@amount, @credit_card, @options) assert_equal('510510******5100', response.params['braintree_transaction']['credit_card_details']['masked_number']) @@ -46,15 +96,30 @@ def test_masked_card_number assert_equal('510510', response.params['braintree_transaction']['credit_card_details']['bin']) end + def test_successful_setup_purchase + assert response = @gateway.setup_purchase + assert_success response + assert_equal 'Client token created', response.message + assert_not_nil response.params['client_token'] + end + + def test_successful_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 response = @gateway.authorize(@amount, @credit_card, order_id: '123') assert_success response assert_equal '1000 Approved', response.message assert_equal '123', response.params['braintree_transaction']['order_id'] end def test_successful_purchase_with_hold_in_escrow - @options.merge({:merchant_account_id => fixtures(:braintree_blue)[:merchant_account_id], :hold_in_escrow => true}) + @options.merge({ merchant_account_id: fixtures(:braintree_blue)[:merchant_account_id], hold_in_escrow: true }) assert response = @gateway.authorize(@amount, @credit_card, @options) assert_success response assert_equal '1000 Approved', response.message @@ -95,12 +160,98 @@ def test_successful_purchase_using_card_token credit_card_token = response.params['credit_card_token'] assert_match %r{^\w+$}, credit_card_token - assert response = @gateway.purchase(@amount, credit_card_token, :payment_method_token => true) + assert response = @gateway.purchase(@amount, credit_card_token, payment_method_token: true) assert_success response assert_equal '1000 Approved', response.message assert_equal 'submitted_for_settlement', response.params['braintree_transaction']['status'] end + def test_successful_purchase_with_level_2_data + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(tax_amount: '20', purchase_order_number: '6789')) + assert_success response + assert_equal '1000 Approved', response.message + end + + def test_successful_purchase_with_level_2_and_3_data + options = { + tax_amount: '20', + purchase_order_number: '6789', + shipping_amount: '300', + discount_amount: '150', + ships_from_postal_code: '90210', + line_items: [ + { + name: 'Product Name', + kind: 'debit', + quantity: '10.0000', + unit_amount: '9.5000', + unit_of_measure: 'unit', + total_amount: '95.00', + tax_amount: '5.00', + discount_amount: '0.00', + product_code: '54321', + commodity_code: '98765' + }, + { + name: 'Other Product Name', + kind: 'debit', + quantity: '1.0000', + unit_amount: '2.5000', + unit_of_measure: 'unit', + total_amount: '90.00', + tax_amount: '2.00', + discount_amount: '1.00', + product_code: '54322', + commodity_code: '98766' + } + ] + } + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(options)) + assert_success response + assert_equal '1000 Approved', response.message + end + + def test_successful_purchase_sending_risk_data + options = @options.merge( + risk_data: { + customer_browser: 'User-Agent Header', + customer_ip: '127.0.0.1' + } + ) + assert response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + end + + def test_successful_purchase_with_paypal_options + options = @options.merge( + paypal_custom_field: 'abc', + paypal_description: 'shoes' + ) + assert response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + end + + # Follow instructions found at https://developer.paypal.com/braintree/articles/guides/payment-methods/venmo#multiple-profiles + # for sandbox control panel https://sandbox.braintreegateway.com/login to create a venmo profile. + # Insert your Profile Id into fixtures. + def test_successful_purchase_with_venmo_profile_id + options = @options.merge(venmo_profile_id: fixtures(:braintree_blue)[:venmo_profile_id], payment_method_nonce: 'fake-venmo-account-nonce') + assert response = @gateway.purchase(@amount, 'fake-venmo-account-nonce', options) + assert_success response + end + + def test_successful_partial_capture + options = @options.merge(venmo_profile_id: fixtures(:braintree_blue)[:venmo_profile_id], payment_method_nonce: 'fake-venmo-account-nonce') + assert auth = @gateway.authorize(@amount, 'fake-venmo-account-nonce', options) + assert_success auth + assert_equal '1000 Approved', auth.message + assert auth.authorization + assert capture_one = @gateway.capture(50, auth.authorization, { partial_capture: true }) + assert_success capture_one + assert capture_two = @gateway.capture(50, auth.authorization, { partial_capture: true }) + assert_success capture_two + end + def test_successful_verify assert response = @gateway.verify(@credit_card, @options) assert_success response @@ -113,23 +264,103 @@ def test_failed_verify assert_match %r{number is not an accepted test number}, response.message end + def test_successful_credit_card_verification + card = credit_card('4111111111111111') + 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 'M', response.avs_result['code'] + end + + def test_successful_credit_card_verification_without_billing_address + options = { + order_ID: '1', + description: 'store purchase' + } + card = credit_card('4111111111111111') + 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 'I', response.avs_result['code'] + end + + def test_successful_credit_card_verification_with_only_address + options = { + order_ID: '1', + description: 'store purchase', + billing_address: { + address1: '456 My Street' + } + } + card = credit_card('4111111111111111') + 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 'B', response.avs_result['code'] + end + + def test_successful_credit_card_verification_with_only_zip + options = { + order_ID: '1', + description: 'store purchase', + billing_address: { + zip: 'K1C2N6' + } + } + card = credit_card('4111111111111111') + 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'] + end + + def test_failed_credit_card_verification + credit_card = credit_card('378282246310005', verification_value: '544') + + assert response = @gateway.verify(credit_card, @options.merge({ allow_card_verification: true })) + assert_failure response + assert_match 'CVV must be 4 digits for American Express and 3 digits for other card types. (81707)', response.message + end + + def test_successful_verify_with_device_data + # Requires Advanced Fraud Tools to be enabled + assert response = @gateway.verify(@credit_card, @options.merge({ device_data: 'device data for verify' })) + assert_success response + assert_equal '1000 Approved', response.message + + assert transaction = response.params['braintree_transaction'] + assert transaction['risk_data'] + assert transaction['risk_data']['id'] + assert_equal 'Approve', transaction['risk_data']['decision'] + assert_equal false, transaction['risk_data']['device_data_captured'] + assert_equal 'fraud_protection', transaction['risk_data']['fraud_service_provider'] + end + def test_successful_validate_on_store - card = credit_card('4111111111111111', :verification_value => '101') - assert response = @gateway.store(card, :verify_card => true) + card = credit_card('4111111111111111', verification_value: '101') + assert response = @gateway.store(card, verify_card: true) assert_success response assert_equal 'OK', response.message end def test_failed_validate_on_store - card = credit_card('4000111111111115', :verification_value => '200') - assert response = @gateway.store(card, :verify_card => true) + card = credit_card('4000111111111115', verification_value: '200') + assert response = @gateway.store(card, verify_card: true) assert_failure response assert_equal 'Processor declined: Do Not Honor (2000)', response.message end def test_successful_store_with_no_validate - card = credit_card('4000111111111115', :verification_value => '200') - assert response = @gateway.store(card, :verify_card => false) + card = credit_card('4000111111111115', verification_value: '200') + assert response = @gateway.store(card, verify_card: false) assert_success response assert_equal 'OK', response.message end @@ -142,36 +373,58 @@ def test_successful_store_with_invalid_card def test_successful_store_with_billing_address billing_address = { - :address1 => '1 E Main St', - :address2 => 'Suite 403', - :city => 'Chicago', - :state => 'Illinois', - :zip => '60622', - :country_name => 'United States of America' + address1: '1 E Main St', + address2: 'Suite 403', + city: 'Chicago', + state: 'Illinois', + zip: '60622', + country_name: 'United States of America' } credit_card = credit_card('5105105105105100') - assert response = @gateway.store(credit_card, :billing_address => billing_address) + assert response = @gateway.store(credit_card, billing_address:) assert_success response assert_equal 'OK', response.message vault_id = response.params['customer_vault_id'] purchase_response = @gateway.purchase(@amount, vault_id) response_billing_details = { - 'country_name'=>'United States of America', - 'region'=>'Illinois', - 'company'=>nil, - 'postal_code'=>'60622', - 'extended_address'=>'Suite 403', - 'street_address'=>'1 E Main St', - 'locality'=>'Chicago' + 'country_name' => 'United States of America', + 'region' => 'Illinois', + 'company' => nil, + 'postal_code' => '60622', + 'extended_address' => 'Suite 403', + 'street_address' => '1 E Main St', + 'locality' => 'Chicago' } assert_equal purchase_response.params['braintree_transaction']['billing_details'], response_billing_details end + def test_successful_store_with_nil_billing_address_options + billing_address = { + name: 'John Smith', + phone: '123-456-7890', + company: nil, + address1: nil, + address2: nil, + city: nil, + state: nil, + zip: nil, + country_name: nil + } + credit_card = credit_card('5105105105105100') + assert response = @gateway.store(credit_card, billing_address:) + assert_success response + assert_equal 'OK', response.message + + vault_id = response.params['customer_vault_id'] + purchase_response = @gateway.purchase(@amount, vault_id) + assert_success purchase_response + end + def test_successful_store_with_credit_card_token credit_card = credit_card('5105105105105100') credit_card_token = generate_unique_id - assert response = @gateway.store(credit_card, credit_card_token: credit_card_token) + assert response = @gateway.store(credit_card, credit_card_token:) assert_success response assert_equal 'OK', response.message assert_equal credit_card_token, response.params['braintree_customer']['credit_cards'][0]['token'] @@ -190,11 +443,41 @@ def test_successful_store_with_new_customer_id def test_successful_store_with_existing_customer_id credit_card = credit_card('5105105105105100') customer_id = generate_unique_id - assert response = @gateway.store(credit_card, customer: customer_id) + assert response = @gateway.store(credit_card, @options.merge(customer: customer_id)) assert_success response assert_equal 1, @braintree_backend.customer.find(customer_id).credit_cards.size - assert response = @gateway.store(credit_card, customer: customer_id) + credit_card = credit_card('4111111111111111') + assert response = @gateway.store(credit_card, @options.merge(customer: customer_id)) + assert_success response + assert_equal 2, @braintree_backend.customer.find(customer_id).credit_cards.size + assert_equal customer_id, response.params['customer_vault_id'] + assert_equal customer_id, response.authorization + assert_not_nil response.params['credit_card_token'] + end + + def test_successful_store_with_existing_customer_id_and_nil_billing_address_options + credit_card = credit_card('5105105105105100') + customer_id = generate_unique_id + options = { + customer: customer_id, + billing_address: { + name: 'John Smith', + phone: '123-456-7890', + company: nil, + address1: nil, + address2: nil, + city: nil, + state: nil, + zip: nil, + country_name: nil + } + } + assert response = @gateway.store(credit_card, options) + assert_success response + assert_equal 1, @braintree_backend.customer.find(customer_id).credit_cards.size + + assert response = @gateway.store(credit_card, options) assert_success response assert_equal 2, @braintree_backend.customer.find(customer_id).credit_cards.size assert_equal customer_id, response.params['customer_vault_id'] @@ -245,28 +528,26 @@ def test_avs end def test_cvv_match - assert response = @gateway.purchase(@amount, credit_card('5105105105105100', :verification_value => '400')) + assert response = @gateway.purchase(@amount, credit_card('5105105105105100', verification_value: '400')) assert_success response - assert_equal({'code' => 'M', 'message' => ''}, response.cvv_result) + assert_equal({ 'code' => 'M', 'message' => '' }, response.cvv_result) end def test_cvv_no_match - assert response = @gateway.purchase(@amount, credit_card('5105105105105100', :verification_value => '200')) + assert response = @gateway.purchase(@amount, credit_card('5105105105105100', verification_value: '200')) assert_success response - assert_equal({'code' => 'N', 'message' => ''}, response.cvv_result) + assert_equal({ 'code' => 'N', 'message' => '' }, response.cvv_result) 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'] end def test_successful_purchase_with_phone - assert response = @gateway.purchase(@amount, @credit_card, :phone => '123-345-5678') + assert response = @gateway.purchase(@amount, @credit_card, phone: '123-345-5678') assert_success response transaction = response.params['braintree_transaction'] assert_equal '123-345-5678', transaction['customer_details']['phone'] @@ -279,6 +560,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 @@ -286,9 +576,37 @@ def test_successful_purchase_with_skip_advanced_fraud_checking_option assert_equal 'submitted_for_settlement', response.params['braintree_transaction']['status'] end + def test_successful_purchase_with_skip_avs + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(skip_avs: true)) + assert_success response + assert_equal '1000 Approved', response.message + assert_equal 'B', response.avs_result['code'] + end + + def test_successful_purchase_with_skip_cvv + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(skip_cvv: true)) + assert_success response + assert_equal '1000 Approved', response.message + assert_equal 'B', response.cvv_result['code'] + end + + def test_successful_purchase_with_device_data + # Requires Advanced Fraud Tools to be enabled + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(device_data: 'device data for purchase')) + assert_success response + assert_equal '1000 Approved', response.message + + assert transaction = response.params['braintree_transaction'] + assert transaction['risk_data'] + assert transaction['risk_data']['id'] + 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 + def test_purchase_with_store_using_random_customer_id assert response = @gateway.purchase( - @amount, credit_card('5105105105105100'), @options.merge(:store => true) + @amount, credit_card('5105105105105100'), @options.merge(store: true) ) assert_success response assert_equal '1000 Approved', response.message @@ -300,7 +618,7 @@ def test_purchase_with_store_using_random_customer_id def test_purchase_with_store_using_specified_customer_id customer_id = rand(1_000_000_000).to_s assert response = @gateway.purchase( - @amount, credit_card('5105105105105100'), @options.merge(:store => customer_id) + @amount, credit_card('5105105105105100'), @options.merge(store: customer_id) ) assert_success response assert_equal '1000 Approved', response.message @@ -309,14 +627,21 @@ def test_purchase_with_store_using_specified_customer_id assert_equal '510510', @braintree_backend.customer.find(response.params['customer_vault_id']).credit_cards[0].bin end + def test_purchase_with_transaction_source + assert response = @gateway.store(@credit_card) + assert_success response + customer_vault_id = response.params['customer_vault_id'] + + assert response = @gateway.purchase(@amount, customer_vault_id, @options.merge(transaction_source: 'unscheduled')) + assert_success response + assert_equal '1000 Approved', response.message + end + 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 - ), - :email => 'old@example.com', - :phone => '321-654-0987' + credit_card('4111111111111111', first_name: 'Old First', last_name: 'Old Last', month: 9, year: 2012), + email: 'old@example.com', + phone: '321-654-0987' ) payment_method_token = response.params['braintree_customer']['credit_cards'][0]['token'] assert response = @gateway.purchase( @@ -329,26 +654,28 @@ def test_purchase_using_specified_payment_method_token def test_successful_purchase_with_addresses billing_address = { - :address1 => '1 E Main St', - :address2 => 'Suite 101', - :company => 'Widgets Co', - :city => 'Chicago', - :state => 'IL', - :zip => '60622', - :country_name => 'United States of America' + address1: '1 E Main St', + address2: 'Suite 101', + company: 'Widgets Co', + city: 'Chicago', + state: 'IL', + zip: '60622', + country_name: 'United States of America' } shipping_address = { - :address1 => '1 W Main St', - :address2 => 'Suite 102', - :company => 'Widgets Company', - :city => 'Bartlett', - :state => 'Illinois', - :zip => '60103', - :country_name => 'Mexico' + address1: '1 W Main St', + address2: 'Suite 102', + company: 'Widgets Company', + city: 'Bartlett', + state: 'Illinois', + zip: '60103', + country_name: 'Mexico' } - assert response = @gateway.purchase(@amount, @credit_card, - :billing_address => billing_address, - :shipping_address => shipping_address + assert response = @gateway.purchase( + @amount, + @credit_card, + billing_address:, + shipping_address: ) assert_success response transaction = response.params['braintree_transaction'] @@ -368,11 +695,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 = { eci: '05', cavv: 'cavv', xid: 'xid' } - assert 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) assert_success response end @@ -387,7 +721,13 @@ def test_unsuccessful_purchase_validation_error assert response = @gateway.purchase(@amount, credit_card('51051051051051000')) assert_failure response assert_match %r{Credit card number is invalid\. \(81715\)}, response.message - assert_equal({'processor_response_code'=>'91577'}, response.params['braintree_transaction']) + assert_equal('91577', response.params['braintree_transaction']['processor_response_code']) + end + + def test_unsuccessful_purchase_with_additional_processor_response + assert response = @gateway.purchase(204700, @credit_card) + assert_failure response + assert_equal('2047 : Call Issuer. Pick Up Card.', response.params['braintree_transaction']['additional_processor_response']) end def test_authorize_and_capture @@ -400,10 +740,11 @@ def test_authorize_and_capture end def test_authorize_and_capture_with_apple_pay_card - credit_card = network_tokenization_credit_card('4111111111111111', - :brand => 'visa', - :eci => '05', - :payment_cryptogram => 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + credit_card = network_tokenization_credit_card( + '4111111111111111', + brand: 'visa', + eci: '05', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' ) assert auth = @gateway.authorize(@amount, credit_card, @options) @@ -414,14 +755,15 @@ def test_authorize_and_capture_with_apple_pay_card 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' + def test_authorize_and_capture_with_google_pay_card + credit_card = network_tokenization_credit_card( + '4111111111111111', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + month: '01', + year: '2024', + source: :google_pay, + transaction_id: '123456789', + eci: '05' ) assert auth = @gateway.authorize(@amount, credit_card, @options) @@ -473,7 +815,7 @@ def test_failed_void assert failed_void = @gateway.void(auth.authorization) assert_failure failed_void assert_match('Transaction can only be voided if status is authorized', failed_void.message) - assert_equal({'processor_response_code'=>'91504'}, failed_void.params['braintree_transaction']) + assert_equal('91504', failed_void.params['braintree_transaction']['processor_response_code']) end def test_failed_capture_with_invalid_transaction_id @@ -483,7 +825,7 @@ def test_failed_capture_with_invalid_transaction_id end def test_invalid_login - gateway = BraintreeBlueGateway.new(:merchant_id => 'invalid', :public_key => 'invalid', :private_key => 'invalid') + gateway = BraintreeBlueGateway.new(merchant_id: 'invalid', public_key: 'invalid', private_key: 'invalid') assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal 'Braintree::AuthenticationError', response.message @@ -518,7 +860,7 @@ def test_unstore_credit_card assert_success response assert_equal 'OK', response.message assert credit_card_token = response.params['credit_card_token'] - assert delete_response = @gateway.unstore(nil, credit_card_token: credit_card_token) + assert delete_response = @gateway.unstore(nil, credit_card_token:) assert_success delete_response end @@ -533,12 +875,9 @@ 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 - ), - :email => 'old@example.com', - :phone => '321-654-0987' + credit_card('4111111111111111', first_name: 'Old First', last_name: 'Old Last', month: 9, year: 2012), + email: 'old@example.com', + phone: '321-654-0987' ) assert_success response assert_equal 'OK', response.message @@ -555,12 +894,9 @@ 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 - ), - :email => 'new@example.com', - :phone => '987-765-5432' + credit_card('5105105105105100', first_name: 'New First', last_name: 'New Last', month: 10, year: 2014), + email: 'new@example.com', + phone: '987-765-5432' ) assert_success response assert_equal 'new@example.com', response.params['braintree_customer']['email'] @@ -574,7 +910,7 @@ def test_successful_update end def test_failed_customer_update - assert response = @gateway.store(credit_card('4111111111111111'), :email => 'email@example.com', :phone => '321-654-0987') + assert response = @gateway.store(credit_card('4111111111111111'), email: 'email@example.com', phone: '321-654-0987') assert_success response assert_equal 'OK', response.message assert customer_vault_id = response.params['customer_vault_id'] @@ -618,7 +954,7 @@ def test_failed_credit_card_update_on_verify assert response = @gateway.update( customer_vault_id, credit_card('4000111111111115'), - {:verify_card => true} + { verify_card: true } ) assert_failure response assert_equal 'Processor declined: Do Not Honor (2000)', response.message @@ -645,14 +981,33 @@ def test_failed_credit end def test_successful_credit_with_merchant_account_id - assert response = @gateway.credit(@amount, @credit_card, :merchant_account_id => fixtures(:braintree_blue)[:merchant_account_id]) + assert response = @gateway.credit(@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 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_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 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.' assert_equal '1000 Approved', response.message assert_equal 'authorized', response.params['braintree_transaction']['status'] @@ -663,9 +1018,38 @@ def test_authorize_with_descriptor assert_success auth end + def test_authorize_with_travel_data + 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, + 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 + def test_successful_validate_on_store_with_verification_merchant_account - card = credit_card('4111111111111111', :verification_value => '101') - assert response = @gateway.store(card, :verify_card => true, :verification_merchant_account_id => fixtures(:braintree_blue)[:merchant_account_id]) + card = credit_card('4111111111111111', verification_value: '101') + assert response = @gateway.store(card, verify_card: true, verification_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.' assert_equal 'OK', response.message end @@ -687,13 +1071,464 @@ 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)) + 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)) + 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)) + 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)) + + 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)) + + 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_subsequent_merchant_unscheduled_transaction + creds_options = stored_credential_options(:merchant, :unscheduled, id: '020190722142652') + response = @gateway.purchase(@amount, credit_card('4111111111111111'), @options.merge(stored_credential: creds_options)) + assert_success response + assert_equal '1000 Approved', response.message + assert_equal 'submitted_for_settlement', response.params['braintree_transaction']['status'] + end + + def test_successful_subsequent_merchant_recurring_transaction + creds_options = stored_credential_options(:cardholder, :recurring, id: '020190722142652') + response = @gateway.purchase(@amount, credit_card('4111111111111111'), @options.merge(stored_credential: creds_options)) + assert_success response + assert_equal '1000 Approved', response.message + assert_equal 'submitted_for_settlement', response.params['braintree_transaction']['status'] + end + + def test_successful_cardholder_purchase_initial + creds_options = stored_credential_options(:cardholder, :recurring, :initial) + response = @gateway.purchase(@amount, credit_card('4111111111111111'), @options.merge(stored_credential: creds_options)) + 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_cardholder_purchase_recurring + creds_options = stored_credential_options(:cardholder, :recurring, id: '020190722142652') + response = @gateway.purchase(@amount, credit_card('4111111111111111'), @options.merge(stored_credential: creds_options)) + assert_success response + assert_equal '1000 Approved', response.message + assert_equal 'submitted_for_settlement', response.params['braintree_transaction']['status'] + end + + def test_successful_cardholder_purchase_unscheduled + creds_options = stored_credential_options(:cardholder, :unscheduled, id: '020190722142652') + response = @gateway.purchase(@amount, credit_card('4111111111111111'), @options.merge(stored_credential: creds_options)) + assert_success response + assert_equal '1000 Approved', response.message + assert_equal 'submitted_for_settlement', response.params['braintree_transaction']['status'] + end + + def test_successful_cardholder_purchase_initial_setup + creds_options = { initiator: 'merchant', reason_type: 'recurring_first', initial_transaction: true } + response = @gateway.purchase(@amount, credit_card('4111111111111111'), @options.merge(stored_credential: creds_options)) + 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'] + assert_equal true, response.params['braintree_transaction']['recurring'] + end + + def test_successful_cardholder_purchase_initial_moto + creds_options = { initiator: 'merchant', reason_type: 'moto', initial_transaction: true } + response = @gateway.purchase(@amount, credit_card('4111111111111111'), @options.merge(stored_credential: creds_options)) + 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_store_bank_account_with_a_new_customer + bank_account = check({ account_number: '1000000000', routing_number: '011000015' }) + response = @gateway.store(bank_account, @options.merge(@check_required_options)) + + assert_success response + assert response.params['bank_account_token'] + assert response.params['verified'] + + customer = @braintree_backend.customer.find(response.params['customer_vault_id']) + bank_accounts = customer.us_bank_accounts + created_bank_account = bank_accounts.first + + assert_equal 1, bank_accounts.size + assert created_bank_account.verified + assert_equal bank_account.routing_number, created_bank_account.routing_number + assert_equal bank_account.account_number[-4..-1], created_bank_account.last_4 + assert_equal 'checking', created_bank_account.account_type + assert_equal 'Jim', customer.first_name + assert_equal 'Smith', customer.last_name + end + + def test_successful_store_bank_account_with_existing_customer + customer_id = generate_unique_id + bank_account = check({ account_number: '1000000000', routing_number: '011000015' }) + response = @gateway.store(bank_account, @options.merge(customer: customer_id).merge(@check_required_options)) + + assert response + assert_success response + + bank_account = check({ account_number: '1000000001', routing_number: '011000015' }) + response = @gateway.store(bank_account, @options.merge(customer: customer_id).merge(@check_required_options)) + + assert response + assert_success response + + customer = @braintree_backend.customer.find(customer_id) + bank_accounts = customer.us_bank_accounts + + assert_equal 2, bank_accounts.size + assert bank_accounts.first.verified + assert bank_accounts.last.verified + end + + def test_successful_store_bank_account_with_customer_id_not_in_merchant_account + customer_id = generate_unique_id + bank_account = check({ account_number: '1000000000', routing_number: '011000015' }) + response = @gateway.store(bank_account, @options.merge(customer: customer_id).merge(@check_required_options)) + + assert response + assert_success response + assert response.params['bank_account_token'] + assert response.params['verified'] + assert_equal response.params['customer_vault_id'], customer_id + + customer = @braintree_backend.customer.find(customer_id) + bank_accounts = customer.us_bank_accounts + created_bank_account = bank_accounts.first + + assert created_bank_account.verified + assert_equal 1, bank_accounts.size + assert_equal bank_account.routing_number, created_bank_account.routing_number + assert_equal bank_account.account_number[-4..-1], created_bank_account.last_4 + assert_equal customer_id, customer.id + assert_equal 'checking', created_bank_account.account_type + assert_equal 'Jim', customer.first_name + assert_equal 'Smith', customer.last_name + end + + def test_successful_store_business_savings_bank_account + customer_id = generate_unique_id + bank_account = check({ account_type: 'savings', account_holder_type: 'business', account_number: '1000000000', routing_number: '011000015' }) + response = @gateway.store(bank_account, @options.merge(customer: customer_id).merge(@check_required_options)) + + assert response + assert_success response + + customer = @braintree_backend.customer.find(customer_id) + bank_accounts = customer.us_bank_accounts + created_bank_account = bank_accounts.first + assert created_bank_account.verified + assert_equal 1, bank_accounts.size + assert_equal 'savings', bank_account.account_type + assert_equal 'business', (created_bank_account.instance_eval { @ownership_type }) + end + + def test_unsuccessful_store_an_unverified_bank_account + customer_id = generate_unique_id + bank_account = check({ account_number: '1000000004', routing_number: '011000015' }) + options = @options.merge(customer: customer_id).merge(@check_required_options) + response = @gateway.store(bank_account, options) + + assert response + assert_failure response + assert_equal 'verification_status: [processor_declined], processor_response: [2046-Declined]', response.message + + customer = @braintree_backend.customer.find(customer_id) + bank_accounts = customer.us_bank_accounts + created_bank_account = bank_accounts.first + + refute created_bank_account.verified + assert_equal 1, bank_accounts.size + end + + def test_sucessful_purchase_using_a_bank_account_token + bank_account = check({ account_number: '1000000000', routing_number: '011000015' }) + response = @gateway.store(bank_account, @options.merge(@check_required_options)) + + assert response + assert_success response + payment_method_token = response.params['bank_account_token'] + sleep 2 + + assert response = @gateway.purchase(@amount, payment_method_token, @options.merge(payment_method_token: true)) + assert_success response + assert_equal '4002 Settlement Pending', response.message + end + + def test_successful_purchase_with_the_same_bank_account_several_times + bank_account = check({ account_number: '1000000000', routing_number: '011000015' }) + response = @gateway.store(bank_account, @options.merge(@check_required_options)) + + assert response + assert_success response + + payment_method_token = response.params['bank_account_token'] + sleep 2 + + # Purchase # 1 + assert response = @gateway.purchase(@amount, payment_method_token, @options.merge(payment_method_token: true)) + assert_success response + assert_equal '4002 Settlement Pending', response.message + + # Purchase # 2 + assert response = @gateway.purchase(120, payment_method_token, @options.merge(payment_method_token: true)) + assert_success response + 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'] + assert_equal nil, response.params['braintree_transaction']['paypal_details']['paypal_payment_token'] + 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_successful_apple_pay_recurring_purchase + network_tokenized_credit_card = network_tokenization_credit_card( + '4111111111111111', + brand: 'visa', + transaction_id: '123', + source: :apple_pay, + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + ) + apple_pay_options = @options.merge(stored_credential: { initiator: 'customer', reason_type: 'recurring' }) + + assert response = @gateway.purchase(@amount, network_tokenized_credit_card, apple_pay_options) + assert_success response + assert_equal true, response.params['braintree_transaction']['recurring'] + assert_equal 'apple_pay_card', response.params['braintree_transaction']['payment_instrument_type'] + end + + def test_successful_apple_pay_recurring_purchase_mit + network_tokenized_credit_card = network_tokenization_credit_card( + '4111111111111111', + brand: 'visa', + transaction_id: '123', + source: :apple_pay, + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + ) + apple_pay_options = @options.merge(stored_credential: { initiator: 'merchant', reason_type: 'recurring' }) + + assert response = @gateway.purchase(@amount, network_tokenized_credit_card, apple_pay_options) + assert_success response + assert_equal true, response.params['braintree_transaction']['recurring'] + assert_equal 'apple_pay_card', response.params['braintree_transaction']['payment_instrument_type'] + 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)) + + assert response + assert_failure response + + payment_method_token = response.params['bank_account_token'] + assert response = @gateway.purchase(@amount, payment_method_token, @options.merge(payment_method_token: true)) + + assert_failure response + assert_equal 'US bank account payment method must be verified prior to transaction. (915172)', response.message + end + + def test_unsuccessful_store_with_incomplete_bank_account + bank_account = check({ account_type: 'blah', + account_holder_type: 'blah', + account_number: nil, + routing_number: nil, + name: nil }) + + response = @gateway.store(bank_account, @options.merge(@check_required_options)) + + assert response + assert_failure response + assert_equal 'cannot be empty', response.message[:account_number].first + assert_equal 'cannot be empty', response.message[:routing_number].first + assert_equal 'cannot be empty', response.message[:name].first + assert_equal 'must be checking or savings', response.message[:account_type].first + assert_equal 'must be personal or business', response.message[:account_holder_type].first + end private + + def stored_credential_options(*args, id: nil) + stored_credential(*args, id:) + end + def assert_avs(address1, zip, expected_avs_code) - response = @gateway.purchase(@amount, @credit_card, billing_address: {address1: address1, zip: zip}) + response = @gateway.purchase(@amount, @credit_card, billing_address: { address1:, zip: }) assert_success response assert_equal expected_avs_code, response.avs_result['code'] end - end diff --git a/test/remote/gateways/remote_braintree_orange_test.rb b/test/remote/gateways/remote_braintree_orange_test.rb index 6f6fb6bd842..6f69e221f56 100644 --- a/test/remote/gateways/remote_braintree_orange_test.rb +++ b/test/remote/gateways/remote_braintree_orange_test.rb @@ -4,13 +4,12 @@ class RemoteBraintreeOrangeTest < Test::Unit::TestCase def setup @gateway = BraintreeGateway.new(fixtures(:braintree_orange)) - @amount = rand(10000) + 1001 + @amount = rand(1001..11000) @credit_card = credit_card('4111111111111111') @check = check() @declined_amount = rand(99) - @options = { :order_id => generate_unique_id, - :billing_address => address - } + @options = { order_id: generate_unique_id, + billing_address: address } end def test_successful_purchase @@ -21,13 +20,13 @@ def test_successful_purchase def test_successful_purchase_with_echeck check = ActiveMerchant::Billing::Check.new( - :name => 'Fredd Bloggs', - :routing_number => '111000025', # Valid ABA # - Bank of America, TX - :account_number => '999999999999', - :account_holder_type => 'personal', - :account_type => 'checking' - ) - assert response = @gateway.purchase(@amount, @check, @options) + name: 'Fredd Bloggs', + routing_number: '111000025', # Valid ABA # - Bank of America, TX + account_number: '999999999999', + account_holder_type: 'personal', + account_type: 'checking' + ) + assert response = @gateway.purchase(@amount, check, @options) assert_equal 'This transaction has been approved', response.message assert_success response end @@ -61,13 +60,13 @@ def test_successful_add_to_vault_and_use assert_success response assert_not_nil customer_id = response.params['customer_vault_id'] - assert second_response = @gateway.purchase(@amount*2, customer_id, @options) + assert second_response = @gateway.purchase(@amount * 2, customer_id, @options) assert_equal 'This transaction has been approved', second_response.message assert second_response.success? end def test_add_to_vault_with_custom_vault_id - @options[:store] = rand(100000)+10001 + @options[:store] = rand(10001..110000) assert response = @gateway.purchase(@amount, @credit_card, @options) assert_equal 'This transaction has been approved', response.message assert_success response @@ -75,7 +74,7 @@ def test_add_to_vault_with_custom_vault_id end def test_add_to_vault_with_custom_vault_id_with_store_method - @options[:billing_id] = rand(100000)+10001 + @options[:billing_id] = rand(10001..110000) assert response = @gateway.store(@credit_card, @options.dup) assert_equal 'Customer Added', response.message assert_success response @@ -90,7 +89,7 @@ def test_add_to_vault_with_store_and_check def test_update_vault test_add_to_vault_with_custom_vault_id - @credit_card = credit_card('4111111111111111', :month => 10) + @credit_card = credit_card('4111111111111111', month: 10) assert response = @gateway.update(@options[:store], @credit_card) assert_success response assert_equal 'Customer Update Successful', response.message @@ -139,7 +138,7 @@ def test_authorize_and_void def test_failed_capture assert response = @gateway.capture(@amount, '') assert_failure response - assert response.message.match(/Invalid Transaction ID \/ Object ID specified:/) + assert response.message.match(/Invalid Transaction ID \/ Object ID specified:/) end def test_authorize_with_three_d_secure_pass_thru @@ -163,9 +162,9 @@ def test_failed_verify def test_invalid_login gateway = BraintreeOrangeGateway.new( - :login => '', - :password => '' - ) + login: '', + password: '' + ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_equal 'Invalid Username', response.message assert_failure response diff --git a/test/remote/gateways/remote_braintree_token_nonce_test.rb b/test/remote/gateways/remote_braintree_token_nonce_test.rb new file mode 100644 index 00000000000..ae9d3446f97 --- /dev/null +++ b/test/remote/gateways/remote_braintree_token_nonce_test.rb @@ -0,0 +1,109 @@ +require 'test_helper' + +class RemoteBraintreeTokenNonceTest < Test::Unit::TestCase + def setup + @gateway = BraintreeGateway.new(fixtures(:braintree_blue)) + @braintree_backend = @gateway.instance_eval { @braintree_gateway } + + ach_mandate = 'By clicking ["Checkout"], I authorize Braintree, a service of PayPal, ' \ + 'on behalf of My Company (i) to verify my bank account information ' \ + 'using bank information and consumer reports and (ii) to debit my bank account.' + + @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: + } + end + + def test_client_token_generation + generator = TokenNonce.new(@braintree_backend) + client_token = generator.client_token + assert_not_nil client_token + assert_not_nil client_token['authorizationFingerprint'] + end + + def test_client_token_generation_with_mid + @options[:merchant_account_id] = '1234' + generator = TokenNonce.new(@braintree_backend, @options) + client_token = generator.client_token + assert_not_nil client_token + assert_equal client_token['merchantAccountId'], '1234' + end + + def test_client_token_generation_with_a_new_mid + @options[:merchant_account_id] = '1234' + generator = TokenNonce.new(@braintree_backend, @options) + client_token = generator.client_token({ merchant_account_id: '5678' }) + assert_not_nil client_token + assert_equal client_token['merchantAccountId'], '5678' + end + + def test_successfully_create_token_nonce_for_bank_account + generator = TokenNonce.new(@braintree_backend, @options) + bank_account = check({ account_number: '4012000033330125', routing_number: '011000015' }) + tokenized_bank_account, err_messages = generator.create_token_nonce_for_payment_method(bank_account) + + assert_not_nil tokenized_bank_account + assert_match %r(^tokenusbankacct_), tokenized_bank_account + assert_nil err_messages + end + + def test_unsucesfull_create_token_with_invalid_state + @options[:billing_address][:state] = nil + generator = TokenNonce.new(@braintree_backend, @options) + bank_account = check({ account_number: '4012000033330125', routing_number: '011000015' }) + tokenized_bank_account, err_messages = generator.create_token_nonce_for_payment_method(bank_account) + + assert_nil tokenized_bank_account + 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 + @options[:billing_address][:zip] = nil + generator = TokenNonce.new(@braintree_backend, @options) + bank_account = check({ account_number: '4012000033330125', routing_number: '011000015' }) + tokenized_bank_account, err_messages = generator.create_token_nonce_for_payment_method(bank_account) + + assert_nil tokenized_bank_account + 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 + config_base = { + merchant_id: 'test', + public_key: 'test', + private_key: 'test', + environment: :sandbox + } + + configuration = Braintree::Configuration.new(config_base) + braintree_backend = Braintree::Gateway.new(configuration) + generator = TokenNonce.new(braintree_backend) + + assert_equal 'https://payments.sandbox.braintree-api.com/graphql', generator.url + + configuration = Braintree::Configuration.new(config_base.update(environment: :production)) + braintree_backend = Braintree::Gateway.new(configuration) + generator = TokenNonce.new(braintree_backend) + + 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_bridge_pay_test.rb b/test/remote/gateways/remote_bridge_pay_test.rb index 88f91a8212c..1b1e175b788 100644 --- a/test/remote/gateways/remote_bridge_pay_test.rb +++ b/test/remote/gateways/remote_bridge_pay_test.rb @@ -9,10 +9,10 @@ def setup @declined_card = credit_card('4000300011100000') @check = check( - :name => 'John Doe', - :routing_number => '490000018', - :account_number => '1234567890', - :number => '1001' + name: 'John Doe', + routing_number: '490000018', + account_number: '1234567890', + number: '1001' ) @options = { @@ -57,7 +57,7 @@ def test_partial_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture end @@ -78,7 +78,7 @@ def test_partial_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount-1, purchase.authorization) + assert refund = @gateway.refund(@amount - 1, purchase.authorization) assert_success refund end @@ -142,4 +142,13 @@ def test_transcript_scrubbing assert_scrubbed(@credit_card.verification_value, transcript) assert_scrubbed(@gateway.options[:password], transcript) end + + def test_account_number_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(150, @check, @options) + end + clean_transcript = @gateway.scrub(transcript) + + assert_scrubbed(@check.account_number, clean_transcript) + end end diff --git a/test/remote/gateways/remote_cams_test.rb b/test/remote/gateways/remote_cams_test.rb index e193a39ce7e..b1e6a61ed75 100644 --- a/test/remote/gateways/remote_cams_test.rb +++ b/test/remote/gateways/remote_cams_test.rb @@ -73,7 +73,7 @@ def test_partial_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture end @@ -94,7 +94,7 @@ def test_partial_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount-1, purchase.authorization) + assert refund = @gateway.refund(@amount - 1, purchase.authorization) assert_success refund end diff --git a/test/remote/gateways/remote_card_connect_test.rb b/test/remote/gateways/remote_card_connect_test.rb index 10cf459c550..4717bb2f2b8 100644 --- a/test/remote/gateways/remote_card_connect_test.rb +++ b/test/remote/gateways/remote_card_connect_test.rb @@ -34,28 +34,28 @@ def test_successful_purchase_with_more_options ship_from_date: '20877', items: [ { - line_no: '1', + lineno: '1', material: 'MATERIAL-1', description: 'DESCRIPTION-1', upc: 'UPC-1', quantity: '1000', uom: 'CS', - unit_cost: '900', - net_amnt: '150', - tax_amnt: '117', - disc_amnt: '0' + unitcost: '900', + netamnt: '150', + taxamnt: '117', + discamnt: '0' }, { - line_no: '2', + lineno: '2', material: 'MATERIAL-2', description: 'DESCRIPTION-2', upc: 'UPC-1', quantity: '2000', uom: 'CS', - unit_cost: '450', - net_amnt: '300', - tax_amnt: '117', - disc_amnt: '0' + unitcost: '450', + netamnt: '300', + taxamnt: '117', + discamnt: '0' } ] } @@ -65,11 +65,82 @@ def test_successful_purchase_with_more_options assert_equal 'Approval Queued for Capture', response.message end - def test_successful_purchase_3DS + def test_successful_purchase_with_more_options_but_no_PO + options = { + order_id: '1', + ip: '127.0.0.1', + email: 'joe@example.com', + tax_amount: '50', + freight_amount: '29', + duty_amount: '67', + order_date: '20170507', + ship_from_date: '20877', + items: [ + { + lineno: '1', + material: 'MATERIAL-1', + description: 'DESCRIPTION-1', + upc: 'UPC-1', + quantity: '1000', + uom: 'CS', + unitcost: '900', + netamnt: '150', + taxamnt: '117', + discamnt: '0' + }, + { + lineno: '2', + material: 'MATERIAL-2', + description: 'DESCRIPTION-2', + upc: 'UPC-1', + quantity: '2000', + uom: 'CS', + unitcost: '450', + netamnt: '300', + taxamnt: '117', + discamnt: '0' + } + ] + } + + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Approval', response.message + end + + def test_successful_purchase_with_user_fields + # `response` does not contain userfields, but the transaction may be checked after + # running the test suite via an authorized call to the inquireByOrderid endpoint: + # /cardconnect/rest/inquireByOrderid// + options = { + order_id: '138510', + ip: '127.0.0.1', + email: 'joe@example.com', + po_number: '5FSD4', + tax_amount: '50', + freight_amount: '29', + duty_amount: '67', + order_date: '20170507', + ship_from_date: '20877', + user_fields: [ + { udf0: 'value0' }, + { udf1: 'value1' }, + { udf2: 'value2' } + ] + } + + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Approval Queued for Capture', response.message + end + + def test_successful_purchase_three_ds three_ds_options = @options.merge( - secure_flag: 'se3453', - secure_value: '233frdf', - secure_xid: '334ef34' + three_d_secure: { + eci: 'se3453', + cavv: 'AJkBByEyYgAAAASwgmEodQAAAAA=', + ds_transaction_id: 'ODUzNTYzOTcwODU5NzY3Qw==' + } ) response = @gateway.purchase(@amount, @credit_card, three_ds_options) assert_success response @@ -83,6 +154,31 @@ def test_successful_purchase_with_profile assert_success purchase_response end + def test_successful_purchase_using_stored_credential_framework + stored_credential_options = { + initial_transaction: true, + reason_type: 'recurring', + initiator: 'merchant' + } + response = @gateway.purchase(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_options })) + assert_success response + assert_equal response.params['cof'], 'M' + + stored_credential_options = { + initial_transaction: false, + reason_type: 'recurring', + initiator: 'merchant' + } + response = @gateway.purchase(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_options })) + assert_success response + assert_equal response.params['cof'], 'M' + end + + def test_successful_purchase_with_telephonic_ecomind + response = @gateway.purchase(@amount, @credit_card, @options.merge({ ecomind: 'T' })) + assert_success response + end + def test_failed_purchase response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response @@ -130,22 +226,27 @@ def test_failed_echeck_purchase assert_equal 'Invalid card', response.message end - def test_successful_refund - purchase = @gateway.purchase(@amount, @credit_card, @options) - assert_success purchase - - assert refund = @gateway.refund(@amount, purchase.authorization) - assert_success refund - assert_equal 'Approval', refund.message - end - - def test_partial_refund - purchase = @gateway.purchase(@amount, @credit_card, @options) - assert_success purchase - - assert refund = @gateway.refund(@amount - 1, purchase.authorization) - assert_success refund - end + # A transaction cannot be refunded before settlement so these tests will + # fail with the following response, to properly test refunds create a purchase + # save the reference and test the next day, check: + # https://cardconnect.com/launchpointe/running-a-business/payment-processing-101#how_long_it_takes + # + # def test_successful_refund + # purchase = @gateway.purchase(@amount, @credit_card, @options) + # assert_success purchase + # + # assert refund = @gateway.refund(@amount, purchase.authorization) + # assert_success refund + # assert_equal 'Approval', refund.message + # end + # + # def test_partial_refund + # purchase = @gateway.purchase(@amount, @credit_card, @options) + # assert_success purchase + # + # assert refund = @gateway.refund(@amount - 1, purchase.authorization) + # assert_success refund + # end def test_failed_refund response = @gateway.refund(@amount, @invalid_txn) @@ -201,9 +302,10 @@ def test_failed_unstore def test_invalid_login gateway = CardConnectGateway.new(username: '', password: '', merchant_id: '') - assert_raises(ActiveMerchant::ResponseError) do - gateway.purchase(@amount, @credit_card, @options) - end + response = gateway.purchase(@amount, @credit_card, @options) + + assert_failure response + assert_match %r{Unable to authenticate. Please check your credentials.}, response.message end def test_transcript_scrubbing diff --git a/test/remote/gateways/remote_card_save_test.rb b/test/remote/gateways/remote_card_save_test.rb index 5774414fc17..a3c34c19f3c 100644 --- a/test/remote/gateways/remote_card_save_test.rb +++ b/test/remote/gateways/remote_card_save_test.rb @@ -1,23 +1,22 @@ require 'test_helper' -class RemoteCardSaveTest < Test::Unit::TestCase - +class RemoteCardSaveTest < Test::Unit::TestCase def setup @gateway = CardSaveGateway.new(fixtures(:card_save)) - + @amount = 100 - @credit_card = credit_card('4976000000003436', :verification_value => '452') - @declined_card = credit_card('4221690000004963', :verification_value => '125') - @addresses = {'4976000000003436' => { :name => 'John Watson', :address1 => '32 Edward Street', :city => 'Camborne,', :state => 'Cornwall', :country => 'GB', :zip => 'TR14 8PA' }, - '4221690000004963' => { :name => 'Ian Lee', :address1 => '274 Lymington Avenue', :city => 'London', :state => 'London', :country => 'GB', :zip => 'N22 6JN' }} - - @options = { - :order_id => '1', - :billing_address => @addresses[@credit_card.number], - :description => 'Store Purchase' + @credit_card = credit_card('4976000000003436', verification_value: '452') + @declined_card = credit_card('4221690000004963', verification_value: '125') + @addresses = { '4976000000003436' => { name: 'John Watson', address1: '32 Edward Street', city: 'Camborne,', state: 'Cornwall', country: 'GB', zip: 'TR14 8PA' }, + '4221690000004963' => { name: 'Ian Lee', address1: '274 Lymington Avenue', city: 'London', state: 'London', country: 'GB', zip: 'N22 6JN' } } + + @options = { + order_id: '1', + billing_address: @addresses[@credit_card.number], + description: 'Store Purchase' } end - + def test_successful_purchase assert response = @gateway.purchase(@amount, @credit_card, @options) assert_success response @@ -25,14 +24,14 @@ def test_successful_purchase end def test_unsuccessful_purchase - @options.merge!(:billing_address => @addresses[@declined_card.number]) + @options[:billing_address] = @addresses[@declined_card.number] assert response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response assert_equal 'Card declined', response.message end def test_authorize_and_capture - amount = @amount+10 + amount = @amount + 10 assert auth = @gateway.authorize(amount, @credit_card, @options) assert_success auth assert auth.message =~ /AuthCode: ([0-9]+)/ @@ -49,8 +48,8 @@ def test_failed_capture def test_invalid_login gateway = CardSaveGateway.new( - :login => '', - :password => '' + login: '', + password: '' ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response diff --git a/test/remote/gateways/remote_card_stream_test.rb b/test/remote/gateways/remote_card_stream_test.rb index c35543a9b9b..48fe71952ec 100644 --- a/test/remote/gateways/remote_card_stream_test.rb +++ b/test/remote/gateways/remote_card_stream_test.rb @@ -6,118 +6,136 @@ def setup @gateway = CardStreamGateway.new(fixtures(:card_stream)) - @amex = credit_card('374245455400001', - :month => '12', - :year => '2014', - :verification_value => '4887', - :brand => :american_express + @amex = credit_card( + '374245455400001', + month: '12', + year: Time.now.year + 1, + verification_value: '4887', + brand: :american_express ) - @mastercard = credit_card('5301250070000191', - :month => '12', - :year => '2014', - :verification_value => '419', - :brand => :master + @mastercard = credit_card( + '5301250070000191', + month: '12', + year: Time.now.year + 1, + verification_value: '419', + brand: :master ) - @visacreditcard = credit_card('4929421234600821', - :month => '12', - :year => '2014', - :verification_value => '356', - :brand => :visa + @visacreditcard = credit_card( + '4929421234600821', + month: '12', + year: Time.now.year + 1, + verification_value: '356', + brand: :visa ) - @visadebitcard = credit_card('4539791001730106', - :month => '12', - :year => '2014', - :verification_value => '289', - :brand => :visa + @visadebitcard = credit_card( + '4539791001730106', + month: '12', + year: Time.now.year + 1, + verification_value: '289', + brand: :visa ) - @declined_card = credit_card('4000300011112220', - :month => '9', - :year => '2014' + @declined_card = credit_card( + '4000300011112220', + month: '9', + year: Time.now.year + 1 ) @amex_options = { - :billing_address => { - :address1 => 'The Hunts Way', - :city => '', - :state => 'Leicester', - :zip => 'SO18 1GW', - :country => 'GB' + billing_address: { + address1: 'The Hunts Way', + city: '', + state: 'Leicester', + zip: 'SO18 1GW', + country: 'GB' }, - :order_id => generate_unique_id, - :description => 'AM test purchase', - :ip => '1.1.1.1' + order_id: generate_unique_id, + description: 'AM test purchase', + ip: '1.1.1.1' } @visacredit_options = { - :billing_address => { - :address1 => 'Flat 6, Primrose Rise', - :address2 => '347 Lavender Road', - :city => '', - :state => 'Northampton', - :zip => 'NN17 8YG', - :country => 'GB' + billing_address: { + address1: 'Flat 6, Primrose Rise', + address2: '347 Lavender Road', + city: '', + state: 'Northampton', + zip: 'NN17 8YG', + country: 'GB' }, - :order_id => generate_unique_id, - :description => 'AM test purchase', - :ip => '1.1.1.1' + order_id: generate_unique_id, + description: 'AM test purchase', + ip: '1.1.1.1' } @visacredit_descriptor_options = { - :billing_address => { - :address1 => 'Flat 6, Primrose Rise', - :address2 => '347 Lavender Road', - :city => '', - :state => 'Northampton', - :zip => 'NN17 8YG', - :country => 'GB' + billing_address: { + address1: 'Flat 6, Primrose Rise', + address2: '347 Lavender Road', + city: '', + state: 'Northampton', + zip: 'NN17 8YG', + country: 'GB' }, - :merchant_name => 'merchant', - :dynamic_descriptor => 'product', - :ip => '1.1.1.1', + order_id: generate_unique_id, + merchant_name: 'merchant', + dynamic_descriptor: 'product', + ip: '1.1.1.1' } @visacredit_reference_options = { - :order_id => generate_unique_id, - :description => 'AM test purchase', - :ip => '1.1.1.1' + order_id: generate_unique_id, + description: 'AM test purchase', + ip: '1.1.1.1' } @visadebit_options = { - :billing_address => { - :address1 => 'Unit 5, Pickwick Walk', - :address2 => '120 Uxbridge Road', - :city => 'Hatch End', - :state => 'Middlesex', - :zip => 'HA6 7HJ', - :country => 'GB' + billing_address: { + address1: 'Unit 5, Pickwick Walk', + address2: '120 Uxbridge Road', + city: 'Hatch End', + state: 'Middlesex', + zip: 'HA6 7HJ', + country: 'GB' }, - :order_id => generate_unique_id, - :description => 'AM test purchase', - :ip => '1.1.1.1' + order_id: generate_unique_id, + description: 'AM test purchase', + ip: '1.1.1.1' } @mastercard_options = { - :billing_address => { - :address1 => '25 The Larches', - :city => 'Narborough', - :state => 'Leicester', - :zip => 'LE10 2RT', - :country => 'GB' + billing_address: { + address1: '25 The Larches', + city: 'Narborough', + state: 'Leicester', + zip: 'LE10 2RT', + country: 'GB' }, - :order_id => generate_unique_id, - :description => 'AM test purchase', - :ip => '1.1.1.1' + order_id: generate_unique_id, + description: 'AM test purchase', + ip: '1.1.1.1' } - @three_ds_enrolled_card = credit_card('4012001037141112', - :month => '12', - :year => '2020', - :brand => :visa + @three_ds_enrolled_card = credit_card( + '4012001037141112', + month: '12', + year: '2020', + brand: :visa ) + + @visacredit_three_ds_options = { + threeds_required: true, + three_d_secure: { + enrolled: 'true', + authentication_response_status: 'Y', + eci: '05', + cavv: 'Y2FyZGluYWxjb21tZXJjZWF1dGg', + xid: '00000000000004717472' + } + } end def test_successful_visacreditcard_authorization_and_capture @@ -166,7 +184,7 @@ def test_failed_visacreditcard_purchase_and_refund assert responseRefund = @gateway.refund(142, responsePurchase.authorization, @visacredit_options) assert_failure responseRefund - assert_equal 'Can not REFUND this SALE transaction', responseRefund.message + assert_equal 'Cannot REFUND this SALE transaction', responseRefund.message assert responseRefund.test? end @@ -223,7 +241,7 @@ def test_failed_visadebitcard_purchase_and_refund assert !responsePurchase.authorization.blank? assert responseRefund = @gateway.refund(142, responsePurchase.authorization, @visadebit_options) - assert_equal 'Can not REFUND this SALE transaction', responseRefund.message + assert_equal 'Cannot REFUND this SALE transaction', responseRefund.message assert_failure responseRefund assert responseRefund.test? end @@ -261,7 +279,7 @@ def test_failed_amex_purchase_and_refund assert !responsePurchase.authorization.blank? assert responseRefund = @gateway.refund(142, responsePurchase.authorization, @amex_options) - assert_equal 'Can not REFUND this SALE transaction', responseRefund.message + assert_equal 'Cannot REFUND this SALE transaction', responseRefund.message assert_failure responseRefund assert responseRefund.test? end @@ -299,7 +317,7 @@ def test_failed_mastercard_purchase_and_refund assert !responsePurchase.authorization.blank? assert responseRefund = @gateway.refund(142, responsePurchase.authorization, @mastercard_options) - assert_equal 'Can not REFUND this SALE transaction', responseRefund.message + assert_equal 'Cannot REFUND this SALE transaction', responseRefund.message assert_failure responseRefund assert responseRefund.test? end @@ -313,7 +331,7 @@ def test_successful_visacreditcard_purchase end def test_successful_visacreditcard_purchase_via_reference - assert response = @gateway.purchase(142, @visacreditcard, @visacredit_options.merge({:type => '9'})) + assert response = @gateway.purchase(142, @visacreditcard, @visacredit_options.merge({ type: '9' })) assert_equal 'APPROVED', response.message assert_success response assert response.test? @@ -343,6 +361,13 @@ def test_failed_purchase_non_existent_currency assert_match %r{MISSING_CURRENCYCODE}, response.message end + def test_successful_purchase_and_amount_for_non_decimal_currency + assert response = @gateway.purchase(14200, @visacreditcard, @visacredit_options.merge(currency: 'JPY')) + assert_success response + assert_equal '392', response.params['currencyCode'] + assert_equal '142', response.params['amount'] + end + def test_successful_visadebitcard_purchase assert response = @gateway.purchase(142, @visadebitcard, @visadebit_options) assert_equal 'APPROVED', response.message @@ -376,8 +401,8 @@ def test_successful_amex_purchase def test_invalid_login gateway = CardStreamGateway.new( - :login => '', - :shared_secret => '' + login: '', + shared_secret: '' ) assert response = gateway.purchase(142, @mastercard, @mastercard_options) assert_match %r{MISSING_MERCHANTID}, response.message @@ -393,7 +418,7 @@ def test_successful_verify def test_failed_verify response = @gateway.verify(@declined_card, @mastercard_options) assert_failure response - assert_match %r{INVALID_CARDNUMBER}, response.message + assert_match %r{Disallowed cardnumber}, response.message end def test_successful_3dsecure_purchase @@ -418,14 +443,46 @@ def test_successful_3dsecure_auth assert !response.params['threeDSPaReq'].blank? end + def test_3dsecure2_auth_authenticated_card + assert response = @gateway.authorize(1202, @visacreditcard, @visacredit_options.merge(@visacredit_three_ds_options)) + assert_equal 'APPROVED', response.message + assert_equal '0', response.params['responseCode'] + assert_equal 'Success', response.params['threeDSResponseMessage'] + assert response.success? + assert response.test? + assert !response.authorization.blank? + end + + def test_3dsecure2_auth_not_authenticated_card + @visacredit_three_ds_options[:three_d_secure][:authentication_response_status] = 'N' + assert response = @gateway.authorize(1202, @visacreditcard, @visacredit_options.merge(@visacredit_three_ds_options)) + assert_equal '3DS DECLINED', response.message + assert_equal '65803', response.params['responseCode'] + assert_equal 'not authenticated', response.params['threeDSCheck'] + refute response.success? + assert response.test? + refute response.authorization.blank? + end + + def test_3dsecure2_auth_not_enrolled_card + @visacredit_three_ds_options[:three_d_secure][:enrolled] = 'false' + assert response = @gateway.authorize(1202, @visacreditcard, @visacredit_options.merge(@visacredit_three_ds_options)) + assert_equal '3DS DECLINED', response.message + assert_equal '65803', response.params['responseCode'] + assert_equal 'not checked', response.params['threeDSCheck'] + refute response.success? + assert response.test? + refute response.authorization.blank? + end + def test_transcript_scrubbing transcript = capture_transcript(@gateway) do @gateway.purchase(@amount, @visacreditcard, @visacredit_options) end clean_transcript = @gateway.scrub(transcript) - assert_scrubbed( @visacreditcard.number, clean_transcript) - assert_scrubbed( @visacreditcard.verification_value.to_s, clean_transcript) + assert_scrubbed(@visacreditcard.number, clean_transcript) + assert_scrubbed(@visacreditcard.verification_value.to_s, clean_transcript) assert_scrubbed(@gateway.options[:shared_secret], clean_transcript) end end diff --git a/test/remote/gateways/remote_cardknox_test.rb b/test/remote/gateways/remote_cardknox_test.rb index e3c375aa324..ca7b0aef440 100644 --- a/test/remote/gateways/remote_cardknox_test.rb +++ b/test/remote/gateways/remote_cardknox_test.rb @@ -33,11 +33,11 @@ def setup zip: '46112', country: 'US', phone: '(555)555-5555', - fax: '(555)555-6666', + fax: '(555)555-6666' } } - @options = {} + @options = {} end def test_successful_credit_card_purchase @@ -124,7 +124,7 @@ def test_partial_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture end @@ -138,7 +138,7 @@ def test_credit_card_purchase_partial_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount-1, purchase.authorization) + assert refund = @gateway.refund(@amount - 1, purchase.authorization) assert_success refund end @@ -146,17 +146,16 @@ def test_failed_credit_card_authorize_partial_refund auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert refund = @gateway.refund(@amount-1, auth.authorization) + assert refund = @gateway.refund(@amount - 1, auth.authorization) assert_failure refund assert_equal 'Refund not allowed on non-captured auth.', refund.message - end def test_failed_partial_check_refund # the gate way does not support this transaction purchase = @gateway.purchase(@amount, @check, @options) assert_success purchase - assert refund = @gateway.refund(@amount-1, purchase.authorization) + assert refund = @gateway.refund(@amount - 1, purchase.authorization) assert_failure refund assert_equal "Transaction is in a state that cannot be refunded\nParameter name: originalReferenceNumber", refund.message # "Only allowed to refund transactions that have settled. This is the best we can do for now testing wise." end @@ -168,7 +167,7 @@ def test_credit_card_capture_partial_refund assert capture = @gateway.capture(@amount, auth.authorization) assert_success capture - assert refund = @gateway.refund(@amount-1, capture.authorization) + assert refund = @gateway.refund(@amount - 1, capture.authorization) assert_success refund end @@ -191,7 +190,7 @@ def test_successful_credit_card_capture_void auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture assert void = @gateway.void(capture.authorization, @options) @@ -215,7 +214,7 @@ def test_successful_credit_card_refund_void assert capture = @gateway.capture(@amount, auth.authorization) assert_success capture - assert refund = @gateway.refund(@amount-1, capture.authorization) + assert refund = @gateway.refund(@amount - 1, capture.authorization) assert_success refund assert void = @gateway.void(refund.authorization, @options) diff --git a/test/remote/gateways/remote_cardprocess_test.rb b/test/remote/gateways/remote_cardprocess_test.rb index 951c07e0ab3..c1763f9bd0e 100644 --- a/test/remote/gateways/remote_cardprocess_test.rb +++ b/test/remote/gateways/remote_cardprocess_test.rb @@ -50,7 +50,7 @@ def test_successful_authorize_and_capture end def test_failed_authorize - @gateway.instance_variable_set(:@test_options, {'customParameters[forceResultCode]' => '800.100.151'}) + @gateway.instance_variable_set(:@test_options, { 'customParameters[forceResultCode]' => '800.100.151' }) response = @gateway.authorize(@amount, @credit_card, @options) assert_failure response assert_equal 'transaction declined (invalid card)', response.message @@ -60,7 +60,7 @@ def test_partial_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture end @@ -89,7 +89,7 @@ def test_partial_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount-1, purchase.authorization) + assert refund = @gateway.refund(@amount - 1, purchase.authorization) assert_success refund end @@ -121,7 +121,7 @@ def test_successful_verify end def test_failed_verify - @gateway.instance_variable_set(:@test_options, {'customParameters[forceResultCode]' => '600.200.100'}) + @gateway.instance_variable_set(:@test_options, { 'customParameters[forceResultCode]' => '600.200.100' }) response = @gateway.verify(@credit_card, @options) assert_failure response assert_match %r{invalid Payment Method}, response.message diff --git a/test/remote/gateways/remote_cashnet_test.rb b/test/remote/gateways/remote_cashnet_test.rb index 87aea788007..2b12a7116a3 100644 --- a/test/remote/gateways/remote_cashnet_test.rb +++ b/test/remote/gateways/remote_cashnet_test.rb @@ -7,7 +7,7 @@ def setup @credit_card = credit_card( '5454545454545454', month: 12, - year: 2015 + year: Time.new.year + 1 ) @options = { order_id: generate_unique_id, @@ -15,6 +15,22 @@ def setup } end + def test_successful_purchase + assert purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + assert purchase.test? + assert_equal 'Success', purchase.message + end + + def test_successful_purchase_with_multiple_items + options = @options.merge({ item_codes: { item_code: 'FEE', item_code2: 'LOBSTER', item_code3: 'CODES', amount: 5679, amount2: 1234, amount3: 4321 } }) + + assert purchase = @gateway.purchase(@amount, @credit_card, options) + assert_success purchase + assert purchase.test? + assert_equal 'Success', purchase.message + end + def test_successful_purchase_and_refund assert purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase @@ -43,6 +59,15 @@ def test_failed_purchase assert_equal '5', response.params['result'] end + def test_failed_purchase_with_multiple_items + options = @options.merge({ item_codes: { item_code2: 'NONE', amount2: 4321 } }) + + assert response = @gateway.purchase(@amount, @credit_card, options) + assert_failure response + assert_match %r{Invalid item code, no code specified}, response.message + assert_equal '4', response.params['result'] + end + def test_failed_refund assert purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase 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..9d702eb3a6a --- /dev/null +++ b/test/remote/gateways/remote_cecabank_rest_json_test.rb @@ -0,0 +1,238 @@ +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:, + exemption_type: 'transaction_risk_analysis_exemption' + } + + @cit_options = @options.merge({ + recurring_end_date: "#{Time.now.year}1231", + recurring_frequency: '1', + stored_credential: { + reason_type: 'unscheduled', + initiator: 'cardholder' + } + }) + + @apple_pay_network_token = network_tokenization_credit_card( + '4507670001000009', + eci: '05', + payment_cryptogram: 'xgQAAAAAAAAAAAAAAAAAAAAAAAAA', + month: '12', + year: Time.now.year, + source: :apple_pay, + verification_value: '989' + ) + + @google_pay_network_token = network_tokenization_credit_card( + '4507670001000009', + eci: '05', + payment_cryptogram: 'AgAAAAAAAIR8CQrXcIhbQAAAAAA', + month: '10', + year: Time.now.year + 1, + source: :google_pay, + verification_value: nil + ) + 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_successful_purchase_with_apple_pay + assert response = @gateway.purchase(@amount, @apple_pay_network_token, { 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_successful_purchase_with_google_pay + assert response = @gateway.purchase(@amount, @apple_pay_network_token, { 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_failed_purchase_with_apple_pay_sending_three_ds_data + assert response = @gateway.purchase(@amount, @apple_pay_network_token, @options) + assert_failure response + assert_equal response.error_code, '1061' + 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, @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_with_apple_pay_using_stored_credential_recurring_mit + @cit_options[:stored_credential][:reason_type] = 'installment' + assert purchase = @gateway.purchase(@amount, @apple_pay_network_token, @cit_options.except(:three_d_secure)) + 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 + options[:order_id] = generate_unique_id + + assert purchase2 = @gateway.purchase(@amount, @apple_pay_network_token, options) + assert_success purchase2 + 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 + options[:order_id] = generate_unique_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: '07', + ds_transaction_id: 'a2bf089f-cefc-4d2c-850f-9153827fe070', + acs_transaction_id: '18c353b0-76e3-4a4c-8033-f14fe9ce39dc', + authentication_response_status: 'I', + three_ds_server_trans_id: '9bd9aa9c-3beb-4012-8e52-214cccb25ec5', + enrolled: 'true', + cavv: 'AJkCC1111111111122222222AAAA', + xid: '22222' + } + end +end diff --git a/test/remote/gateways/remote_cecabank_test.rb b/test/remote/gateways/remote_cecabank_test.rb index 1a6c0131cd2..217ed8cc501 100644 --- a/test/remote/gateways/remote_cecabank_test.rb +++ b/test/remote/gateways/remote_cecabank_test.rb @@ -5,12 +5,12 @@ def setup @gateway = CecabankGateway.new(fixtures(:cecabank)) @amount = 100 - @credit_card = credit_card('5540500001000004', {:month => 12, :year => Time.now.year, :verification_value => 989}) - @declined_card = credit_card('5540500001000004', {:month => 11, :year => Time.now.year + 1, :verification_value => 001}) + @credit_card = credit_card('5540500001000004', { 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, - :description => 'Active Merchant Test Purchase' + order_id: generate_unique_id, + description: 'Active Merchant Test Purchase' } end @@ -23,10 +23,9 @@ def test_successful_purchase def test_unsuccessful_purchase assert response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response - assert_equal 'ERROR', response.message + assert_match 'ERROR', response.message end - def test_successful_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase @@ -37,20 +36,19 @@ def test_successful_refund end def test_unsuccessful_refund - assert response = @gateway.refund(@amount, 'wrongreference', @options) + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert response = @gateway.refund(@amount, purchase.authorization, @options.merge(currency: 'USD')) assert_failure response - assert_equal 'ERROR', response.message + assert_match 'Error', response.message end def test_invalid_login - gateway = CecabankGateway.new( - :merchant_id => '', - :acquirer_bin => '', - :terminal_id => '', - :key => '' - ) + gateway = CecabankGateway.new(fixtures(:cecabank).merge(signature_key: 'invalid')) + assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response - assert_equal 'ERROR', response.message + assert_match 'ERROR', response.message end end diff --git a/test/remote/gateways/remote_cenpos_test.rb b/test/remote/gateways/remote_cenpos_test.rb index 479545edb08..0edde763895 100644 --- a/test/remote/gateways/remote_cenpos_test.rb +++ b/test/remote/gateways/remote_cenpos_test.rb @@ -5,13 +5,19 @@ def setup @gateway = CenposGateway.new(fixtures(:cenpos)) @amount = SecureRandom.random_number(10000) - @credit_card = credit_card('4111111111111111', month: 02, year: 18, verification_value: 999) + @declined_amount = 100 + @credit_card = credit_card('4003440008007566', month: 12, year: 2025, verification_value: 999) + @declined_card = credit_card('4000300011112220') @invalid_card = credit_card('9999999999999999') @options = { order_id: SecureRandom.random_number(1000000), - billing_address: address + billing_address: { + name: 'Jim Smith', + address1: 'D8320', + zip: 'D5284' + } } end @@ -61,21 +67,21 @@ def test_successful_purchase_with_currency end def test_failed_purchase - response = @gateway.purchase(@amount, @declined_card, @options) + response = @gateway.purchase(@declined_amount, @declined_card, @options) assert_failure response assert_equal 'Decline transaction', response.message assert_equal Gateway::STANDARD_ERROR_CODE[:card_declined], response.error_code end def test_failed_purchase_cvv_result - response = @gateway.purchase(@amount, @declined_card, @options) + response = @gateway.purchase(@declined_amount, @declined_card, @options) %w(code message).each do |key| assert_equal nil, response.cvv_result[key] end end def test_failed_purchase_avs_result - response = @gateway.purchase(@amount, @declined_card, @options) + response = @gateway.purchase(@declined_amount, @declined_card, @options) %w(code message).each do |key| assert_equal nil, response.avs_result[key] end @@ -93,7 +99,7 @@ def test_successful_authorize_and_capture end def test_failed_authorize - response = @gateway.authorize(@amount, @declined_card, @options) + response = @gateway.authorize(@declined_amount, @declined_card, @options) assert_failure response assert_equal 'Decline transaction', response.message assert_equal Gateway::STANDARD_ERROR_CODE[:card_declined], response.error_code @@ -104,10 +110,10 @@ def test_failed_capture assert_success response assert_equal 'Succeeded', response.message - capture = @gateway.capture(@amount, response.authorization) + @gateway.capture(@amount, response.authorization) capture = @gateway.capture(@amount, response.authorization) assert_failure capture - assert_equal 'Duplicated force transaction.', capture.message + assert_match(/Duplicated.*transaction/, capture.message) end def test_successful_void @@ -132,7 +138,7 @@ def test_failed_void response = @gateway.authorize(@amount, @credit_card, @options) assert_success response - void = @gateway.void(response.authorization) + @gateway.void(response.authorization) void = @gateway.void(response.authorization) assert_failure void assert_equal 'Original Transaction not found', void.message @@ -166,11 +172,13 @@ def test_failed_credit assert_equal Gateway::STANDARD_ERROR_CODE[:invalid_number], response.error_code end - def test_successful_verify - response = @gateway.verify(@credit_card, @options) - assert_success response - assert_match %r{Succeeded}, response.message - end + # This test appears to fail due to the amount of 100 being set in verify + # That amount is automatically triggering a decline message in tests + # def test_successful_verify + # response = @gateway.verify(@credit_card, @options) + # assert_success response + # assert_match %r{Succeeded}, response.message + # end def test_failed_verify response = @gateway.verify(@declined_card, @options) diff --git a/test/remote/gateways/remote_checkout_test.rb b/test/remote/gateways/remote_checkout_test.rb index 206442a70fa..4977d1889f4 100644 --- a/test/remote/gateways/remote_checkout_test.rb +++ b/test/remote/gateways/remote_checkout_test.rb @@ -9,7 +9,7 @@ def setup year: '2017', verification_value: '956' ) - @declined_card = credit_card( + @declined_card = credit_card( '4543474002249996', month: '06', year: '2018', @@ -27,13 +27,14 @@ def test_successful_purchase end def test_successful_purchase_with_extra_options - response = @gateway.purchase(100, @credit_card, @options.merge( + options = @options.merge( currency: 'EUR', email: 'bob@example.com', order_id: generate_unique_id, customer: generate_unique_id, ip: '127.0.0.1' - )) + ) + response = @gateway.purchase(100, @credit_card, options) assert_success response assert_equal 'Successful', response.message end @@ -60,7 +61,7 @@ def test_successful_authorize_and_capture auth = @gateway.authorize(100, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(100, auth.authorization, {currency: 'CAD'}) + assert capture = @gateway.capture(100, auth.authorization, { currency: 'CAD' }) assert_success capture assert_equal 'Successful', capture.message end @@ -101,7 +102,7 @@ def test_successful_refund assert response = @gateway.purchase(100, @credit_card, @options) assert_success response - assert refund = @gateway.refund(100, response.authorization, {currency: 'CAD'}) + assert refund = @gateway.refund(100, response.authorization, { currency: 'CAD' }) assert_success refund assert_equal 'Successful', refund.message end @@ -110,7 +111,7 @@ def test_failed_refund assert response = @gateway.purchase(100, @credit_card, @options) assert_success response - assert refund = @gateway.refund(100, '||||', {currency: 'CAD'}) + assert refund = @gateway.refund(100, '||||', { currency: 'CAD' }) assert_failure refund end diff --git a/test/remote/gateways/remote_checkout_v2_test.rb b/test/remote/gateways/remote_checkout_v2_test.rb index 893347aa7f8..ada2775b998 100644 --- a/test/remote/gateways/remote_checkout_v2_test.rb +++ b/test/remote/gateways/remote_checkout_v2_test.rb @@ -1,25 +1,186 @@ +require 'timecop' require 'test_helper' class RemoteCheckoutV2Test < Test::Unit::TestCase def setup - @gateway = CheckoutV2Gateway.new(fixtures(:checkout_v2)) + gateway_fixtures = fixtures(:checkout_v2) + gateway_token_fixtures = fixtures(:checkout_v2_token) + @gateway = CheckoutV2Gateway.new(secret_key: gateway_fixtures[:secret_key]) + @gateway_oauth = CheckoutV2Gateway.new({ client_id: gateway_fixtures[:client_id], client_secret: gateway_fixtures[:client_secret] }) + @gateway_token = CheckoutV2Gateway.new(secret_key: gateway_token_fixtures[:secret_key], public_key: gateway_token_fixtures[:public_key]) @amount = 200 - @credit_card = credit_card('4242424242424242', verification_value: '100', month: '6', year: '2018') + @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('4000300011112220') + @declined_card = credit_card('42424242424242424', verification_value: '234', month: '6', year: Time.now.year + 1) + @amex_card = credit_card('341829238058580', brand: 'american_express', verification_value: '1234', 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', + payment_cryptogram: 'AgAAAAAAAIR8CQrXcIhbQAAAAAA', + 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: 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: Time.now.year + 1, + source: :google_pay, + verification_value: nil + ) + + @google_pay_master_cryptogram_3ds_network_token = network_tokenization_credit_card( + '5436031030606378', + payment_cryptogram: 'AgAAAAAAAIR8CQrXcIhbQAAAAAA', + 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: Time.now.year + 1, + source: :apple_pay, + verification_value: nil + ) @options = { order_id: '1', billing_address: address, + shipping_address: address, description: 'Purchase', - email: 'longbob.longsen@example.com' + email: 'longbob.longsen@example.com', + processing_channel_id: 'pc_lxgl7aqahkzubkundd2l546hdm' } @additional_options = @options.merge( card_on_file: true, transaction_indicator: 2, - previous_charge_id: 'charge_12312' + previous_charge_id: 'pay_123', + processing_channel_id: 'pc_123' + ) + @additional_options_3ds = @options.merge( + execute_threed: true, + three_d_secure: { + version: '1.0.2', + eci: '06', + cavv: 'AgAAAAAAAIR8CQrXcIhbQAAAAAA', + xid: 'MDAwMDAwMDAwMDAwMDAwMzIyNzY=', + authentication_response_status: 'Y' + } + ) + @additional_options_3ds2 = @options.merge( + execute_threed: true, + attempt_n3d: true, + challenge_indicator: 'no_preference', + exemption: 'trusted_listing', + three_d_secure: { + version: '2.0.0', + eci: '06', + cavv: 'AgAAAAAAAIR8CQrXcIhbQAAAAAA', + ds_transaction_id: 'MDAwMDAwMDAwMDAwMDAwMzIyNzY=', + 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' + } + } + ) + @minimun_level2_options = @options.merge({ + order_data: { + tax_amount: 130 + } + }) + 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 @@ -33,18 +194,265 @@ def test_transcript_scrubbing assert_scrubbed(@gateway.options[:secret_key], transcript) end + def test_transcript_scrubbing_via_oauth + declined_card = credit_card('4000300011112220', verification_value: '309') + transcript = capture_transcript(@gateway_oauth) do + @gateway_oauth.purchase(@amount, @credit_card, @options) + end + transcript = @gateway_oauth.scrub(transcript) + assert_scrubbed(declined_card.number, transcript) + assert_scrubbed(declined_card.verification_value, transcript) + assert_scrubbed(@gateway_oauth.options[:client_id], transcript) + assert_scrubbed(@gateway_oauth.options[:client_secret], transcript) + assert_scrubbed(@gateway_oauth.options[:access_token], transcript) + end + + def test_network_transaction_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(100, @apple_pay_network_token, @options) + end + transcript = @gateway.scrub(transcript) + assert_scrubbed(@apple_pay_network_token.payment_cryptogram, transcript) + assert_scrubbed(@apple_pay_network_token.number, transcript) + assert_scrubbed(@gateway.options[:secret_key], transcript) + end + + def test_store_transcript_scrubbing + response = nil + transcript = capture_transcript(@gateway) do + response = @gateway_token.store(@credit_card, @options) + end + token = response.responses.first.params['token'] + transcript = @gateway.scrub(transcript) + assert_scrubbed(token, transcript) + end + def test_successful_purchase response = @gateway.purchase(@amount, @credit_card, @options) assert_success response assert_equal 'Succeeded', response.message end + def test_successful_inquire + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Succeeded', response.message + + response = @gateway.inquire(response.authorization, {}) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_unsuccessful_inquire + response = @gateway.inquire('123EDSE', {}) + assert_failure response + assert_equal '404: Not Found', response.message + end + + def test_successful_purchase_via_oauth + response = @gateway_oauth.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_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 + assert_equal 'Succeeded', response.message + assert_not_nil response.params['source']['payment_account_reference'] + end + + def test_successful_purchase_with_vts_network_token_via_oauth + response = @gateway_oauth.purchase(100, @vts_network_token, @options) + assert_success response + assert_equal 'Succeeded', response.message + assert_not_nil response.params['source']['payment_account_reference'] + end + + def test_successful_purchase_with_mdes_network_token + response = @gateway.purchase(100, @mdes_network_token, @options) + assert_success response + assert_equal 'Succeeded', response.message + assert_not_nil response.params['source']['payment_account_reference'] + end + + def test_successful_purchase_with_google_pay_visa_cryptogram_3ds_network_token + response = @gateway.purchase(100, @google_pay_visa_cryptogram_3ds_network_token, @options) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_google_pay_visa_cryptogram_3ds_network_token_via_oauth + response = @gateway_oauth.purchase(100, @google_pay_visa_cryptogram_3ds_network_token, @options) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_google_pay_master_cryptogram_3ds_network_token + response = @gateway.purchase(100, @google_pay_master_cryptogram_3ds_network_token, @options) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_google_pay_pan_only_network_token + response = @gateway.purchase(100, @google_pay_pan_only_network_token, @options) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_apple_pay_network_token + response = @gateway.purchase(100, @apple_pay_network_token, @options) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_apple_pay_network_token_via_oauth + response = @gateway_oauth.purchase(100, @apple_pay_network_token, @options) + assert_success response + assert_equal 'Succeeded', response.message + end + + # # currently, checkout does not provide any valid test card numbers for testing mada cards + # def test_successful_purchase_with_mada_card + # response = @gateway.purchase(@amount, @mada_card, @options) + # assert_success response + # assert_equal 'Succeeded', response.message + # end + def test_successful_purchase_with_additional_options response = @gateway.purchase(@amount, @credit_card, @additional_options) assert_success response assert_equal 'Succeeded', response.message end + def test_successful_purchase_with_stored_credentials + initial_options = @options.merge( + stored_credential: { + initial_transaction: true, + reason_type: 'recurring' + } + ) + initial_response = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success initial_response + assert_equal 'Succeeded', initial_response.message + assert_not_nil initial_response.params['id'] + network_transaction_id = initial_response.params['id'] + + stored_options = @options.merge( + stored_credential: { + initial_transaction: false, + reason_type: 'installment', + network_transaction_id: + } + ) + response = @gateway.purchase(@amount, @credit_card, stored_options) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_stored_credentials_via_oauth + initial_options = @options.merge( + stored_credential: { + initial_transaction: true, + reason_type: 'recurring' + } + ) + initial_response = @gateway_oauth.purchase(@amount, @credit_card, initial_options) + assert_success initial_response + assert_equal 'Succeeded', initial_response.message + assert_not_nil initial_response.params['id'] + network_transaction_id = initial_response.params['id'] + + stored_options = @options.merge( + stored_credential: { + initial_transaction: false, + reason_type: 'installment', + network_transaction_id: + } + ) + response = @gateway_oauth.purchase(@amount, @credit_card, stored_options) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_stored_credentials_merchant_initiated_transaction_id + stored_options = @options.merge( + stored_credential: { + reason_type: 'installment' + }, + merchant_initiated_transaction_id: 'pay_7emayabnrtjkhkrbohn4m2zyoa321' + ) + response = @gateway.purchase(@amount, @credit_card, stored_options) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_moto_flag + response = @gateway.authorize(@amount, @credit_card, @options.merge(transaction_indicator: 3)) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_moto_flag_via_oauth + response = @gateway_oauth.authorize(@amount, @credit_card, @options.merge(transaction_indicator: 3)) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_manual_entry_flag + response = @gateway.authorize(@amount, @credit_card, @options.merge(metadata: { manual_entry: true })) + + assert_success response + assert_equal 'Succeeded', response.message + end + def test_successful_purchase_includes_avs_result response = @gateway.purchase(@amount, @credit_card, @options) assert_success response @@ -53,6 +461,14 @@ def test_successful_purchase_includes_avs_result assert_equal 'U.S.-issuing bank does not support AVS.', response.avs_result['message'] end + def test_successful_purchase_includes_avs_result_via_oauth + response = @gateway_oauth.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Succeeded', response.message + assert_equal 'G', response.avs_result['code'] + assert_equal 'Non-U.S. issuing bank does not support AVS.', response.avs_result['message'] + end + def test_successful_authorize_includes_avs_result response = @gateway.authorize(@amount, @credit_card, @options) assert_success response @@ -68,6 +484,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 @@ -75,6 +497,41 @@ def test_successful_authorize_includes_cvv_result assert_equal 'Y', response.cvv_result['code'] end + def test_successful_authorize_includes_cvv_result_via_oauth + response = @gateway_oauth.authorize(@amount, @credit_card, @options) + assert_success response + assert_equal 'Succeeded', response.message + assert_equal 'Y', response.cvv_result['code'] + end + + def test_successful_authorize_with_estimated_type + response = @gateway.authorize(@amount, @credit_card, @options.merge({ authorization_type: 'Estimated' })) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_authorize_with_incremental_authoriation + response = @gateway_oauth.authorize(@amount, @credit_card, @options.merge({ authorization_type: 'Estimated' })) + assert_success response + assert_equal 'Succeeded', response.message + + response = @gateway_oauth.authorize(@amount, @credit_card, @options.merge({ incremental_authorization: response.authorization })) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_authorize_with_estimated_type_via_oauth + response = @gateway_oauth.authorize(@amount, @credit_card, @options.merge({ authorization_type: 'Estimated' })) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_authorize_with_processing_channel_id + response = @gateway.authorize(@amount, @credit_card, @options.merge({ processing_channel_id: 'pc_ovo75iz4hdyudnx6tu74mum3fq' })) + assert_success response + assert_equal 'Succeeded', response.message + end + def test_successful_purchase_with_descriptors options = @options.merge(descriptor_name: 'shop', descriptor_city: 'london') response = @gateway.purchase(@amount, @credit_card, options) @@ -82,14 +539,205 @@ def test_successful_purchase_with_descriptors assert_equal 'Succeeded', response.message end + def test_successful_purchase_with_descriptors_via_oauth + options = @options.merge(descriptor_name: 'shop', descriptor_city: 'london') + response = @gateway_oauth.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_metadata + options = @options.merge( + metadata: { + coupon_code: 'NY2018', + partner_id: '123989' + } + ) + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + 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_risk_data_true + options = @options.merge( + risk: { + enabled: 'true', + device_session_id: '12345-abcd' + } + ) + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_risk_data_false + options = @options.merge( + risk: { + enabled: 'false' + } + ) + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_empty_risk_data + options = @options.merge( + risk: {} + ) + response = @gateway.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: { + coupon_code: 'NY2018', + partner_id: '123989' + } + ) + response = @gateway_oauth.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Succeeded', response.message + end + def test_successful_purchase_with_minimal_options response = @gateway.purchase(@amount, @credit_card, billing_address: address) assert_success response assert_equal 'Succeeded', response.message end + def test_successful_purchase_with_shipping_address + response = @gateway.purchase(@amount, @credit_card, shipping_address: address) + assert_success response + assert_equal 'Succeeded', response.message + end + def test_successful_purchase_without_phone_number - response = @gateway.purchase(@amount, @credit_card, billing_address: address.update(phone: '')) + response = @gateway.purchase(@amount, @credit_card, billing_address: address.update(phone: nil)) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_without_name + credit_card = credit_card('4242424242424242', verification_value: '100', month: '6', year: Time.now.year + 1, first_name: nil, last_name: nil) + response = @gateway.purchase(@amount, credit_card, @options) + assert_equal response.params['source']['name'], '' assert_success response assert_equal 'Succeeded', response.message end @@ -101,21 +749,33 @@ def test_successful_purchase_with_ip end def test_failed_purchase - response = @gateway.purchase(@amount, @declined_card, @options) + response = @gateway.purchase(100, @credit_card_dnh, @options) assert_failure response assert_equal 'Invalid Card Number', response.message end + def test_failed_purchase_via_oauth + response = @gateway_oauth.purchase(100, @declined_card, @options) + assert_failure response + assert_equal 'request_invalid: card_number_invalid', response.message + end + def test_avs_failed_purchase - response = @gateway.purchase(@amount, @credit_card, billing_address: address.update(address1: 'Test_A')) + response = @gateway.purchase(@amount, @declined_card, billing_address: address.update(address1: 'Test_A')) assert_failure response - assert_equal '40111 - Street Match Only', response.message + assert_equal 'request_invalid: card_number_invalid', response.message end def test_avs_failed_authorize - response = @gateway.authorize(@amount, @credit_card, billing_address: address.update(address1: 'Test_A')) + response = @gateway.authorize(@amount, @declined_card, billing_address: address.update(address1: 'Test_A')) + assert_failure response + 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 '40111 - Street Match Only', response.message + assert_equal 'request_invalid: country_address_invalid', response.message end def test_successful_authorize_and_capture @@ -126,6 +786,30 @@ def test_successful_authorize_and_capture assert_success capture end + def test_successful_authorize_and_capture_via_oauth + auth = @gateway_oauth.authorize(@amount, @credit_card, @options) + assert_success auth + + assert capture = @gateway_oauth.capture(nil, auth.authorization) + assert_success capture + end + + def test_successful_authorize_and_partial_capture + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert capture = @gateway.capture((@amount / 2).to_i, auth.authorization, { capture_type: 'NonFinal' }) + assert_success capture + end + + def test_successful_authorize_and_partial_capture_via_oauth + auth = @gateway_oauth.authorize(@amount, @credit_card, @options) + assert_success auth + + assert capture = @gateway_oauth.capture((@amount / 2).to_i, auth.authorization, { capture_type: 'NonFinal' }) + assert_success capture + end + def test_successful_authorize_and_capture_with_additional_options auth = @gateway.authorize(@amount, @credit_card, @additional_options) assert_success auth @@ -134,16 +818,78 @@ def test_successful_authorize_and_capture_with_additional_options assert_success capture end + def test_successful_authorize_and_capture_with_3ds + auth = @gateway.authorize(@amount, @credit_card, @additional_options_3ds) + assert_success auth + + assert capture = @gateway.capture(nil, auth.authorization) + assert_success capture + end + + def test_successful_authorize_and_capture_with_3ds_via_oauth + auth = @gateway_oauth.authorize(@amount, @credit_card, @additional_options_3ds) + assert_success auth + + assert capture = @gateway_oauth.capture(nil, auth.authorization) + assert_success capture + end + + def test_successful_authorize_and_capture_with_3ds2 + auth = @gateway.authorize(@amount, @credit_card, @additional_options_3ds2) + assert_success auth + + assert capture = @gateway.capture(nil, auth.authorization) + assert_success capture + end + + def test_successful_authorize_and_capture_with_3ds2_via_oauth + auth = @gateway_oauth.authorize(@amount, @credit_card, @additional_options_3ds2) + assert_success auth + + assert capture = @gateway_oauth.capture(nil, auth.authorization) + assert_success capture + end + + def test_successful_authorize_and_capture_with_metadata + options = @options.merge( + metadata: { + coupon_code: 'NY2018', + partner_id: '123989' + } + ) + + auth = @gateway.authorize(@amount, @credit_card, options) + assert_success auth + + assert capture = @gateway.capture(nil, auth.authorization) + assert_success capture + end + + def test_direct_3ds_authorize + auth = @gateway.authorize(@amount, @threeds_card, @options.merge(execute_threed: true)) + + assert_equal 'Pending', auth.message + assert_equal 'Y', auth.params['3ds']['enrolled'] + assert auth.params['_links']['redirect'] + end + def test_failed_authorize - response = @gateway.authorize(@amount, @declined_card, @options) + response = @gateway.authorize(12314, @declined_card, @options) assert_failure response + assert_equal 'request_invalid: card_number_invalid', response.message + end + + def test_failed_authorize_via_oauth + response = @gateway_oauth.authorize(12314, @declined_card, @options) + assert_failure response + assert_equal 'request_invalid: card_number_invalid', response.message end def test_partial_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture end @@ -152,10 +898,170 @@ def test_failed_capture assert_failure response end + def test_failed_capture_via_oauth + response = @gateway_oauth.capture(nil, '') + assert_failure response + end + + def test_successful_credit + @credit_card.first_name = 'John' + @credit_card.last_name = 'Doe' + response = @gateway_oauth.credit(@amount, @credit_card, @options.merge({ source_type: 'currency_account', source_id: 'ca_spwmped4qmqenai7hcghquqle4', account_holder_type: 'individual' })) + assert_success response + assert_equal 'Succeeded', response.message + assert_equal true, response.primary_response.pending + 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 + assert_equal true, response.primary_response.pending + 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_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 + sleep 1 + + assert refund = @gateway.refund(@amount, purchase.authorization) + assert_success refund + end + + def test_successful_refund_via_oauth + purchase = @gateway_oauth.purchase(@amount, @credit_card, @options) + assert_success purchase + + sleep 1 + + assert refund = @gateway_oauth.refund(@amount, purchase.authorization) + assert_success refund + end + + def test_successful_refund_with_metadata + options = @options.merge( + metadata: { + coupon_code: 'NY2018', + partner_id: '123989' + } + ) + + purchase = @gateway.purchase(@amount, @credit_card, options) + assert_success purchase + + sleep 1 + assert refund = @gateway.refund(@amount, purchase.authorization) assert_success refund end @@ -164,7 +1070,9 @@ def test_partial_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount-1, purchase.authorization) + sleep 1 + + assert refund = @gateway.refund(@amount - 1, purchase.authorization) assert_success refund end @@ -173,6 +1081,11 @@ def test_failed_refund assert_failure response end + def test_failed_refund_via_oauth + response = @gateway_oauth.refund(nil, '') + assert_failure response + end + def test_successful_void auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth @@ -181,28 +1094,156 @@ 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_type: 'id')) + assert_success response + assert_success verify + end + + def test_successful_void_via_oauth + auth = @gateway_oauth.authorize(@amount, @credit_card, @options) + assert_success auth + + assert void = @gateway_oauth.void(auth.authorization) + assert_success void + end + + def test_successful_void_with_metadata + options = @options.merge( + metadata: { + coupon_code: 'NY2018', + partner_id: '123989' + } + ) + + 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('') assert_failure response end + def test_failed_void_via_oauth + response = @gateway_oauth.void('') + assert_failure response + end + def test_successful_verify response = @gateway.verify(@credit_card, @options) assert_success response assert_match %r{Succeeded}, response.message end + def test_successful_verify_via_oauth + response = @gateway_oauth.verify(@credit_card, @options) + 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 response = @gateway.verify(@declined_card, @options) assert_failure response - assert_match %r{Invalid Card Number}, response.message - assert_equal Gateway::STANDARD_ERROR_CODE[:invalid_number], response.error_code + assert_match %r{request_invalid: card_number_invalid}, response.message end def test_expired_card_returns_error_code response = @gateway.purchase(@amount, @expired_card, @options) assert_failure response - assert_equal 'Validation error: Expired Card', response.message - assert_equal '70000: 70077', response.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 + + # checkout states they provide valid amex cards however, they will fail + # a transaction with either CVV mismatch or invalid card error. For + # the purpose of this test, it's to simulate the truncation of reference id + def test_truncate_id_for_amex_transactions + @options[:order_id] = '1111111111111111111111111111112' + + response = @gateway.purchase(@amount, @amex_card, @options) + assert_failure response + assert_equal '111111111111111111111111111111', response.params['reference'] + assert_equal 30, response.params['reference'].length + assert_equal 'American Express', response.params['source']['scheme'] + end + + def test_non_truncate_id_for_non_amex_transactions + @options[:order_id] = '1111111111111111111111111111112' + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal '1111111111111111111111111111112', response.params['reference'] + assert_equal 31, response.params['reference'].length + assert_equal 'Visa', response.params['source']['scheme'] + end + + def test_successful_purchase_with_minimun_level_2_data + response = @gateway.purchase(@amount, @credit_card, @minimun_level2_options) + assert_success response + end + + def test_successful_authorize_and_capture_with_minimun_level_2_data + authorize = @gateway.authorize(@amount, @credit_card, @options) + assert_success authorize + + response = @gateway.capture(@amount, authorize.authorization, @minimun_level2_options) + assert_success response + end + + def test_successful_purchase_with_minimun_level_2_data_for_amex + amex_options = { + currency: 'USD', + line_items: [{ item_name: 'Paint', quantity: '1', unit_cost: '1270' }] + } + amex_card = credit_card('345678901234564', brand: 'american_express', verification_value: '1000', month: '12', year: Time.now.year) + + response = @gateway.purchase(1500, amex_card, @minimun_level2_options.merge(amex_options)) + assert_success response + end + + def test_successful_purchase_with_minimun_level_3_data + order_data = { + processing: { order_id: '01234' }, + tax_number: '123456', + from_address_zip: '000123456', + tax_amount: 30, + discount_amount: 0, + shipping_amount: 200, + duty_amount: 0 + } + line_items = { + line_items: [ + { + commodity_code: '123', + name: 'Paint', + quantity: 1, + unit_price: 1270, + tax_amount: 30, + discount_amount: 0, + total_amount: 1270, + reference: 'Paint123', + unit_of_measure: 'Liters' + } + ] + } + options = @options.merge({ currency: 'USD' }).merge(order_data).merge(line_items) + + response = @gateway.purchase(1500, @credit_card, options) + assert_success response end end diff --git a/test/remote/gateways/remote_citrus_pay_test.rb b/test/remote/gateways/remote_citrus_pay_test.rb index 1acf5ed9a4b..cfb4711d02a 100644 --- a/test/remote/gateways/remote_citrus_pay_test.rb +++ b/test/remote/gateways/remote_citrus_pay_test.rb @@ -34,7 +34,7 @@ def test_successful_purchase_sans_options def test_successful_purchase_with_more_options more_options = @options.merge({ ip: '127.0.0.1', - email: 'joe@example.com', + email: 'joe@example.com' }) assert response = @gateway.purchase(@amount, @credit_card, @options.merge(more_options)) @@ -56,7 +56,7 @@ def test_adds_3dsecure_id_to_authorize def test_failed_purchase assert response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response - assert_match /FAILURE/, response.message + assert_match %r{FAILURE}, response.message end def test_successful_authorize_and_capture @@ -73,7 +73,7 @@ def test_successful_authorize_and_capture def test_failed_authorize assert response = @gateway.authorize(@amount, @declined_card, @options) assert_failure response - assert_match /FAILURE/, response.message + assert_match(/FAILURE/, response.message) end def test_successful_refund @@ -104,9 +104,9 @@ def test_successful_verify def test_invalid_login gateway = CitrusPayGateway.new( - :userid => 'nosuch', - :password => 'thing' - ) + userid: 'nosuch', + password: 'thing' + ) response = gateway.authorize(@amount, @credit_card, @options) assert_failure response assert_equal 'ERROR - INVALID_REQUEST - Invalid credentials.', response.message @@ -130,5 +130,4 @@ def test_verify_credentials gateway = CitrusPayGateway.new(userid: 'unknown', password: 'unknown') assert !gateway.verify_credentials end - end diff --git a/test/remote/gateways/remote_clearhaus_test.rb b/test/remote/gateways/remote_clearhaus_test.rb index 9a716294dcb..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' @@ -74,10 +74,17 @@ def test_successful_purchase_with_text_on_statement assert_equal 'Approved', response.message end + def test_successful_purchase_and_amount_for_non_decimal_currency + response = @gateway.purchase(14200, @credit_card, @options.merge(currency: 'JPY')) + assert_success response + assert_equal 142, response.params['amount'] + assert_equal 'Approved', response.message + end + def test_successful_purchase_with_more_options options = { order_id: '1', - ip: '127.0.0.1', + ip: '127.0.0.1' } response = @gateway.purchase(@amount, @credit_card, @options.merge(options)) @@ -113,7 +120,7 @@ def test_partial_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture end @@ -136,7 +143,7 @@ def test_partial_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount-1, purchase.authorization) + assert refund = @gateway.refund(@amount - 1, purchase.authorization) assert_success refund end @@ -184,7 +191,7 @@ def test_failed_verify end def test_successful_authorize_with_nonfractional_currency - assert response = @gateway.authorize(100, @credit_card, @options.merge(:currency => 'KRW')) + assert response = @gateway.authorize(100, @credit_card, @options.merge(currency: 'KRW')) assert_equal 1, response.params['amount'] assert_success response end diff --git a/test/remote/gateways/remote_commerce_hub_test.rb b/test/remote/gateways/remote_commerce_hub_test.rb new file mode 100644 index 00000000000..ad6aadcb814 --- /dev/null +++ b/test/remote/gateways/remote_commerce_hub_test.rb @@ -0,0 +1,407 @@ +require 'test_helper' + +class RemoteCommerceHubTest < Test::Unit::TestCase + def setup + # Uncomment the sleep if you want to run the entire set of remote tests without + # getting 'The transaction limit was exceeded. Please try again!' errors + # sleep 10 + + @gateway = CommerceHubGateway.new(fixtures(:commerce_hub)) + + @amount = 1204 + @credit_card = credit_card('4005550000000019', month: '02', year: '2035', verification_value: '123', first_name: 'John', last_name: 'Doe') + @google_pay = network_tokenization_credit_card( + '4005550000000019', + brand: 'visa', + eci: '05', + month: '02', + year: '2035', + source: :google_pay, + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + ) + @apple_pay = network_tokenization_credit_card( + '4005550000000019', + brand: 'visa', + eci: '05', + month: '02', + year: '2035', + source: :apple_pay, + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + ) + @declined_apple_pay = network_tokenization_credit_card( + '4000300011112220', + brand: 'visa', + eci: '05', + month: '02', + year: '2035', + source: :apple_pay, + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + ) + @network_token = network_tokenization_credit_card( + '4005550000000019', + brand: 'visa', + eci: '05', + month: '02', + year: '2035', + source: :network_token, + 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', + three_ds_server_trans_id: 'df8b9557-e41b-4e17-87e9-2328694a2ea0' + } + @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 + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_payment_name_override + billing_address = { + address1: 'Infinite Loop', + address2: 1, + country: 'US', + city: 'Cupertino', + state: 'CA', + zip: '95014' + } + + response = @gateway.purchase(@amount, @credit_card, @options.merge(billing_address:)) + assert_success response + assert_equal 'Approved', response.message + assert_equal 'John', response.params['billingAddress']['firstName'] + assert_equal 'Doe', response.params['billingAddress']['lastName'] + end + + def test_successful_purchase_with_name_override_on_alternative_payment_methods + billing_address = { + address1: 'Infinite Loop', + address2: 1, + country: 'US', + city: 'Cupertino', + state: 'CA', + zip: '95014' + } + + response = @gateway.purchase(@amount, @google_pay, @options.merge(billing_address:)) + assert_success response + assert_equal 'Approved', response.message + assert_equal 'DecryptedWallet', response.params['source']['sourceType'] + assert_equal 'Longbob', response.params['billingAddress']['firstName'] + assert_equal 'Longsen', response.params['billingAddress']['lastName'] + end + + def test_successful_purchase_with_billing_name_override + response = @gateway.purchase(@amount, @credit_card, @options.merge(billing_address: address)) + assert_success response + assert_equal 'Approved', response.message + assert_equal 'Jim', response.params['billingAddress']['firstName'] + assert_equal 'Smith', response.params['billingAddress']['lastName'] + 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 + assert_equal response.params['additionalData3DS']['serverTransactionId'], @three_d_secure[:three_ds_server_trans_id] + 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_equal 'Y', response.avs_result['code'] + end + + def test_successful_purchase_with_billing_and_shipping + response = @gateway.purchase(@amount, @credit_card, @options.merge({ billing_address: address, shipping_address: address })) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_stored_credential_framework + stored_credential_options = { + initial_transaction: true, + reason_type: 'recurring', + initiator: 'merchant' + } + first_response = @gateway.purchase(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_options })) + assert_success first_response + + ntxid = first_response.params['transactionDetails']['retrievalReferenceNumber'] + stored_credential_options = { + initial_transaction: false, + reason_type: 'recurring', + initiator: 'merchant', + network_transaction_id: ntxid + } + response = @gateway.purchase(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_options })) + assert_success response + end + + def test_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_match 'Unable to assign card to brand: Invalid', response.message + assert_equal '104', response.error_code + end + + def test_successful_authorize + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_authorize_and_capture + authorize = @gateway.authorize(@amount, @credit_card, @options) + assert_success authorize + + capture = @gateway.capture(@amount, authorize.authorization) + assert_success capture + end + + def test_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_match 'Unable to assign card to brand: Invalid', response.message + end + + def test_successful_authorize_and_void + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + + response = @gateway.void(response.authorization, @options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_failed_void + response = @gateway.void('123', @options) + assert_failure response + assert_equal 'Referenced transaction is invalid or not found', response.message + end + + def test_successful_verify + response = @gateway.verify(@credit_card, @options) + assert_success response + assert_equal 'VERIFIED', response.message + end + + def test_successful_verify_with_address + @options[:billing_address] = { + address1: '112 Main St.', + city: 'Atlanta', + state: 'GA', + zip: '30301', + country: 'US' + } + + response = @gateway.verify(@credit_card, @options) + + assert_success response + assert_equal 'VERIFIED', response.message + end + + def test_failed_verify + response = @gateway.verify(@declined_card, @options) + + assert_failure response + end + + def test_successful_purchase_and_refund + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + + response = @gateway.refund(nil, response.authorization, @options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_and_partial_refund + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + + response = @gateway.refund(@amount - 1, response.authorization, @options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_failed_refund + response = @gateway.refund(nil, 'abc123|123', @options) + assert_failure response + assert_equal 'Referenced transaction is invalid or not found', response.message + end + + def test_successful_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 + assert_equal 'TOKENIZE', response.message + end + + def test_successful_store_with_purchase + response = @gateway.store(@credit_card, @options) + assert_success response + assert_equal 'TOKENIZE', response.message + + response = @gateway.purchase(@amount, response.authorization, @options) + assert_success response + end + + def test_successful_purchase_with_google_pay + response = @gateway.purchase(@amount, @google_pay, @options) + assert_success response + assert_equal 'Approved', response.message + assert_equal 'DecryptedWallet', response.params['source']['sourceType'] + end + + def test_successful_purchase_with_apple_pay + response = @gateway.purchase(@amount, @apple_pay, @options) + assert_success response + assert_equal 'Approved', response.message + assert_equal 'DecryptedWallet', response.params['source']['sourceType'] + end + + def test_successful_purchase_with_network_token + response = @gateway.purchase(@amount, @network_token, @options) + assert_success response + assert_equal 'Approved', response.message + assert_equal 'PaymentToken', response.params['source']['sourceType'] + end + + def test_failed_purchase_with_declined_apple_pay + response = @gateway.purchase(@amount, @declined_apple_pay, @options) + assert_failure response + assert_match 'Unable to assign card to brand: Invalid', response.message + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @credit_card, @options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@credit_card.number, transcript) + assert_scrubbed(@gateway.options[:api_key], transcript) + assert_scrubbed(@gateway.options[:api_secret], transcript) + end + + def test_transcript_scrubbing_apple_pay + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @apple_pay, @options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@apple_pay.number, transcript) + assert_scrubbed(@gateway.options[:api_key], transcript) + assert_scrubbed(@gateway.options[:api_secret], transcript) + assert_scrubbed(@apple_pay.payment_cryptogram, transcript) + end + + def test_successful_purchase_with_encrypted_credit_card + @options[:encryption_data] = { + keyId: '6d0b6b63-3658-4c90-b7a4-bffb8a928288', + encryptionType: 'RSA', + encryptionBlock: 'udJ89RebrHLVxa3ofdyiQ/RrF2Y4xKC/qw4NuV1JYrTDEpNeIq9ZimVffMjgkyKL8dlnB2R73XFtWA4klHrpn6LZrRumYCgoqAkBRJCrk09+pE5km2t2LvKtf/Bj2goYQNFA9WLCCvNGwhofp8bNfm2vfGsBr2BkgL+PH/M4SqyRHz0KGKW/NdQ4Mbdh4hLccFsPjtDnNidkMep0P02PH3Se6hp1f5GLkLTbIvDLPSuLa4eNgzb5/hBBxrq5M5+5n9a1PhQnVT1vPU0WbbWe1SGdGiVCeSYmmX7n+KkVmc1lw0dD7NXBjKmD6aGFAWGU/ls+7JVydedDiuz4E7HSDQ==', + encryptionBlockFields: 'card.cardData:16,card.nameOnCard:10,card.expirationMonth:2,card.expirationYear:4,card.securityCode:3', + encryptionTarget: 'MANUAL' + } + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + end +end diff --git a/test/remote/gateways/remote_commercegate_test.rb b/test/remote/gateways/remote_commercegate_test.rb index 3ca37b555c2..95dd2af370c 100644 --- a/test/remote/gateways/remote_commercegate_test.rb +++ b/test/remote/gateways/remote_commercegate_test.rb @@ -7,11 +7,11 @@ def setup @amount = 1000 @options = { - address: address + address: } @credit_card = credit_card(fixtures(:commercegate)[:card_number]) - @expired_credit_card = credit_card(fixtures(:commercegate)[:card_number], year: Time.now.year-1) + @expired_credit_card = credit_card(fixtures(:commercegate)[:card_number], year: Time.now.year - 1) end def test_successful_authorize diff --git a/test/remote/gateways/remote_conekta_test.rb b/test/remote/gateways/remote_conekta_test.rb index c466b8ad74c..4f04fd19aa2 100644 --- a/test/remote/gateways/remote_conekta_test.rb +++ b/test/remote/gateways/remote_conekta_test.rb @@ -9,8 +9,8 @@ def setup @credit_card = ActiveMerchant::Billing::CreditCard.new( number: '4242424242424242', verification_value: '183', - month: '01', - year: '2019', + month: '12', + year: Date.today.year + 2, first_name: 'Mario F.', last_name: 'Moreno Reyes' ) @@ -25,7 +25,7 @@ def setup ) @options = { - :device_fingerprint => '41l9l92hjco6cuekf0c7dq68v4', + device_fingerprint: '41l9l92hjco6cuekf0c7dq68v4', description: 'Blue clip', billing_address: { address1: 'Rio Missisipi #123', @@ -34,18 +34,18 @@ def setup country: 'Mexico', zip: '5555', name: 'Mario Reyes', - phone: '12345678', + phone: '12345678' }, carrier: 'Estafeta', email: 'bob@something.com', line_items: [{ - name: 'Box of Cohiba S1s', - description: 'Imported From Mex.', - unit_price: 20000, - quantity: 1, - sku: '7500244909', - type: 'food' - }] + name: 'Box of Cohiba S1s', + description: 'Imported From Mex.', + unit_price: 20000, + quantity: 1, + sku: '7500244909', + type: 'food' + }] } end @@ -56,11 +56,16 @@ def test_successful_purchase end def test_successful_purchase_with_installments - assert response = @gateway.purchase(@amount * 300, @credit_card, @options.merge({monthly_installments: 3})) + assert response = @gateway.purchase(@amount * 300, @credit_card, @options.merge({ monthly_installments: 3 })) assert_success response assert_equal nil, response.message end + def test_unsuccessful_purchase_with_not_supported_currency + assert response = @gateway.purchase(8000, @credit_card, @options.merge({ currency: 'COP' })) + assert_equal 'At this time we process only Mexican pesos or U.S. dollars.', response.params['message'] + end + def test_unsuccessful_purchase assert response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response @@ -143,7 +148,7 @@ def test_successful_purchase_passing_more_details city: 'Wanaque', state: 'NJ', country: 'USA', - zip: '01085', + zip: '01085' }, line_items: [ { @@ -172,12 +177,6 @@ def test_successful_purchase_passing_more_details assert_equal 'Guerrero', response.params['details']['billing_address']['city'] end - def test_failed_purchase_with_no_details - assert response = @gateway.purchase(@amount, @credit_card, {}) - assert_failure response - assert_equal 'Falta el correo del comprador.', response.message - end - def test_invalid_key gateway = ConektaGateway.new(key: 'invalid_token') assert response = gateway.purchase(@amount, @credit_card, @options) diff --git a/test/remote/gateways/remote_creditcall_test.rb b/test/remote/gateways/remote_creditcall_test.rb index d84c8695f10..67669780996 100644 --- a/test/remote/gateways/remote_creditcall_test.rb +++ b/test/remote/gateways/remote_creditcall_test.rb @@ -89,7 +89,7 @@ def test_partial_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture end @@ -112,7 +112,7 @@ def test_partial_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount-1, purchase.authorization) + assert refund = @gateway.refund(@amount - 1, purchase.authorization) assert_success refund end @@ -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 @@ -167,6 +167,5 @@ def test_transcript_scrubbing assert_scrubbed(@credit_card.number, transcript) assert_scrubbed(@credit_card.verification_value, transcript) assert_scrubbed(@gateway.options[:transaction_key], transcript) - end end diff --git a/test/remote/gateways/remote_credorax_test.rb b/test/remote/gateways/remote_credorax_test.rb index 52f611fa200..a00385b3042 100644 --- a/test/remote/gateways/remote_credorax_test.rb +++ b/test/remote/gateways/remote_credorax_test.rb @@ -5,14 +5,119 @@ def setup @gateway = CredoraxGateway.new(fixtures(:credorax)) @amount = 100 - @credit_card = credit_card('4176661000001015', verification_value: '281', month: '12', year: '2022') - @declined_card = credit_card('4176661000001111', verification_value: '681', month: '12', year: '2022') + @adviser_amount = 1000001 + @credit_card = credit_card('4176661000001015', verification_value: '281', month: '12') + @fully_auth_card = credit_card('5223450000000007', brand: 'mastercard', verification_value: '090', month: '12') + @declined_card = credit_card('4176661000001111', verification_value: '681', month: '12') + @three_ds_card = credit_card('5455330200000016', verification_value: '737', month: '10', year: Time.now.year + 2) + @inquiry_match_card = credit_card('4123560000000072') + @inquiry_no_match_card = credit_card('4123560000000429') + @inquiry_unverified_card = credit_card('4176660000000266') + @address = { + name: 'Jon Smith', + address1: '123 Your Street', + address2: 'Apt 2', + city: 'Toronto', + state: 'ON', + zip: 'K2C3N7', + country: 'CA', + phone_number: '(123)456-7890' + } @options = { order_id: '1', currency: 'EUR', - billing_address: address, + billing_address: @address, description: 'Store Purchase' } + @normalized_3ds_2_options = { + reference: '345123', + shopper_email: 'john.smith@test.com', + shopper_ip: '77.110.174.153', + shopper_reference: 'John Smith', + billing_address: @address, + shipping_address: @address, + order_id: '123', + execute_threed: true, + three_ds_version: '2', + three_ds_challenge_window_size: '01', + three_ds_reqchallengeind: '04', + stored_credential: { reason_type: 'unscheduled' }, + three_ds_2: { + channel: 'browser', + notification_url: 'www.example.com', + browser_info: { + accept_header: 'unknown', + depth: 24, + java: false, + language: 'US', + height: 1000, + width: 500, + timezone: '-120', + user_agent: 'unknown' + } + } + } + + @apple_pay_card = network_tokenization_credit_card( + '4176661000001015', + month: 10, + year: Time.new.year + 2, + first_name: 'John', + last_name: 'Smith', + verification_value: '737', + payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', + eci: '07', + transaction_id: 'abc123', + source: :apple_pay + ) + + @google_pay_card = network_tokenization_credit_card( + '4176661000001015', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + month: '01', + year: Time.new.year + 2, + source: :google_pay, + transaction_id: '123456789', + eci: '07' + ) + + @nt_credit_card = network_tokenization_credit_card( + '4176661000001015', + brand: 'visa', + source: :network_token, + payment_cryptogram: 'AgAAAAAAosVKVV7FplLgQRYAAAA=' + ) + end + + def test_successful_purchase_with_apple_pay + response = @gateway.purchase(@amount, @apple_pay_card, @options) + assert_success response + assert_equal '1', response.params['H9'] + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_google_pay + response = @gateway.purchase(@amount, @google_pay_card, @options) + assert_success response + assert_equal '1', response.params['H9'] + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_network_token + response = @gateway.purchase(@amount, @nt_credit_card, @options) + assert_success response + assert_equal '1', response.params['H9'] + assert_equal 'Succeeded', response.message + end + + def test_transcript_scrubbing_network_tokenization_card + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @apple_pay_card, @options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@apple_pay_card.number, transcript) + assert_scrubbed(@apple_pay_card.payment_cryptogram, transcript) end def test_invalid_login @@ -28,6 +133,20 @@ def test_successful_purchase assert_equal 'Succeeded', response.message end + def test_successful_purchase_and_amount_for_non_decimal_currency + response = @gateway.purchase(14200, @credit_card, @options.merge(currency: 'JPY')) + assert_success response + assert_equal '142', response.params['A4'] + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_and_amount_for_isk + response = @gateway.purchase(14200, @credit_card, @options.merge(currency: 'ISK')) + assert_success response + assert_equal '142', response.params['A4'] + assert_equal 'Succeeded', response.message + end + def test_successful_purchase_with_extra_options response = @gateway.purchase(@amount, @credit_card, @options.merge(transaction_type: '10')) assert_success response @@ -35,12 +154,171 @@ def test_successful_purchase_with_extra_options assert_equal 'Succeeded', response.message end + def test_successful_purchase_with_aft_fields + aft_options = @options.merge( + aft: true, + sender_ref_number: 'test', + sender_fund_source: '01', + sender_country_code: 'USA', + sender_street_address: 'sender street', + sender_city: 'city', + sender_state: 'NY', + sender_first_name: 'george', + sender_last_name: 'smith', + recipient_street_address: 'street', + recipient_postal_code: '12345', + recipient_city: 'chicago', + recipient_province_code: '312', + recipient_country_code: 'USA', + recipient_first_name: 'logan', + recipient_last_name: 'bill' + ) + + response = @gateway.purchase(@amount, @credit_card, aft_options) + assert_success response + assert_equal '1', response.params['H9'] + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_auth_data_via_3ds1_fields + options = @options.merge( + eci: '02', + cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA=', + xid: '00000000000000000501', + # Having processor-specification enabled in Credorax test account causes 3DS tests to fail without a r1 (processor) parameter. + processor: 'CREDORAX' + ) + + response = @gateway.purchase(@amount, @fully_auth_card, options) + assert_success response + assert_equal '1', response.params['H9'] + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_auth_data_via_3ds1_fields_passing_3ds_version + options = @options.merge( + eci: '02', + cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA=', + xid: '00000000000000000501', + # Having processor-specification enabled in Credorax test account causes 3DS tests to fail without a r1 (processor) parameter. + processor: 'CREDORAX', + three_ds_version: '1.0.2' + ) + + response = @gateway.purchase(@amount, @fully_auth_card, options) + assert_success response + assert_equal '1', response.params['H9'] + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_auth_data_via_normalized_3ds1_options + version = '1.0.2' + eci = '02' + cavv = 'jJ81HADVRtXfCBATEp01CJUAAAA=' + xid = '00000000000000000501' + + options = @options.merge( + three_d_secure: { + version:, + eci:, + cavv:, + xid: + }, + # Having processor-specification enabled in Credorax test account causes 3DS tests to fail without a r1 (processor) parameter. + processor: 'CREDORAX' + ) + + response = @gateway.purchase(@amount, @fully_auth_card, options) + assert_success response + assert_equal '1', response.params['H9'] + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_3ds2_fields + options = @options.merge(@normalized_3ds_2_options) + response = @gateway.purchase(@amount, @three_ds_card, options) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_3ds_adviser + threeds_options = @options.merge(@normalized_3ds_2_options) + options = threeds_options.merge(three_ds_initiate: '03', f23: '1') + response = @gateway.purchase(@adviser_amount, @three_ds_card, options) + assert_success response + assert_equal 'Succeeded', response.message + assert_equal '01', response.params['SMART_3DS_RESULT'] + end + + def test_successful_moto_purchase + response = @gateway.purchase(@amount, @three_ds_card, @options.merge(metadata: { manual_entry: true })) + assert_success response + assert_equal '1', response.params['H9'] + assert_equal '3', response.params['A2'] + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_auth_data_via_normalized_3ds2_options + version = '2.2.0' + eci = '02' + cavv = 'jJ81HADVRtXfCBATEp01CJUAAAA=' + ds_transaction_id = '97267598-FAE6-48F2-8083-C23433990FBC' + options = @options.merge( + three_d_secure: { + version:, + eci:, + cavv:, + ds_transaction_id: + }, + # Having processor-specification enabled in Credorax test account causes 3DS tests to fail without a r1 (processor) parameter. + processor: 'CREDORAX' + ) + + response = @gateway.purchase(@amount, @fully_auth_card, options) + assert_success response + assert_equal '1', response.params['H9'] + assert_equal 'Succeeded', response.message + end + def test_failed_purchase response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response assert_equal 'Transaction not allowed for cardholder', response.message end + def test_failed_purchase_invalid_auth_data_via_3ds1_fields + options = @options.merge( + eci: '02', + cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA=', + xid: 'this is not a valid xid, it will be rejected' + ) + + response = @gateway.purchase(@amount, @fully_auth_card, options) + assert_failure response + assert_equal '-9', response.params['Z2'] + assert_match 'Parameter i8 is invalid', response.message + end + + def test_failed_purchase_invalid_auth_data_via_normalized_3ds2_options + version = '2.0' + eci = '02' + cavv = 'BOGUS;:' + ds_transaction_id = '97267598-FAE6-48F2-8083-C23433990FBC' + options = @options.merge( + three_d_secure: { + version:, + eci:, + cavv:, + ds_transaction_id: + } + ) + + response = @gateway.purchase(@amount, @fully_auth_card, options) + assert_failure response + assert_equal '-9', response.params['Z2'] + assert_match 'malformed', response.message + end + def test_successful_authorize_and_capture response = @gateway.authorize(@amount, @credit_card, @options) assert_success response @@ -52,6 +330,85 @@ def test_successful_authorize_and_capture assert_equal 'Succeeded', capture.message end + def test_successful_authorize_with_authorization_details + options_with_auth_details = @options.merge({ authorization_type: '2', multiple_capture_count: '5' }) + response = @gateway.authorize(@amount, @credit_card, options_with_auth_details) + assert_success response + assert_equal 'Succeeded', response.message + assert response.authorization + end + + def test_successful_zero_authorize_with_name_inquiry_match + extra_options = @options.merge({ account_name_inquiry: true, first_name: 'Art', last_name: 'Vandelay' }) + response = @gateway.authorize(0, @inquiry_match_card, extra_options) + assert_success response + assert_equal 'Succeeded', response.message + assert_equal '2', response.params['O'] + assert_equal 'A', response.params['Z26'] + assert_equal 'A', response.params['Z27'] + assert_equal 'A', response.params['Z28'] + assert response.authorization + end + + def test_successful_zero_authorize_with_name_inquiry_no_match + extra_options = @options.merge({ account_name_inquiry: true, first_name: 'Art', last_name: 'Vandelay' }) + response = @gateway.authorize(0, @inquiry_no_match_card, extra_options) + assert_success response + assert_equal 'Succeeded', response.message + assert_equal '2', response.params['O'] + assert_equal 'C', response.params['Z26'] + assert_equal 'C', response.params['Z27'] + assert_equal 'C', response.params['Z28'] + assert response.authorization + end + + def test_successful_zero_authorize_with_name_inquiry_unverified + extra_options = @options.merge({ account_name_inquiry: true, first_name: 'Art', last_name: 'Vandelay' }) + response = @gateway.authorize(0, @inquiry_unverified_card, extra_options) + assert_success response + assert_equal 'Succeeded', response.message + assert_equal '2', response.params['O'] + assert_equal 'U', response.params['Z26'] + assert response.authorization + end + + def test_successful_authorize_with_auth_data_via_3ds1_fields + options = @options.merge( + eci: '02', + cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA=', + xid: '00000000000000000501', + # Having processor-specification enabled in Credorax test account causes 3DS tests to fail without a r1 (processor) parameter. + processor: 'CREDORAX' + ) + + response = @gateway.authorize(@amount, @fully_auth_card, options) + assert_success response + assert_equal 'Succeeded', response.message + assert response.authorization + end + + def test_successful_authorize_with_auth_data_via_normalized_3ds2_options + version = '2.0' + eci = '02' + cavv = 'jJ81HADVRtXfCBATEp01CJUAAAA=' + ds_transaction_id = '97267598-FAE6-48F2-8083-C23433990FBC' + options = @options.merge( + three_d_secure: { + version:, + eci:, + cavv:, + ds_transaction_id: + }, + # Having processor-specification enabled in Credorax test account causes 3DS tests to fail without a r1 (processor) parameter. + processor: 'CREDORAX' + ) + + response = @gateway.authorize(@amount, @fully_auth_card, options) + assert_success response + assert_equal 'Succeeded', response.message + assert response.authorization + end + def test_failed_authorize response = @gateway.authorize(@amount, @declined_card, @options) assert_failure response @@ -64,7 +421,7 @@ def test_failed_capture capture = @gateway.capture(0, auth.authorization) assert_failure capture - assert_equal 'Invalid amount', capture.message + assert_equal 'System malfunction', capture.message end def test_successful_purchase_and_void @@ -115,6 +472,22 @@ def test_successful_refund assert_equal 'Succeeded', refund.message end + def test_successful_refund_with_recipient_fields + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + + refund_options = { + recipient_street_address: 'street', + recipient_city: 'chicago', + recipient_province_code: '312', + recipient_country_code: 'USA' + } + + refund = @gateway.refund(@amount, response.authorization, refund_options) + assert_success refund + assert_equal 'Succeeded', refund.message + end + def test_successful_refund_and_void response = @gateway.purchase(@amount, @credit_card, @options) assert_success response @@ -134,16 +507,55 @@ def test_failed_refund assert_equal 'Referred to transaction has not been found.', response.message end + def test_successful_referral_cft + options = @options.merge(@normalized_3ds_2_options) + response = @gateway.purchase(@amount, @three_ds_card, options) + assert_success response + assert_equal 'Succeeded', response.message + + cft_options = { referral_cft: true, email: 'john.smith@test.com' } + referral_cft = @gateway.refund(@amount, response.authorization, cft_options) + assert_success referral_cft + assert_equal 'Succeeded', referral_cft.message + # Confirm that the operation code was `referral_cft` + assert_equal '34', referral_cft.params['O'] + end + + def test_successful_referral_cft_with_first_and_last_name + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Succeeded', response.message + + cft_options = { referral_cft: true, email: 'john.smith@test.com', first_name: 'John', last_name: 'Smith' } + referral_cft = @gateway.refund(@amount, response.authorization, cft_options) + assert_success referral_cft + assert_equal 'Succeeded', referral_cft.message + # Confirm that the operation code was `referral_cft` + assert_equal '34', referral_cft.params['O'] + end + + def test_failed_referral_cft + options = @options.merge(@normalized_3ds_2_options) + response = @gateway.purchase(@amount, @three_ds_card, options) + assert_success response + assert_equal 'Succeeded', response.message + + cft_options = { referral_cft: true, email: 'john.smith@test.com' } + referral_cft = @gateway.refund(@amount, '123;123;123', cft_options) + assert_failure referral_cft + assert_equal 'Referred to transaction has not been found.', referral_cft.message + end + def test_successful_credit - response = @gateway.credit(@amount, @credit_card, @options) + response = @gateway.credit(@amount, @credit_card, @options.merge(first_name: 'Test', last_name: 'McTest')) assert_success response assert_equal 'Succeeded', response.message end - def test_failed_credit + def test_failed_credit_with_zero_amount response = @gateway.credit(0, @declined_card, @options) assert_failure response - assert_equal 'Invalid amount', response.message + assert_equal 'Transaction not allowed for cardholder', response.message end def test_successful_verify @@ -158,6 +570,107 @@ def test_failed_verify assert_equal 'Transaction not allowed for cardholder', response.message end + def test_purchase_using_stored_credential_recurring_cit + initial_options = stored_credential_options(:cardholder, :recurring, :initial) + assert purchase = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success purchase + assert_equal '9', purchase.params['A9'] + assert network_transaction_id = purchase.params['Z13'] + + used_options = stored_credential_options(:recurring, :cardholder, id: network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + end + + def test_failed_purchase_using_stored_credential_recurring_mit + initial_options = stored_credential_options(:merchant, :recurring, :initial) + assert purchase = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success purchase + assert_equal '1', purchase.params['A9'] + assert network_transaction_id = purchase.params['Z13'] + + used_options = stored_credential_options(:merchant, :recurring, id: network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_failure purchase + assert_match 'Parameter g6 is invalid', purchase.message + end + + def test_successful_purchase_using_stored_credential_recurring_mit + initial_options = stored_credential_options(:merchant, :recurring, :initial) + assert purchase = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success purchase + assert_equal '1', purchase.params['A9'] + assert initial_network_transaction_id = purchase.params['Z50'] + + used_options = stored_credential_options(:merchant, :recurring, id: initial_network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + end + + 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) + assert_success purchase + assert_equal '9', purchase.params['A9'] + assert network_transaction_id = purchase.params['Z13'] + + used_options = stored_credential_options(:cardholder, :installment, id: network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + end + + def test_purchase_using_stored_credential_installment_mit + initial_options = stored_credential_options(:merchant, :installment, :initial) + assert purchase = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success purchase + assert_equal '8', purchase.params['A9'] + 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) + assert_success purchase + end + + def test_purchase_using_stored_credential_unscheduled_cit + initial_options = stored_credential_options(:cardholder, :unscheduled, :initial) + assert purchase = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success purchase + assert_equal '9', purchase.params['A9'] + assert network_transaction_id = purchase.params['Z13'] + + used_options = stored_credential_options(:cardholder, :unscheduled, id: network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + end + + def test_purchase_using_stored_credential_unscheduled_mit + initial_options = stored_credential_options(:merchant, :unscheduled, :initial) + assert purchase = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success purchase + assert_equal '8', purchase.params['A9'] + 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) + assert_success purchase + end + + def test_authorize_and_capture_with_stored_credential + initial_options = stored_credential_options(:cardholder, :recurring, :initial) + assert authorization = @gateway.authorize(@amount, @credit_card, initial_options) + assert_success authorization + assert_equal '9', authorization.params['A9'] + assert network_transaction_id = authorization.params['Z13'] + + assert capture = @gateway.capture(@amount, authorization.authorization) + assert_success capture + + used_options = stored_credential_options(:cardholder, :recurring, id: network_transaction_id) + assert authorization = @gateway.authorize(@amount, @credit_card, used_options) + assert_success authorization + assert @gateway.capture(@amount, authorization.authorization) + end + def test_transcript_scrubbing transcript = capture_transcript(@gateway) do @gateway.purchase(@amount, @credit_card, @options) @@ -165,7 +678,26 @@ def test_transcript_scrubbing clean_transcript = @gateway.scrub(transcript) assert_scrubbed(@credit_card.number, clean_transcript) - assert_scrubbed(@credit_card.verification_value.to_s, clean_transcript) + assert_cvv_scrubbed(clean_transcript) + end + + def test_purchase_passes_processor + # returns a successful response when a valid processor parameter is sent + assert good_response = @gateway.purchase(@amount, @credit_card, @options.merge(processor: 'CREDORAX')) + assert_success good_response + assert_equal 'Succeeded', good_response.message + assert_equal 'CREDORAX', good_response.params['Z33'] + + # returns a failed response when an invalid processor parameter is sent + assert bad_response = @gateway.purchase(@amount, @credit_card, @options.merge(processor: 'invalid')) + assert_failure bad_response + end + + def test_purchase_passes_d2_field + response = @gateway.purchase(@amount, @credit_card, @options.merge(echo: 'Echo Parameter')) + assert_success response + assert_equal 'Succeeded', response.message + assert_equal 'Echo Parameter', response.params['D2'] end # ######################################################################### @@ -478,4 +1010,15 @@ def test_transcript_scrubbing # assert_success void # assert_equal "Succeeded", void.message # end + + private + + def assert_cvv_scrubbed(transcript) + assert_match(/b5=\[FILTERED\]/, transcript) + end + + def stored_credential_options(*args, id: nil) + @options.merge(order_id: generate_unique_id, + stored_credential: stored_credential(*args, id:)) + end end diff --git a/test/remote/gateways/remote_ct_payment_certification_test.rb b/test/remote/gateways/remote_ct_payment_certification_test.rb new file mode 100644 index 00000000000..2bc5d80aa16 --- /dev/null +++ b/test/remote/gateways/remote_ct_payment_certification_test.rb @@ -0,0 +1,242 @@ +require 'test_helper' + +class RemoteCtPaymentCertificationTest < Test::Unit::TestCase + def setup + @gateway = CtPaymentGateway.new(fixtures(:ct_payment)) + + @amount = 100 + @declined_card = credit_card('4502244713161718') + @options = { + billing_address: address, + description: 'Store Purchase', + merchant_terminal_number: ' ', + order_id: generate_unique_id[0, 11] + } + end + + def test1 + @credit_card = credit_card('4501161107217214', month: '07', year: 2025) + response = @gateway.purchase(@amount, @credit_card, @options) + print_result(1, response) + end + + def test2 + @credit_card = credit_card('5194419000000007', month: '07', year: 2025) + @credit_card.brand = 'master' + response = @gateway.purchase(@amount, @credit_card, @options) + print_result(2, response) + end + + def test3 + @credit_card = credit_card('341400000000000', month: '07', year: 2025, verification_value: '1234') + @credit_card.brand = 'american_express' + response = @gateway.purchase(@amount, @credit_card, @options) + print_result(3, response) + end + + def test6 + @credit_card = credit_card('341400000000000', month: '07', year: 2025, verification_value: '1234') + @credit_card.brand = 'american_express' + response = @gateway.credit(@amount, @credit_card, @options) + print_result(6, response) + end + + def test4 + @credit_card = credit_card('4501161107217214', month: '07', year: 2025) + response = @gateway.credit(@amount, @credit_card, @options) + print_result(4, response) + end + + def test5 + @credit_card = credit_card('5194419000000007', month: '07', year: 2025) + @credit_card.brand = 'master' + response = @gateway.credit(@amount, @credit_card, @options) + print_result(5, response) + end + + def test7 + @credit_card = credit_card('4501161107217214', month: '07', year: 2025) + response = @gateway.authorize(@amount, @credit_card, @options) + print_result(7, response) + + capture_response = @gateway.capture(@amount, response.authorization, @options.merge(order_id: generate_unique_id[0, 11])) + print_result(10, capture_response) + end + + def test8 + @credit_card = credit_card('5194419000000007', month: '07', year: 2025) + @credit_card.brand = 'master' + response = @gateway.authorize(@amount, @credit_card, @options) + print_result(8, response) + + capture_response = @gateway.capture(@amount, response.authorization, @options.merge(order_id: generate_unique_id[0, 11])) + print_result(11, capture_response) + end + + def test9 + @credit_card = credit_card('341400000000000', month: '07', year: 2025, verification_value: '1234') + @credit_card.brand = 'american_express' + response = @gateway.authorize(@amount, @credit_card, @options.merge(order_id: generate_unique_id[0, 11])) + print_result(9, response) + + capture_response = @gateway.capture(@amount, response.authorization, @options) + print_result(12, capture_response) + end + + def test13 + @credit_card = credit_card('4501161107217214', month: '07', year: 2025) + response = @gateway.purchase('000', @credit_card, @options) + print_result(13, response) + end + + def test14 + @credit_card = credit_card('4501161107217214', month: '07', year: 2025) + response = @gateway.purchase(-100, @credit_card, @options) + print_result(14, response) + end + + def test15 + @credit_card = credit_card('4501161107217214', month: '07', year: 2025) + response = @gateway.purchase('-1A0', @credit_card, @options) + print_result(15, response) + end + + def test16 + @credit_card = credit_card('5194419000000007', month: '07', year: 2025) + @credit_card.brand = 'visa' + response = @gateway.purchase(@amount, @credit_card, @options) + print_result(16, response) + end + + def test17 + @credit_card = credit_card('4501161107217214', month: '07', year: 2025) + @credit_card.brand = 'master' + response = @gateway.purchase(@amount, @credit_card, @options) + print_result(17, response) + end + + def test18 + @credit_card = credit_card('', month: '07', year: 2025) + response = @gateway.purchase(@amount, @credit_card, @options) + print_result(18, response) + end + + def test19 + @credit_card = credit_card('4501123412341234', month: '07', year: 2025) + response = @gateway.purchase(@amount, @credit_card, @options) + print_result(19, response) + end + + def test20 + # requires editing the model to run with a 3 digit expiration date + @credit_card = credit_card('4501161107217214', month: '07', year: 2) + response = @gateway.purchase(@amount, @credit_card, @options) + print_result(20, response) + end + + def test21 + @credit_card = credit_card('4501161107217214', month: 17, year: 2017) + response = @gateway.purchase(@amount, @credit_card, @options) + print_result(21, response) + end + + def test22 + @credit_card = credit_card('4501161107217214', month: '01', year: 2016) + response = @gateway.purchase(@amount, @credit_card, @options) + print_result(22, response) + end + + def test24 + @credit_card = credit_card('4502244713161718', month: '07', year: 2025) + response = @gateway.purchase(@amount, @credit_card, @options) + print_result(24, response) + end + + def test25 + # Needs an edit to the Model to run + @credit_card = credit_card('4501161107217214', month: '07', year: 2025) + response = @gateway.purchase(@amount, @credit_card, @options) + print_result(25, response) + end + + def test26 + @credit_card = credit_card('4501161107217214', month: '07', year: 2025) + response = @gateway.credit('000', @credit_card, @options) + print_result(26, response) + end + + def test27 + @credit_card = credit_card('4501161107217214', month: '07', year: 2025) + response = @gateway.credit(-100, @credit_card, @options) + print_result(27, response) + end + + def test28 + @credit_card = credit_card('4501161107217214', month: '07', year: 2025) + response = @gateway.credit('-1A0', @credit_card, @options) + print_result(28, response) + end + + def test29 + @credit_card = credit_card('5194419000000007', month: '07', year: 2025) + @credit_card.brand = 'visa' + response = @gateway.credit(@amount, @credit_card, @options) + print_result(29, response) + end + + def test30 + @credit_card = credit_card('4501161107217214', month: '07', year: 2025) + @credit_card.brand = 'master' + response = @gateway.credit(@amount, @credit_card, @options) + print_result(30, response) + end + + def test31 + @credit_card = credit_card('', month: '07', year: 2025) + response = @gateway.purchase(@amount, @credit_card, @options) + print_result(31, response) + end + + def test32 + @credit_card = credit_card('4501123412341234', month: '07', year: 2025) + response = @gateway.credit(@amount, @credit_card, @options) + print_result(32, response) + end + + def test33 + # requires edit to model to make 3 digit expiration date + @credit_card = credit_card('4501161107217214', month: '07', year: 2) + response = @gateway.credit(@amount, @credit_card, @options) + print_result(33, response) + end + + def test34 + @credit_card = credit_card('4501161107217214', month: 17, year: 2017) + response = @gateway.credit(@amount, @credit_card, @options) + print_result(34, response) + end + + def test35 + @credit_card = credit_card('4501161107217214', month: '01', year: 2016) + response = @gateway.credit(@amount, @credit_card, @options) + print_result(35, response) + end + + def test37 + @credit_card = credit_card('4502244713161718', month: '07', year: 2025) + response = @gateway.purchase(@amount, @credit_card, @options) + print_result(37, response) + end + + def test38 + # Needs an edit to the Model to run + @credit_card = credit_card('4501161107217214', month: '07', year: 2025) + response = @gateway.purchase(@amount, @credit_card, @options) + print_result(38, response) + end + + def print_result(test_number, response) + puts "Test #{test_number} | transaction number: #{response.params['transactionNumber']}, invoice number #{response.params['invoiceNumber']}, timestamp: #{response.params['timeStamp']}, result: #{response.params['returnCode']}" + puts response.inspect + end +end diff --git a/test/remote/gateways/remote_ct_payment_test.rb b/test/remote/gateways/remote_ct_payment_test.rb new file mode 100644 index 00000000000..164475a5db2 --- /dev/null +++ b/test/remote/gateways/remote_ct_payment_test.rb @@ -0,0 +1,172 @@ +require 'test_helper' + +class RemoteCtPaymentTest < Test::Unit::TestCase + def setup + @gateway = CtPaymentGateway.new(fixtures(:ct_payment)) + + @amount = 100 + @credit_card = credit_card('4501161107217214') + @declined_card = credit_card('4502244713161718') + @options = { + billing_address: address, + description: 'Store Purchase', + order_id: generate_unique_id[0, 11], + email: 'bigbird@sesamestreet.com' + + } + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + 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 'Transaction declined', 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, @options.merge(order_id: generate_unique_id[0, 11])) + assert_success capture + assert_equal 'APPROVED', capture.message + end + + def test_failed_authorize + response = @gateway.authorize(@amount, @declined_card, @options) + assert_failure response + assert_equal 'Transaction declined', 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, @options.merge(order_id: generate_unique_id[0, 11])) + assert_success capture + end + + def test_failed_capture + response = @gateway.capture(@amount, '0123456789asd;0123456789asdf;12345678', @options) + assert_failure response + assert_equal 'The original transaction number does not match any actual transaction', response.message + end + + def test_successful_refund + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization, @options.merge(order_id: generate_unique_id[0, 11])) + assert_success refund + assert_equal 'APPROVED', refund.message + end + + def test_partial_refund + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount - 1, purchase.authorization, @options.merge(order_id: generate_unique_id[0, 11])) + assert_success refund + end + + def test_failed_refund + response = @gateway.refund(@amount, '0123456789asd;0123456789asdf;12345678', @options.merge(order_id: generate_unique_id[0, 11])) + assert_failure response + assert_equal 'The original transaction number does not match any actual transaction', response.message + end + + def test_successful_void + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert void = @gateway.void(purchase.authorization) + assert_success void + assert_equal 'APPROVED', void.message + end + + def test_failed_void + response = @gateway.void('0123456789asd;0123456789asdf;12345678') + assert_failure response + assert_equal 'The original transaction number does not match any actual transaction', response.message + end + + def test_successful_credit + assert response = @gateway.credit(@amount, @credit_card, @options) + assert_success response + assert_equal 'APPROVED', response.message + end + + def test_successful_store + assert response = @gateway.store(@credit_card, @options) + + assert_success response + assert !response.authorization.split(';')[3].nil? + end + + def test_successful_purchase_using_stored_card + assert store_response = @gateway.store(@credit_card, @options) + assert_success store_response + + response = @gateway.purchase(@amount, store_response.authorization, @options) + assert_success response + assert_equal 'APPROVED', response.message + end + + def test_successful_authorize_using_stored_card + assert store_response = @gateway.store(@credit_card, @options) + assert_success store_response + + response = @gateway.authorize(@amount, store_response.authorization, @options) + assert_success response + assert_equal 'APPROVED', 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(@declined_card, @options) + assert_failure response + assert_match %r{Transaction declined}, response.message + end + + def test_invalid_login + gateway = CtPaymentGateway.new(api_key: '', company_number: '12345', merchant_number: '12345') + + response = gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_match %r{Invalid API KEY}, 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(Base64.strict_encode64(@credit_card.number), transcript) + assert_scrubbed(@gateway.options[:api_key], transcript) + end + + def test_transcript_scrubbing_store + transcript = capture_transcript(@gateway) do + @gateway.store(@credit_card, @options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@credit_card.number, transcript) + assert_scrubbed(@credit_card.verification_value, transcript) + assert_scrubbed(Base64.strict_encode64(@credit_card.number), transcript) + assert_scrubbed(@gateway.options[:api_key], transcript) + end +end diff --git a/test/remote/gateways/remote_culqi_test.rb b/test/remote/gateways/remote_culqi_test.rb index d8241b2aef3..c4c1fb7131a 100644 --- a/test/remote/gateways/remote_culqi_test.rb +++ b/test/remote/gateways/remote_culqi_test.rb @@ -58,7 +58,7 @@ def test_partial_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture assert_match %r{Transaction has been successfully captured}, capture.message end @@ -97,7 +97,7 @@ def test_partial_refund auth = @gateway.authorize(@amount, @credit_card, @options) capture = @gateway.capture(@amount, auth.authorization) - refund = @gateway.refund(@amount-1, capture.authorization) + refund = @gateway.refund(@amount - 1, capture.authorization) assert_success refund assert_match %r{reversed}, refund.message end 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..d6f61965138 --- /dev/null +++ b/test/remote/gateways/remote_cyber_source_rest_test.rb @@ -0,0 +1,676 @@ +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') + @carnet_card = credit_card('5062280000000002', brand: 'carnet') + + @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_authorize_with_carnet_card + response = @gateway.authorize(@amount, @carnet_card, @options) + assert_success response + assert response.test? + assert_equal 'AUTHORIZED', response.message + assert_equal '002', response.params['paymentInformation']['card']['type'] + refute_empty response.params['_links']['capture'] + 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_successful_refund_with_merchant_category_code + purchase = @gateway.purchase(@amount, @visa_card, @options) + response = @gateway.refund(@amount, purchase.authorization, @options.merge(merchant_category_code: '1111')) + + 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_successful_credit_with_merchant_category_code + response = @gateway.credit(@amount, @visa_card, @options.merge(merchant_category_code: '1111')) + + 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_successful_void_with_merchant_category_code + authorize = @gateway.authorize(@amount, @visa_card, @options) + response = @gateway.void(authorize.authorization, @options.merge(merchant_category_code: '1111')) + 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_apple_pay_recurring + auth = @gateway.authorize(@amount, @apple_pay, @options) + response = @gateway.authorize(@amount, @apple_pay, @options.merge(stored_credential: stored_credential_options(:merchant, :recurring, ntid: auth.network_transaction_id))) + + 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_merchant_catefory_code + response = @gateway.purchase(@amount, @visa_card, @options.merge(merchant_category_code: '1111')) + 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 106a3cf3a0d..bb51cf92fa3 100644 --- a/test/remote/gateways/remote_cyber_source_test.rb +++ b/test/remote/gateways/remote_cyber_source_test.rb @@ -1,70 +1,174 @@ require 'test_helper' class RemoteCyberSourceTest < Test::Unit::TestCase + # Reduce code duplication: use `assert_successful_response` when feasible! def setup Base.mode = :test - @gateway = CyberSourceGateway.new({nexus: 'NC'}.merge(fixtures(:cyber_source))) + @gateway = CyberSourceGateway.new({ nexus: 'NC' }.merge(fixtures(:cyber_source))) + @gateway_latam = CyberSourceGateway.new({}.merge(fixtures(:cyber_source_latam_pe))) - @credit_card = credit_card('4111111111111111', verification_value: '321') + @credit_card = credit_card('4111111111111111', verification_value: '987') @declined_card = credit_card('801111111111111') + @master_credit_card = credit_card( + '5555555555554444', + verification_value: '321', + month: '12', + year: (Time.now.year + 2).to_s, + brand: :master + ) @pinless_debit_card = credit_card('4002269999999999') - @three_ds_unenrolled_card = credit_card('4000000000000051', + @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', verification_value: '321', month: '12', - year: "#{Time.now.year + 2}", + year: (Time.now.year + 2).to_s, brand: :visa ) - @three_ds_enrolled_card = credit_card('4000000000000002', + @three_ds_enrolled_card = credit_card( + '4000000000001091', verification_value: '321', month: '12', - year: "#{Time.now.year + 2}", + year: (Time.now.year + 2).to_s, brand: :visa ) - @three_ds_invalid_card = credit_card('4000000000000010', + @three_ds_invalid_card = credit_card( + '4000000000002537', verification_value: '321', month: '12', - year: "#{Time.now.year + 2}", + year: (Time.now.year + 2).to_s, brand: :visa ) + @three_ds_enrolled_mastercard = credit_card( + '5200000000002235', + verification_value: '321', + month: '12', + year: (Time.now.year + 2).to_s, + brand: :master + ) + @three_ds_frictionless_card = credit_card( + '4000000000002313', + verification_value: '321', + month: '12', + year: (Time.now.year + 2).to_s, + brand: :visa + ) + @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 + ) + + @carnet_credit_card = credit_card( + '5062280000000002', + verification_value: '321', + month: '12', + year: (Time.now.year + 2).to_s, + brand: :carnet + ) @amount = 100 @options = { - :order_id => generate_unique_id, - :line_items => [ + order_id: generate_unique_id, + line_items: [ { - :declared_value => 100, - :quantity => 2, - :code => 'default', - :description => 'Giant Walrus', - :sku => 'WA323232323232323' - }, - { - :declared_value => 100, - :quantity => 2, - :description => 'Marble Snowcone', - :sku => 'FAKE1232132113123' + declared_value: 100, + quantity: 2, + code: 'default', + description: 'Giant Walrus', + sku: 'WA323232323232323', + tax_amount: 10, + national_tax: 5 } ], - :currency => 'USD', - :ignore_avs => 'true', - :ignore_cvv => 'true' + currency: 'USD', + ignore_avs: 'true', + ignore_cvv: 'true', + commerce_indicator: 'internet', + user_po: 'ABC123', + merchant_descriptor_country: 'US', + merchant_descriptor_state: 'NY', + merchant_descriptor_city: 'test123', + submerchant_id: 'AVSBSGDHJMNGFR', + taxable: true, + sales_slip_number: '456', + airline_agent_code: '7Q', + tax_management_indicator: 1, + invoice_amount: '3', + original_amount: '4', + reference_data_code: 'ABC123', + invoice_number: '123', + first_recurring_payment: true, + mobile_remote_payment_type: 'A1', + vat_tax_rate: '1', + reconciliation_id: '1936831', + aggregator_id: 'ABCDE' + } + + @capture_options = { + gratuity_amount: '3.50' } @subscription_options = { - :order_id => generate_unique_id, - :credit_card => @credit_card, - :subscription => { - :frequency => 'weekly', - :start_date => Date.today.next_week, - :occurrences => 4, - :auto_renew => true, - :amount => 100 + order_id: generate_unique_id, + credit_card: @credit_card, + subscription: { + frequency: 'weekly', + start_date: Date.today.next_week, + occurrences: 4, + auto_renew: true, + amount: 100 } } + + @three_ds_options = { + three_ds_2: { + browser_info: { + accept_header: 'unknown', + depth: 100, + java: false, + language: 'US', + height: 1000, + width: 500, + timezone: '-120', + user_agent: 'unknown' + } + }, + return_url: 'return_url.com', + payer_auth_enroll_service: true + } + + @issuer_additional_data = 'PR25000000000011111111111112222222sk111111111111111111111111111' + + '1111111115555555222233101abcdefghijkl7777777777777777777777777promotionCde' end + # Scrubbing is working but may fail at the @credit_card.verification_value assertion + # if the the 3 digits are showing up in the Cybersource requestID def test_transcript_scrubbing transcript = capture_transcript(@gateway) do @gateway.purchase(@amount, @credit_card, @options) @@ -77,114 +181,534 @@ 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 def test_successful_authorization assert response = @gateway.authorize(@amount, @credit_card, @options) - assert_equal 'Successful transaction', response.message - assert_success response - assert response.test? + assert_successful_response(response) + assert !response.authorization.blank? + end + + def test_successful_authorization_with_merchant_catefory_code + options = @options.merge(merchant_category_code: '1111') + assert response = @gateway.authorize(@amount, @credit_card, options) + assert_successful_response(response) + assert !response.authorization.blank? + end + + def test_successful_authorization_with_reconciliation_id + options = @options.merge(reconciliation_id: '1936831') + assert response = @gateway.authorize(@amount, @credit_card, options) + assert_successful_response(response) + 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) + assert_successful_response(response) + assert !response.authorization.blank? + ensure + ActiveMerchant::Billing::CyberSourceGateway.application_id = nil + end + + def test_successful_authorize_with_solution_id_and_stored_creds + ActiveMerchant::Billing::CyberSourceGateway.application_id = 'A1000000' + @options[:stored_credential] = { + initiator: 'cardholder', + reason_type: '', + initial_transaction: true, + network_transaction_id: '' + } + @options[:commerce_indicator] = 'internet' + + assert response = @gateway.authorize(@amount, @master_credit_card, @options) + assert_successful_response(response) assert !response.authorization.blank? + ensure + ActiveMerchant::Billing::CyberSourceGateway.application_id = nil + end + + def test_successful_authorization_with_issuer_additional_data + @options[:issuer_additional_data] = @issuer_additional_data + + assert response = @gateway.authorize(@amount, @credit_card, @options) + assert_successful_response(response) + assert !response.authorization.blank? + end + + def test_successful_authorization_with_issuer_additional_data_and_partner_solution_id + @options[:issuer_additional_data] = @issuer_additional_data + + ActiveMerchant::Billing::CyberSourceGateway.application_id = 'A1000000' + + assert auth = @gateway.authorize(@amount, @credit_card, @options) + assert_successful_response(auth) + + assert void = @gateway.void(auth.authorization, @options) + assert_successful_response(void) + ensure + ActiveMerchant::Billing::CyberSourceGateway.application_id = nil + end + + def test_successful_authorize_with_merchant_descriptor_and_partner_solution_id + @options[:merchant_descriptor] = 'Spreedly' + + ActiveMerchant::Billing::CyberSourceGateway.application_id = 'A1000000' + + assert auth = @gateway.authorize(@amount, @credit_card, @options) + assert_successful_response(auth) + + assert void = @gateway.void(auth.authorization, @options) + assert_successful_response(void) + ensure + ActiveMerchant::Billing::CyberSourceGateway.application_id = nil + end + + def test_successful_authorize_with_issuer_additional_data_stored_creds_merchant_desc_and_partner_solution_id + @options[:issuer_additional_data] = @issuer_additional_data + @options[:stored_credential] = { + initiator: 'cardholder', + reason_type: '', + initial_transaction: true, + network_transaction_id: '' + } + @options[:commerce_indicator] = 'internet' + @options[:merchant_descriptor] = 'Spreedly' + + ActiveMerchant::Billing::CyberSourceGateway.application_id = 'A1000000' + + assert auth = @gateway.authorize(@amount, @credit_card, @options) + assert_successful_response(auth) + ensure + ActiveMerchant::Billing::CyberSourceGateway.application_id = nil + end + + def test_successful_authorization_with_elo + assert response = @gateway.authorize(@amount, @elo_credit_card, @options) + assert_successful_response(response) + assert !response.authorization.blank? + end + + def test_successful_authorization_with_installment_data + options = @options.merge( + installment_total_count: 2, + installment_total_amount: 0.50, + installment_plan_type: 1, + first_installment_date: '300101', + installment_annual_interest_rate: 1.09, + installment_grace_period_duration: 1 + ) + assert response = @gateway.authorize(@amount, @credit_card, options) + assert_successful_response(response) + assert !response.authorization.blank? + end + + def test_successful_authorization_with_less_installment_data + options = @options.merge(installment_grace_period_duration: '1') + + assert response = @gateway.authorize(@amount, @credit_card, options) + assert_successful_response(response) + assert !response.authorization.blank? + end + + def test_successful_authorization_with_merchant_tax_id + options = @options.merge(merchant_tax_id: '123') + assert response = @gateway.authorize(@amount, @credit_card, options) + 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_purchase_with_merchant_catefory_code + options = @options.merge(merchant_category_code: '1111') + assert response = @gateway.purchase(@amount, @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) + assert_successful_response(response) + end + + def test_successful_authorization_with_airline_agent_code + options = @options.merge(airline_agent_code: '7Q') + assert response = @gateway.authorize(@amount, @credit_card, options) + assert_successful_response(response) + end + + def test_successful_authorization_with_tax_mgmt_indicator + options = @options.merge(tax_management_indicator: '3') + assert response = @gateway.authorize(@amount, @credit_card, options) + assert_successful_response(response) + end + + def test_successful_bank_account_purchase_with_sec_code + options = @options.merge(sec_code: 'WEB') + bank_account = check({ account_number: '4100', routing_number: '011000015' }) + assert response = @gateway.purchase(@amount, bank_account, options) + assert_successful_response(response) end def test_unsuccessful_authorization assert response = @gateway.authorize(@amount, @declined_card, @options) assert response.test? assert_equal 'Invalid account number', response.message - assert_equal false, response.success? + assert_equal false, response.success? + end + + def test_purchase_and_void + assert purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_successful_response(purchase) + assert void = @gateway.void(purchase.authorization, @options) + assert_successful_response(void) + end + + def test_purchase_and_void_with_merchant_category_code + assert purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_successful_response(purchase) + + void_options = @options.merge(merchant_category_code: '1111') + assert void = @gateway.void(purchase.authorization, void_options) + assert_successful_response(void) + end + + # Note: This test will only pass with test account credentials which + # have asynchronous adjustments enabled. + def test_successful_asynchronous_adjust + assert authorize = @gateway_latam.authorize(@amount, @credit_card, @options) + assert_successful_response(authorize) + assert adjust = @gateway_latam.adjust(@amount * 2, authorize.authorization, @options) + assert_success adjust + assert capture = @gateway_latam.capture(@amount, authorize.authorization, @options.merge({ national_tax_indicator: 1 })) + assert_successful_response(capture) end def test_authorize_and_void assert auth = @gateway.authorize(@amount, @credit_card, @options) - assert_success auth + assert_successful_response(auth) assert void = @gateway.void(auth.authorization, @options) - assert_equal 'Successful transaction', void.message - assert_success void - assert void.test? + assert_successful_response(void) end def test_capture_and_void assert auth = @gateway.authorize(@amount, @credit_card, @options) - assert_success auth - assert capture = @gateway.capture(@amount, auth.authorization, @options) - assert_success capture + assert_successful_response(auth) + assert capture = @gateway.capture(@amount, auth.authorization, @options.merge({ national_tax_indicator: 1 })) + assert_successful_response(capture) + assert void = @gateway.void(capture.authorization, @options) + assert_successful_response(void) + end + + def test_capture_and_void_with_elo + assert auth = @gateway.authorize(@amount, @elo_credit_card, @options) + assert_successful_response(auth) + assert capture = @gateway.capture(@amount, auth.authorization, @options.merge({ national_tax_indicator: 1 })) + assert_successful_response(capture) assert void = @gateway.void(capture.authorization, @options) - assert_equal 'Successful transaction', void.message - assert_success void - assert void.test? + assert_successful_response(void) + end + + def test_void_with_issuer_additional_data + @options[:issuer_additional_data] = @issuer_additional_data + + assert auth = @gateway.authorize(@amount, @credit_card, @options) + assert_successful_response(auth) + assert void = @gateway.void(auth.authorization, @options) + assert_successful_response(void) + end + + def test_void_with_mdd_fields + (1..20).each { |e| @options["mdd_field_#{e}".to_sym] = "value #{e}" } + + assert auth = @gateway.authorize(@amount, @credit_card, @options) + assert_successful_response(auth) + assert void = @gateway.void(auth.authorization, @options) + assert_successful_response(void) + end + + def test_successful_void_with_solution_id + ActiveMerchant::Billing::CyberSourceGateway.application_id = 'A1000000' + + assert auth = @gateway.authorize(@amount, @credit_card, @options) + assert_successful_response(auth) + + assert void = @gateway.void(auth.authorization, @options) + assert_successful_response(void) + ensure + ActiveMerchant::Billing::CyberSourceGateway.application_id = nil end def test_successful_tax_calculation assert response = @gateway.calculate_tax(@credit_card, @options) - assert_equal 'Successful transaction', response.message assert response.params['totalTaxAmount'] assert_not_equal '0', response.params['totalTaxAmount'] - assert_success response + assert_successful_response(response) end def test_successful_purchase assert response = @gateway.purchase(@amount, @credit_card, @options) - assert_equal 'Successful transaction', response.message - assert_success response - assert response.test? + assert_successful_response(response) + end + + def test_successful_purchase_with_carnet_card + assert response = @gateway.purchase(@amount, @carnet_credit_card, @options) + assert_successful_response(response) + assert_equal '002', response.params['cardType'] + end + + def test_successful_purchase_with_bank_account + bank_account = check({ account_number: '4100', routing_number: '011000015' }) + assert response = @gateway.purchase(10000, bank_account, @options) + assert_successful_response(response) + end + + # To properly run this test couple of test your account needs to be enabled to + # handle canadian bank accounts. + def test_successful_purchase_with_a_canadian_bank_account_full_number + bank_account = check({ account_number: '4100', routing_number: '011000015' }) + @options[:currency] = 'CAD' + assert response = @gateway.purchase(10000, bank_account, @options) + assert_successful_response(response) + end + + def test_successful_purchase_with_a_canadian_bank_account_8_digit_number + bank_account = check({ account_number: '4100', routing_number: '11000015' }) + @options[:currency] = 'CAD' + assert response = @gateway.purchase(10000, bank_account, @options) + assert_successful_response(response) + end + + def test_successful_purchase_with_bank_account_savings_account + bank_account = check({ account_number: '4100', routing_number: '011000015', account_type: 'savings' }) + assert response = @gateway.purchase(10000, bank_account, @options) + assert_successful_response(response) + end + + def test_unsuccessful_purchase_with_bank_account_card_declined + bank_account = check({ account_number: '4201', routing_number: '011000015' }) + assert response = @gateway.purchase(10000, bank_account, @options) + assert_failure response + assert_equal 'General decline by the processor', response.message + end + + def test_unsuccessful_purchase_with_bank_account_merchant_configuration + bank_account = check({ account_number: '4241', routing_number: '011000015' }) + assert response = @gateway.purchase(10000, bank_account, @options) + assert_failure response + assert_equal 'A problem exists with your CyberSource merchant configuration', response.message + end + + def test_successful_purchase_with_national_tax_indicator + assert purchase = @gateway.purchase(@amount, @credit_card, @options.merge(national_tax_indicator: 1)) + assert_successful_response(purchase) + end + + def test_successful_purchase_with_issuer_additional_data + @options[:issuer_additional_data] = @issuer_additional_data + + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_successful_response(response) + end + + def test_successful_purchase_with_merchant_descriptor + @options[:merchant_descriptor] = 'Spreedly' + + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_successful_response(response) + end + + def test_successful_purchase_with_issuer_additional_data_and_partner_solution_id + @options[:issuer_additional_data] = @issuer_additional_data + + ActiveMerchant::Billing::CyberSourceGateway.application_id = 'A1000000' + + assert purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_successful_response(purchase) + ensure + ActiveMerchant::Billing::CyberSourceGateway.application_id = nil + end + + def test_successful_purchase_with_merchant_descriptor_and_partner_solution_id + @options[:merchant_descriptor] = 'Spreedly' + + ActiveMerchant::Billing::CyberSourceGateway.application_id = 'A1000000' + + assert purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_successful_response(purchase) + ensure + ActiveMerchant::Billing::CyberSourceGateway.application_id = nil + end + + def test_successful_purchase_with_issuer_additional_data_stored_creds_merchant_desc_and_partner_solution_id + @options[:issuer_additional_data] = @issuer_additional_data + @options[:stored_credential] = { + initiator: 'cardholder', + reason_type: '', + initial_transaction: true, + network_transaction_id: '' + } + @options[:commerce_indicator] = 'internet' + @options[:merchant_descriptor] = 'Spreedly' + + ActiveMerchant::Billing::CyberSourceGateway.application_id = 'A1000000' + + assert purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_successful_response(purchase) + ensure + ActiveMerchant::Billing::CyberSourceGateway.application_id = nil + end + + def test_successful_purchase_with_reconciliation_id + options = @options.merge(reconciliation_id: '1936831') + assert response = @gateway.purchase(@amount, @credit_card, options) + 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) + assert_successful_response(response) + end + + def test_authorize_with_national_tax_indicator + assert authorize = @gateway.authorize(@amount, @credit_card, @options.merge(national_tax_indicator: 1)) + assert_successful_response(authorize) + end + + def test_successful_purchase_with_customer_id + options = @options.merge(customer_id: '7500BB199B4270EFE00588D0AFCAD') + assert response = @gateway.purchase(@amount, @credit_card, options) + assert_successful_response(response) + end + + def test_successful_purchase_with_elo + assert response = @gateway.purchase(@amount, @elo_credit_card, @options) + assert_successful_response(response) end def test_successful_purchase_sans_options assert response = @gateway.purchase(@amount, @credit_card) assert_equal 'Successful transaction', response.message - assert_success response + assert_successful_response(response) end def test_successful_purchase_with_billing_address_override - @options[:billing_address] = address + billing_address = { + address1: '111 North Pole Lane', + city: 'Santaland', + state: '', + phone: nil + } + @options[:billing_address] = billing_address @options[:email] = 'override@example.com' assert response = @gateway.purchase(@amount, @credit_card, @options) - assert_equal 'Successful transaction', response.message - assert_success response + assert_equal true, response.success? + assert_successful_response(response) end def test_successful_purchase_with_long_country_name @options[:billing_address] = address(country: 'united states', state: 'NC') assert response = @gateway.purchase(@amount, @credit_card, @options) - assert_equal 'Successful transaction', response.message - assert_success response + assert_successful_response(response) end def test_successful_purchase_without_decision_manager @options[:decision_manager_enabled] = 'false' assert response = @gateway.purchase(@amount, @credit_card, @options) - assert_equal 'Successful transaction', response.message - assert_success response - assert response.test? + assert_successful_response(response) end def test_successful_purchase_with_decision_manager_profile @options[:decision_manager_enabled] = 'true' @options[:decision_manager_profile] = 'Regular' assert response = @gateway.purchase(@amount, @credit_card, @options) - assert_equal 'Successful transaction', response.message - assert_success response - assert response.test? + assert_successful_response(response) end - def test_successful_pinless_debit_card_puchase - assert response = @gateway.purchase(@amount, @pinless_debit_card, @options.merge(:pinless_debit_card => true)) - assert_equal 'Successful transaction', response.message - assert_success response + def test_successful_purchase_with_solution_id + ActiveMerchant::Billing::CyberSourceGateway.application_id = 'A1000000' + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_successful_response(response) + assert !response.authorization.blank? + ensure + ActiveMerchant::Billing::CyberSourceGateway.application_id = nil + end + + def test_successful_purchase_with_solution_id_and_stored_creds + ActiveMerchant::Billing::CyberSourceGateway.application_id = 'A1000000' + @options[:stored_credential] = { + initiator: 'cardholder', + reason_type: '', + initial_transaction: true, + network_transaction_id: '' + } + @options[:commerce_indicator] = 'internet' + + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_successful_response(response) + assert !response.authorization.blank? + ensure + ActiveMerchant::Billing::CyberSourceGateway.application_id = nil + end + + def test_successful_purchase_with_country_submitted_as_empty_string + @options[:billing_address] = { country: '' } + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_successful_response(response) end def test_unsuccessful_purchase @@ -195,141 +719,399 @@ def test_unsuccessful_purchase def test_authorize_and_capture assert auth = @gateway.authorize(@amount, @credit_card, @options) - assert_success auth - assert_equal 'Successful transaction', auth.message + assert_successful_response(auth) + + assert capture = @gateway.capture(@amount, auth.authorization, @capture_options) + assert_successful_response(capture) + end + + def test_authorize_and_capture_with_elo + assert auth = @gateway.authorize(@amount, @elo_credit_card, @options) + assert_successful_response(auth) assert capture = @gateway.capture(@amount, auth.authorization) - assert_success capture + assert_successful_response(capture) + end + + def test_successful_capture_with_issuer_additional_data + @options[:issuer_additional_data] = @issuer_additional_data + assert auth = @gateway.authorize(@amount, @credit_card, @options) + assert_successful_response(auth) + + assert response = @gateway.capture(@amount, auth.authorization) + assert_successful_response(response) + assert !response.authorization.blank? + end + + def test_successful_capture_with_merchant_category_code + assert auth = @gateway.authorize(@amount, @credit_card, @options) + assert_successful_response(auth) + + capture_options = @options.merge(merchant_category_code: '1111') + assert response = @gateway.capture(@amount, auth.authorization, capture_options) + assert_successful_response(response) + assert !response.authorization.blank? + end + + def test_successful_capture_with_solution_id + ActiveMerchant::Billing::CyberSourceGateway.application_id = 'A1000000' + assert auth = @gateway.authorize(@amount, @credit_card, @options) + assert_successful_response(auth) + + assert response = @gateway.capture(@amount, auth.authorization, @options.merge({ national_tax_indicator: 1 })) + assert_successful_response(response) + assert !response.authorization.blank? + ensure + ActiveMerchant::Billing::CyberSourceGateway.application_id = nil end def test_successful_authorization_and_failed_capture assert auth = @gateway.authorize(@amount, @credit_card, @options) - assert_success auth - assert_equal 'Successful transaction', auth.message + assert_successful_response(auth) - assert capture = @gateway.capture(@amount + 10, auth.authorization, @options) + assert capture = @gateway.capture(@amount + 100000000, auth.authorization, @options.merge({ national_tax_indicator: 1 })) assert_failure capture - assert_equal 'The requested amount exceeds the originally authorized amount', capture.message + assert_equal 'One or more fields contains invalid data: (Amount limit)', capture.message end def test_failed_capture_bad_auth_info - assert auth = @gateway.authorize(@amount, @credit_card, @options) - assert capture = @gateway.capture(@amount, 'a;b;c', @options) + assert @gateway.authorize(@amount, @credit_card, @options) + assert capture = @gateway.capture(@amount, 'a;b;c', @options.merge({ national_tax_indicator: 1 })) assert_failure capture end def test_invalid_login - gateway = CyberSourceGateway.new( :login => 'asdf', :password => 'qwer' ) + gateway = CyberSourceGateway.new(login: 'asdf', password: 'qwer') assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal "wsse:FailedCheck: \nSecurity Data : UsernameToken authentication failed.\n", response.message end + # Unable to test refunds for Elo cards, as the test account is setup to have + # Elo transactions routed to Comercio Latino which has very specific rules on + # refunds (i.e. that you cannot do a "Stand-Alone" refund). This means we need + # to go through a Capture cycle at least a day before submitting a refund. def test_successful_refund assert response = @gateway.purchase(@amount, @credit_card, @options) - assert_equal 'Successful transaction', response.message - assert_success response - assert response.test? + assert_successful_response(response) + assert response = @gateway.refund(@amount, response.authorization) - assert_equal 'Successful transaction', response.message - assert_success response + assert_successful_response(response) end - def test_successful_validate_pinless_debit_card - assert response = @gateway.validate_pinless_debit_card(@pinless_debit_card, @options) - assert response.test? - assert_equal 'Y', response.params['status'] - assert_equal true, response.success? + def test_successful_refund_with_solution_id + ActiveMerchant::Billing::CyberSourceGateway.application_id = 'A1000000' + + assert purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_successful_response(purchase) + + assert refund = @gateway.refund(@amount, purchase.authorization, @options) + assert_successful_response(refund) + ensure + ActiveMerchant::Billing::CyberSourceGateway.application_id = nil + end + + def test_successful_refund_with_merchant_category_code + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_successful_response(response) + + refund_options = @options.merge(merchant_category_code: '1111') + assert response = @gateway.refund(@amount, response.authorization, refund_options) + assert_successful_response(response) + end + + def test_successful_refund_with_bank_account_follow_on + bank_account = check({ account_number: '4100', routing_number: '011000015' }) + assert response = @gateway.purchase(10000, bank_account, @options) + assert_successful_response(response) + + assert response = @gateway.refund(10000, response.authorization, @options) + assert_successful_response(response) end def test_network_tokenization_authorize_and_capture - credit_card = network_tokenization_credit_card('4111111111111111', - :brand => 'visa', - :eci => '05', - :payment_cryptogram => 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + assert auth = @gateway.authorize(@amount, @visa_network_token, @options) + assert_successful_response(auth) + + assert capture = @gateway.capture(@amount, auth.authorization) + assert_successful_response(capture) + end + + def test_network_tokenization_with_amex_cc_and_basic_cryptogram + assert auth = @gateway.authorize(@amount, @amex_network_token, @options) + assert_successful_response(auth) + + assert capture = @gateway.capture(@amount, auth.authorization) + assert_successful_response(capture) + end + + def test_network_tokenization_with_mastercard + assert auth = @gateway.authorize(@amount, @mastercard_network_token, @options) + assert_successful_response(auth) + + assert capture = @gateway.capture(@amount, auth.authorization) + assert_successful_response(capture) + end + + def test_network_tokenization_with_amex_cc_longer_cryptogram + # Generate a random 40 bytes binary amex cryptogram => Base64.encode64(Random.bytes(40)) + long_cryptogram = "NZwc40C4eTDWHVDXPekFaKkNYGk26w+GYDZmU50cATbjqOpNxR/eYA==\n" + + credit_card = network_tokenization_credit_card( + '378282246310005', + brand: 'american_express', + eci: '05', + payment_cryptogram: long_cryptogram, + source: :network_token ) assert auth = @gateway.authorize(@amount, credit_card, @options) - assert_success auth - assert_equal 'Successful transaction', auth.message + assert_successful_response(auth) + + assert capture = @gateway.capture(@amount, auth.authorization) + assert_successful_response(capture) + end + + def test_purchase_with_network_tokenization_with_amex_cc + assert auth = @gateway.purchase(@amount, @amex_network_token, @options) + assert_successful_response(auth) + end + + def test_purchase_with_apple_pay_network_tokenization_visa_subsequent_auth + credit_card = network_tokenization_credit_card('4111111111111111', + brand: 'visa', + eci: '05', + source: :apple_pay, + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=') + @options[:stored_credential] = { + initiator: 'merchant', + reason_type: 'unscheduled', + network_transaction_id: '016150703802094' + } + + assert auth = @gateway.purchase(@amount, credit_card, @options) + assert_successful_response(auth) + end + + def test_purchase_with_apple_pay_network_tokenization_mastercard_subsequent_auth + credit_card = network_tokenization_credit_card('5555555555554444', + brand: 'master', + eci: '05', + source: :apple_pay, + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=') + @options[:stored_credential] = { + initiator: 'merchant', + reason_type: 'unscheduled', + network_transaction_id: '0602MCC603474' + } + + assert auth = @gateway.purchase(@amount, credit_card, @options) + assert_successful_response(auth) + end + + def test_successful_auth_and_capture_nt_mastercard_with_tax_options_and_no_xml_parsing_errors + credit_card = network_tokenization_credit_card('5555555555554444', + brand: 'master', + eci: '05', + source: :network_token, + 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_success capture + 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', + source: :network_token, + 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}" } + assert response = @gateway.authorize(@amount, @credit_card, @options) - assert_success response + assert_successful_response(response) end def test_successful_purchase_with_mdd_fields (1..20).each { |e| @options["mdd_field_#{e}".to_sym] = "value #{e}" } assert response = @gateway.purchase(@amount, @credit_card, @options) - assert_success response + assert_successful_response(response) + end + + def test_successful_capture_with_mdd_fields + assert auth = @gateway.authorize(@amount, @credit_card, @options) + assert_successful_response(auth) + + (1..20).each { |e| @options["mdd_field_#{e}".to_sym] = "value #{e}" } + assert capture = @gateway.capture(@amount, auth.authorization, @options.merge({ national_tax_indicator: 1 })) + assert_successful_response(capture) + end + + # this test should probably be removed, the fields do not appear to be part of the + # most current XSD file, also they are not added to the request correctly as top level fields + def test_merchant_description + merchant_options = { + merchantInformation: { + merchantDescriptor: { + name: 'Test Name', + address1: '123 Main Dr', + locality: 'Durham' + } + } + } + + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(merchant_options)) + assert_successful_response(response) + end + + def test_successful_capture_with_tax + assert auth = @gateway.authorize(@amount, @credit_card, @options) + assert_successful_response(auth) + + capture_options = @options.merge(local_tax_amount: '0.17', national_tax_amount: '0.05', national_tax_indicator: 1) + assert capture = @gateway.capture(@amount, auth.authorization, capture_options) + assert_successful_response(capture) end def test_successful_authorize_with_nonfractional_currency - assert response = @gateway.authorize(100, @credit_card, @options.merge(:currency => 'JPY')) + assert response = @gateway.authorize(100, @credit_card, @options.merge(currency: 'JPY')) assert_equal '1', response.params['amount'] - assert_success response + assert_successful_response(response) + end + + def test_successful_authorize_with_additional_purchase_totals_data + assert response = @gateway.authorize(100, @credit_card, @options.merge(discount_management_indicator: 'T', purchase_tax_amount: 7.89)) + assert_successful_response(response) end def test_successful_subscription_authorization assert response = @gateway.store(@credit_card, @subscription_options) - assert_equal 'Successful transaction', response.message - assert_success response - assert response.test? + assert_successful_response(response) - assert response = @gateway.authorize(@amount, response.authorization, :order_id => generate_unique_id) - assert_equal 'Successful transaction', response.message - assert_success response - assert response.test? + assert response = @gateway.authorize(@amount, response.authorization, order_id: generate_unique_id) + assert_successful_response(response) + assert !response.authorization.blank? + end + + def test_successful_subscription_authorization_with_bank_account + bank_account = check({ account_number: '4100', routing_number: '011000015' }) + assert response = @gateway.store(bank_account, order_id: generate_unique_id) + assert_successful_response(response) + + assert response = @gateway.purchase(@amount, response.authorization, order_id: generate_unique_id) + assert_successful_response(response) assert !response.authorization.blank? end def test_successful_subscription_purchase assert response = @gateway.store(@credit_card, @subscription_options) - assert_equal 'Successful transaction', response.message - assert_success response - assert response.test? + assert_successful_response(response) - assert response = @gateway.purchase(@amount, response.authorization, :order_id => generate_unique_id) - assert_equal 'Successful transaction', response.message - assert_success response + assert response = @gateway.purchase(@amount, response.authorization, order_id: generate_unique_id) + assert_successful_response(response) + end + + def test_successful_subscription_purchase_with_elo + assert response = @gateway.store(@elo_credit_card, @subscription_options) + assert_successful_response(response) + + assert response = @gateway.purchase(@amount, response.authorization, order_id: generate_unique_id) + assert_successful_response(response) + end + + def test_successful_standalone_credit_to_card + assert response = @gateway.credit(@amount, @credit_card, @options) + assert_successful_response(response) + end + + def test_successful_standalone_credit_to_card_with_merchant_descriptor + @options[:merchant_descriptor] = 'Spreedly' + assert response = @gateway.credit(@amount, @credit_card, @options) + assert_successful_response(response) + end + + def test_successful_standalone_credit_to_card_with_issuer_additional_data + @options[:issuer_additional_data] = @issuer_additional_data + assert response = @gateway.credit(@amount, @credit_card, @options) + assert_successful_response(response) + end + + def test_successful_standalone_credit_to_card_with_mdd_fields + (1..20).each { |e| @options["mdd_field_#{e}".to_sym] = "value #{e}" } + assert response = @gateway.credit(@amount, @credit_card, @options) + assert_successful_response(response) + end + + def test_failed_standalone_credit_to_card + assert response = @gateway.credit(@amount, @declined_card, @options) + + assert_equal 'Invalid account number', response.message + assert_failure response assert response.test? end + def test_successful_standalone_credit_to_subscription + assert response = @gateway.store(@credit_card, @subscription_options) + assert_successful_response(response) + + assert response = @gateway.credit(@amount, response.authorization, order_id: generate_unique_id) + assert_successful_response(response) + end - def test_successful_subscription_credit + def test_successful_standalone_credit_to_subscription_with_merchant_descriptor + @subscription_options[:merchant_descriptor] = 'Spreedly' assert response = @gateway.store(@credit_card, @subscription_options) - assert_equal 'Successful transaction', response.message - assert_success response - assert response.test? + assert_successful_response(response) - assert response = @gateway.credit(@amount, response.authorization, :order_id => generate_unique_id) + assert response = @gateway.credit(@amount, response.authorization, order_id: generate_unique_id) + assert_successful_response(response) + end - assert_equal 'Successful transaction', response.message - assert_success response - assert response.test? + def test_successful_credit_with_bank_account + bank_account = check({ account_number: '4100', routing_number: '011000015' }) + assert response = @gateway.credit(10000, bank_account, order_id: generate_unique_id) + + assert_successful_response(response) end def test_successful_create_subscription assert response = @gateway.store(@credit_card, @subscription_options) - assert_equal 'Successful transaction', response.message - assert_success response - assert response.test? + assert_successful_response(response) + assert_equal 'credit_card', response.authorization.split(';')[7] + end + + def test_successful_create_subscription_with_bank_account + bank_account = check({ account_number: '4100', routing_number: '011000015' }) + assert response = @gateway.store(bank_account, @subscription_options) + assert_successful_response(response) + assert_equal 'check', response.authorization.split(';')[7] + end + + def test_successful_create_subscription_with_elo + assert response = @gateway.store(@elo_credit_card, @subscription_options) + assert_successful_response(response) end def test_successful_create_subscription_with_setup_fee - assert response = @gateway.store(@credit_card, @subscription_options.merge(:setup_fee => 100)) - assert_equal 'Successful transaction', response.message - assert_success response - assert response.test? + assert response = @gateway.store(@credit_card, @subscription_options.merge(setup_fee: 100)) + assert_successful_response(response) end def test_successful_create_subscription_with_monthly_options - response = @gateway.store(@credit_card, @subscription_options.merge(:setup_fee => 99.0, :subscription => {:amount => 49.0, :automatic_renew => false, frequency: 'monthly'})) + response = @gateway.store(@credit_card, @subscription_options.merge(setup_fee: 99.0, subscription: { amount: 49.0, automatic_renew: false, frequency: 'monthly' })) assert_equal 'Successful transaction', response.message response = @gateway.retrieve(response.authorization, order_id: @subscription_options[:order_id]) assert_equal '0.49', response.params['recurringAmount'] @@ -338,27 +1120,23 @@ def test_successful_create_subscription_with_monthly_options def test_successful_update_subscription_creditcard assert response = @gateway.store(@credit_card, @subscription_options) - assert_equal 'Successful transaction', response.message - assert_success response - assert response.test? + assert_successful_response(response) - assert response = @gateway.update(response.authorization, @credit_card, {:order_id => generate_unique_id, :setup_fee => 100}) - assert_equal 'Successful transaction', response.message - assert_success response - assert response.test? + assert response = @gateway.update(response.authorization, @credit_card, { order_id: generate_unique_id, setup_fee: 100 }) + assert_successful_response(response) end def test_successful_update_subscription_billing_address assert response = @gateway.store(@credit_card, @subscription_options) - assert_equal 'Successful transaction', response.message - assert_success response - assert response.test? + 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_equal 'Successful transaction', response.message - assert_success response - assert response.test? + 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 def test_successful_delete_subscription @@ -366,7 +1144,17 @@ def test_successful_delete_subscription assert response.success? assert response.test? - assert response = @gateway.unstore(response.authorization, :order_id => generate_unique_id) + assert response = @gateway.unstore(response.authorization, order_id: generate_unique_id) + assert response.success? + assert response.test? + end + + def test_successful_delete_subscription_with_elo + assert response = @gateway.store(@elo_credit_card, @subscription_options) + assert response.success? + assert response.test? + + assert response = @gateway.unstore(response.authorization, order_id: generate_unique_id) assert response.success? assert response.test? end @@ -376,67 +1164,282 @@ def test_successful_retrieve_subscription assert response.success? assert response.test? - assert response = @gateway.retrieve(response.authorization, :order_id => generate_unique_id) + assert response = @gateway.retrieve(response.authorization, order_id: generate_unique_id) assert response.success? assert response.test? end def test_3ds_enroll_request_via_purchase - assert response = @gateway.purchase(1202, @three_ds_enrolled_card, @options.merge(payer_auth_enroll_service: true)) + assert response = @gateway.purchase(1202, @three_ds_enrolled_card, @three_ds_options) assert_equal '475', response.params['reasonCode'] assert !response.params['acsURL'].blank? assert !response.params['paReq'].blank? - assert !response.params['xid'].blank? assert !response.success? end def test_3ds_enroll_request_via_authorize - assert response = @gateway.authorize(1202, @three_ds_enrolled_card, @options.merge(payer_auth_enroll_service: true)) + assert response = @gateway.authorize(1202, @three_ds_enrolled_card, @three_ds_options) assert_equal '475', response.params['reasonCode'] assert !response.params['acsURL'].blank? assert !response.params['paReq'].blank? - assert !response.params['xid'].blank? assert !response.success? end def test_successful_3ds_requests_with_unenrolled_card - assert response = @gateway.purchase(1202, @three_ds_unenrolled_card, @options.merge(payer_auth_enroll_service: true)) + assert response = @gateway.purchase(1202, @three_ds_unenrolled_card, @three_ds_options) assert response.success? - assert response = @gateway.authorize(1202, @three_ds_unenrolled_card, @options.merge(payer_auth_enroll_service: true)) + assert response = @gateway.authorize(1202, @three_ds_unenrolled_card, @three_ds_options) assert response.success? end def test_successful_3ds_validate_purchase_request - assert response = @gateway.purchase(1202, @three_ds_enrolled_card, @options.merge(payer_auth_validate_service: true, pares: pares)) + assert response = @gateway.purchase(1202, @three_ds_frictionless_card, @three_ds_options) assert_equal '100', response.params['reasonCode'] - assert_equal '0', response.params['authenticationResult'] + assert_equal '6', response.params['authenticationResult'] assert response.success? end def test_failed_3ds_validate_purchase_request - assert response = @gateway.purchase(1202, @three_ds_invalid_card, @options.merge(payer_auth_validate_service: true, pares: pares)) + assert response = @gateway.purchase(1202, @three_ds_invalid_card, @three_ds_options) assert_equal '476', response.params['reasonCode'] assert !response.success? end def test_successful_3ds_validate_authorize_request - assert response = @gateway.authorize(1202, @three_ds_enrolled_card, @options.merge(payer_auth_validate_service: true, pares: pares)) + assert response = @gateway.authorize(1202, @three_ds_frictionless_card, @three_ds_options) assert_equal '100', response.params['reasonCode'] - assert_equal '0', response.params['authenticationResult'] + assert_equal '6', response.params['authenticationResult'] assert response.success? end def test_failed_3ds_validate_authorize_request - assert response = @gateway.authorize(1202, @three_ds_invalid_card, @options.merge(payer_auth_validate_service: true, pares: pares)) + assert response = @gateway.authorize(1202, @three_ds_invalid_card, @three_ds_options) + assert_equal '476', response.params['reasonCode'] assert !response.success? end - def pares - <<-PARES -eNqdmFuTqkgSgN+N8D90zD46M4B3J+yOKO6goNyFN25yEUHkUsiv31K7T/ec6dg9u75YlWRlZVVmflWw1uNrGNJa6DfX8G0thVXlRuFLErz+tgm67sRlbJr3ky4G9LWn8N/e1nughtVD4dFawFAodT8OqbBx4NLdj/o8y3JqKlavSLsNr1VS5G/En/if4zX20UUTXf3Yzeu3teuXpCC/TeerMTFfY+/d9Tm8CvRbEB7dJqvX2LO7xj7H7Zt7q0JOd0nwpo3VacjVvMc4pZcXfcjFpMqLc6UHr2vsrrEO3Dp8G+P4Ap+PZy/E9C+c+AtfrrGHfH25mwPnokG2CRxfY18Fa7Q71zD3b2/LKXr0o7cOu0uRh0gDre1He419+nZx8zf87z+kepeu9cPbuk7OX31a3X0iFmvsIV9XtVs31Zu9xt5ba99t2zcAAAksNjsr4N5MVctyGIaN2H6E1vpQWYd+8obPkFPo/zEKZFFxTer4fHf174I1dncFe4Tzba0lUY4mu4Yv3TnLURDjur78hWEQwj/h5M/iGmHIYRzDVxhSCKok+tdvz1FhIOTH4n8aRrl5kSe+myW9W6PEkMI6LoKXH759Z0ZX75YITGWoP5CpP3ximv9xl+ATYoZsYt8b/bKyX5nlZ2evlftHFbvEfYKfDL2t1fAY3jMifDFU4fW3f/1KZdBJFFb1/+PKhxtfLXzYM92sCd8qN5U5lrrNDZOFzkiecUIszvyCVJjXj3FPzTX2w/f3hT2j+GW3noobXm8xXJ3KK2aZztNbVdsLWbbOASZgzSY45eYqFNiK5ReRNLKbzvZSIDJj+zqBzIEkIx1L9ZTabYeDJa/MV51fF9A0dxDvxzf5CiPmttuVVBLHxmZSNp53lnBcJzh+IS3YpejKebycHjQlvMggwkvHdZjhYBHf8M1R4ikKjHxMGxlCfuCv+IqmxjTRk9GMnO2ynnXsWMvZSYdlk+Vmvpz1pVns4v05ugRWIGZNMhxUGLzoqs+VDe14Jtzli63TT06WBvpJg2+2UVLie+5mgGDlEVjip+7EmZhCvRdndtQHmKm0vaUDejhYTRgglbR5qysx6I1gf+vTyWJ3ahaXNOWBUrXRYnwasbKlbi3XsJLNuA3g6+uXrHqPzCa8PSNxmKElubX7bGmNl4Z+LbuIEJT8SrnXIMnd7IUOz8XLI4DX3192xucDQGlI8NmnijOiqR/+/rJ9lRCvCqSv6a+7OCl+f6FeDW2N/TzPY2IqvNbJEdUVwqUkCLTVo32vtAhAgQSRQAFNgLRii5vCEeLWl4HCsKQCoJMyWwmcOEAYDBlLlGlKHa2DLRnJ5nCAhkoksypca9nxKfDvUhIUEmvIsX9WL96ZrZTxqvYs82aPjQi1bz7NaBIJHhYpCEXplJ2GA8ea4a7lXCRVgUxk06ai0DSoDecg4wIvE3ZC0ooOQhbinUQzNyn1OzkFM5kWXSS7PWVKNxx8SCV+2VE9EJ8+2TrITF1ScEjBh3WBgere5bJWUpb3ld9lPAMd+e6JNxGQJS4F9vuKdObLigRGbj2LyPyznEmqAZmnxS0DO9o+iCfXmsUeRZIKIXW8Djy0Tw8rks4yX62omWctI2Oc5d7ZvKGokEIKZDI6lfEp4VYQJ+9RAGBHAWUJ7s+HAyraoB4DSmYSEIl4LuOMDMYCIZJ71pj7U99OwbapLHXFMLI66s7eKosO9qmWU56LwmJCul2tccin+XTKE4tV7EatfZaSNCQFH9bYXMNCetuoK2kl0SN6An3f3xmIMwGIT8KlZZS5pV/wpTIz8FzIF9fhIK6EhVLuzEDAg4MI+sybxjVzA/TGuEmsEHDZbZFBtjKxdKfgilSRZDLRoGjQmpWlzUEZGeJ+7CK6jCNPPgQe2ZInYsxH5YEWZoId7i5G2RJNax3USyCJo1OXS/jNLKdCtZiMSaCR4jKPaXvXqjl/6Et+OMBDRoth7MfSnLa3o7ItpxyV8CZcmjrVbJtyWykIypti158qotvx1VkJTm48GzeYBAUaKIAsJhUcDkL9mUO8KjEgBUCiIEdZFKcBjhsxAkpL5cjGxN7nzMYgZElgguweT/ugZg5F0s5BfGT2cGCPWdzRQfCwpkzRoa8YasSpRuIhBMUdRVxBGyn1FouIkytA/p5XKp4iAEO2AMZRSKQkIPDhgLC0ZSKTIV5IsXXC55ue+a566chmgKyLBwZfHlr7igWzo4Dn4m63WjXm3kMV3G7GNc3KJz9Ur5pt1AxBnafhdFf03bi2pnQlT8pZhWNWN7Mu+6RtWe/I6AbUz1wcFd6puR7FdrSYDwcYP5lcIsJ0ZNh7zOxcqcSFOjoUhaui645OzZ5qHGeazOnrqlxJ1+2eSJtTNOo7bBrgyvIanQyHuh9xP/PqO4BROI0Alp6/AOzbLYAh/asAo/t78d0L1ZdQ/mVerrZ+yoQSCZ+wiqCpjNmbw2WNbXW0NyZqFNzU0Uh0dHgTEUqqABnwhAENTjfNUu9WLs751LE60N8xINGsmvkTJTLOqzag/g624UDS72hjelmXP9GmKz9kEmf/R7DR4Ak2ZEmdQv7pz4YmzU84fQHYHWZ+DjomBcrTYiVRuig6KJ1R5Z5dhD5kiRQeewAg3Jqc2SOv+8ASIgVnYOQsf9558pl8OIIWJ4KCQ4u+QWKmIqgK7g5MOZ+0XJ4jemPuucVRUPf5rma5LL6U7RxuXQ4ax+NodrIvC4k53wRDanhGdkGrnhJRq2/UajccHM67ebQItvRyk3PEnFrl1y5dFuT0PEFYMqbn0dG2dlx+js/7Yt7HZFuSVXvsV5OYiTYHec4EG7kxo+GgKfvamoPtDhry3CPLjaJN7okBAJeGPTl7z5+AgQolAQC3wBZtwRGA7U2ViJFJcmnxxgo+jjHdwGGkjs0G5UYccOYJ7XDmP7IgS+9QkEj8YY2OFIsk1WUi3MTJQTed7U3A2YUW3Vh3OND14irp4PiAhSYxHA2siFSZKN1jhOVFme2MOa7LKcst80SEKId+OjqM+9GBjoxIIZfNxsBWkyVmbmYUa4iJghm7gzu+8jeiAxMvJwhiR80zcl4FSr2Q01jx442ebHWlimZHrNQymRgOto7dtFMgbPTdxmG4ayKWQJ+Lp3K0OcQ1rU2jtLyw+XKXOqWoLo7ulVFHgTebYaLWXho+Sr1OPy7AcHCGCar/njbEqWk2ib1Z6iWb3cbm1eTZ6PVXIdCmCAJJ+AEBEYh0tx8xmanGGwngHKWVnCZ4E/qRkgaQ+OgfpYOS+5vi+XoroMHnreA/3XIQBP7LPefzlvPj1oBuOd3zlsOKrYegcC+p4YCPfRmFv5NSZiLpNpR1cLPusvQhw3/IUnIqKRWknr5yDBRNo2dkCVSPmdGNAUBGH8cXr2f29z15gBBCTrfuBb66/SokhoP/gglTIqUPSEjvkNC88QpHo0kEguNHRIaDj5igJAWIBjKgKTJRNmSkUNPwevRaVWGow9Vezev9QtlZJaWDcZpjs3SywiKsxD0p8RVKHQ6u49ExWZz6zY28KaVz4ntbnC0nGDi0G9GFeM2id5cJkwbRKezMS2ZrYcnsZzuDlqaRqx0XJS9F5h6VycYt8nF7TfnOCimzY5NpNyWLIBPzY4ZhNZdu8FKm+3pxwqZyqLHWzSsT5f2mQACop8+THcXu42wXhB5bmeepaHFBHFcOzM7lZZr4DPOPs/073eHgQ5sGD22dBAZE4SSx/vtijxSQsEuSy0gWSqEshkxiw9xVEJhqg78mbmrU3nxGzJe1fLxwDDO59rxHzgrpzPiHrvK8WlDJpo33y3MdhU7GZ81W6fFSHfnjYpbBcDjo4CLNjoAvSxRlLaU2W76plphc5At/tEhKra8VXiLN0FuM59Ddt5zgHZitL1vFyttHamkZ44sToxvD5ubwK/BtsWOfr03Yj1epz5esx7ekx8eu+/ePrx/B/g0UAjN8 - PARES + def test_successful_authorize_via_normalized_3ds2_fields + options = @options.merge( + three_d_secure: { + version: '2.0', + eci: '05', + cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA=', + xid: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=', + ds_transaction_id: '97267598-FAE6-48F2-8083-C23433990FBC', + cavv_algorithm: 1, + enrolled: 'Y', + authentication_response_status: 'Y' + }, + commerce_indicator: 'vbv' + ) + + response = @gateway.authorize(@amount, @three_ds_enrolled_card, options) + assert_successful_response(response) + end + + def test_successful_mastercard_authorize_via_normalized_3ds2_fields + options = @options.merge( + three_d_secure: { + version: '2.0', + eci: '05', + cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA=', + xid: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=', + ds_transaction_id: '97267598-FAE6-48F2-8083-C23433990FBC' + }, + commerce_indicator: 'spa', + collection_indicator: 2 + ) + + response = @gateway.authorize(@amount, @three_ds_enrolled_mastercard, options) + assert_successful_response(response) + end + + def test_successful_purchase_via_normalized_3ds2_fields + options = @options.merge( + three_d_secure: { + version: '2.0', + eci: '05', + cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA=', + xid: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=', + ds_transaction_id: '97267598-FAE6-48F2-8083-C23433990FBC' + } + ) + + response = @gateway.purchase(@amount, @three_ds_enrolled_card, options) + assert_successful_response(response) + end + + def test_successful_mastercard_purchase_via_normalized_3ds2_fields + options = @options.merge( + three_d_secure: { + version: '2.0', + eci: '05', + cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA=', + xid: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=', + ds_transaction_id: '97267598-FAE6-48F2-8083-C23433990FBC' + }, + commerce_indicator: 'spa', + collection_indicator: 2 + ) + + response = @gateway.purchase(@amount, @three_ds_enrolled_mastercard, options) + assert_successful_response(response) + end + + def test_successful_first_cof_authorize + @options[:stored_credential] = { + initiator: 'cardholder', + reason_type: '', + initial_transaction: true, + network_transaction_id: '' + } + @options[:commerce_indicator] = 'internet' + assert response = @gateway.authorize(@amount, @credit_card, @options) + assert_successful_response(response) + end + + def test_successful_subsequent_unscheduled_cof_authorize + @options[:stored_credential] = { + initiator: 'merchant', + reason_type: 'unscheduled', + initial_transaction: false, + network_transaction_id: '016150703802094' + } + assert response = @gateway.authorize(@amount, @credit_card, @options) + assert_successful_response(response) + end + + def test_successful_recurring_cof_authorize + @options[:stored_credential] = { + initiator: 'merchant', + reason_type: 'recurring', + initial_transaction: false, + network_transaction_id: '' + } + assert response = @gateway.authorize(@amount, @credit_card, @options) + assert_successful_response(response) + end + + def test_successful_subsequent_recurring_cof_authorize + @options[:stored_credential] = { + initiator: 'merchant', + reason_type: 'recurring', + initial_transaction: false, + network_transaction_id: '016150703802094' + } + assert response = @gateway.authorize(@amount, @credit_card, @options) + assert_successful_response(response) + end + + def test_successful_subsequent_installment_cof_authorize + @options[:stored_credential] = { + initiator: 'merchant', + reason_type: 'installment', + initial_transaction: false, + network_transaction_id: '016150703802094' + } + assert response = @gateway.authorize(@amount, @credit_card, @options) + assert_successful_response(response) + end + + def test_successful_subsequent_unscheduled_cof_purchase + @options[:stored_credential] = { + initiator: 'merchant', + reason_type: 'unscheduled', + initial_transaction: false, + network_transaction_id: '016150703802094' + } + assert response = @gateway.purchase(@amount, @credit_card, @options) + 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: 'authentication_outage')) + 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: { + address1: 'Unspecified', + city: 'Unspecified', + state: 'NC', + zip: '1234567890', + country: 'US' + } + }) + + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_equal 'One or more fields contains invalid data: c:billTo/c:postalCode', response.message + end + + def test_successful_verify_with_elo + response = @gateway.verify(@elo_credit_card, @options) + assert_successful_response(response) end def test_verify_credentials @@ -446,4 +1449,34 @@ def test_verify_credentials assert !gateway.verify_credentials end + def test_successful_verify + response = @gateway.verify(@credit_card, @options) + assert_success response + assert_match '1.00', response.params['amount'] + assert_equal 'Successful transaction', response.message + end + + def test_successful_verify_zero_amount_visa + @options[:zero_amount_auth] = true + response = @gateway.verify(@credit_card, @options) + assert_success response + assert_match '0.00', response.params['amount'] + assert_equal 'Successful transaction', response.message + end + + def test_successful_verify_zero_amount_master + @options[:zero_amount_auth] = true + response = @gateway.verify(@master_credit_card, @options) + assert_success response + assert_match '0.00', response.params['amount'] + assert_equal 'Successful transaction', response.message + end + + private + + def assert_successful_response(response) + assert_equal 'Successful transaction', response.message + assert_success response + assert response.test? + end end diff --git a/test/remote/gateways/remote_d_local_test.rb b/test/remote/gateways/remote_d_local_test.rb new file mode 100644 index 00000000000..e2698bee482 --- /dev/null +++ b/test/remote/gateways/remote_d_local_test.rb @@ -0,0 +1,406 @@ +require 'test_helper' +ActiveMerchant::Billing::DLocalGateway.application_id = 'ActiveMerchant' +class RemoteDLocalTest < Test::Unit::TestCase + def setup + @gateway = DLocalGateway.new(fixtures(:d_local)) + + @amount = 1000 + @credit_card = credit_card('4111111111111111') + @credit_card_naranja = credit_card('5895627823453005') + @cabal_credit_card = credit_card('5896 5700 0000 0004') + # No test card numbers, all txns are approved by default, + # but errors can be invoked directly with the `description` field + @options = { + billing_address: address(country: 'Brazil'), + document: '71575743221', + currency: 'BRL' + } + @options_colombia = { + billing_address: address(country: 'Colombia'), + document: '11186456', + currency: 'COP' + } + @options_argentina = { + billing_address: address(country: 'Argentina'), + document: '10563145', + currency: 'ARS' + } + @options_argentina_installments = { + billing_address: address(country: 'Argentina'), + document: '10563145', + currency: 'ARS', + installments: '3', + installments_id: 'INS54434' + } + @options_mexico = { + billing_address: address(country: 'Mexico'), + document: '128475869794933', + currency: 'MXN' + } + @options_peru = { + billing_address: address(country: 'Peru'), + document: '184853849', + currency: 'PEN' + } + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_match 'The payment was paid', response.message + end + + def test_successful_purchase_with_country_override + options = { + billing_address: address(country: 'Mexico'), + document: '71575743221', + currency: 'BRL', + country: 'Brazil' + } + + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_match 'The payment was paid', response.message + assert_match 'BR', response.params['card']['country'] + end + + def test_successful_purchase_with_ip_and_phone + response = @gateway.purchase(@amount, @credit_card, @options.merge(ip: '127.0.0.1')) + assert_success response + assert_match 'The payment was paid', response.message + end + + def test_successful_purchase_with_save_option + response = @gateway.purchase(@amount, @credit_card, @options.merge(save: true)) + assert_success response + assert_equal true, response.params['card']['save'] + assert_equal 'CREDIT', response.params['card']['type'] + assert_not_empty response.params['card']['card_id'] + assert_match 'The payment was paid', response.message + end + + def test_successful_purchase_with_network_tokens + credit_card = network_tokenization_credit_card('4242424242424242', payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=') + response = @gateway.purchase(@amount, credit_card, @options) + assert_success response + assert_match 'The payment was paid', response.message + end + + def test_successful_purchase_with_network_tokens_and_store_credential_type + 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, 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'] + assert_match 'The payment was paid', response.message + end + + def test_successful_purchase_with_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 * 50, @credit_card_naranja, @options_argentina) + assert_success response + assert_match 'The payment was paid', response.message + end + + def test_successful_purchase_cabal + response = @gateway.purchase(@amount, @cabal_credit_card, @options) + assert_success response + assert_match 'The payment was paid', response.message + end + + def test_successful_inquire_with_payment_id + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_match 'The payment was paid', response.message + + authorization = response.params['id'] + response = @gateway.inquire(authorization, @options) + assert_success response + assert_match 'PAID', response.params['status'] + assert_match 'The payment was paid.', response.params['status_detail'] + end + + def test_successful_inquire_with_order_id + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_match 'The payment was paid', response.message + + purchase_payment_id = response.params['id'] + order_id = response.params['order_id'] + + response = @gateway.inquire(nil, { order_id: }) + check_payment_id = response.params['payment_id'] + assert_success response + assert_match purchase_payment_id, check_payment_id + end + + def test_successful_purchase_with_original_order_id + response = @gateway.purchase(@amount, @credit_card, @options.merge(original_order_id: '123ABC')) + assert_success response + assert_match 'The payment was paid', response.message + assert_match '123ABC', response.params['original_order_id'] + end + + def test_successful_purchase_with_more_options + options = @options.merge( + order_id: '1', + ip: '127.0.0.1', + device_id: '123', + email: 'joe@example.com', + birth_date: '03-01-1970', + document2: '87648987569', + idempotency_key: generate_unique_id, + user_reference: generate_unique_id + ) + + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_match 'The payment was paid', response.message + end + + def test_successful_purchase_with_additional_data + options = @options.merge( + additional_data: { submerchant: { name: 'socks' } } + ) + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_match 'The payment was paid', response.message + end + + def test_successful_purchase_with_force_type_debit + options = @options_argentina.merge(force_type: 'DEBIT') + + response = @gateway.purchase(@amount * 50, @credit_card, options) + assert_success response + assert_match 'The payment was paid', response.message + end + + # You may need dLocal to enable your test account to support individual countries + def test_successful_purchase_colombia + response = @gateway.purchase(100000, @credit_card, @options_colombia) + assert_success response + assert_match 'The payment was paid', response.message + end + + def test_successful_purchase_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, @cabal_credit_card, @options_mexico) + assert_success response + assert_match 'The payment was paid', response.message + end + + def test_successful_purchase_peru + response = @gateway.purchase(@amount, @credit_card, @options_peru) + assert_success response + assert_match 'The payment was paid', response.message + end + + def test_successful_purchase_partial_address + response = @gateway.purchase(@amount, @credit_card, @options.merge(billing_address: address(address1: 'My Street', country: 'Brazil'))) + assert_success response + assert_match 'The payment was paid', response.message + end + + def test_failed_purchase + response = @gateway.purchase(@amount, @credit_card, @options.merge(description: '300')) + assert_failure response + assert_match 'The payment was rejected', response.message + end + + def test_failed_purchase_with_network_tokens + credit_card = network_tokenization_credit_card( + '4242424242424242', + payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=' + ) + response = @gateway.purchase(@amount, credit_card, @options.merge(description: '300')) + assert_failure response + assert_match 'The payment was rejected', response.message + end + + def test_failed_document_format + response = @gateway.purchase(@amount, @credit_card, @options.merge(document: 'bad_document')) + assert_failure response + assert_match 'Invalid parameter: payer.document', response.message + end + + def test_successful_authorize_and_capture + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + assert_match 'The payment was authorized', auth.message + + assert capture = @gateway.capture(@amount, auth.authorization, @options) + assert_success capture + assert_match 'The payment was paid', capture.message + end + + def test_successful_authorize_and_capture_with_cabal + auth = @gateway.authorize(@amount, @cabal_credit_card, @options) + assert_success auth + assert_match 'The payment was authorized', auth.message + + assert capture = @gateway.capture(@amount, auth.authorization, @options) + assert_success capture + assert_match 'The payment was paid', capture.message + end + + def test_failed_authorize + response = @gateway.authorize(@amount, @credit_card, @options.merge(description: '309')) + assert_failure response + assert_equal '309', response.error_code + assert_match 'Card expired', 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, @options) + assert_success capture + end + + def test_failed_capture + response = @gateway.capture(@amount, 'bad_id') + assert_failure response + + assert_equal '4000', response.error_code + assert_match 'Payment not found', response.message + end + + def test_successful_refund + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization, @options.merge(notification_url: 'http://example.com')) + assert_success refund + assert_match 'The refund was paid', refund.message + end + + def test_partial_refund + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount - 1, purchase.authorization, @options.merge(notification_url: 'http://example.com')) + assert_success refund + end + + def test_failed_refund + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + response = @gateway.refund(@amount + 100, purchase.authorization, @options.merge(notification_url: 'http://example.com')) + assert_failure response + assert_match 'Amount exceeded', response.message + end + + def test_successful_refund_with_description + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization, @options.merge(notification_url: 'http://example.com', description: 'test')) + assert_success refund + assert_match 'The refund was paid', refund.message + assert_match 'test', refund.params['description'] + end + + def test_successful_void + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert void = @gateway.void(auth.authorization) + assert_success void + assert_match 'The payment was cancelled', void.message + end + + def test_failed_void + response = @gateway.void('') + assert_failure response + assert_match 'Invalid request', response.message + end + + def test_successful_verify + response = @gateway.verify(@credit_card, @options) + assert_success response + assert_equal 0, response.params['amount'] + assert_match %r{The payment was verified}, response.message + end + + def test_successful_verify_with_cabal + response = @gateway.verify(@cabal_credit_card, @options) + assert_success response + assert_equal 0, response.params['amount'] + assert_match %r{The payment was verified}, response.message + end + + def test_failed_verify + response = @gateway.verify(@credit_card, @options.merge(description: '315')) + assert_failure response + assert_equal '315', response.error_code + assert_match %r{Invalid security code}, response.message + end + + def test_invalid_login + gateway = DLocalGateway.new(login: '', trans_key: '', secret_key: '') + + response = gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_match %r{Invalid parameter}, 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[:trans_key], transcript) + end + + def test_successful_authorize_with_3ds_v1_options + @options[:three_d_secure] = { + version: '1.0', + cavv: '3q2+78r+ur7erb7vyv66vv\/\/\/\/8=', + eci: '05', + xid: 'ODUzNTYzOTcwODU5NzY3Qw==', + enrolled: 'true', + authentication_response_status: 'Y' + } + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + assert_match 'The payment was authorized', auth.message + end + + def test_successful_authorize_with_3ds_v2_options + @options[:three_d_secure] = { + version: '2.2.0', + cavv: '3q2+78r+ur7erb7vyv66vv\/\/\/\/8=', + eci: '05', + ds_transaction_id: 'ODUzNTYzOTcwODU5NzY3Qw==', + enrolled: 'Y', + authentication_response_status: 'Y' + } + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + assert_match 'The payment was authorized', auth.message + end +end diff --git a/test/remote/gateways/remote_data_cash_test.rb b/test/remote/gateways/remote_data_cash_test.rb index 9ff6635e5f9..c1297dca20b 100644 --- a/test/remote/gateways/remote_data_cash_test.rb +++ b/test/remote/gateways/remote_data_cash_test.rb @@ -1,64 +1,62 @@ require 'test_helper' class RemoteDataCashTest < Test::Unit::TestCase - def setup # gateway to connect to Datacash @gateway = DataCashGateway.new(fixtures(:data_cash)) @mastercard = CreditCard.new( - :number => '5120790000000034', - :month => 3, - :year => Date.today.year + 2, - :first_name => 'Mark', - :last_name => 'McBride', - :brand => :master + number: '5120790000000034', + month: 3, + year: Date.today.year + 2, + first_name: 'Mark', + last_name: 'McBride', + brand: :master ) @mastercard_declined = CreditCard.new( - :number => '5473000000000106', - :month => 3, - :year => Date.today.year + 2, - :first_name => 'Mark', - :last_name => 'McBride', - :brand => :master, - :verification_value => '547' + number: '5473000000000106', + month: 3, + year: Date.today.year + 2, + first_name: 'Mark', + last_name: 'McBride', + brand: :master, + verification_value: '547' ) @visa_delta = CreditCard.new( - :number => '4539792100000003', - :month => 3, - :year => Date.today.year + 2, - :first_name => 'Mark', - :last_name => 'McBride', - :brand => :visa, - :verification_value => '444' + number: '4539792100000003', + month: 3, + year: Date.today.year + 2, + first_name: 'Mark', + last_name: 'McBride', + brand: :visa, + verification_value: '444' ) @solo = CreditCard.new( - :first_name => 'Cody', - :last_name => 'Fauser', - :number => '633499100000000004', - :month => 3, - :year => Date.today.year + 2, - :brand => :solo, - :issue_number => 5, - :start_month => 12, - :start_year => 2006 + first_name: 'Cody', + last_name: 'Fauser', + number: '633499100000000004', + month: 3, + year: Date.today.year + 2, + brand: :solo, + start_month: 12, + start_year: 2006 ) @address = { - :name => 'Mark McBride', - :address1 => 'Flat 12/3', - :address2 => '45 Main Road', - :city => 'Sometown', - :state => 'Somecounty', - :zip => 'A987AA', - :phone => '(555)555-5555' + name: 'Mark McBride', + address1: 'Flat 12/3', + address2: '45 Main Road', + city: 'Sometown', + state: 'Somecounty', + zip: 'A987AA', + phone: '(555)555-5555' } @params = { - :order_id => generate_unique_id + order_id: generate_unique_id } @amount = 198 @@ -72,8 +70,8 @@ def test_successful_purchase assert response.test? end - #the amount is changed to £1.99 - the DC test server won't check the - #address details - this is more a check on the passed ExtendedPolicy + # the amount is changed to £1.99 - the DC test server won't check the + # address details - this is more a check on the passed ExtendedPolicy def test_successful_purchase_without_address_check response = @gateway.purchase(199, @mastercard, @params) assert_success response @@ -109,8 +107,8 @@ def test_successful_purchase_with_account_set_up_and_repeat_payments assert !response.authorization.to_s.split(';')[2].blank? assert response.test? - #Make second payment on the continuous authorization that was set up in the first purchase - second_order_params = { :order_id => generate_unique_id } + # Make second payment on the continuous authorization that was set up in the first purchase + second_order_params = { order_id: generate_unique_id } purchase = @gateway.purchase(201, response.authorization, second_order_params) assert_success purchase end @@ -121,8 +119,8 @@ def test_successful_purchase_with_account_set_up_and_repeat_payments_with_visa_d assert_success response assert !response.authorization.to_s.split(';')[2].blank? - #Make second payment on the continuous authorization that was set up in the first purchase - second_order_params = { :order_id => generate_unique_id } + # Make second payment on the continuous authorization that was set up in the first purchase + second_order_params = { order_id: generate_unique_id } purchase = @gateway.purchase(201, response.authorization, second_order_params) assert_success purchase end @@ -135,20 +133,20 @@ def test_purchase_with_account_set_up_for_repeat_payments_fails_for_solo_card end def test_successful_authorization_and_capture_with_account_set_up_and_second_purchase - #Authorize first payment + # Authorize first payment @params[:set_up_continuous_authority] = true first_authorization = @gateway.authorize(@amount, @mastercard, @params) assert_success first_authorization assert !first_authorization.authorization.to_s.split(';')[2].blank? assert first_authorization.test? - #Capture first payment + # Capture first payment capture = @gateway.capture(@amount, first_authorization.authorization, @params) assert_success capture assert capture.test? - #Collect second purchase - second_order_params = { :order_id => generate_unique_id } + # Collect second purchase + second_order_params = { order_id: generate_unique_id } purchase = @gateway.purchase(201, first_authorization.authorization, second_order_params) assert_success purchase assert purchase.test? @@ -165,6 +163,7 @@ def test_duplicate_order_id end def test_invalid_verification_number + @mastercard.number = 1000350000000007 @mastercard.verification_value = 123 response = @gateway.purchase(@amount, @mastercard, @params) assert_failure response @@ -310,7 +309,7 @@ def test_successful_refund_of_a_repeat_payment assert response.test? # Make second payment on the continuous authorization that was set up in the first purchase - second_order_params = { :order_id => generate_unique_id } + second_order_params = { order_id: generate_unique_id } purchase = @gateway.purchase(201, response.authorization, second_order_params) assert_success purchase @@ -326,7 +325,7 @@ def test_order_id_that_is_too_short end def test_order_id_that_is_too_long - @params[:order_id] = "#{@params[:order_id]}1234356" + @params[:order_id] = "#{@params[:order_id]}1234356" response = @gateway.purchase(@amount, @mastercard, @params) assert_success response assert response.test? diff --git a/test/remote/gateways/remote_datatrans_test.rb b/test/remote/gateways/remote_datatrans_test.rb new file mode 100644 index 00000000000..5415f84e886 --- /dev/null +++ b/test/remote/gateways/remote_datatrans_test.rb @@ -0,0 +1,310 @@ +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: Time.now.year + 1) + @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 + @no_country_billing_address = address(country: nil) + + @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_succesful_store_transaction + store = @gateway.store(@credit_card, @options) + assert_success store + assert_include store.params, 'overview' + assert_equal store.params['overview'], { 'total' => 1, 'successful' => 1, 'failed' => 0 } + assert store.params['responses'].is_a?(Array) + assert_include store.params['responses'][0], 'alias' + assert_equal store.params['responses'][0]['maskedCC'], '424242xxxxxx4242' + assert_include store.params['responses'][0], 'fingerprint' + end + + def test_successful_unstore + store = @gateway.store(@credit_card, @options) + assert_success store + + unstore = @gateway.unstore(store.authorization, @options) + assert_success unstore + assert_equal unstore.params['response_code'], 204 + end + + def test_successful_store_purchase_unstore_flow + store = @gateway.store(@credit_card, @options) + assert_success store + + purchase = @gateway.purchase(@amount, store.authorization, @options) + assert_success purchase + assert_include purchase.params, 'transactionId' + + # second purchase to validate multiple use token + second_purchase = @gateway.purchase(@amount, store.authorization, @options) + assert_success second_purchase + + unstore = @gateway.unstore(store.authorization, @options) + assert_success unstore + + # purchase after unstore to validate deletion + response = @gateway.purchase(@amount, store.authorization, @options) + assert_failure response + assert_equal response.error_code, 'INVALID_ALIAS' + assert_equal response.message, 'authorize.card.alias' + 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_successful_verify + verify_response = @gateway.verify(@credit_card, @options) + assert_success verify_response + end + + def test_failed_verify + verify_response = @gateway.verify(@credit_card, @options.merge({ currency: 'DKK' })) + assert_failure verify_response + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @credit_card, @options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@credit_card.number, transcript) + assert_scrubbed(@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_no_country_billing_address + response = @gateway.purchase(@amount, @credit_card, @options.merge({ billing_address: @no_country_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 new file mode 100644 index 00000000000..27282cf5c4a --- /dev/null +++ b/test/remote/gateways/remote_decidir_plus_test.rb @@ -0,0 +1,357 @@ +require 'test_helper' +require 'securerandom' + +class RemoteDecidirPlusTest < Test::Unit::TestCase + def setup + @gateway_purchase = DecidirPlusGateway.new(fixtures(:decidir_plus)) + @gateway_auth = DecidirPlusGateway.new(fixtures(:decidir_plus_preauth)) + + @amount = 100 + @credit_card = credit_card('4484590159923090') + @american_express = credit_card('376414000000009') + @cabal = credit_card('5896570000000008') + @patagonia_365 = credit_card('5046562602769006') + @visa_debit = credit_card('4517721004856075') + @declined_card = credit_card('4000300011112220') + @options = { + billing_address: address, + description: 'Store Purchase', + card_brand: 'visa' + } + @sub_payments = [ + { + site_id: '04052018', + installments: 1, + amount: 1500 + }, + { + site_id: '04052018', + installments: '1', + amount: '1500' + } + ] + @fraud_detection = { + send_to_cs: 'false', + channel: 'Web', + dispatch_method: 'Store Pick Up', + csmdds: [ + { + code: '17', + description: 'Campo MDD17' + } + ] + } + @aggregate_data = { + indicator: '1', + identification_number: '308103480', + bill_to_pay: 'test1', + bill_to_refund: 'test2', + merchant_name: 'Heavenly Buffaloes', + street: 'Sesame', + number: '123', + postal_code: '22001', + category: 'yum', + channel: '005', + geographic_code: 'C1234', + city: 'Ciudad de Buenos Aires', + merchant_id: 'dec_agg', + province: 'Buenos Aires', + country: 'Argentina', + merchant_email: 'merchant@mail.com', + merchant_phone: '2678433111' + } + end + + def test_successful_purchase + assert response = @gateway_purchase.store(@credit_card) + payment_reference = response.authorization + + response = @gateway_purchase.purchase(@amount, payment_reference, @options) + assert_success response + assert_equal 'approved', response.message + end + + def test_failed_purchase + assert @gateway_purchase.store(@credit_card) + + response = @gateway_purchase.purchase(@amount, '', @options) + assert_failure response + assert_equal 'invalid_param: token', response.message + end + + def test_successful_authorize_and_capture + options = @options.merge(fraud_detection: @fraud_detection) + + assert response = @gateway_auth.store(@credit_card, options) + payment_reference = response.authorization + + response = @gateway_auth.authorize(@amount, payment_reference, options) + assert_success response + + assert capture_response = @gateway_auth.capture(@amount, response.authorization, options) + assert_success capture_response + end + + def test_failed_authorize + options = @options.merge(fraud_detection: @fraud_detection) + + assert response = @gateway_auth.store(@declined_card, options) + payment_reference = response.authorization + + response = @gateway_auth.authorize(@amount, payment_reference, options) + assert_failure response + assert_equal response.error_code, 3 + end + + def test_successful_refund + response = @gateway_purchase.store(@credit_card) + + purchase = @gateway_purchase.purchase(@amount, response.authorization, @options) + assert_success purchase + assert_equal 'approved', purchase.message + + assert refund = @gateway_purchase.refund(@amount, purchase.authorization) + assert_success refund + assert_equal 'approved', refund.message + end + + def test_partial_refund + assert response = @gateway_purchase.store(@credit_card) + + purchase = @gateway_purchase.purchase(@amount, response.authorization, @options) + assert_success purchase + + assert refund = @gateway_purchase.refund(@amount - 1, purchase.authorization) + assert_success refund + end + + def test_failed_refund + response = @gateway_purchase.refund(@amount, '') + assert_failure response + assert_equal 'not_found_error', response.message + end + + def test_successful_void + options = @options.merge(fraud_detection: @fraud_detection) + + assert response = @gateway_auth.store(@credit_card, options) + payment_reference = response.authorization + + response = @gateway_auth.authorize(@amount, payment_reference, options) + assert_success response + assert_equal 'pre_approved', response.message + authorization = response.authorization + + assert void_response = @gateway_auth.void(authorization) + assert_success void_response + end + + def test_failed_void + assert response = @gateway_auth.void('') + assert_failure response + assert_equal 'not_found_error', response.message + end + + def test_successful_verify + assert response = @gateway_auth.verify(@credit_card, @options.merge(fraud_detection: @fraud_detection)) + assert_success response + assert_equal 'active', response.message + end + + def test_failed_verify + assert response = @gateway_auth.verify(@declined_card, @options) + assert_failure response + assert_equal '10734: Fraud Detection Data is required', response.message + end + + def test_successful_store + assert response = @gateway_purchase.store(@credit_card) + assert_success response + assert_equal 'active', response.message + assert_equal @credit_card.number[0..5], response.authorization.split('|')[1] + end + + def test_successful_store_name_override + @credit_card.name = '' + options = { name_override: 'Rick Deckard' } + assert response = @gateway_purchase.store(@credit_card, options) + assert_success response + assert_equal 'active', response.message + assert_equal options[:name_override], response.params.dig('cardholder', 'name') + end + + def test_successful_unstore + customer = { + id: 'John', + email: 'decidir@decidir.com' + } + + assert response = @gateway_purchase.store(@credit_card) + payment_reference = response.authorization + + response = @gateway_purchase.purchase(@amount, payment_reference, @options.merge({ customer: })) + assert_success response + + assert_equal 'approved', response.message + token_id = response.params['customer_token'] + + assert unstore_response = @gateway_purchase.unstore(token_id) + assert_success unstore_response + end + + def test_successful_purchase_with_options + options = @options.merge(sub_payments: @sub_payments) + + assert response = @gateway_purchase.store(@credit_card) + payment_reference = response.authorization + + response = @gateway_purchase.purchase(@amount, payment_reference, options) + assert_success response + assert_equal 'approved', response.message + end + + def test_successful_purchase_with_fraud_detection + options = @options.merge(fraud_detection: @fraud_detection) + + assert response = @gateway_purchase.store(@credit_card) + payment_reference = response.authorization + + response = @gateway_purchase.purchase(@amount, payment_reference, options) + assert_success response + assert_equal({ 'send_to_cs' => false, 'status' => nil }, response.params['fraud_detection']) + end + + def test_successful_purchase_with_wallet_id + options = @options.merge(wallet_id: 'moto') + + assert response = @gateway_purchase.store(@credit_card) + payment_reference = response.authorization + + response = @gateway_purchase.purchase(@amount, payment_reference, options) + assert_success response + assert_equal 1, response.params['payment_method_id'] + end + + def test_successful_purchase_with_card_brand + options = @options.merge(card_brand: 'cabal') + + assert response = @gateway_purchase.store(@cabal) + payment_reference = response.authorization + + response = @gateway_purchase.purchase(@amount, payment_reference, options) + assert_success response + assert_equal 63, response.params['payment_method_id'] + end + + def test_successful_purchase_with_card_brand_patagonia_365 + options = @options.merge(card_brand: 'patagonia_365') + + assert response = @gateway_purchase.store(@patagonia_365) + payment_reference = response.authorization + + response = @gateway_purchase.purchase(@amount, payment_reference, options) + assert_success response + assert_equal 55, response.params['payment_method_id'] + end + + def test_successful_purchase_with_payment_method_id + options = @options.merge(payment_method_id: '63') + + assert response = @gateway_purchase.store(@cabal) + payment_reference = response.authorization + + response = @gateway_purchase.purchase(@amount, payment_reference, options) + assert_success response + assert_equal 63, response.params['payment_method_id'] + end + + def test_successful_purchase_with_establishment_name + establishment_name = 'Heavenly Buffaloes' + options = @options.merge(establishment_name:) + + assert response = @gateway_purchase.store(@credit_card) + payment_reference = response.authorization + + response = @gateway_purchase.purchase(@amount, payment_reference, options) + assert_success response + assert_equal 'approved', response.message + end + + def test_successful_purchase_with_aggregate_data + options = @options.merge(aggregate_data: @aggregate_data) + + assert response = @gateway_purchase.store(@credit_card) + payment_reference = response.authorization + + response = @gateway_purchase.purchase(@amount, payment_reference, options) + assert_success response + assert_equal 'approved', response.message + end + + def test_successful_purchase_with_additional_data_validation + store_options = { + card_holder_identification_type: 'dni', + card_holder_identification_number: '44567890', + card_holder_door_number: '348', + card_holder_birthday: '01012017' + } + assert response = @gateway_purchase.store(@credit_card, store_options) + payment_reference = response.authorization + + response = @gateway_purchase.purchase(@amount, payment_reference, @options) + assert_success response + assert_equal 'approved', response.message + end + + def test_failed_purchase_with_payment_method_id + options = @options.merge(payment_method_id: '1') + + assert response = @gateway_purchase.store(@cabal) + payment_reference = response.authorization + + response = @gateway_purchase.purchase(@amount, payment_reference, options) + assert_failure response + end + + def test_successful_purchase_with_debit + options = @options.merge(debit: 'true', card_brand: 'visa') + + assert response = @gateway_purchase.store(@visa_debit) + payment_reference = response.authorization + + response = @gateway_purchase.purchase(@amount, payment_reference, options) + assert_success response + assert_equal 31, response.params['payment_method_id'] + end + + def test_failed_purchase_with_debit + options = @options.merge(debit: 'true', card_brand: 'visa') + + assert response = @gateway_purchase.store(@credit_card) + payment_reference = response.authorization + + response = @gateway_purchase.purchase(@amount, payment_reference, options) + assert_failure response + assert_equal 'invalid_param: bin', response.message + end + + def test_invalid_login + gateway = DecidirPlusGateway.new(public_key: '12345', private_key: 'abcde') + + response = gateway.store(@credit_card, @options) + assert_failure response + assert_match %r{Invalid authentication credentials}, response.message + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway_purchase) do + @gateway_purchase.store(@credit_card, @options) + end + transcript = @gateway_purchase.scrub(transcript) + + assert_scrubbed(@credit_card.number, transcript) + assert_scrubbed(@credit_card.verification_value, transcript) + assert_scrubbed(@gateway_purchase.options[:public_key], transcript) + assert_scrubbed(@gateway_purchase.options[:private_key], transcript) + end +end diff --git a/test/remote/gateways/remote_decidir_test.rb b/test/remote/gateways/remote_decidir_test.rb new file mode 100644 index 00000000000..5d8e6f5b7b3 --- /dev/null +++ b/test/remote/gateways/remote_decidir_test.rb @@ -0,0 +1,384 @@ +require 'test_helper' + +class RemoteDecidirTest < Test::Unit::TestCase + def setup + @gateway_for_purchase = DecidirGateway.new(fixtures(:decidir_purchase)) + @gateway_for_auth = DecidirGateway.new(fixtures(:decidir_authorize)) + + @amount = 100 + @credit_card = credit_card('4507990000004905') + @master_card_credit_card = credit_card('5299910010000015') + @amex_credit_card = credit_card('373953192351004') + @patagonia_365_card = credit_card('5046562602769006') + @diners_club_credit_card = credit_card('36463664750005') + @cabal_credit_card = credit_card('5896570000000008') + @naranja_credit_card = credit_card('5895627823453005') + @declined_card = credit_card('4000300011112220') + @options = { + order_id: SecureRandom.uuid, + billing_address: address, + description: 'Store Purchase' + } + @sub_payments = [ + { + site_id: '04052018', + installments: '1', + amount: '1500' + }, + { + site_id: '04052018', + installments: 1, + amount: 1500 + } + ] + @network_token = network_tokenization_credit_card( + '4012001037141112', + brand: 'visa', + eci: '07', + payment_cryptogram: '060103078512340000000FA08400317400000000', + name: 'Tesest payway', + verification_value: '840', + month: '12', + year: '2027' + ) + + @failed_message = ['PEDIR AUTORIZACION | request_authorization_card', 'COMERCIO INVALIDO | invalid_card'] + @failed_code = ['1, call_issuer', '3, config_error'] + end + + def test_successful_purchase + response = @gateway_for_purchase.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'approved', response.message + assert response.authorization + end + + def test_successful_purchase_with_master_card + response = @gateway_for_purchase.purchase(@amount, @master_card_credit_card, @options) + assert_success response + assert_equal 'approved', response.message + assert response.authorization + end + + def test_successful_purchase_with_amex + response = @gateway_for_purchase.purchase(@amount, @amex_credit_card, @options) + assert_success response + assert_equal 'approved', response.message + assert response.authorization + end + + def test_successful_purchase_with_patagonia_365 + @patagonia_365_card.brand = 'patagonia_365' + response = @gateway_for_purchase.purchase(@amount, @patagonia_365_card, @options) + assert_success response + assert_equal 'approved', response.message + assert response.authorization + end + + def test_successful_purchase_with_network_token_visa + 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. + # + # def test_successful_purchase_with_diners_club + # response = @gateway_for_purchase.purchase(@amount, @diners_club_credit_card, @options) + # assert_success response + # assert_equal 'approved', response.message + # assert response.authorization + # end + + def test_successful_purchase_with_cabal + response = @gateway_for_purchase.purchase(@amount, @cabal_credit_card, @options) + assert_success response + assert_equal 'approved', response.message + assert response.authorization + end + + def test_successful_purchase_with_naranja + response = @gateway_for_purchase.purchase(@amount, @naranja_credit_card, @options) + assert_success response + assert_equal 'approved', response.message + assert response.authorization + end + + def test_successful_purchase_with_more_options + options = { + ip: '127.0.0.1', + email: 'joe@example.com', + card_holder_door_number: '1234', + card_holder_birthday: '01011980', + card_holder_identification_type: 'dni', + card_holder_identification_number: '123456', + establishment_name: 'Heavenly Buffaloes', + device_unique_identifier: '1', + fraud_detection: { + send_to_cs: false, + channel: 'Web', + dispatch_method: 'Store Pick Up', + csmdds: [ + { + code: 17, + description: 'Campo MDD17' + } + ], + device_unique_id: '1', + bill_to: { + postal_code: '12345', + last_name: 'Smith', + country: 'US', + street1: '123 Mockingbird Lane', + state: 'TN', + email: 'dootdoot@hotmail.com', + customer_id: '111111', + phone_number: '555-5555', + first_name: 'Joe', + city: 'Pantsville' + }, + customer_in_site: { + password: '', + is_guest: false, + street: '123 Mockingbird Lane', + cellphone_number: '555-1212', + num_of_transactions: 48, + date_of_birth: '8-4-80', + days_in_site: 105 + }, + purchase_totals: { + currency: 'USD', + amount: 100 + } + }, + installments: '12', + site_id: '99999999' + } + + response = @gateway_for_purchase.purchase(@amount, credit_card('4509790112684851'), @options.merge(options)) + assert_success response + assert_equal 'approved', response.message + assert_equal 'Heavenly Buffaloes', response.params['establishment_name'] + assert_equal '99999999', response.params['site_id'] + assert_equal({ 'status' => nil }, response.params['fraud_detection']) + assert response.authorization + end + + def test_successful_purchase_with_sub_payments + options = @options.merge(sub_payments: @sub_payments) + + assert response = @gateway_for_purchase.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'approved', response.message + end + + def test_successful_purchase_wallet_id + options = @options.merge(wallet_id: 'moto') + + response = @gateway_for_purchase.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'approved', response.message + assert response.authorization + 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: { + send_to_cs: false, + channel: 'Web', + dispatch_method: 'Store Pick Up', + csmdds: [ + { + codee: 17, + descriptione: 'Campo MDD17' + } + ] + } + } + + response = @gateway_for_purchase.purchase(@amount, credit_card('4509790112684851'), @options.merge(options)) + assert_failure response + assert_equal 'param_required: fraud_detection.csmdds.[0].code, param_required: fraud_detection.csmdds.[0].description', response.message + assert_equal(nil, response.params['fraud_detection']) + end + + def test_failed_purchase + response = @gateway_for_purchase.purchase(@amount, @declined_card, @options) + assert_failure response + 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 + response = @gateway_for_purchase.purchase(@amount, @declined_card, @options.merge(installments: -1)) + assert_failure response + assert_equal 'invalid_param: installments', response.message + assert_equal 'invalid_request_error', response.error_code + end + + def test_successful_authorize_and_capture + auth = @gateway_for_auth.authorize(@amount, @credit_card, @options) + assert_success auth + assert_equal 'pre_approved', auth.message + assert auth.authorization + + assert capture = @gateway_for_auth.capture(@amount, auth.authorization) + assert_success capture + assert_equal 'approved', capture.message + assert capture.authorization + end + + def test_failed_authorize + response = @gateway_for_auth.authorize(@amount, @declined_card, @options) + assert_failure response + 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 + auth = @gateway_for_auth.authorize(@amount, @credit_card, @options) + assert_success auth + + assert capture = @gateway_for_auth.capture(1, auth.authorization) + assert_failure capture + assert_equal 'amount: Amount out of ranges: 80 - 105', capture.message + assert_equal 'invalid_request_error', capture.error_code + assert_nil capture.authorization + end + + def test_failed_capture + response = @gateway_for_auth.capture(@amount, '') + + assert_equal 'not_found_error', response.message + assert_match Gateway::STANDARD_ERROR_CODE[:processing_error], response.error_code + end + + def test_successful_refund + purchase = @gateway_for_purchase.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert refund = @gateway_for_purchase.refund(@amount, purchase.authorization) + assert_success refund + assert_equal 'approved', refund.message + assert refund.authorization + end + + def test_partial_refund + purchase = @gateway_for_purchase.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert refund = @gateway_for_purchase.refund(@amount - 1, purchase.authorization) + assert_success refund + assert_equal 'approved', refund.message + assert refund.authorization + end + + def test_failed_refund + response = @gateway_for_purchase.refund(@amount, '') + assert_failure response + assert_equal 'not_found_error', response.message + assert_match Gateway::STANDARD_ERROR_CODE[:processing_error], response.error_code + end + + def test_successful_void + auth = @gateway_for_auth.authorize(@amount, @credit_card, @options) + assert_success auth + + assert void = @gateway_for_auth.void(auth.authorization) + assert_success void + assert_equal 'approved', void.message + assert void.authorization + end + + def test_failed_void + response = @gateway_for_auth.void('') + assert_failure response + assert_equal 'not_found_error', response.message + assert_match Gateway::STANDARD_ERROR_CODE[:processing_error], response.error_code + end + + def test_successful_verify + response = @gateway_for_auth.verify(@credit_card, @options) + assert_success response + assert_match %r{pre_approved}, response.message + end + + def test_failed_verify + response = @gateway_for_auth.verify(@declined_card, @options) + assert_failure response + assert_match %r{PEDIR AUTORIZACION | request_authorization_card}, response.message + end + + def test_successful_inquire + response = @gateway_for_purchase.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'approved', response.message + assert response.authorization + + response = @gateway_for_purchase.inquire(response.params['id']) + assert_success response + assert_equal 'approved', response.message + assert response.authorization + end + + def test_invalid_login_without_api_key + gateway = DecidirGateway.new(api_key: '') + + response = gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_match %r{No API key found in request}, response.message + end + + def test_invalid_login + gateway = DecidirGateway.new(api_key: 'xxxxxxx') + + response = gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_match %r{Invalid authentication credentials}, response.message + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway_for_purchase) do + @gateway_for_purchase.purchase(@amount, @credit_card, @options) + end + transcript = @gateway_for_purchase.scrub(transcript) + + assert_scrubbed(@credit_card.number, transcript) + assert_scrubbed(@credit_card.verification_value, transcript) + assert_scrubbed(@gateway_for_purchase.options[:api_key], transcript) + end +end diff --git a/test/remote/gateways/remote_deepstack_test.rb b/test/remote/gateways/remote_deepstack_test.rb new file mode 100644 index 00000000000..f7a8a20a963 --- /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:, + 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_dibs_test.rb b/test/remote/gateways/remote_dibs_test.rb index 1e6b16e36b1..daf302ac6df 100644 --- a/test/remote/gateways/remote_dibs_test.rb +++ b/test/remote/gateways/remote_dibs_test.rb @@ -5,11 +5,11 @@ def setup @gateway = DibsGateway.new(fixtures(:dibs)) cc_options = { - :month => 6, - :year => 24, - :verification_value => '684', - :brand => 'visa' - } + month: 6, + year: 24, + verification_value: '684', + brand: 'visa' + } @amount = 100 @credit_card = credit_card('4711100000000000', cc_options) diff --git a/test/remote/gateways/remote_digitzs_test.rb b/test/remote/gateways/remote_digitzs_test.rb index 285dd840f9d..271aa7f5f62 100644 --- a/test/remote/gateways/remote_digitzs_test.rb +++ b/test/remote/gateways/remote_digitzs_test.rb @@ -83,7 +83,7 @@ def test_partial_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount-1, purchase.authorization, @options) + assert refund = @gateway.refund(@amount - 1, purchase.authorization, @options) assert_success refund end @@ -99,17 +99,17 @@ def test_successful_store end def test_successful_store_without_billing_address - assert response = @gateway.store(@credit_card, {merchant_id: 'spreedly-susanswidg-32268973-2091076-148408385'}) + assert response = @gateway.store(@credit_card, { merchant_id: 'spreedly-susanswidg-32268973-2091076-148408385' }) assert_success response end def test_store_adds_card_to_existing_customer - assert response = @gateway.store(@credit_card, @options.merge({customer_id: 'spreedly-susanswidg-32268973-2091076-148408385-5980208887457495-148700575'})) + assert response = @gateway.store(@credit_card, @options.merge({ customer_id: 'spreedly-susanswidg-32268973-2091076-148408385-5980208887457495-148700575' })) assert_success response end def test_store_creates_new_customer_and_adds_card - assert response = @gateway.store(@credit_card, @options.merge({customer_id: 'nonexistant'})) + assert response = @gateway.store(@credit_card, @options.merge({ customer_id: 'nonexistant' })) assert_success response end @@ -132,5 +132,4 @@ def test_transcript_scrubbing assert_scrubbed(@gateway.options[:api_key], transcript) assert_scrubbed(@gateway.options[:app_key], transcript) end - end diff --git a/test/remote/gateways/remote_ebanx_test.rb b/test/remote/gateways/remote_ebanx_test.rb index d1035d9b62b..79cec02cc07 100644 --- a/test/remote/gateways/remote_ebanx_test.rb +++ b/test/remote/gateways/remote_ebanx_test.rb @@ -17,8 +17,28 @@ def setup phone_number: '8522847035' }), order_id: generate_unique_id, - document: '853.513.468-93' + document: '853.513.468-93', + device_id: '34c376b2767', + metadata: { + metadata_1: 'test', + metadata_2: 'test2' + }, + tags: EbanxGateway::TAGS, + soft_descriptor: 'ActiveMerchant', + email: 'neymar@test.com', + processing_type: 'local' } + + @hiper_card = credit_card('6062825624254001') + @elo_card = credit_card('6362970000457013') + + @network_token = network_tokenization_credit_card( + '4111111111111111', + brand: 'visa', + payment_cryptogram: 'network_token_example_cryptogram', + month: 12, + year: 2030 + ) end def test_successful_purchase @@ -27,13 +47,32 @@ 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, ip: '127.0.0.1', email: 'joe@example.com', birth_date: '10/11/1980', - person_type: 'personal' + person_type: 'personal', + payment_type_code: 'visa' }) response = @gateway.purchase(@amount, @credit_card, options) @@ -41,6 +80,19 @@ def test_successful_purchase_with_more_options assert_equal 'Accepted', response.message end + def test_successful_purchase_with_notification_url + response = @gateway.purchase(@amount, @credit_card, @options.merge(notification_url: 'https://notify.example.com/')) + assert_success response + assert_equal 'Accepted', response.message + end + + def test_successful_purchase_passing_processing_type_in_header + response = @gateway.purchase(@amount, @credit_card, @options.merge({ processing_type: 'local' })) + + assert_success response + assert_equal 'Accepted', response.message + end + def test_successful_purchase_as_brazil_business_with_responsible_fields options = @options.update(document: '32593371000110', person_type: 'business', @@ -71,13 +123,13 @@ def test_successful_purchase_as_colombian response = @gateway.purchase(500, @credit_card, options) assert_success response - assert_equal 'Sandbox - Test credit card, transaction captured', response.message + assert_equal 'Accepted', response.message end def test_failed_purchase response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response - assert_equal 'Sandbox - Test credit card, transaction declined reason insufficientFunds', response.message + assert_equal 'Invalid card or card type', response.message assert_equal 'NOK', response.error_code end @@ -86,7 +138,7 @@ def test_successful_authorize_and_capture assert_success auth assert_equal 'Accepted', auth.message - assert capture = @gateway.capture(@amount, auth.authorization) + assert capture = @gateway.capture(@amount, auth.authorization, @options) assert_success capture assert_equal 'Accepted', capture.message end @@ -94,20 +146,37 @@ def test_successful_authorize_and_capture def test_failed_authorize response = @gateway.authorize(@amount, @declined_card, @options) assert_failure response - assert_equal 'Sandbox - Test credit card, transaction declined reason insufficientFunds', response.message + assert_equal 'Invalid card or card type', response.message assert_equal 'NOK', response.error_code end - def test_partial_capture + 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 - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization, { processing_type: 'local' }) assert_success capture end + # Partial capture is only available in Brazil and the EBANX Integration Team must be contacted to enable + def test_failed_partial_capture_when_include_capture_amount_is_passed + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert capture = @gateway.capture(@amount - 1, auth.authorization, @options.merge(include_capture_amount: true)) + assert_failure capture + assert_equal 'Partial capture not available', capture.message + end + def test_failed_capture - response = @gateway.capture(@amount, '') + response = @gateway.capture(@amount, '', { processing_type: 'local' }) assert_failure response assert_equal 'Parameters hash or merchant_payment_code not informed', response.message end @@ -116,7 +185,7 @@ def test_successful_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - refund_options = @options.merge({description: 'full refund'}) + refund_options = @options.merge({ description: 'full refund' }) assert refund = @gateway.refund(@amount, purchase.authorization, refund_options) assert_success refund assert_equal 'Accepted', refund.message @@ -127,29 +196,30 @@ def test_partial_refund assert_success purchase refund_options = @options.merge(description: 'refund due to returned item') - assert refund = @gateway.refund(@amount-1, purchase.authorization, refund_options) + assert refund = @gateway.refund(@amount - 1, purchase.authorization, refund_options) assert_success refund end def test_failed_refund - response = @gateway.refund(@amount, '') + response = @gateway.refund(@amount, '', { processing_type: 'local' }) assert_failure response - assert_match('Parameter hash not informed', response.message) + assert_equal 'Parameters hash or merchant_payment_code not informed', response.message + assert_equal 'BP-REF-1', response.error_code end def test_successful_void auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert void = @gateway.void(auth.authorization) + assert void = @gateway.void(auth.authorization, { processing_type: 'local' }) assert_success void assert_equal 'Accepted', void.message end def test_failed_void - response = @gateway.void('') + response = @gateway.void('', { processing_type: 'local' }) assert_failure response - assert_equal 'Parameter hash not informed', response.message + assert_equal 'Parameters hash or merchant_payment_code not informed', response.message end def test_successful_store_and_purchase @@ -170,6 +240,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 @@ -190,10 +277,62 @@ def test_successful_verify assert_match %r{Accepted}, response.message end + def test_successful_verify_for_chile + options = @options.merge({ + order_id: generate_unique_id, + ip: '127.0.0.1', + email: 'jose@example.com.cl', + birth_date: '10/11/1980', + billing_address: address({ + address1: '1040 Rua E', + city: 'Medellín', + state: 'AN', + zip: '29269', + country: 'CL', + phone_number: '8522847035' + }) + }) + + response = @gateway.verify(@credit_card, options) + assert_success response + assert_match %r{Accepted}, response.message + end + + def test_successful_verify_for_mexico + options = @options.merge({ + order_id: generate_unique_id, + ip: '127.0.0.1', + email: 'joao@example.com.mx', + birth_date: '10/11/1980', + billing_address: address({ + address1: '1040 Rua E', + city: 'Toluca de Lerdo', + state: 'MX', + zip: '29269', + country: 'MX', + phone_number: '8522847035' + }) + }) + response = @gateway.verify(@credit_card, options) + assert_success response + assert_match %r{Accepted}, response.message + 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{Accepted}, 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, { processing_type: 'local' }) + assert_success inquire + + assert_equal 'Accepted', purchase.message end def test_invalid_login @@ -215,4 +354,143 @@ def test_transcript_scrubbing assert_scrubbed(@gateway.options[:integration_key], transcript) end + def test_successful_purchase_with_long_order_id + options = @options.update(order_id: SecureRandom.hex(50)) + + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Accepted', response.message + end + + def test_successful_purchase_with_supplied_payment_merchant_code + options = @options.merge(merchant_payment_code: SecureRandom.hex(40)) + + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Accepted', response.message + end + + def test_successful_purchase_with_stored_credentials_cardholder_recurring + options = @options.merge!({ + stored_credential: { + initial_transaction: true, + initiator: 'cardholder', + reason_type: 'recurring', + network_transaction_id: nil + } + }) + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + end + + def test_successful_purchase_with_stored_credentials_cardholder_unscheduled + options = @options.merge!({ + stored_credential: { + initial_transaction: true, + initiator: 'cardholder', + reason_type: 'unscheduled', + network_transaction_id: nil + } + }) + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + end + + def test_successful_purchase_with_stored_credentials_cardholder_installment + options = @options.merge!({ + stored_credential: { + initial_transaction: true, + initiator: 'cardholder', + reason_type: 'installment', + network_transaction_id: nil + } + }) + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + end + + def test_successful_purchase_with_stored_credentials_merchant_installment + options = @options.merge!({ + stored_credential: { + initial_transaction: false, + initiator: 'merchant', + reason_type: 'installment', + network_transaction_id: '1234' + } + }) + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + end + + def test_successful_purchase_with_stored_credentials_merchant_unscheduled + options = @options.merge!({ + stored_credential: { + initial_transaction: false, + initiator: 'merchant', + reason_type: 'unscheduled', + network_transaction_id: '1234' + } + }) + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + end + + def test_successful_purchase_with_stored_credentials_merchant_recurring + options = @options.merge!({ + stored_credential: { + initial_transaction: false, + initiator: 'merchant', + reason_type: 'recurring', + network_transaction_id: '1234' + } + }) + response = @gateway.purchase(@amount, @credit_card, options) + + assert_success response + end + + def test_successful_purchase_with_stored_credentials_cardholder_not_initial + options = @options.merge!({ + stored_credential: { + initial_transaction: false, + initiator: 'cardholder', + reason_type: 'unscheduled', + network_transaction_id: '1234' + } + }) + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + end + + def test_successful_purchase_with_network_token + response = @gateway.purchase(@amount, @network_token, @options) + assert_success response + assert_equal 'Accepted', response.message + end + + def test_failed_purchase_with_invalid_network_token + @network_token.number = '5102026827345142' + response = @gateway.purchase(@amount, @network_token, @options) + assert_failure response + assert_equal 'Invalid card or card type', response.message + assert_equal 'NOK', response.error_code + end + + def test_failed_purchase_with_invalid_network_token_expire_date + @network_token.year = nil + response = @gateway.purchase(@amount, @network_token, @options) + assert_failure response + assert_equal 'Field network_token_expire_date is invalid', response.message + assert_equal 'BP-DR-136', response.error_code + end + + def test_network_token_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @network_token, @options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@network_token.number, transcript) + assert_scrubbed(@network_token.payment_cryptogram, transcript) + end end diff --git a/test/remote/gateways/remote_efsnet_test.rb b/test/remote/gateways/remote_efsnet_test.rb index 052ab40a589..c2745e2551f 100644 --- a/test/remote/gateways/remote_efsnet_test.rb +++ b/test/remote/gateways/remote_efsnet_test.rb @@ -1,22 +1,20 @@ require 'test_helper' class RemoteEfsnetTest < Test::Unit::TestCase - def setup Base.mode = :test @gateway = EfsnetGateway.new(fixtures(:efsnet)) - + @credit_card = credit_card('4000100011112224') - + @amount = 100 @declined_amount = 156 - @options = { :order_id => generate_unique_id, - :billing_address => address - } + @options = { order_id: generate_unique_id, + billing_address: address } end - + def test_successful_purchase assert response = @gateway.purchase(@amount, @credit_card, @options) assert_success response @@ -45,10 +43,10 @@ def test_unsuccessful_purchase def test_authorize_and_capture amount = @amount assert auth = @gateway.authorize(amount, @credit_card, @options) - assert_success auth + assert_success auth assert_equal 'Approved', auth.message assert auth.authorization - + assert capture = @gateway.capture(amount, auth.authorization, @options) assert_success capture end @@ -71,8 +69,8 @@ def test_failed_capture def test_invalid_login gateway = EfsnetGateway.new( - :login => '', - :password => '' + login: '', + password: '' ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_equal 'Invalid credentials', response.message diff --git a/test/remote/gateways/remote_elavon_test.rb b/test/remote/gateways/remote_elavon_test.rb index 63d07da8491..290f0ef0afa 100644 --- a/test/remote/gateways/remote_elavon_test.rb +++ b/test/remote/gateways/remote_elavon_test.rb @@ -3,15 +3,87 @@ class RemoteElavonTest < Test::Unit::TestCase def setup @gateway = ElavonGateway.new(fixtures(:elavon)) + @tokenization_gateway = all_fixtures[:elavon_tokenization] ? ElavonGateway.new(fixtures(:elavon_tokenization)) : ElavonGateway.new(fixtures(:elavon)) + @bad_creds_gateway = ElavonGateway.new(login: 'foo', password: 'bar', user: 'me') + @multi_currency_gateway = ElavonGateway.new(fixtures(:elavon_multi_currency)) - @credit_card = credit_card('4124939999999990') + @credit_card = credit_card('4000000000000002') + @mastercard = credit_card('5121212121212124') @bad_credit_card = credit_card('invalid') @options = { - :email => 'paul@domain.com', - :description => 'Test Transaction', - :billing_address => address, - :ip => '203.0.113.0' + email: 'paul@domain.com', + description: 'Test Transaction', + billing_address: address, + ip: '203.0.113.0' + } + @shipping_address = { + address1: '733 Foster St.', + city: 'Durham', + state: 'NC', + phone: '8887277750', + country: 'USA', + zip: '27701' + } + @level_3_data = { + customer_code: 'bob', + salestax: '3.45', + salestax_indicator: 'Y', + level3_indicator: 'Y', + ship_to_zip: '12345', + ship_to_country: 'US', + shipping_amount: '1234', + ship_from_postal_code: '54321', + discount_amount: '5', + duty_amount: '2', + national_tax_indicator: '0', + national_tax_amount: '10', + order_date: '280810', + other_tax: '3', + summary_commodity_code: '123', + merchant_vat_number: '222', + customer_vat_number: '333', + freight_tax_amount: '4', + vat_invoice_number: '26', + tracking_number: '45', + shipping_company: 'UFedzon', + other_fees: '2', + line_items: [ + { + description: 'thing', + product_code: '23', + commodity_code: '444', + quantity: '15', + unit_of_measure: 'kropogs', + unit_cost: '4.5', + discount_indicator: 'Y', + tax_indicator: 'Y', + discount_amount: '1', + tax_rate: '8.25', + tax_amount: '12', + tax_type: '000', + extended_total: '500', + total: '525', + alternative_tax: '111' + }, + { + description: 'thing2', + product_code: '23', + commodity_code: '444', + quantity: '15', + unit_of_measure: 'kropogs', + unit_cost: '4.5', + discount_indicator: 'Y', + tax_indicator: 'Y', + discount_amount: '1', + tax_rate: '8.25', + tax_amount: '12', + tax_type: '000', + extended_total: '500', + total: '525', + alternative_tax: '111' + } + ] } @amount = 100 end @@ -39,7 +111,7 @@ def test_authorize_and_capture assert_equal 'APPROVAL', auth.message assert auth.authorization - assert capture = @gateway.capture(@amount, auth.authorization, :credit_card => @credit_card) + assert capture = @gateway.capture(@amount, auth.authorization, @options.merge(credit_card: @credit_card)) assert_success capture end @@ -54,7 +126,7 @@ def test_authorize_and_capture_with_auth_code end def test_unsuccessful_capture - assert response = @gateway.capture(@amount, '', :credit_card => @credit_card) + assert response = @gateway.capture(@amount, '', credit_card: @credit_card) assert_failure response assert_equal 'The FORCE Approval Code supplied in the authorization request appears to be invalid or blank. The FORCE Approval Code must be 6 or less alphanumeric characters.', response.message end @@ -69,7 +141,6 @@ def test_successful_verify assert response = @gateway.verify(@credit_card, @options) assert_success response assert_equal 'APPROVAL', response.message - assert_success response.responses.last, 'The void should succeed' end def test_failed_verify @@ -120,7 +191,7 @@ def test_purchase_and_unsuccessful_void assert purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert response = @gateway.void(purchase.authorization) + assert @gateway.void(purchase.authorization) assert response = @gateway.void(purchase.authorization) assert_failure response assert_equal 'The transaction ID is invalid for this transaction type', response.message @@ -136,75 +207,389 @@ def test_authorize_and_successful_void assert response.authorization end + def test_stored_credentials_with_pass_in_card + # Initial CIT authorize + initial_params = { + stored_credential: { + initial_transaction: true, + reason_type: 'recurring', + initiator: 'cardholder', + network_transaction_id: nil + } + } + # X.16 amount invokes par_value and association_token_data in response + assert initial = @gateway.authorize(116, @mastercard, @options.merge(initial_params)) + assert_success initial + approval_code = initial.authorization.split(';').first + ntid = initial.network_transaction_id + par_value = initial.params['par_value'] + association_token_data = initial.params['association_token_data'] + + # Subsequent unscheduled MIT purchase, with additional data + unscheduled_params = { + approval_code:, + par_value:, + association_token_data:, + stored_credential: { + reason_type: 'unscheduled', + initiator: 'merchant', + network_transaction_id: ntid + } + } + assert unscheduled = @gateway.purchase(@amount, @mastercard, @options.merge(unscheduled_params)) + assert_success unscheduled + + # Subsequent recurring MIT purchase + recurring_params = { + approval_code:, + stored_credential: { + reason_type: 'recurring', + initiator: 'merchant', + network_transaction_id: ntid + } + } + assert recurring = @gateway.purchase(@amount, @mastercard, @options.merge(recurring_params)) + assert_success recurring + + # Subsequent installment MIT purchase + installment_params = { + installments: '4', + payment_number: '2', + approval_code:, + stored_credential: { + reason_type: 'installment', + initiator: 'merchant', + network_transaction_id: ntid + } + } + assert installment = @gateway.purchase(@amount, @mastercard, @options.merge(installment_params)) + assert_success installment + end + + def test_stored_credentials_with_tokenized_card + # Store card + assert store = @tokenization_gateway.store(@mastercard, @options) + assert_success store + stored_card = store.authorization + + # Initial CIT authorize + initial_params = { + stored_credential: { + initial_transaction: true, + reason_type: 'recurring', + initiator: 'cardholder', + network_transaction_id: nil + } + } + assert initial = @tokenization_gateway.authorize(116, stored_card, @options.merge(initial_params)) + assert_success initial + ntid = initial.network_transaction_id + par_value = initial.params['par_value'] + association_token_data = initial.params['association_token_data'] + + # Subsequent unscheduled MIT purchase, with additional data + unscheduled_params = { + par_value:, + association_token_data:, + stored_credential: { + reason_type: 'unscheduled', + initiator: 'merchant', + network_transaction_id: ntid + } + } + assert unscheduled = @tokenization_gateway.purchase(@amount, stored_card, @options.merge(unscheduled_params)) + assert_success unscheduled + + # Subsequent recurring MIT purchase + recurring_params = { + stored_credential: { + reason_type: 'recurring', + initiator: 'merchant', + network_transaction_id: ntid + } + } + assert recurring = @tokenization_gateway.purchase(@amount, stored_card, @options.merge(recurring_params)) + assert_success recurring + + # Subsequent installment MIT purchase + installment_params = { + installments: '4', + payment_number: '2', + stored_credential: { + reason_type: 'installment', + initiator: 'merchant', + network_transaction_id: ntid + } + } + assert installment = @tokenization_gateway.purchase(@amount, stored_card, @options.merge(installment_params)) + assert_success installment + end + + def test_stored_credentials_with_manual_token + # Initial CIT get token request + get_token_params = { + add_recurring_token: 'Y', + stored_credential: { + initial_transaction: true, + reason_type: 'recurring', + initiator: 'cardholder', + network_transaction_id: nil + } + } + assert get_token = @tokenization_gateway.authorize(116, @mastercard, @options.merge(get_token_params)) + assert_success get_token + ntid = get_token.network_transaction_id + token = get_token.params['token'] + par_value = get_token.params['par_value'] + association_token_data = get_token.params['association_token_data'] + + # Subsequent unscheduled MIT purchase, with additional data + unscheduled_params = { + ssl_token: token, + par_value:, + association_token_data:, + stored_credential: { + reason_type: 'unscheduled', + initiator: 'merchant', + network_transaction_id: ntid + } + } + assert unscheduled = @tokenization_gateway.purchase(@amount, @credit_card, @options.merge(unscheduled_params)) + assert_success unscheduled + + # Subsequent recurring MIT purchase + recurring_params = { + ssl_token: token, + stored_credential: { + reason_type: 'recurring', + initiator: 'merchant', + network_transaction_id: ntid + } + } + assert recurring = @tokenization_gateway.purchase(@amount, @credit_card, @options.merge(recurring_params)) + assert_success recurring + + # Subsequent installment MIT purchase + installment_params = { + ssl_token: token, + installments: '4', + payment_number: '2', + stored_credential: { + reason_type: 'installment', + initiator: 'merchant', + network_transaction_id: ntid + } + } + assert installment = @tokenization_gateway.purchase(@amount, @credit_card, @options.merge(installment_params)) + assert_success installment + end + + def test_successful_purchase_with_recurring_token + options = { + email: 'human@domain.com', + description: 'Test Transaction', + billing_address: address, + ip: '203.0.113.0', + merchant_initiated_unscheduled: 'Y', + add_recurring_token: 'Y' + } + + purchase = @gateway.purchase(@amount, @credit_card, options) + + assert_success purchase + assert_equal 'APPROVAL', purchase.message + end + + # This test is essentially replicating a test on line 373 in order to get it passing. + # This test was part of the work to enable recurring transactions for Elavon. Recurring + # transactions aren't possible with Elavon unless cards are stored there as well. This work + # will be removed in a later cleanup ticket. + def test_successful_purchase_with_ssl_token + store_response = @tokenization_gateway.store(@credit_card, @options) + token = store_response.params['token'] + options = { + email: 'paul@domain.com', + description: 'Test Transaction', + billing_address: address, + ip: '203.0.113.0', + merchant_initiated_unscheduled: 'Y', + ssl_token: token + } + + purchase = @tokenization_gateway.purchase(@amount, token, options) + + assert_success purchase + assert_equal 'APPROVAL', purchase.message + end + def test_successful_store_without_verify - assert response = @gateway.store(@credit_card, @options) + assert response = @tokenization_gateway.store(@credit_card, @options) assert_success response assert_nil response.message assert response.test? end def test_successful_store_with_verify_false - assert response = @gateway.store(@credit_card, @options.merge(verify: false)) + assert response = @tokenization_gateway.store(@credit_card, @options.merge(verify: false)) assert_success response assert_nil response.message assert response.test? end def test_successful_store_with_verify_true - assert response = @gateway.store(@credit_card, @options.merge(verify: true)) + assert response = @tokenization_gateway.store(@credit_card, @options.merge(verify: true)) assert_success response assert_equal 'APPROVAL', response.message assert response.test? end def test_unsuccessful_store - assert response = @gateway.store(@bad_credit_card, @options) + assert response = @tokenization_gateway.store(@bad_credit_card, @options) assert_failure response assert_equal 'The Credit Card Number supplied in the authorization request appears to be invalid.', response.message assert response.test? end def test_successful_update - store_response = @gateway.store(@credit_card, @options) + store_response = @tokenization_gateway.store(@credit_card, @options) token = store_response.params['token'] - credit_card = credit_card('4124939999999990', :month => 10) - assert response = @gateway.update(token, credit_card, @options) + credit_card = credit_card('4000000000000002', month: 10) + assert response = @tokenization_gateway.update(token, credit_card, @options) assert_success response assert response.test? end def test_unsuccessful_update - assert response = @gateway.update('ABC123', @credit_card, @options) + assert response = @tokenization_gateway.update('ABC123', @credit_card, @options) assert_failure response assert_match %r{invalid}i, response.message assert response.test? end def test_successful_purchase_with_token - store_response = @gateway.store(@credit_card, @options) + store_response = @tokenization_gateway.store(@credit_card, @options) token = store_response.params['token'] - assert response = @gateway.purchase(@amount, token, @options) + assert response = @tokenization_gateway.purchase(@amount, token, @options) assert_success response assert response.test? + assert_not_empty response.params['token'] assert_equal 'APPROVAL', response.message end def test_failed_purchase_with_token - assert response = @gateway.purchase(@amount, 'ABC123', @options) + assert response = @tokenization_gateway.purchase(@amount, 'ABC123', @options) assert_failure response assert response.test? assert_match %r{invalid}i, response.message end + def test_successful_authorize_with_token + store_response = @tokenization_gateway.store(@credit_card, @options) + token = store_response.params['token'] + assert response = @tokenization_gateway.authorize(@amount, token, @options) + assert_success response + assert response.test? + assert_not_empty response.params['token'] + assert_equal 'APPROVAL', response.message + end + def test_successful_purchase_with_custom_fields - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(custom_fields: {a_key: 'a value'})) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(custom_fields: { my_field: 'a value' })) + + assert_success response + assert_match response.params['my_field'], 'a value' + assert_equal 'APPROVAL', response.message + assert response.test? + assert response.authorization + end + + def test_failed_purchase_with_multi_currency_terminal_setting_disabled + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(currency: 'ZAR', multi_currency: true)) + + assert_failure response + assert response.test? + assert_equal 'The transaction currency sent is not supported', response.message + assert response.authorization + end + + def test_successful_purchase_with_multi_currency_gateway_setting + assert response = @multi_currency_gateway.purchase(@amount, @credit_card, @options.merge(currency: 'JPY')) + + assert_success response + assert response.test? + assert_equal 'APPROVAL', response.message + assert response.authorization + end + + def test_successful_purchase_with_multi_currency_transaction_setting + @multi_currency_gateway.options[:multi_currency] = false + assert response = @multi_currency_gateway.purchase(@amount, @credit_card, @options.merge(currency: 'JPY', multi_currency: true)) + + assert_success response + assert response.test? + assert_equal 'APPROVAL', response.message + assert response.authorization + end + + def test_successful_purchase_with_level_3_fields + assert response = @gateway.purchase(500, @credit_card, @options.merge(level_3_data: @level_3_data)) + + assert_success response + assert_equal 'APPROVAL', response.message + assert response.authorization + end + + def test_successful_purchase_with_shipping_address + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(shipping_address: @shipping_address)) + + assert_success response + assert_equal 'APPROVAL', response.message + assert response.authorization + end + + def test_successful_purchase_with_shipping_address_and_l3 + 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 + assert response.authorization + end + + def test_successful_purchase_with_truncated_data + credit_card = @credit_card + credit_card.first_name = 'Rick & ™ \" < > Martínez įncogníto' + credit_card.last_name = 'Lesly Andrea Mart™nez estrada the last name' + @options[:billing_address][:city] = 'Saint-François-Xavier-de-Brompton' + @options[:billing_address][:address1] = 'Bats & Cats' + + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + end + + def test_successful_special_character_encoding_truncation + special_card = credit_card('4000000000000002') + special_card.first_name = 'Césaire' + special_card.last_name = 'Castañeda&%' + + response = @gateway.purchase(@amount, special_card, @options) assert_success response assert response.test? assert_equal 'APPROVAL', response.message + assert_equal 'Césaire', response.params['first_name'] + assert_equal 'Castañeda&%', response.params['last_name'] assert response.authorization end + def test_invalid_byte_sequence + special_card = credit_card('4000000000000002') + special_card.last_name = "Castaneda \255" # add invalid utf-8 byte + + response = @gateway.purchase(@amount, special_card, @options) + assert_success response + assert response.test? + end + def test_transcript_scrubbing transcript = capture_transcript(@gateway) do @gateway.purchase(@amount, @credit_card, @options) @@ -212,8 +597,15 @@ 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[:password], transcript) end + def test_invalid_login + assert response = @bad_creds_gateway.purchase(@amount, @credit_card, @options) + + assert_failure response + assert response.test? + assert_equal 'The credentials supplied in the authorization request are invalid.', response.message + end end diff --git a/test/remote/gateways/remote_element_test.rb b/test/remote/gateways/remote_element_test.rb index 4cc1b565471..3a9f2dbc42f 100644 --- a/test/remote/gateways/remote_element_test.rb +++ b/test/remote/gateways/remote_element_test.rb @@ -8,22 +8,49 @@ 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( + '4000100011112224', + month: '01', + year: Time.new.year + 2, + first_name: 'Jane', + last_name: 'Doe', + verification_value: '888', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + eci: '05', + transaction_id: '123456789', + source: :google_pay + ) + + @apple_pay_network_token = network_tokenization_credit_card( + '4895370015293175', + month: '10', + year: Time.new.year + 2, + first_name: 'John', + last_name: 'Smith', + verification_value: '737', + payment_cryptogram: 'CeABBJQ1AgAAAAAgJDUCAAAAAAA=', + eci: '07', + transaction_id: 'abc123', + source: :apple_pay + ) end def test_successful_purchase 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 @@ -50,17 +77,147 @@ def test_successful_purchase_with_shipping_address assert_equal 'Approved', response.message end + def test_successful_purchase_with_billing_email + response = @gateway.purchase(@amount, @credit_card, @options.merge(email: 'test@example.com')) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_card_present_code + response = @gateway.purchase(@amount, @credit_card, @options.merge(card_present_code: 'Present')) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_payment_type + 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 + 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 + 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_success response + assert_equal 'Approved', 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_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_duplicate_override_flag + options = { + order_id: '2', + billing_address: address.merge(zip: '87654'), + description: 'Store Purchase' + } + + 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: 'true')) + assert_success response + assert_equal 'Approved', response.message + + # 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 + + 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_merchant_descriptor + response = @gateway.purchase(@amount, @credit_card, @options.merge(merchant_descriptor: 'Flowerpot Florists')) + 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 + response = @gateway.purchase(@amount, @apple_pay_network_token, @options) + assert_success response + assert_equal 'Approved', response.message + end + def test_successful_authorize_and_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth 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 @@ -70,7 +227,7 @@ def test_partial_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture end @@ -93,7 +250,7 @@ def test_partial_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount-1, purchase.authorization) + assert refund = @gateway.refund(@amount - 1, purchase.authorization) assert_success refund end @@ -103,6 +260,20 @@ def test_failed_refund 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_void auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth @@ -121,7 +292,7 @@ def test_failed_void def test_successful_verify response = @gateway.verify(@credit_card, @options) assert_success response - assert_match %r{Approved}, response.message + assert_equal 'Success', response.message end def test_successful_store diff --git a/test/remote/gateways/remote_epay_test.rb b/test/remote/gateways/remote_epay_test.rb index 3cde00ce108..226f4d2132c 100644 --- a/test/remote/gateways/remote_epay_test.rb +++ b/test/remote/gateways/remote_epay_test.rb @@ -5,23 +5,22 @@ def setup Base.mode = :test @gateway = EpayGateway.new(fixtures(:epay)) - @credit_card = credit_card('3333333333333000') @credit_card_declined = credit_card('3333333333333102') - @amount = 100 - @options = {order_id: '1'} + @options_xid = { order_id: generate_unique_id, three_d_secure: { eci: '7', xid: '123', cavv: '456', version: '2', ds_transaction_id: nil } } + @options_ds_transaction_id = { order_id: generate_unique_id, three_d_secure: { eci: '7', xid: nil, cavv: '456', version: '2', ds_transaction_id: '798' } } end - def test_successful_purchase - response = @gateway.purchase(@amount, @credit_card, @options) + def test_successful_purchase_xid + response = @gateway.purchase(@amount, @credit_card, @options_xid) assert_success response assert !response.authorization.blank? assert response.test? end - def test_successful_authorize_and_capture - response = @gateway.authorize(@amount, @credit_card, @options) + def test_successful_authorize_and_capture_xid + response = @gateway.authorize(@amount, @credit_card, @options_xid) assert_success response assert !response.authorization.blank? @@ -29,42 +28,84 @@ def test_successful_authorize_and_capture assert_success capture_response end - def test_failed_authorization - response = @gateway.authorize(@amount, @credit_card_declined, @options) - assert_failure response - end - - def test_failed_purchase - response = @gateway.purchase(@amount, @credit_card_declined, @options) + def test_failed_authorization_xid + response = @gateway.authorize(@amount, @credit_card_declined, @options_xid) assert_failure response end - def test_failed_capture - response = @gateway.capture(@amount, 0) + def test_failed_purchase_xid + response = @gateway.purchase(@amount, @credit_card_declined, @options_xid) assert_failure response end - def test_successful_refund - response = @gateway.purchase(@amount, @credit_card, @options) + def test_successful_refund_xid + response = @gateway.purchase(@amount, @credit_card, @options_xid) assert_success response refund_response = @gateway.refund(@amount, response.authorization) assert_success refund_response end - def test_failed_refund - response = @gateway.refund(@amount, 0) + def test_successful_void_xid + response = @gateway.authorize(@amount, @credit_card, @options_xid) + assert_success response + + void_response = @gateway.void(response.authorization) + assert_success void_response + end + + def test_successful_purchase_ds_transaction_id + response = @gateway.purchase(@amount, @credit_card, @options_ds_transaction_id) + assert_success response + assert !response.authorization.blank? + assert response.test? + end + + def test_successful_authorize_and_capture_ds_transaction_id + response = @gateway.authorize(@amount, @credit_card, @options_ds_transaction_id) + assert_success response + assert !response.authorization.blank? + + capture_response = @gateway.capture(@amount, response.authorization) + assert_success capture_response + end + + def test_failed_authorization_ds_transaction_id + response = @gateway.authorize(@amount, @credit_card_declined, @options_ds_transaction_id) + assert_failure response + end + + def test_failed_purchase_ds_transaction_id + response = @gateway.purchase(@amount, @credit_card_declined, @options_ds_transaction_id) assert_failure response end - def test_successful_void - response = @gateway.authorize(@amount, @credit_card, @options) + def test_successful_refund_ds_transaction_id + response = @gateway.purchase(@amount, @credit_card, @options_ds_transaction_id) + assert_success response + + refund_response = @gateway.refund(@amount, response.authorization) + assert_success refund_response + end + + def test_successful_void_ds_transaction_id + response = @gateway.authorize(@amount, @credit_card, @options_ds_transaction_id) assert_success response void_response = @gateway.void(response.authorization) assert_success void_response end + def test_failed_capture + response = @gateway.capture(@amount, 0) + assert_failure response + end + + def test_failed_refund + response = @gateway.refund(@amount, 0) + assert_failure response + end + def test_failed_void response = @gateway.void(0) assert_failure response diff --git a/test/remote/gateways/remote_evo_ca_test.rb b/test/remote/gateways/remote_evo_ca_test.rb index 9bc99560d4a..f1b6cb32ebe 100644 --- a/test/remote/gateways/remote_evo_ca_test.rb +++ b/test/remote/gateways/remote_evo_ca_test.rb @@ -7,12 +7,12 @@ def setup @amount = 100 @credit_card = credit_card('4111111111111111') @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase', - :invoice => 'AB-1234', - :email => 'evo@example.com', - :ip => '127.0.0.1' + order_id: '1', + billing_address: address, + description: 'Store Purchase', + invoice: 'AB-1234', + email: 'evo@example.com', + ip: '127.0.0.1' } end @@ -72,7 +72,7 @@ def test_purchase_and_void def test_purchase_and_update assert response = @gateway.purchase(@amount, @credit_card, @options) assert_success response - assert response = @gateway.update(response.authorization, :shipping_carrier => 'fedex', :tracking_number => '12345') + assert response = @gateway.update(response.authorization, shipping_carrier: 'fedex', tracking_number: '12345') assert_success response assert_equal EvoCaGateway::MESSAGES[100], response.message end @@ -102,7 +102,7 @@ def test_successful_credit def test_avs_match # To simulate an AVS Match, pass 888 in the address1 field, 77777 for zip. - opts = @options.merge(:billing_address => address({:address1 => '888', :zip => '77777'})) + opts = @options.merge(billing_address: address({ address1: '888', zip: '77777' })) assert response = @gateway.purchase(@amount, @credit_card, opts) assert_success response assert_equal 'Y', response.avs_result['code'] @@ -112,7 +112,7 @@ def test_avs_match def test_cvv_match # To simulate a CVV Match, pass 999 in the cvv field. - assert response = @gateway.purchase(@amount, credit_card('4111111111111111', :verification_value => 999), @options) + assert response = @gateway.purchase(@amount, credit_card('4111111111111111', verification_value: 999), @options) assert_success response assert_equal 'M', response.cvv_result['code'] end diff --git a/test/remote/gateways/remote_eway_managed_test.rb b/test/remote/gateways/remote_eway_managed_test.rb index 1322e32676c..c36eca3c21a 100644 --- a/test/remote/gateways/remote_eway_managed_test.rb +++ b/test/remote/gateways/remote_eway_managed_test.rb @@ -2,17 +2,17 @@ class RemoteEwayManagedTest < Test::Unit::TestCase def setup - @gateway = EwayManagedGateway.new(fixtures(:eway_managed).merge({ :test => true })) + @gateway = EwayManagedGateway.new(fixtures(:eway_managed).merge({ test: true })) - @valid_card='4444333322221111' - @valid_customer_id='9876543211000' + @valid_card = '4444333322221111' + @valid_customer_id = '9876543211000' @credit_card = credit_card(@valid_card) @options = { - :billing_address => { - :country => 'au', - :title => 'Mr.' + billing_address: { + country: 'au', + title: 'Mr.' } } @@ -29,9 +29,9 @@ def test_successful_purchase def test_invalid_login gateway = EwayManagedGateway.new( - :login => '', - :password => '', - :username => '' + login: '', + password: '', + username: '' ) assert response = gateway.purchase(@amount, @valid_customer_id, @options) assert_equal 'Login failed. ', response.message diff --git a/test/remote/gateways/remote_eway_rapid_test.rb b/test/remote/gateways/remote_eway_rapid_test.rb index edf59a27625..6fe3a50da56 100644 --- a/test/remote/gateways/remote_eway_rapid_test.rb +++ b/test/remote/gateways/remote_eway_rapid_test.rb @@ -17,14 +17,104 @@ def setup } end - def test_successful_purchase + def test_successful_purchase_with_billing_address response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + assert_equal 'Transaction Approved Successful', response.message + + customer = response.params['Customer'] + + assert_address_match(customer, @options[:billing_address]) + end + + def test_successful_purchase_with_address + @options[:billing_address] = nil + @options[:address] = address + + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + assert_equal 'Transaction Approved Successful', response.message + + customer = response.params['Customer'] + + assert_address_match(customer, @options[:address]) + end + + def test_successful_purchase_without_address + email = 'test@example.com' + + @options[:billing_address] = nil + @options[:email] = email + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response assert_equal 'Transaction Approved Successful', response.message + + customer = response.params['Customer'] + + assert_equal customer['FirstName'], @credit_card.first_name + assert_equal customer['LastName'], @credit_card.last_name + assert_equal customer['Email'], email + end + + def test_successful_purchase_with_3ds1 + eci = '05' + cavv = 'AgAAAAAA4n1uzQPRaATeQAAAAAA=' + xid = 'AAAAAAAA4n1uzQPRaATeQAAAAAA=' + authentication_response_status = 'Y' + @options[:three_d_secure] = { + eci:, + cavv:, + xid:, + authentication_response_status: + } + + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + assert_equal 'Transaction Approved Successful', response.message + end + + def test_successful_purchase_with_3ds2 + eci = '05' + cavv = 'AgAAAAAA4n1uzQPRaATeQAAAAAA=' + authentication_response_status = 'Y' + version = '2.1.0' + ds_transaction_id = '8fe2e850-a028-407e-9a18-c8cf7598ca10' + + @options[:three_d_secure] = { + version:, + eci:, + cavv:, + ds_transaction_id:, + authentication_response_status: + } + + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + assert_equal 'Transaction Approved Successful', response.message + end + + def test_successful_purchase_with_shipping_address + @options[:shipping_address] = address + + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + assert_equal 'Transaction Approved Successful', response.message + + # eWAY Rapid does not include the shipping address in the request response, + # so we can only test that the transaction is successful. 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', @@ -73,12 +163,12 @@ def test_successful_purchase_with_overly_long_fields billing_address: { address1: 'The Billing Address 1 Cannot Be More Than Fifty Characters.', address2: 'The Billing Address 2 Cannot Be More Than Fifty Characters.', - city: 'TheCityCannotBeMoreThanFiftyCharactersOrItAllFallsApart', + city: 'TheCityCannotBeMoreThanFiftyCharactersOrItAllFallsApart' }, shipping_address: { address1: 'The Shipping Address 1 Cannot Be More Than Fifty Characters.', address2: 'The Shipping Address 2 Cannot Be More Than Fifty Characters.', - city: 'TheCityCannotBeMoreThanFiftyCharactersOrItAllFallsApart', + city: 'TheCityCannotBeMoreThanFiftyCharactersOrItAllFallsApart' } } @credit_card.first_name = 'FullNameOnACardMustBeLessThanFiftyCharacters' @@ -95,6 +185,61 @@ def test_failed_purchase assert_equal 'Invalid Payment TotalAmount', response.message end + def test_successful_authorize_with_billing_address + response = @gateway.authorize(@amount, @credit_card, @options) + + assert_success response + assert_equal 'Transaction Approved Successful', response.message + + customer = response.params['Customer'] + + assert_address_match(customer, @options[:billing_address]) + end + + def test_successful_authorize_with_address + @options[:billing_address] = nil + @options[:address] = address + + response = @gateway.authorize(@amount, @credit_card, @options) + + assert_success response + assert_equal 'Transaction Approved Successful', response.message + + customer = response.params['Customer'] + + assert_address_match(customer, @options[:address]) + end + + def test_successful_authorize_without_address + email = 'test@example.com' + + @options[:billing_address] = nil + @options[:email] = email + + response = @gateway.authorize(@amount, @credit_card, @options) + + assert_success response + assert_equal 'Transaction Approved Successful', response.message + + customer = response.params['Customer'] + + assert_equal customer['FirstName'], @credit_card.first_name + assert_equal customer['LastName'], @credit_card.last_name + assert_equal customer['Email'], email + end + + def test_successful_authorize_with_shipping_address + @options[:shipping_address] = address + + response = @gateway.authorize(@amount, @credit_card, @options) + + assert_success response + assert_equal 'Transaction Approved Successful', response.message + + # eWAY Rapid does not include the shipping address in the request response, + # so we can only test that the transaction is successful. + end + def test_successful_authorize_and_capture authorize = @gateway.authorize(@amount, @credit_card, @options) assert_success authorize @@ -107,7 +252,7 @@ def test_successful_authorize_and_capture def test_failed_authorize response = @gateway.authorize(@failed_amount, @credit_card, @options) assert_failure response - assert_equal 'Error Failed', response.message + assert_equal '', response.message end def test_failed_capture @@ -127,7 +272,7 @@ def test_successful_void def test_failed_void response = @gateway.void('bogus') assert_failure response - assert_equal 'Invalid Auth Transaction ID for Capture/Void', response.message + assert_equal 'Failed', response.message end def test_successful_refund @@ -150,23 +295,98 @@ def test_successful_store response = @gateway.store(@credit_card, @options) assert_success response assert_equal 'Transaction Approved Successful', response.message + + customer = response.params['Customer'] + + assert_address_match(customer, @options[:billing_address]) + end + + def test_successful_store_with_shipping_address + @options[:shipping_address] = address + + response = @gateway.store(@credit_card, @options) + + assert_success response + assert_equal 'Transaction Approved Successful', response.message + + # eWAY Rapid does not include the shipping address in the request response, + # so we can only test that the transaction is successful. end def test_failed_store - @options[:billing_address].merge!(country: nil) + @options[:billing_address][:country] = nil response = @gateway.store(@credit_card, @options) assert_failure response assert_equal 'V6044', response.params['Errors'] assert_equal 'Customer CountryCode Required', response.message end - def test_successful_update + def test_successful_update_with_billing_address response = @gateway.store(@credit_card, @options) assert_success response assert_equal 'Transaction Approved Successful', response.message response = @gateway.update(response.authorization, @credit_card, @options) assert_success response assert_equal 'Transaction Approved Successful', response.message + + customer = response.params['Customer'] + assert_address_match(customer, @options[:billing_address]) + end + + def test_successful_update_with_address + @options[:billing_address] = nil + @options[:address] = address + + response = @gateway.store(@credit_card, @options) + assert_success response + assert_equal 'Transaction Approved Successful', response.message + + response = @gateway.update(response.authorization, @credit_card, @options) + + assert_success response + assert_equal 'Transaction Approved Successful', response.message + + customer = response.params['Customer'] + + assert_address_match(customer, @options[:address]) + end + + def test_successful_update_without_address + email = 'test@example.com' + @options[:email] = email + + response = @gateway.store(@credit_card, @options) + assert_success response + assert_equal 'Transaction Approved Successful', response.message + + @options[:billing_address] = nil + + response = @gateway.update(response.authorization, @credit_card, @options) + + assert_success response + assert_equal 'Transaction Approved Successful', response.message + + customer = response.params['Customer'] + + assert_equal customer['FirstName'], @credit_card.first_name + assert_equal customer['LastName'], @credit_card.last_name + assert_equal customer['Email'], email + end + + def test_successful_update_with_shipping_address + @options[:shipping_address] = address + + response = @gateway.store(@credit_card, @options) + assert_success response + assert_equal 'Transaction Approved Successful', response.message + + response = @gateway.update(response.authorization, @credit_card, @options) + + assert_success response + assert_equal 'Transaction Approved Successful', response.message + + # eWAY Rapid does not include the shipping address in the request response, + # so we can only test that the transaction is successful. end def test_successful_store_purchase @@ -190,12 +410,31 @@ def test_invalid_login end def test_transcript_scrubbing + credit_card_success = credit_card('4444333322221111', verification_value: 976225) + transcript = capture_transcript(@gateway) do - @gateway.purchase(100, @credit_card_success, @params) + @gateway.purchase(100, credit_card_success, {}) end + clean_transcript = @gateway.scrub(transcript) - assert_scrubbed(@credit_card_success.number, clean_transcript) - assert_scrubbed(@credit_card_success.verification_value.to_s, clean_transcript) + assert_scrubbed(credit_card_success.number, clean_transcript) + assert_scrubbed(credit_card_success.verification_value.to_s, clean_transcript) + end + + private + + def assert_address_match(customer, address) + assert_equal customer['FirstName'], address[:name].split[0] + assert_equal customer['LastName'], address[:name].split[1] + assert_equal customer['CompanyName'], address[:company] + assert_equal customer['Street1'], address[:address1] + assert_equal customer['Street2'], address[:address2] + assert_equal customer['City'], address[:city] + assert_equal customer['State'], address[:state] + assert_equal customer['PostalCode'], address[:zip] + assert_equal customer['Country'], address[:country].to_s.downcase + assert_equal customer['Phone'], address[:phone] + assert_equal customer['Fax'], address[:fax] end end diff --git a/test/remote/gateways/remote_eway_test.rb b/test/remote/gateways/remote_eway_test.rb index 693a51282e5..4c306709656 100644 --- a/test/remote/gateways/remote_eway_test.rb +++ b/test/remote/gateways/remote_eway_test.rb @@ -4,21 +4,17 @@ 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', - :email => 'bob@testbob.com', - :billing_address => { :address1 => '47 Bobway', - :city => 'Bobville', - :state => 'WA', - :country => 'AU', - :zip => '2000' - } , - :description => 'purchased items' + order_id: '1230123', + email: 'bob@testbob.com', + billing_address: { address1: '47 Bobway', + city: 'Bobville', + state: 'WA', + country: 'AU', + zip: '2000' }, + description: 'purchased items' } end @@ -72,7 +68,7 @@ def test_failed_refund end def test_transcript_scrubbing - @credit_card_success.verification_value = '431' + @credit_card_success.verification_value = '431' transcript = capture_transcript(@gateway) do @gateway.purchase(100, @credit_card_success, @params) end diff --git a/test/remote/gateways/remote_exact_test.rb b/test/remote/gateways/remote_exact_test.rb index 879c7b3aa42..c3a46234636 100644 --- a/test/remote/gateways/remote_exact_test.rb +++ b/test/remote/gateways/remote_exact_test.rb @@ -6,9 +6,9 @@ def setup @credit_card = credit_card @amount = 100 @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase' + order_id: '1', + billing_address: address, + description: 'Store Purchase' } end @@ -21,7 +21,7 @@ def test_successful_purchase def test_unsuccessful_purchase # ask for error 13 response (Amount Error) via dollar amount 5,000 + error @amount = 501300 - assert response = @gateway.purchase(@amount, @credit_card, @options ) + assert response = @gateway.purchase(@amount, @credit_card, @options) assert_match %r{Transaction Normal}, response.message assert_failure response end @@ -49,8 +49,8 @@ def test_failed_capture end def test_invalid_login - gateway = ExactGateway.new( :login => 'NotARealUser', - :password => 'NotARealPassword' ) + gateway = ExactGateway.new(login: 'NotARealUser', + password: 'NotARealPassword') assert response = gateway.purchase(@amount, @credit_card, @options) assert_match %r{^Invalid Login}, response.message assert_failure response diff --git a/test/remote/gateways/remote_ezic_test.rb b/test/remote/gateways/remote_ezic_test.rb index e2051de0ad3..85ec4c441d3 100644 --- a/test/remote/gateways/remote_ezic_test.rb +++ b/test/remote/gateways/remote_ezic_test.rb @@ -46,9 +46,9 @@ def test_failed_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount+30, auth.authorization) + assert capture = @gateway.capture(@amount + 30, auth.authorization) assert_failure capture - assert_match /Settlement amount cannot exceed authorized amount/, capture.message + assert_match(/Settlement amount cannot exceed authorized amount/, capture.message) end def test_successful_refund @@ -64,7 +64,7 @@ def test_partial_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount-1, purchase.authorization) + assert refund = @gateway.refund(@amount - 1, purchase.authorization) assert_success refund assert_equal 'TEST RETURNED', refund.message assert_equal '-0.99', refund.params['settle_amount'] @@ -76,7 +76,7 @@ def test_failed_refund assert refund = @gateway.refund(@amount + 49, purchase.authorization) assert_failure refund - assert_match /Amount of refunds exceed original sale/, refund.message + assert_match(/Amount of refunds exceed original sale/, refund.message) end def test_failed_void @@ -114,6 +114,6 @@ def test_invalid_login gateway = EzicGateway.new(account_id: '11231') response = gateway.purchase(@amount, @credit_card, @options) assert_failure response - assert_match /Invalid account number/, response.message + assert_match(/Invalid account number/, response.message) end end diff --git a/test/remote/gateways/remote_fat_zebra_test.rb b/test/remote/gateways/remote_fat_zebra_test.rb index 0f75fcdac91..8e85b032369 100644 --- a/test/remote/gateways/remote_fat_zebra_test.rb +++ b/test/remote/gateways/remote_fat_zebra_test.rb @@ -9,8 +9,8 @@ def setup @declined_card = credit_card('4557012345678902') @options = { - :order_id => rand(100000).to_s, - :ip => '1.2.3.4' + order_id: generate_unique_id, + ip: '1.2.3.4' } end @@ -21,16 +21,16 @@ def test_successful_purchase end def test_successful_multi_currency_purchase - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(:currency => 'USD')) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(currency: 'USD')) assert_success response assert_equal 'Approved', response.message assert_equal 'USD', response.params['response']['currency'] end def test_unsuccessful_multi_currency_purchase - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(:currency => 'XYZ')) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(currency: 'XYZ')) assert_failure response - assert_match /Currency XYZ is not valid for this merchant/, response.message + assert_match(/Currency XYZ is not valid for this merchant/, response.message) end def test_successful_purchase_sans_cvv @@ -64,12 +64,12 @@ def test_successful_authorize_and_capture end def test_multi_currency_authorize_and_capture - assert auth_response = @gateway.authorize(@amount, @credit_card, @options.merge(:currency => 'USD')) + assert auth_response = @gateway.authorize(@amount, @credit_card, @options.merge(currency: 'USD')) assert_success auth_response assert_equal 'Approved', auth_response.message assert_equal 'USD', auth_response.params['response']['currency'] - assert capture_response = @gateway.capture(@amount, auth_response.authorization, @options.merge(:currency => 'USD')) + assert capture_response = @gateway.capture(@amount, auth_response.authorization, @options.merge(currency: 'USD')) assert_success capture_response assert_equal 'Approved', capture_response.message assert_equal 'USD', capture_response.params['response']['currency'] @@ -93,7 +93,7 @@ def test_unsuccessful_purchase end def test_refund - purchase = @gateway.purchase(@amount, @credit_card, @options) + assert purchase = @gateway.purchase(@amount, @credit_card, @options) assert response = @gateway.refund(@amount, purchase.authorization, @options) assert_success response @@ -103,47 +103,116 @@ def test_refund def test_invalid_refund @gateway.purchase(@amount, @credit_card, @options) - assert response = @gateway.refund(@amount, nil, @options) + assert response = @gateway.refund(@amount, '', @options) assert_failure response assert_match %r{Invalid credit card for unmatched refund}, response.message end + def test_successful_void + auth = @gateway.authorize(@amount, @credit_card, @options) + + assert response = @gateway.void(auth.authorization, @options) + assert_success response + assert_match %r{Voided}, response.message + end + + def test_successful_void_refund + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + refund = @gateway.refund(@amount, purchase.authorization, @options) + assert_success refund + + assert response = @gateway.void(refund.authorization, @options) + assert_success response + assert_match %r{Voided}, response.message + end + + def test_failed_void + assert response = @gateway.void('123', @options) + assert_failure response + assert_match %r{Not Found}, response.message + end + def test_store assert card = @gateway.store(@credit_card) assert_success card - assert_false card.authorization.nil? + assert_not_nil card.authorization + end + + def test_successful_store_without_cvv + credit_card = @credit_card + credit_card.verification_value = nil + assert card = @gateway.store(credit_card, recurring: true) + + assert_success card + assert_not_nil card.authorization + end + + def test_failed_store_without_cvv + credit_card = @credit_card + credit_card.verification_value = nil + assert card = @gateway.store(credit_card) + + assert_failure card + assert_match %r{CVV is required}, card.message end def test_purchase_with_token assert card = @gateway.store(@credit_card) - assert purchase = @gateway.purchase(@amount, card.authorization, @options.merge(:cvv => 123)) + assert purchase = @gateway.purchase(@amount, card.authorization, @options.merge(cvv: 123)) assert_success purchase end def test_successful_purchase_with_descriptor - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(:merchant => 'Merchant', :merchant_location => 'Location')) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(merchant: 'Merchant', merchant_location: 'Location')) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_metadata + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(metadata: { description: 'Invoice #1234356' })) assert_success response assert_equal 'Approved', response.message + assert_equal 'Invoice #1234356', response.params['response']['metadata']['description'] end def test_successful_purchase_with_3DS_information - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(:cavv => 'MDRjN2MxZTAxYjllNTBkNmM2MTA=', :xid => 'MGVmMmNlMzI4NjAyOWU2ZDgwNTQ=', :sli => '05')) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(cavv: 'MDRjN2MxZTAxYjllNTBkNmM2MTA=', xid: 'MGVmMmNlMzI4NjAyOWU2ZDgwNTQ=', sli: '05')) assert_success response assert_equal 'Approved', response.message end def test_failed_purchase_with_incomplete_3DS_information - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(:cavv => 'MDRjN2MxZTAxYjllNTBkNmM2MTA=', :sli => '05')) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(xid: 'MGVmMmNlMzI4NjAyOWU2ZDgwNTZ=', sli: '05')) assert_failure response - assert_match %r{Extra/xid is required for SLI 05}, response.message + assert_match %r{Extra/cavv is required for SLI 05}, response.message + end + + def test_successful_purchase_with_3DS_information_using_standard_fields + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(three_d_secure: { cavv: 'MDRjN2MxZTAxYjllNTBkNmM2MTA=', xid: 'MGVmMmNlMzI4NjAyOWU2ZDgwNTQ=', eci: '05' })) + assert_success response + assert_equal 'Approved', response.message + end + + def test_failed_purchase_with_incomplete_3DS_information_using_standard_fields + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(three_d_secure: { xid: 'MGVmMmNlMzI4NjAyOWU2ZDgwNTQ=', eci: '05' })) + assert_failure response + assert_match %r{Extra/cavv is required for SLI 05}, response.message + end + + def test_successful_purchase_with_card_on_file_information + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(recurring: true, extra: { card_on_file: true, auth_reason: 'U' })) + assert_success response + assert_equal 'Approved', response.message end def test_invalid_login gateway = FatZebraGateway.new( - :username => 'invalid', - :token => 'wrongtoken' - ) + username: 'invalid', + token: 'wrongtoken' + ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal 'Invalid Login', response.message @@ -158,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: 'f25084f0-5b16-4c0a-ae5d-b24808a95e4b', + 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_federated_canada_test.rb b/test/remote/gateways/remote_federated_canada_test.rb index 3b48d1795af..94eb40e86c9 100644 --- a/test/remote/gateways/remote_federated_canada_test.rb +++ b/test/remote/gateways/remote_federated_canada_test.rb @@ -1,7 +1,6 @@ require 'test_helper' class RemoteFederatedCanadaTest < Test::Unit::TestCase - def setup @gateway = FederatedCanadaGateway.new(fixtures(:federated_canada)) @@ -11,9 +10,9 @@ def setup @credit_card = credit_card('4111111111111111') # Visa @options = { - :order_id => generate_unique_id, - :billing_address => address, - :description => 'Active Merchant Remote Test Purchase' + order_id: generate_unique_id, + billing_address: address, + description: 'Active Merchant Remote Test Purchase' } end @@ -77,9 +76,9 @@ def test_authorize_and_capture def test_invalid_login gateway = FederatedCanadaGateway.new( - :login => '', - :password => '' - ) + login: '', + password: '' + ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal 'Error in transaction data or system error', response.message diff --git a/test/remote/gateways/remote_finansbank_test.rb b/test/remote/gateways/remote_finansbank_test.rb index a59bac99a27..df5da0c203b 100644 --- a/test/remote/gateways/remote_finansbank_test.rb +++ b/test/remote/gateways/remote_finansbank_test.rb @@ -1,4 +1,5 @@ # encoding: utf-8 + require 'test_helper' class RemoteFinansbankTest < Test::Unit::TestCase @@ -11,10 +12,10 @@ def setup @declined_card = credit_card('4000300011112220') @options = { - :order_id => '#' + generate_unique_id, - :billing_address => address, - :description => 'Store Purchase', - :email => 'xyz@gmail.com' + order_id: '#' + generate_unique_id, + billing_address: address, + description: 'Store Purchase', + email: 'xyz@gmail.com' } end @@ -88,9 +89,9 @@ def test_void def test_invalid_login gateway = FinansbankGateway.new( - :login => '', - :password => '', - :client_id => '' + login: '', + password: '', + client_id: '' ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response diff --git a/test/remote/gateways/remote_first_giving_test.rb b/test/remote/gateways/remote_first_giving_test.rb index f884bcc8c04..e3a291058b8 100644 --- a/test/remote/gateways/remote_first_giving_test.rb +++ b/test/remote/gateways/remote_first_giving_test.rb @@ -1,8 +1,6 @@ require 'test_helper' class RemoteFirstGivingTest < Test::Unit::TestCase - - def setup @gateway = FirstGivingGateway.new(fixtures(:first_giving)) @@ -33,11 +31,11 @@ def test_failed_purchase end def test_successful_refund - assert purchase = @gateway.purchase(@amount, @credit_card, @options) - assert_success purchase + assert purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase - assert response = @gateway.refund(@amount, purchase.authorization) - assert_equal 'REFUND_REQUESTED_AWAITING_REFUND', response.message + assert response = @gateway.refund(@amount, purchase.authorization) + assert_equal 'REFUND_REQUESTED_AWAITING_REFUND', response.message end def test_failed_refund @@ -48,10 +46,10 @@ def test_failed_refund def test_invalid_login gateway = FirstGivingGateway.new( - application_key: '25151616', - security_token: '63131jnkj', - charity_id: '1234' - ) + application_key: '25151616', + security_token: '63131jnkj', + charity_id: '1234' + ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal 'An error occurred. Please check your input and try again.', response.message 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_first_pay_test.rb b/test/remote/gateways/remote_first_pay_test.rb index 923667078b3..3aa090b7869 100644 --- a/test/remote/gateways/remote_first_pay_test.rb +++ b/test/remote/gateways/remote_first_pay_test.rb @@ -108,12 +108,25 @@ def test_invalid_login ) response = gateway.purchase(@amount, @credit_card, @options) assert_failure response + assert_match(/Merchant: 1234 has encountered error #DTO-200-TC./, response.error_code) end def test_recurring_payment - @options.merge!({recurring: 'none', recurring_start_date: DateTime.now, recurring_end_date: DateTime.now}) + @options.merge!({ recurring: 1, recurring_start_date: DateTime.now.strftime('%m/%d/%Y'), recurring_end_date: DateTime.now.strftime('%m/%d/%Y'), recurring_type: 'monthly' }) response = @gateway.purchase(@amount, @credit_card, @options) assert_success response assert_equal 'Approved', response.message end + + def test_transcript_scrubbing + @credit_card.verification_value = 789 + 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[:gateway_id], transcript) + end end diff --git a/test/remote/gateways/remote_firstdata_e4_test.rb b/test/remote/gateways/remote_firstdata_e4_test.rb index 3a541b57290..e8a84ac81da 100755 --- a/test/remote/gateways/remote_firstdata_e4_test.rb +++ b/test/remote/gateways/remote_firstdata_e4_test.rb @@ -8,9 +8,9 @@ def setup @credit_card_with_track_data = credit_card_with_track_data('4003000123456781') @amount = 100 @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase' + order_id: '1', + billing_address: address, + description: 'Store Purchase' } @options_with_authentication_data = @options.merge({ eci: '5', @@ -26,7 +26,8 @@ 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 ) @@ -37,7 +38,7 @@ def test_successful_purchase_with_network_tokenization end def test_successful_purchase_with_specified_currency - options_with_specified_currency = @options.merge({currency: 'GBP'}) + options_with_specified_currency = @options.merge({ currency: 'GBP' }) assert response = @gateway.purchase(@amount, @credit_card, options_with_specified_currency) assert_match(/Transaction Normal/, response.message) assert_success response @@ -89,7 +90,7 @@ def test_successful_purchase_with_card_authentication def test_unsuccessful_purchase # ask for error 13 response (Amount Error) via dollar amount 5,000 + error @amount = 501300 - assert response = @gateway.purchase(@amount, @credit_card, @options ) + assert response = @gateway.purchase(@amount, @credit_card, @options) assert_match(/Transaction Normal/, response.message) assert_failure response end @@ -104,7 +105,7 @@ def test_bad_creditcard_number def test_trans_error # ask for error 42 (unable to send trans) as the cents bit... @amount = 500042 - assert response = @gateway.purchase(@amount, @credit_card, @options ) + assert response = @gateway.purchase(@amount, @credit_card, @options) assert_match(/Unable to Send Transaction/, response.message) # 42 is 'unable to send trans' assert_failure response assert_equal response.error_code, 'processing_error' @@ -119,7 +120,7 @@ def test_purchase_and_credit end def test_purchase_and_credit_with_specified_currency - options_with_specified_currency = @options.merge({currency: 'GBP'}) + options_with_specified_currency = @options.merge({ currency: 'GBP' }) assert purchase = @gateway.purchase(@amount, @credit_card, options_with_specified_currency) assert_success purchase assert purchase.authorization @@ -178,8 +179,8 @@ def test_failed_verify end def test_invalid_login - gateway = FirstdataE4Gateway.new(:login => 'NotARealUser', - :password => 'NotARealPassword' ) + gateway = FirstdataE4Gateway.new(login: 'NotARealUser', + password: 'NotARealPassword') assert response = gateway.purchase(@amount, @credit_card, @options) assert_match %r{Unauthorized Request}, response.message assert_failure response @@ -204,7 +205,7 @@ def test_refund end def test_refund_with_specified_currency - options_with_specified_currency = @options.merge({currency: 'GBP'}) + options_with_specified_currency = @options.merge({ currency: 'GBP' }) assert purchase = @gateway.purchase(@amount, @credit_card, options_with_specified_currency) assert_match(/Transaction Normal/, purchase.message) assert_success purchase @@ -248,5 +249,4 @@ def test_transcript_scrubbing assert_scrubbed(cc_with_different_cvc.verification_value, transcript) assert_scrubbed(@gateway.options[:password], transcript) end - end diff --git a/test/remote/gateways/remote_firstdata_e4_v27_test.rb b/test/remote/gateways/remote_firstdata_e4_v27_test.rb new file mode 100644 index 00000000000..ef3f1ddb6ea --- /dev/null +++ b/test/remote/gateways/remote_firstdata_e4_v27_test.rb @@ -0,0 +1,272 @@ +require 'test_helper' + +class RemoteFirstdataE4V27Test < Test::Unit::TestCase + def setup + @gateway = FirstdataE4V27Gateway.new(fixtures(:firstdata_e4_v27)) + @credit_card = credit_card + @credit_card_master = credit_card('5500000000000004', brand: 'master') + @bad_credit_card = credit_card('4111111111111113') + @credit_card_with_track_data = credit_card_with_track_data('4003000123456781') + @amount = 100 + @options = { + order_id: '1', + billing_address: address, + description: 'Store Purchase' + } + @options_with_authentication_data = @options.merge({ + eci: '5', + cavv: 'TESTCAVV', + xid: 'TESTXID' + }) + end + + def test_successful_purchase + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_match(/Transaction Normal/, response.message) + assert_success response + 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_success response + assert_equal 'Transaction Normal - Approved', response.message + assert_false response.authorization.blank? + end + + def test_successful_purchase_with_track_data + assert response = @gateway.purchase(@amount, @credit_card_with_track_data, @options) + assert_match(/Transaction Normal/, response.message) + assert_success response + end + + def test_successful_purchase_with_level_3 + level_3_xml = <<-LEVEL3 + + 107.20 + 3 + The Description + 2.33 + + LEVEL3 + + response = @gateway.purchase(500, @credit_card, @options.merge(level_3: level_3_xml)) + assert_success response + assert_equal 'Transaction Normal - Approved', response.message + end + + def test_successful_purchase_with_tax_fields + response = @gateway.purchase(500, @credit_card, @options.merge(tax1_amount: 50, tax1_number: 'A458')) + assert_success response + assert_equal '50.0', response.params['tax1_amount'] + assert_equal '', response.params['tax1_number'], 'E4 blanks this out in the response' + end + + def test_successful_purchase_with_customer_ref + response = @gateway.purchase(500, @credit_card, @options.merge(customer: '267')) + assert_success response + assert_equal '267', response.params['customer_ref'] + end + + def test_successful_purchase_with_card_authentication + assert response = @gateway.purchase(@amount, @credit_card, @options_with_authentication_data) + assert_equal response.params['cavv'], @options_with_authentication_data[:cavv] + assert_equal response.params['ecommerce_flag'], @options_with_authentication_data[:eci] + assert_equal response.params['xid'], @options_with_authentication_data[:xid] + assert_success response + end + + def test_successful_purchase_with_stored_credentials_initial + stored_credential = { + stored_credential: { + initial_transaction: true, + reason_type: 'unscheduled', + initiator: 'customer' + } + } + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential)) + assert_match(/Transaction Normal/, response.message) + assert_success response + assert_equal '1', response.params['stored_credentials_indicator'] + assert_equal 'C', response.params['stored_credentials_initiation'] + assert_equal 'U', response.params['stored_credentials_schedule'] + assert_not_nil response.params['stored_credentials_transaction_id'] + end + + def test_successful_purchase_with_stored_credentials_initial_master + stored_credential = { + stored_credential: { + initial_transaction: true, + reason_type: 'unscheduled', + initiator: 'customer' + } + } + assert response = @gateway.purchase(@amount, @credit_card_master, @options.merge(stored_credential)) + assert_match(/Transaction Normal/, response.message) + assert_success response + assert_equal 'S', response.params['stored_credentials_indicator'] + assert_equal 'U', response.params['stored_credentials_schedule'] + assert_not_nil response.params['stored_credentials_transaction_id'] + end + + def test_successful_purchase_with_stored_credentials_subsequent_recurring + stored_credential = { + stored_credential: { + initial_transaction: false, + reason_type: 'recurring', + initiator: 'merchant' + } + } + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential)) + assert_match(/Transaction Normal/, response.message) + assert_success response + assert_equal 'S', response.params['stored_credentials_indicator'] + assert_equal 'S', response.params['stored_credentials_schedule'] + assert_not_nil response.params['stored_credentials_transaction_id'] + end + + def test_unsuccessful_purchase + # ask for error 13 response (Amount Error) via dollar amount 5,000 + error + @amount = 501300 + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_match(/Transaction Normal/, response.message) + assert_failure response + end + + def test_bad_creditcard_number + assert response = @gateway.purchase(@amount, @bad_credit_card, @options) + assert_match(/Invalid Credit Card/, response.message) + assert_failure response + assert_equal response.error_code, 'invalid_number' + end + + def test_trans_error + # ask for error 42 (unable to send trans) as the cents bit... + @amount = 500042 + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_match(/Unable to Send Transaction/, response.message) # 42 is 'unable to send trans' + assert_failure response + assert_equal response.error_code, 'processing_error' + end + + def test_purchase_and_credit + assert purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + assert purchase.authorization + assert credit = @gateway.refund(@amount, purchase.authorization) + assert_success credit + end + + def test_purchase_and_void + assert purchase = @gateway.purchase(29234, @credit_card, @options) + assert_success purchase + + assert purchase.authorization + assert void = @gateway.void(purchase.authorization) + assert_success void + end + + def test_purchase_and_void_with_even_dollar_amount + assert purchase = @gateway.purchase(5000, @credit_card, @options) + assert_success purchase + + assert purchase.authorization + assert void = @gateway.void(purchase.authorization) + assert_success void + end + + def test_authorize_and_capture + 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_failed_capture + assert response = @gateway.capture(@amount, 'ET838747474;frob') + assert_failure response + assert_match(/Invalid Authorization Number/i, response.message) + end + + def test_successful_verify + assert response = @gateway.verify(@credit_card, @options) + assert_success response + + assert_equal 'Transaction Normal - Approved', response.message + assert_equal '0.0', response.params['dollar_amount'] + assert_equal '05', response.params['transaction_type'] + end + + def test_failed_verify + assert response = @gateway.verify(@bad_credit_card, @options) + assert_failure response + assert_match %r{Invalid Credit Card Number}, response.message + assert_equal response.error_code, 'invalid_number' + end + + def test_invalid_login + gateway = FirstdataE4V27Gateway.new(login: 'NotARealUser', + password: 'NotARealPassword', + key_id: 'NotARealKey', + hmac_key: 'NotARealHMAC') + assert response = gateway.purchase(@amount, @credit_card, @options) + assert_match %r{Unauthorized Request}, response.message + assert_failure response + end + + def test_response_contains_cvv_and_avs_results + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'M', response.cvv_result['code'] + assert_equal '4', response.avs_result['code'] + end + + def test_refund + assert purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_match(/Transaction Normal/, purchase.message) + assert_success purchase + + assert response = @gateway.refund(50, purchase.authorization) + assert_success response + assert_match(/Transaction Normal/, response.message) + assert response.authorization + end + + def test_refund_with_track_data + assert purchase = @gateway.purchase(@amount, @credit_card_with_track_data, @options) + assert_match(/Transaction Normal/, purchase.message) + assert_success purchase + + assert response = @gateway.refund(50, purchase.authorization) + assert_success response + assert_match(/Transaction Normal/, response.message) + assert response.authorization + end + + def test_verify_credentials + assert @gateway.verify_credentials + + gateway = FirstdataE4V27Gateway.new(login: 'unknown', password: 'unknown', key_id: 'unknown', hmac_key: 'unknown') + assert !gateway.verify_credentials + gateway = FirstdataE4V27Gateway.new(login: fixtures(:firstdata_e4)[:login], password: 'unknown', key_id: 'unknown', hmac_key: 'unknown') + assert !gateway.verify_credentials + end + + def test_transcript_scrubbing + cc_with_different_cvc = credit_card(verification_value: '999') + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, cc_with_different_cvc, @options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(cc_with_different_cvc.number, transcript) + assert_scrubbed(cc_with_different_cvc.verification_value, transcript) + assert_scrubbed(@gateway.options[:password], transcript) + assert_scrubbed(@gateway.options[:hmac_key], transcript) + end +end 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..9c8813ac5e4 --- /dev/null +++ b/test/remote/gateways/remote_flex_charge_test.rb @@ -0,0 +1,286 @@ +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'), + extra_data: '' + } + + @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 'APPROVED', response.message + end + + def test_successful_purchase_mit_with_billing_address + set_credentials! + @options[:billing_address] = address.merge(name: 'Jhon Doe', country: 'US') + response = @gateway.purchase(@amount, @credit_card_mit, @options) + assert_success response + assert_equal 'APPROVED', response.message + end + + def test_successful_authorize_cit + @cit_options[:phone] = '998888' + set_credentials! + response = @gateway.authorize(@amount, @credit_card_mit, @cit_options) + assert_success response + assert_equal 'CAPTUREREQUIRED', response.message + end + + def test_successful_authorize_and_capture_cit + @cit_options[:phone] = '998888' + set_credentials! + response = @gateway.authorize(@amount, @credit_card_mit, @cit_options) + assert_success response + assert_equal 'CAPTUREREQUIRED', response.message + + assert capture = @gateway.capture(@amount, response.authorization) + assert_success capture + end + + def test_failed_purchase_invalid_time + set_credentials! + response = @gateway.purchase(@amount, @credit_card_cit, @options.merge({ mit_expiry_date_utc: '' })) + assert_failure response + assert_equal nil, response.error_code + assert_not_nil response.params['TraceId'] + assert_equal response.message, '{"ExpiryDateUtc":["The field ExpiryDateUtc is invalid."]}' + end + + def test_failed_purchase_required_fields + 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'] + error_list = JSON.parse response.message + assert_equal error_list.length, 7 + assert_equal error_list['OrderId'], ["Merchant's orderId is required"] + assert_equal error_list['Transaction.Id'], ['The Id field is required.'] + assert_equal error_list['Transaction.ResponseCode'], ['The ResponseCode field is required.'] + assert_equal error_list['Transaction.AvsResultCode'], ['The AvsResultCode field is required.'] + assert_equal error_list['Transaction.CvvResultCode'], ['The CvvResultCode field is required.'] + assert_equal error_list['Transaction.CavvResultCode'], ['The CavvResultCode field is required.'] + assert_equal error_list['Transaction.ResponseCodeSource'], ['The ResponseCodeSource field is required.'] + 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_successful_void + @cit_options[:phone] = '998888' + set_credentials! + response = @gateway.authorize(@amount, @credit_card_mit, @cit_options) + assert_success response + + assert void = @gateway.void(response.authorization) + assert_success void + 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 + @cit_options[:phone] = '998888' + set_credentials! + + response = @gateway.authorize(@amount, @credit_card_mit, @cit_options) + assert_success response + + response = @gateway.inquire(response.authorization, {}) + assert_success response + assert_equal 'CAPTUREREQUIRED', response.message + 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_flo2cash_simple_test.rb b/test/remote/gateways/remote_flo2cash_simple_test.rb index 15aafebb4bb..f952d7c0be1 100644 --- a/test/remote/gateways/remote_flo2cash_simple_test.rb +++ b/test/remote/gateways/remote_flo2cash_simple_test.rb @@ -7,7 +7,7 @@ def setup @gateway = Flo2cashSimpleGateway.new(fixtures(:flo2cash_simple)) @amount = 100 - @credit_card = credit_card('5123456789012346', brand: :master, month: 5, year: 2017, verification_value: 111 ) + @credit_card = credit_card('5123456789012346', brand: :master, month: 5, year: 2017, verification_value: 111) @declined_card = credit_card('4000300011112220') @options = { diff --git a/test/remote/gateways/remote_forte_test.rb b/test/remote/gateways/remote_forte_test.rb index 4fdc41bb801..915f8df537b 100644 --- a/test/remote/gateways/remote_forte_test.rb +++ b/test/remote/gateways/remote_forte_test.rb @@ -10,13 +10,13 @@ def setup @check = check @bad_check = check({ - :name => 'Jim Smith', - :bank_name => 'Bank of Elbonia', - :routing_number => '1234567890', - :account_number => '0987654321', - :account_holder_type => '', - :account_type => 'checking', - :number => '0' + name: 'Jim Smith', + bank_name: 'Bank of Elbonia', + routing_number: '1234567890', + account_number: '0987654321', + account_holder_type: '', + account_type: 'checking', + number: '0' }) @options = { @@ -24,7 +24,6 @@ def setup description: 'Store Purchase', order_id: '1' } - end def test_invalid_login @@ -44,6 +43,38 @@ def test_successful_purchase_with_echeck response = @gateway.purchase(@amount, @check, @options) assert_success response assert_equal 'APPROVED', response.message + assert_equal 'PPD', response.params['echeck']['sec_code'] + end + + def test_successful_purchase_with_xdata + @options = @options.merge({ + xdata: { + xdata_1: 'some customer metadata', + xdata_2: 'some customer metadata', + xdata_3: 'some customer metadata', + xdata_4: 'some customer metadata', + xdata_5: 'some customer metadata', + xdata_6: 'some customer metadata', + xdata_7: 'some customer metadata', + xdata_8: 'some customer metadata', + xdata_9: 'some customer metadata' + } + }) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + (1..9).each { |n| assert_equal 'some customer metadata', response.params['xdata']["xdata_#{n}"] } + end + + def test_successful_purchase_with_echeck_with_more_options + options = { + sec_code: 'WEB' + } + + response = @gateway.purchase(@amount, @check, options) + assert_success response + assert_equal 'APPROVED', response.message + assert_equal 'WEB', response.params['echeck']['sec_code'] end def test_failed_purchase_with_echeck @@ -57,7 +88,7 @@ def test_successful_purchase_with_more_options order_id: '1', ip: '127.0.0.1', email: 'joe@example.com', - address: address + address: } response = @gateway.purchase(@amount, @credit_card, options) @@ -83,6 +114,22 @@ def test_successful_authorize_and_capture assert_equal 'APPROVED', capture.message end + def test_successful_authorize_capture_void + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + wait_for_authorization_to_clear + + assert capture = @gateway.capture(@amount, auth.authorization, @options) + assert_success capture + assert_match auth.authorization.split('#')[0], capture.authorization + assert_match auth.authorization.split('#')[1], capture.authorization + assert_equal 'APPROVED', capture.message + + void = @gateway.void(capture.authorization) + assert_success void + end + def test_failed_authorize @amount = 1985 response = @gateway.authorize(@amount, @declined_card, @options) @@ -96,7 +143,7 @@ def test_partial_capture wait_for_authorization_to_clear - assert capture = @gateway.capture(@amount-1, auth.authorization, @options) + assert capture = @gateway.capture(@amount - 1, auth.authorization, @options) assert_success capture end @@ -119,7 +166,7 @@ def test_partial_credit purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.credit(@amount-1, @credit_card, @options) + assert refund = @gateway.credit(@amount - 1, @credit_card, @options) assert_success refund end @@ -187,10 +234,19 @@ def test_transcript_scrubbing assert_scrubbed(@credit_card.verification_value, transcript) end + def test_account_number_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @check, @options) + end + + clean_transcript = @gateway.scrub(transcript) + + assert_scrubbed(@check.account_number, clean_transcript) + end + private def wait_for_authorization_to_clear sleep(10) end - end diff --git a/test/remote/gateways/remote_fortis_test.rb b/test/remote/gateways/remote_fortis_test.rb new file mode 100644 index 00000000000..ab605d71bd4 --- /dev/null +++ b/test/remote/gateways/remote_fortis_test.rb @@ -0,0 +1,223 @@ +require 'test_helper' + +class RemoteFortisTest < Test::Unit::TestCase + def setup + @gateway = FortisGateway.new(fixtures(:fortis)) + @amount = 100 + @credit_card = credit_card('5454545454545454', verification_value: '999') + @incomplete_credit_card = credit_card('54545454545454') + @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' + } + @complete_options = { + billing_address: @address, + description: 'Store Purchase' + } + end + + def test_invalid_login + gateway = FortisGateway.new( + user_id: 'abc123', + user_api_key: 'abc123', + developer_id: 'abc123', + location_id: @gateway.options[:location_id] + ) + + response = gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_match 'Unauthorized', response.message + end + + def test_successful_authorize + response = @gateway.authorize(0, @credit_card, @options) + assert_success response + assert_equal 'CC - Approved / ACH - Accepted', response.message + assert_equal 'Y', response.avs_result['postal_match'] + assert_equal 'Y', response.avs_result['street_match'] + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'CC - Approved / ACH - Accepted', response.message + end + + def test_successful_reference_purchase + purchase1 = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase1 + + assert purchase = @gateway.purchase(@amount, purchase1.authorization) + assert_success purchase + assert_equal 'CC - Approved / ACH - Accepted', purchase.message + end + + def test_successful_reference_authorize + authorize1 = @gateway.authorize(@amount, @credit_card, @options) + assert_success authorize1 + + assert authorize = @gateway.authorize(@amount, authorize1.authorization) + assert_success authorize + assert_equal 'CC - Approved / ACH - Accepted', authorize.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 'AuthCompleted', capture.message + end + + def test_failed_capture + response = @gateway.capture(@amount, 'abc123') + assert_failure response + assert_match %r{"transaction_id" with value "abc123" fails to match the Fortis}, response.message + end + + def test_partial_capture + auth = @gateway.authorize(1000, @credit_card, @options) + assert_success auth + + assert capture = @gateway.capture(800, auth.authorization) + assert_success capture + end + + def test_failed_authorize_declined + response = @gateway.authorize(622, @credit_card, @options) + assert_failure response + assert_equal 'Card Expired', response.message + end + + def test_failed_authorize_generic_fail + response = @gateway.authorize(601, @credit_card, @options) + assert_failure response + assert_equal 'Generic Decline', response.message + end + + def test_failed_purchase + response = @gateway.purchase(622, @credit_card, @options) + assert_failure response + assert_equal 'Card Expired', response.message + end + + def test_successful_purchase_with_more_options + response = @gateway.purchase(@amount, @credit_card, @complete_options) + assert_success response + assert_equal 'CC - Approved / ACH - Accepted', response.message + end + + def test_successful_verify + response = @gateway.verify(@credit_card, @options) + assert_success response + assert_equal 'CC - Approved / ACH - Accepted', response.message + end + + def test_successful_void + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert void = @gateway.void(auth.authorization) + assert_success void + assert_equal 'Voided', void.message + end + + def test_failed_void + response = @gateway.void('abc123') + assert_failure response + assert_match %r{"transaction_id" with value "abc123" fails to match the Fortis}, response.message + end + + def test_successful_refund + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization) + assert_success refund + assert_equal 'CC - Approved / ACH - Accepted', refund.message + end + + def test_partial_refund + purchase = @gateway.purchase(1000, @credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(800, purchase.authorization) + assert_success refund + end + + def test_failed_refund + response = @gateway.refund(@amount, 'abc123') + assert_failure response + assert_match %r{"transaction_id" with value "abc123" fails to match the Fortis}, response.message + end + + def test_successful_credit + response = @gateway.credit(@amount, @credit_card, @complete_options) + assert_success response + assert_equal 'CC - Approved / ACH - Accepted', response.message + end + + def test_failed_credit + response = @gateway.credit(622, @credit_card, @options) + assert_failure response + assert_equal 'Card Expired', response.message + end + + def test_storing_credit_card + store = @gateway.store(@credit_card, @options) + assert_success store + end + + def test_storing_credit_card_with_location_as_option + @gateway = FortisGateway.new(fixtures(:fortis).except(:location_id)) + @options[:location_id] = fixtures(:fortis)[:location_id] + + store = @gateway.store(@credit_card, @options) + assert_success store + end + + def test_failded_store_credit_card + response = @gateway.store(@incomplete_credit_card, @options) + assert_failure response + assert_equal '"account_number" must be a credit card', response.message + end + + def test_authorize_with_stored_credit_card + store = @gateway.store(@credit_card, @options) + assert_success store + + response = @gateway.authorize(@amount, store.authorization, @options) + assert_success response + assert_equal 'CC - Approved / ACH - Accepted', response.message + end + + def test_unstore + store = @gateway.store(@credit_card, @options) + assert_success store + + unstore = @gateway.unstore(store.authorization) + assert_success unstore + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.authorize(@amount, @credit_card, @options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@credit_card.number, transcript) + assert_scrubbed(@gateway.options[:user_api_key], transcript) + assert_scrubbed(@gateway.options[:developer_id], transcript) + end +end diff --git a/test/remote/gateways/remote_garanti_test.rb b/test/remote/gateways/remote_garanti_test.rb index 189fba6ddd0..df6922f6c73 100644 --- a/test/remote/gateways/remote_garanti_test.rb +++ b/test/remote/gateways/remote_garanti_test.rb @@ -2,7 +2,6 @@ # NOTE: tests may fail randomly because Garanti returns random(!) responses for their test server class RemoteGarantiTest < Test::Unit::TestCase - def setup @gateway = GarantiGateway.new(fixtures(:garanti)) @@ -11,9 +10,9 @@ def setup @credit_card = credit_card('4282209027132016', month: 5, year: 2018, verification_value: 358) @options = { - :order_id => generate_unique_id, - :billing_address => address, - :description => 'Store Purchase' + order_id: generate_unique_id, + billing_address: address, + description: 'Store Purchase' } end @@ -51,11 +50,11 @@ def test_failed_capture def test_invalid_login gateway = GarantiGateway.new( - :login => 'PROVAUT', - :terminal_id => '30691300', - :merchant_id => '', - :password => '' - ) + login: 'PROVAUT', + terminal_id: '30691300', + merchant_id: '', + password: '' + ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal '0651', response.params['reason_code'] diff --git a/test/remote/gateways/remote_global_collect_test.rb b/test/remote/gateways/remote_global_collect_test.rb index b04a60ae53a..e325c35e5df 100644 --- a/test/remote/gateways/remote_global_collect_test.rb +++ b/test/remote/gateways/remote_global_collect_test.rb @@ -3,28 +3,132 @@ class RemoteGlobalCollectTest < Test::Unit::TestCase def setup @gateway = GlobalCollectGateway.new(fixtures(:global_collect)) + @gateway_preprod = GlobalCollectGateway.new(fixtures(:global_collect_preprod)) + @gateway_preprod.options[:url_override] = 'preproduction' + + @gateway_direct = GlobalCollectGateway.new(fixtures(:global_collect_direct)) + @gateway_direct.options[:url_override] = 'ogone_direct' @amount = 100 @credit_card = credit_card('4567350000427977') + @credit_card_challenge_3ds2 = credit_card('4874970686672022') + @naranja_card = credit_card('5895620033330020', brand: 'naranja') + @cabal_card = credit_card('6271701225979642', brand: 'cabal') @declined_card = credit_card('5424180279791732') + @preprod_card = credit_card('4111111111111111') + @apple_pay = network_tokenization_credit_card( + '4567350000427977', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + month: '01', + year: Time.new.year + 2, + first_name: 'John', + last_name: 'Smith', + eci: '05', + source: :apple_pay + ) + + @google_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ + source: :google_pay, + payment_data: "{ 'version': 'EC_v1', 'data': 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9'}" + }) + @accepted_amount = 4005 @rejected_amount = 2997 @options = { + email: 'example@example.com', billing_address: address, description: 'Store Purchase' } + @long_address = { + billing_address: { + address1: '1234 Supercalifragilisticexpialidociousthiscantbemorethanfiftycharacters', + city: 'Portland', + state: 'ME', + zip: '09901', + country: 'US' + } + } + @preprod_options = { + order_id: SecureRandom.hex(15), + email: 'email@example.com', + billing_address: address + } end def test_successful_purchase response = @gateway.purchase(@accepted_amount, @credit_card, @options) assert_success response assert_equal 'Succeeded', response.message + 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) + assert_success response + assert_equal 'Succeeded', response.message + assert_equal 'CAPTURE_REQUESTED', response.params['payment']['status'] + end + + def test_successful_purchase_with_cabal + options = @preprod_options.merge(requires_approval: false, currency: 'ARS') + response = @gateway_preprod.purchase(1000, @cabal_card, options) + assert_success response + assert_equal 'Succeeded', response.message + assert_equal 'CAPTURE_REQUESTED', response.params['payment']['status'] + end + + def test_successful_purchase_with_apple_pay + options = @preprod_options.merge(requires_approval: false, currency: 'USD') + response = @gateway_preprod.purchase(4500, @apple_pay, options) + assert_success response + assert_equal 'Succeeded', response.message + assert_equal 'CAPTURE_REQUESTED', response.params['payment']['status'] + end + + def test_successful_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_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 'PENDING_CAPTURE', response.params['payment']['status'] + end + + 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 end def test_successful_purchase_with_fraud_fields options = @options.merge( - fraud_fields: - { + fraud_fields: { 'website' => 'www.example.com', 'giftMessage' => 'Happy Day!' } @@ -54,20 +158,286 @@ def test_successful_purchase_with_more_options assert_equal 'Succeeded', response.message end + def test_successful_purchase_with_installments + 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 + + # When requires_approval is true (or not present), + # `purchase` will make both an `auth` and a `capture` call + def test_successful_purchase_with_requires_approval_true + options = @options.merge(requires_approval: true) + + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Succeeded', response.message + 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_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: { + 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' + } + ) + + response = @gateway.authorize(@amount, @credit_card, options) + assert_success response + assert_match 'jJ81HADVRtXfCBATEp01CJUAAAA=', response.params['payment']['paymentOutput']['cardPaymentMethodSpecificOutput']['threeDSecureResults']['cavv'] + 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: { + code: 111, + name: 'Spreedly Airlines', + flight_date: '20190810', + passenger_name: 'Randi Smith', + is_eticket: 'true', + is_restricted_ticket: 'true', + is_third_party: 'true', + issue_date: 'tday', + merchant_customer_id: 'MIDs', + agent_numeric_code: '12345', + passengers: [ + { first_name: 'Randi', + surname: 'Smith', + surname_prefix: 'S', + title: 'Mr' }, + { first_name: 'Julia', + surname: 'Smith', + surname_prefix: 'S', + title: 'Mrs' } + ], + flight_legs: [ + { airline_class: 'ZZ', + arrival_airport: 'BDL', + arrival_time: '0520', + carrier_code: 'SA', + conjunction_ticket: 'ct-12', + coupon_number: '1', + date: '20190810', + departure_time: '1220', + endorsement_or_restriction: 'no', + exchange_ticket: 'no', + fare: '20000', + fare_basis: 'fareBasis', + fee: '12', + flight_number: '1', + number: 596, + origin_airport: 'RDU', + passenger_class: 'coach', + stopover_code: 'permitted', + taxes: '700' }, + { arrival_airport: 'RDU', + origin_airport: 'BDL', + date: '20190817', + carrier_code: 'SA', + number: 597, + airline_class: 'ZZ' } + ] + } + ) + + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_failed_purchase_with_insufficient_airline_data + options = @options.merge( + airline_data: { + flight_date: '20190810', + passenger_name: 'Randi Smith' + } + ) + + response = @gateway.purchase(@amount, @credit_card, options) + assert_failure response + assert_equal 'PARAMETER_NOT_FOUND_IN_REQUEST', response.message + property_names = response.params['errors'].collect { |e| e['propertyName'] } + assert property_names.include? 'order.additionalInput.airlineData.code' + assert property_names.include? 'order.additionalInput.airlineData.name' + end + + def test_successful_purchase_with_lodging_data + options = @options.merge( + lodging_data: { + charges: [ + { charge_amount: '1000', + charge_amount_currency_code: 'USD', + charge_type: 'giftshop' } + ], + check_in_date: '20211223', + check_out_date: '20211227', + folio_number: 'randAssortmentofChars', + is_confirmed_reservation: 'true', + is_facility_fire_safety_conform: 'true', + is_no_show: 'false', + is_preference_smoking_room: 'false', + number_of_adults: '2', + number_of_nights: '1', + number_of_rooms: '1', + program_code: 'advancedDeposit', + property_customer_service_phone_number: '5555555555', + property_phone_number: '5555555555', + renter_name: 'Guy', + rooms: [ + { daily_room_rate: '25000', + daily_room_rate_currency_code: 'USD', + daily_room_tax_amount: '5', + daily_room_tax_amount_currency_code: 'USD', + number_of_nights_at_room_rate: '1', + room_location: 'Courtyard', + type_of_bed: 'Queen', + type_of_room: 'Walled' } + ] + } + ) + + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + end + def test_successful_purchase_with_very_long_name - credit_card = credit_card('4567350000427977', { first_name: 'thisisaverylongfirstname'}) + credit_card = credit_card('4567350000427977', { first_name: 'thisisaverylongfirstname' }) + + response = @gateway.purchase(@amount, credit_card, @options) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_blank_name + credit_card = credit_card('4567350000427977', { first_name: nil, last_name: nil }) response = @gateway.purchase(@amount, credit_card, @options) assert_success response assert_equal 'Succeeded', response.message end + def test_unsuccessful_purchase_with_blank_name_ogone_direct + credit_card = credit_card('4567350000427977', { first_name: nil, last_name: nil }) + + response = @gateway_direct.purchase(@amount, credit_card, @options) + assert_failure response + assert_equal 'PARAMETER_NOT_FOUND_IN_REQUEST', response.message + end + + def test_successful_purchase_with_pre_authorization_flag + response = @gateway.purchase(@accepted_amount, @credit_card, @options.merge(pre_authorization: true)) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_payment_product_id + options = @preprod_options.merge(requires_approval: false, currency: 'ARS') + response = @gateway_preprod.purchase(1000, @cabal_card, options) + assert_success response + assert_equal 'Succeeded', response.message + assert_equal 135, response.params['payment']['paymentOutput']['cardPaymentMethodSpecificOutput']['paymentProductId'] + end + + def test_successful_purchase_with_truncated_split_address + response = @gateway.purchase(@amount, @credit_card, @long_address) + assert_success response + assert_equal 'Succeeded', response.message + end + def test_failed_purchase response = @gateway.purchase(@rejected_amount, @declined_card, @options) assert_failure response 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 @@ -77,17 +447,29 @@ def test_successful_authorize_and_capture assert_equal 'Succeeded', capture.message end + def test_authorize_with_optional_idempotency_key_header + response = @gateway.authorize(@accepted_amount, @credit_card, @options.merge(idempotency_key: 'test123')) + assert_success response + assert_equal 'Succeeded', response.message + end + def test_failed_authorize response = @gateway.authorize(@amount, @declined_card, @options) assert_failure response 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 capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture end @@ -130,27 +512,69 @@ 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 assert_match %r{UNKNOWN_PAYMENT_ID}, response.message end + def test_failed_repeat_void + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert void = @gateway.void(auth.authorization) + assert_success void + assert_equal 'Succeeded', void.message + + assert repeat_void = @gateway.void(auth.authorization) + assert_failure repeat_void + end + + def test_successful_inquire + response = @gateway.purchase(@accepted_amount, @credit_card, @options) + assert_success response + + response = @gateway.inquire(response.params['payment']['id']) + assert_success response + assert_equal 'Succeeded', response.message + assert response.authorization + end + def test_successful_verify response = @gateway.verify(@credit_card, @options) assert_success response assert_equal 'Succeeded', response.message end + def test_successful_verify_ogone_direct + response = @gateway_direct.verify(@credit_card, @options) + assert_success response + assert_equal 'Succeeded', response.message + end + def test_failed_verify response = @gateway.verify(@declined_card, @options) assert_failure response assert_equal 'Not authorised', response.message end + def test_failed_verify_ogone_direct + response = @gateway_direct.verify(@declined_card, @options) + assert_failure response + assert_equal false, response.params['paymentResult']['payment']['statusOutput']['isAuthorized'] + end + def test_invalid_login gateway = GlobalCollectGateway.new(merchant_id: '', api_key_id: '', secret_api_key: '') - response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_match %r{MISSING_OR_INVALID_AUTHORIZATION}, response.message @@ -166,4 +590,39 @@ def test_transcript_scrubbing assert_scrubbed(@gateway.options[:secret_api_key], transcript) end + def test_scrub_apple_payment + options = @preprod_options.merge(requires_approval: false) + transcript = capture_transcript(@gateway) do + @gateway_preprod.purchase(@amount, @apple_pay, options) + end + transcript = @gateway.scrub(transcript) + assert_scrubbed(@apple_pay.payment_cryptogram, transcript) + assert_scrubbed(@apple_pay.number, transcript) + end + + def test_successful_preprod_auth_and_capture + options = @preprod_options.merge(requires_approval: true) + auth = @gateway_preprod.authorize(@accepted_amount, @preprod_card, options) + assert_success auth + + assert capture = @gateway_preprod.capture(@amount, auth.authorization, options) + assert_success capture + assert_equal 'CAPTURE_REQUESTED', capture.params['payment']['status'] + end + + def test_successful_preprod_purchase + options = @preprod_options.merge(requires_approval: false) + assert purchase = @gateway_preprod.purchase(@accepted_amount, @preprod_card, options) + assert_success purchase + end + + def test_successful_preprod_void + options = @preprod_options.merge(requires_approval: true) + auth = @gateway_preprod.authorize(@amount, @preprod_card, options) + assert_success auth + + assert void = @gateway_preprod.void(auth.authorization) + assert_success void + assert_equal 'Succeeded', void.message + end end diff --git a/test/remote/gateways/remote_global_transport_test.rb b/test/remote/gateways/remote_global_transport_test.rb index 1f5c45b4cb4..22ca103956f 100644 --- a/test/remote/gateways/remote_global_transport_test.rb +++ b/test/remote/gateways/remote_global_transport_test.rb @@ -9,7 +9,7 @@ def setup @options = { email: 'john@example.com', order_id: '1', - billing_address: address, + billing_address: address } end @@ -63,7 +63,7 @@ def test_failed_capture assert capture = @gateway.capture(1000, auth.authorization) assert_failure capture - assert_match /must be less than or equal to the original amount/, capture.message + assert_match(/must be less than or equal to the original amount/, capture.message) end def test_successful_refund @@ -90,7 +90,7 @@ def test_failed_refund assert refund = @gateway.refund(1000, purchase.authorization) assert_failure refund - assert_match /Refund Exceeds Available Refund Amount/, refund.message + assert_match(/Refund Exceeds Available Refund Amount/, refund.message) end def test_successful_void @@ -137,5 +137,4 @@ def test_transcript_scrubbing assert_scrubbed(@credit_card.verification_value, transcript) assert_scrubbed(@gateway.options[:global_password], transcript) end - end diff --git a/test/remote/gateways/remote_hdfc_test.rb b/test/remote/gateways/remote_hdfc_test.rb index e3f2ed395d5..2b92b92fea6 100644 --- a/test/remote/gateways/remote_hdfc_test.rb +++ b/test/remote/gateways/remote_hdfc_test.rb @@ -11,12 +11,12 @@ def setup # Use an American Express card to simulate a failure since HDFC does not # support any proper decline cards outside of 3D secure failures. - @declined_card = credit_card('377182068239368', :brand => :american_express) + @declined_card = credit_card('377182068239368', brand: :american_express) @options = { - :order_id => generate_unique_id, - :billing_address => address, - :description => 'Store Purchase' + order_id: generate_unique_id, + billing_address: address, + description: 'Store Purchase' } end @@ -61,15 +61,15 @@ def test_successful_refund end def test_passing_billing_address - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(:billing_address => address)) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(billing_address: address)) assert_success response end def test_invalid_login gateway = HdfcGateway.new( - :login => '', - :password => '' - ) + login: '', + password: '' + ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal 'TranPortal ID required.', response.message 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..d9bb2fa87d1 --- /dev/null +++ b/test/remote/gateways/remote_hi_pay_test.rb @@ -0,0 +1,260 @@ +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') + @threeds_credit_card = credit_card('5300000000000006') + + @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_challenge_without_threeds_params + response = @gateway.purchase(@amount, @threeds_credit_card, @options.merge(@billing_address)) + 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_frictionless_with_threeds_params + response = @gateway.purchase(@amount, @threeds_credit_card, @options.merge(@billing_address).merge(@execute_threed)) + 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_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 '4010202', response.error_code + assert_equal 'Invalid Card Number', response.params['reason']['message'] + + assert_kind_of MultiResponse, response + # Complete tokenization, failed in the purchase 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 2650965ecc5..a1fa3636564 100644 --- a/test/remote/gateways/remote_hps_test.rb +++ b/test/remote/gateways/remote_hps_test.rb @@ -3,10 +3,13 @@ class RemoteHpsTest < Test::Unit::TestCase def setup @gateway = HpsGateway.new(fixtures(:hps)) + @check_gateway = HpsGateway.new(fixtures(:hps_echeck)) @amount = 100 + @check_amount = 2000 @declined_amount = 1034 @credit_card = credit_card('4000100011112224') + @check = check(account_number: '1357902468', routing_number: '122000030', number: '1234', account_type: 'SAVINGS') @options = { order_id: '1', @@ -21,6 +24,13 @@ def test_successful_purchase assert_equal 'Success', response.message end + def test_successful_check_purchase + options = @options.merge(company_name: 'Hot Buttered Toast Incorporated') + response = @check_gateway.purchase(@check_amount, @check, options) + assert_success response + assert_equal 'Success', response.message + end + def test_successful_purchase_without_cardholder response = @gateway.purchase(@amount, @credit_card) assert_success response @@ -43,6 +53,13 @@ def test_successful_purchase_with_descriptor assert_equal 'Success', response.message end + def test_successful_purchase_with_hyphenated_zip + @options[:billing_address][:zip] = '12345-1234' + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Success', response.message + end + def test_successful_purchase_no_address options = { order_id: '1', @@ -97,7 +114,7 @@ def test_partial_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture end @@ -106,7 +123,7 @@ def test_failed_capture assert_failure response end - def test_successful_refund + def test_successful_purchase_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase @@ -116,11 +133,37 @@ def test_successful_refund assert_equal '0', refund.params['GatewayRspCode'] end - def test_partial_refund + def test_successful_capture_refund + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert capture = @gateway.capture(nil, auth.authorization) + assert_success capture + + assert refund = @gateway.refund(@amount, capture.authorization) + assert_success refund + assert_equal 'Success', refund.params['GatewayRspMsg'] + assert_equal '0', refund.params['GatewayRspCode'] + end + + def test_partial_purchase_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount-1, purchase.authorization) + assert refund = @gateway.refund(@amount - 1, purchase.authorization) + assert_success refund + assert_equal 'Success', refund.params['GatewayRspMsg'] + assert_equal '0', refund.params['GatewayRspCode'] + end + + def test_partial_capture_refund + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert capture = @gateway.capture(nil, auth.authorization) + assert_success capture + + assert refund = @gateway.refund(@amount - 1, capture.authorization) assert_success refund assert_equal 'Success', refund.params['GatewayRspMsg'] assert_equal '0', refund.params['GatewayRspCode'] @@ -131,6 +174,17 @@ def test_failed_refund assert_failure response end + def test_successful_credit + credit = @gateway.credit(@amount, @credit_card, @options) + assert_success credit + assert_equal 'Success', credit.params['GatewayRspMsg'] + end + + def test_failed_credit + credit = @gateway.credit(nil, @credit_card) + assert_failure credit + end + def test_successful_void auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth @@ -140,6 +194,16 @@ def test_successful_void assert_equal 'Success', void.params['GatewayRspMsg'] end + def test_successful_check_void + options = @options.merge(company_name: 'Hot Buttered Toast Incorporated') + purchase = @check_gateway.purchase(@check_amount, @check, options) + assert_success purchase + + assert void = @check_gateway.void(purchase.authorization, @options.merge(check_void: true)) + assert_success void + assert_equal 'Success', void.params['GatewayRspMsg'] + end + def test_failed_void response = @gateway.void('123') assert_failure response @@ -207,6 +271,31 @@ def test_successful_purchase_with_swipe_no_encryption assert_equal 'Success', response.message end + def test_successful_purchase_with_stored_credentials + initial_options = @options.merge( + stored_credential: { + initial_transaction: true, + reason_type: 'recurring' + } + ) + initial_response = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success initial_response + assert_equal 'Success', initial_response.message + assert_not_nil initial_response.params['CardBrandTxnId'] + network_transaction_id = initial_response.params['CardBrandTxnId'] + + used_options = @options.merge( + stored_credential: { + initial_transaction: false, + reason_type: 'unscheduled', + network_transaction_id: + } + ) + response = @gateway.purchase(@amount, @credit_card, used_options) + assert_success response + assert_equal 'Success', response.message + end + def test_failed_purchase_with_swipe_bad_track_data @credit_card.track_data = '%B547888879888877776?;?' response = @gateway.purchase(@amount, @credit_card, @options) @@ -235,6 +324,13 @@ def test_successful_purchase_with_swipe_encryption_type_02 assert_equal 'Success', response.message end + def test_successful_purchase_with_truncated_invoicenbr + response = @gateway.purchase(@amount, @credit_card, @options.merge(order_id: '04863692e6b56aaed85760b3d0879afd18b980da0521f6454c007a838435e561')) + + assert_success response + assert_equal 'Success', response.message + end + def tests_successful_verify response = @gateway.verify(@credit_card, @options) @@ -261,4 +357,215 @@ def test_transcript_scrubbing assert_scrubbed(@credit_card.verification_value, transcript) assert_scrubbed(@gateway.options[:secret_api_key], transcript) end + + def test_transcript_scrubbing_with_cryptogram + credit_card = network_tokenization_credit_card( + '4242424242424242', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + eci: '05', + source: :apple_pay + ) + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, credit_card, @options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(credit_card.number, transcript) + assert_scrubbed(@gateway.options[:secret_api_key], transcript) + assert_scrubbed(credit_card.payment_cryptogram, transcript) + end + + def test_account_number_scrubbing + options = @options.merge(company_name: 'Hot Buttered Toast Incorporated') + transcript = capture_transcript(@check_gateway) do + @check_gateway.purchase(@check_amount, @check, options) + end + clean_transcript = @check_gateway.scrub(transcript) + assert_scrubbed(@check.account_number, clean_transcript) + end + + def test_successful_purchase_with_apple_pay_raw_cryptogram_with_eci + credit_card = network_tokenization_credit_card( + '4242424242424242', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + eci: '05', + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + eci: '05', + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + 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_google_pay_raw_cryptogram_with_eci + credit_card = network_tokenization_credit_card( + '4242424242424242', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + eci: '05', + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + eci: '05', + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + source: :google_pay + ) + assert response = @gateway.authorize(@amount, credit_card, @options) + assert_success response + assert_equal 'Success', response.message + end + + def test_three_d_secure_visa + @credit_card.number = '4012002000060016' + @credit_card.brand = 'visa' + + options = { + three_d_secure: { + cavv: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + eci: '05', + xid: 'TTBCSkVTa1ZpbDI1bjRxbGk5ODE=' + } + } + + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Success', response.message + end + + def test_three_d_secure_mastercard + @credit_card.number = '5473500000000014' + @credit_card.brand = 'master' + + options = { + three_d_secure: { + cavv: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + eci: '05', + xid: 'TTBCSkVTa1ZpbDI1bjRxbGk5ODE=' + } + } + + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Success', response.message + end + + def test_three_d_secure_discover + @credit_card.number = '6011000990156527' + @credit_card.brand = 'discover' + + options = { + three_d_secure: { + cavv: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + eci: '05', + xid: 'TTBCSkVTa1ZpbDI1bjRxbGk5ODE=' + } + } + + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Success', response.message + end + + def test_three_d_secure_amex + @credit_card.number = '372700699251018' + @credit_card.brand = 'american_express' + + options = { + three_d_secure: { + cavv: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + eci: '05', + xid: 'TTBCSkVTa1ZpbDI1bjRxbGk5ODE=' + } + } + + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Success', response.message + end + + def test_three_d_secure_jcb + @credit_card.number = '372700699251018' + @credit_card.brand = 'jcb' + + options = { + three_d_secure: { + cavv: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + eci: '05', + xid: 'TTBCSkVTa1ZpbDI1bjRxbGk5ODE=' + } + } + + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Success', response.message + end end diff --git a/test/remote/gateways/remote_iats_payments_test.rb b/test/remote/gateways/remote_iats_payments_test.rb index 4a1a256da8e..f7b4bfe2ec1 100644 --- a/test/remote/gateways/remote_iats_payments_test.rb +++ b/test/remote/gateways/remote_iats_payments_test.rb @@ -9,9 +9,14 @@ def setup @credit_card = credit_card('4222222222222220') @check = check(routing_number: '111111111', account_number: '12345678') @options = { - :order_id => generate_unique_id, - :billing_address => address, - :description => 'Store purchase' + order_id: generate_unique_id, + billing_address: address, + description: 'Store purchase' + } + @customer_details = { + phone: '5555555555', + email: 'test@example.com', + country: 'US' } end @@ -23,6 +28,14 @@ def test_successful_purchase assert response.authorization end + def test_successful_purchase_with_customer_details + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(@customer_details)) + assert_success response + assert response.test? + assert_equal 'Success', response.message + assert response.authorization + end + def test_failed_purchase credit_card = credit_card('4111111111111111') assert response = @gateway.purchase(200, credit_card, @options) @@ -61,7 +74,7 @@ def test_failed_refund purchase = @gateway.purchase(@amount, credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount, purchase.authorization) + assert refund = @gateway.refund(@amount + 50, purchase.authorization) assert_failure refund end @@ -71,11 +84,8 @@ def test_successful_check_refund assert refund = @gateway.refund(@amount, purchase.authorization) - # This is a dubious test. Basically testing that the refund failed b/c - # the original purchase hadn't yet cleared. No way to test immediate failure - # due to the delay in original tx processing, even for text txs. - assert_failure refund - assert_equal 'REJECT: 3', refund.message + assert_success refund + assert_equal 'Success', refund.message end def test_failed_check_refund @@ -95,18 +105,33 @@ def test_successful_store_and_unstore assert_equal 'Success', unstore.message end + def test_successful_store_and_purchase_and_refund + assert store = @gateway.store(@credit_card, @options) + assert_success store + assert store.authorization + assert_equal 'Success', store.message + + assert purchase = @gateway.purchase(@amount, store.authorization, @options) + assert_success purchase + assert purchase.authorization + assert_equal 'Success', purchase.message + + assert refund = @gateway.refund(@amount, purchase.authorization) + assert_success refund + end + def test_failed_store credit_card = credit_card('4111') assert store = @gateway.store(credit_card, @options) assert_failure store - assert_match /Invalid credit card number/, store.message + assert_match(/Invalid credit card number/, store.message) end def test_invalid_login gateway = IatsPaymentsGateway.new( - :agent_code => 'X', - :password => 'Y', - :region => 'na' + agent_code: 'X', + password: 'Y', + region: 'na' ) assert response = gateway.purchase(@amount, @credit_card) @@ -136,5 +161,4 @@ def test_check_purchase_scrubbing assert_scrubbed(@gateway.options[:agent_code], transcript) assert_scrubbed(@gateway.options[:password], transcript) end - end diff --git a/test/remote/gateways/remote_inspire_test.rb b/test/remote/gateways/remote_inspire_test.rb index d9594ceda57..7541d44aa2a 100644 --- a/test/remote/gateways/remote_inspire_test.rb +++ b/test/remote/gateways/remote_inspire_test.rb @@ -4,12 +4,11 @@ class RemoteBraintreeTest < Test::Unit::TestCase def setup @gateway = InspireGateway.new(fixtures(:inspire)) - @amount = rand(10000) + 1001 - @credit_card = credit_card('4111111111111111', :brand => 'visa') + @amount = rand(1001..11000) + @credit_card = credit_card('4111111111111111', brand: 'visa') @declined_amount = rand(99) - @options = { :order_id => generate_unique_id, - :billing_address => address - } + @options = { order_id: generate_unique_id, + billing_address: address } end def test_successful_purchase @@ -20,12 +19,12 @@ def test_successful_purchase def test_successful_purchase_with_echeck check = ActiveMerchant::Billing::Check.new( - :name => 'Fredd Bloggs', - :routing_number => '111000025', # Valid ABA # - Bank of America, TX - :account_number => '999999999999', - :account_holder_type => 'personal', - :account_type => 'checking' - ) + name: 'Fredd Bloggs', + routing_number: '111000025', # Valid ABA # - Bank of America, TX + account_number: '999999999999', + account_holder_type: 'personal', + account_type: 'checking' + ) response = @gateway.purchase(@amount, check, @options) assert_success response assert_equal 'This transaction has been approved', response.message @@ -51,13 +50,13 @@ def test_successful_add_to_vault_and_use assert_equal 'This transaction has been approved', response.message assert_not_nil customer_id = response.params['customer_vault_id'] - second_response = @gateway.purchase(@amount*2, customer_id, @options) + second_response = @gateway.purchase(@amount * 2, customer_id, @options) assert_equal 'This transaction has been approved', second_response.message assert_success second_response end def test_add_to_vault_with_custom_vault_id - @options[:store] = rand(100000)+10001 + @options[:store] = rand(10001..110000) response = @gateway.purchase(@amount, @credit_card, @options) assert_success response assert_equal 'This transaction has been approved', response.message @@ -65,7 +64,7 @@ def test_add_to_vault_with_custom_vault_id end def test_add_to_vault_with_custom_vault_id_with_store_method - @options[:billing_id] = rand(100000)+10001 + @options[:billing_id] = rand(10001..110000) response = @gateway.store(@credit_card, @options.dup) assert_success response assert_equal 'This transaction has been approved', response.message @@ -74,7 +73,7 @@ def test_add_to_vault_with_custom_vault_id_with_store_method def test_update_vault test_add_to_vault_with_custom_vault_id - @credit_card = credit_card('4111111111111111', :month => 10) + @credit_card = credit_card('4111111111111111', month: 10) response = @gateway.update(@options[:store], @credit_card) assert_success response assert_equal 'Customer Update Successful', response.message @@ -140,7 +139,7 @@ def test_partial_refund response = @gateway.purchase(@amount, @credit_card) assert_success response - response = @gateway.refund(@amount-500, response.authorization) + response = @gateway.refund(@amount - 500, response.authorization) assert_success response end @@ -151,13 +150,11 @@ def test_failed_refund def test_invalid_login gateway = InspireGateway.new( - :login => '', - :password => '' - ) + login: '', + password: '' + ) response = gateway.purchase(@amount, @credit_card, @options) assert_equal 'Invalid Username', response.message assert_failure response end end - - diff --git a/test/remote/gateways/remote_instapay_test.rb b/test/remote/gateways/remote_instapay_test.rb index 2919d97b98d..3565cfaaae8 100644 --- a/test/remote/gateways/remote_instapay_test.rb +++ b/test/remote/gateways/remote_instapay_test.rb @@ -1,7 +1,6 @@ require 'test_helper' class RemoteInstapayTest < Test::Unit::TestCase - def setup @gateway = InstapayGateway.new(fixtures(:instapay)) @@ -10,10 +9,10 @@ def setup @declined_card = credit_card('4000300011112220') @options = { - :order_id => generate_unique_id, - :billing_address => address, - :shipping_address => address, - :description => 'Store Purchase' + order_id: generate_unique_id, + billing_address: address, + shipping_address: address, + description: 'Store Purchase' } end @@ -24,12 +23,12 @@ def test_successful_purchase end def test_failed_purchase - assert response = @gateway.purchase(@amount, @declined_card, @options) + assert response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response end def test_succesful_authorization - assert response = @gateway.authorize(@amount, @credit_card, @options) + assert response = @gateway.authorize(@amount, @credit_card, @options) assert_success response assert_equal InstapayGateway::SUCCESS_MESSAGE, response.message end @@ -38,22 +37,22 @@ def test_failed_authorization assert response = @gateway.authorize(@amount, @declined_card, @options) assert_failure response end - + def test_authorization_and_capture assert authorization = @gateway.authorize(@amount, @credit_card, @options) assert_success authorization - + assert capture = @gateway.capture(@amount, authorization.authorization) assert_success capture assert_equal InstapayGateway::SUCCESS_MESSAGE, capture.message end - + def test_invalid_login gateway = InstapayGateway.new( - :login => 'X', - :password => 'Y' + login: 'X', + password: 'Y' ) - + assert response = gateway.purchase(@amount, @credit_card) assert_failure response assert_equal 'Invalid merchant', response.message diff --git a/test/remote/gateways/remote_ipg_test.rb b/test/remote/gateways/remote_ipg_test.rb new file mode 100644 index 00000000000..026c0d05a24 --- /dev/null +++ b/test/remote/gateways/remote_ipg_test.rb @@ -0,0 +1,199 @@ +require 'test_helper' + +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', 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: '2029') + @options = { + currency: 'ARS' + } + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'APPROVED', response.message + end + + def test_successful_purchase_with_store + payment_token = generate_unique_id + response = @gateway.store(@credit_card, @options.merge({ hosted_data_id: payment_token })) + assert_success response + assert_equal 'true', response.params['successfully'] + + response = @gateway.purchase(@amount, payment_token, @options) + assert_success response + assert_equal 'APPROVED', response.message + end + + def test_successful_purchase_with_store_without_passing_hosted + response = @gateway.store(@credit_card, @options) + assert_success response + assert_equal 'true', response.params['successfully'] + payment_token = response.authorization + assert payment_token + + response = @gateway.purchase(@amount, payment_token, @options) + assert_success response + assert_equal 'APPROVED', response.message + end + + def test_successful_unstore + response = @gateway.store(@credit_card, @options) + assert_success response + assert_equal 'true', response.params['successfully'] + payment_token = response.authorization + assert payment_token + + response = @gateway.unstore(payment_token) + assert_success response + assert_equal 'true', response.params['successfully'] + end + + def test_successful_purchase_with_stored_credential + @options[:stored_credential] = { + initial_transaction: true, + reason_type: '', + initiator: 'merchant', + network_transaction_id: nil + } + order_id = generate_unique_id + assert response = @gateway.purchase(@amount, @credit_card, @options.merge({ order_id: })) + assert_success response + + @options[:stored_credential] = { + initial_transaction: false, + reason_type: '', + initiator: 'merchant', + network_transaction_id: response.params['IpgTransactionId'] + } + + assert recurring_purchase = @gateway.purchase(@amount, @credit_card, @options.merge({ order_id: })) + assert_success recurring_purchase + assert_equal 'APPROVED', recurring_purchase.message + end + + def test_successful_purchase_with_3ds2_options + options = @options.merge( + three_d_secure: { + version: '2.1.0', + cavv: 'jEET5Odser3oCRAyNTY5BVgAAAA=', + xid: 'jHDMyjJJF9bLBCFT/YUbqMhoQ0s=', + directory_response_status: 'Y', + authentication_response_status: 'Y', + ds_transaction_id: '925a0317-9143-5130-8000-0000000f8742' + } + ) + response = @gateway.purchase(@amount, @visa_card, options) + assert_success response + assert_equal 'APPROVED', response.message + end + + def test_failed_purchase + response = @gateway.purchase(@amount, @declined_card, @options) + assert_failure response + assert_match 'DECLINED', response.message + assert_equal 'SGS-050005', response.error_code + end + + def test_failed_purchase_with_passed_in_store_id + 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 + order_id = generate_unique_id + response = @gateway.authorize(@amount, @credit_card, @options.merge!({ order_id: })) + assert_success response + assert_equal 'APPROVED', response.message + + capture = @gateway.capture(@amount, response.authorization, @options) + assert_success capture + assert_equal 'APPROVED', capture.message + end + + def test_failed_authorize + response = @gateway.authorize(@amount, @declined_card, @options) + assert_failure response + 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_match 'FAILED', response.message + assert_equal 'SGS-005001', response.error_code + end + + def test_successful_void + order_id = generate_unique_id + response = @gateway.authorize(@amount, @credit_card, @options.merge!({ order_id: })) + assert_success response + + void = @gateway.void(response.authorization, @options) + assert_success void + assert_equal 'APPROVED', void.message + end + + def test_failed_void + response = @gateway.void('', @options) + assert_failure response + end + + def test_successful_refund + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + + refund = @gateway.refund(@amount, response.authorization, @options) + assert_success refund + assert_equal 'APPROVED', refund.message + end + + def test_failed_refund + response = @gateway.refund(@amount, '', @options) + assert_failure response + assert_match 'FAILED', response.message + assert_equal 'SGS-005001', response.error_code + end + + def test_successful_verify + response = @gateway.verify(@credit_card, @options) + assert_success response + assert_match 'APPROVED', response.message + end + + def test_failed_verify + response = @gateway.verify(@declined_card, @options) + assert_failure response + assert_match 'DECLINED', response.message + assert_equal 'SGS-050005', response.error_code + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @credit_card, @options) + end + transcript = @gateway.scrub(transcript) + assert_scrubbed(@credit_card.number, transcript) + assert_scrubbed(@credit_card.verification_value, transcript) + 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_ipp_test.rb b/test/remote/gateways/remote_ipp_test.rb index 16e17ce473b..443e7b186eb 100644 --- a/test/remote/gateways/remote_ipp_test.rb +++ b/test/remote/gateways/remote_ipp_test.rb @@ -9,7 +9,7 @@ def setup @options = { order_id: '1', billing_address: address, - description: 'Store Purchase', + description: 'Store Purchase' } end @@ -76,7 +76,7 @@ def test_failed_refund def test_invalid_login gateway = IppGateway.new( username: '', - password: '', + password: '' ) response = gateway.purchase(200, @credit_card, @options) assert_failure response diff --git a/test/remote/gateways/remote_iridium_test.rb b/test/remote/gateways/remote_iridium_test.rb index 92dee585764..24460d4c943 100644 --- a/test/remote/gateways/remote_iridium_test.rb +++ b/test/remote/gateways/remote_iridium_test.rb @@ -7,21 +7,21 @@ def setup @gateway = IridiumGateway.new(fixtures(:iridium)) @amount = 100 - @avs_card = credit_card('4921810000005462', {:verification_value => '441'}) - @cv2_card = credit_card('4976000000003436', {:verification_value => '777'}) - @avs_cv2_card = credit_card('4921810000005462', {:verification_value => '777'}) - @credit_card = credit_card('4976000000003436', {:verification_value => '452'}) + @avs_card = credit_card('4921810000005462', { verification_value: '441' }) + @cv2_card = credit_card('4976000000003436', { verification_value: '777' }) + @avs_cv2_card = credit_card('4921810000005462', { verification_value: '777' }) + @credit_card = credit_card('4976000000003436', { verification_value: '452' }) @declined_card = credit_card('4221690000004963') - our_address = address(:address1 => '32 Edward Street', - :address2 => 'Camborne', - :state => 'Cornwall', - :zip => 'TR14 8PA', - :country => '826') + our_address = address(address1: '32 Edward Street', + address2: 'Camborne', + state: 'Cornwall', + zip: 'TR14 8PA', + country: '826') @options = { - :order_id => generate_unique_id, - :billing_address => our_address, - :description => 'Store Purchase' + order_id: generate_unique_id, + billing_address: our_address, + description: 'Store Purchase' } end @@ -101,7 +101,7 @@ def test_successful_authorization_and_failed_capture end def test_failed_capture_bad_auth_info - assert auth = @gateway.authorize(@amount, @credit_card, @options) + assert @gateway.authorize(@amount, @credit_card, @options) assert capture = @gateway.capture(@amount, 'a;b;c', @options) assert_failure capture end @@ -111,16 +111,16 @@ def test_successful_purchase_by_reference assert_success response assert(reference = response.authorization) - assert response = @gateway.purchase(@amount, reference, {:order_id => generate_unique_id}) + assert response = @gateway.purchase(@amount, reference, { order_id: generate_unique_id }) assert_success response end def test_failed_purchase_by_reference assert response = @gateway.authorize(1, @credit_card, @options) assert_success response - assert(reference = response.authorization) + assert response.authorization - assert response = @gateway.purchase(@amount, 'bogusref', {:order_id => generate_unique_id}) + assert response = @gateway.purchase(@amount, 'bogusref', { order_id: generate_unique_id }) assert_failure response end @@ -129,7 +129,7 @@ def test_successful_authorize_by_reference assert_success response assert(reference = response.authorization) - assert response = @gateway.authorize(@amount, reference, {:order_id => generate_unique_id}) + assert response = @gateway.authorize(@amount, reference, { order_id: generate_unique_id }) assert_success response end @@ -145,7 +145,7 @@ def test_failed_credit assert response = @gateway.purchase(@amount, @credit_card, @options) assert_success response - assert response = @gateway.credit(@amount*2, response.authorization) + assert response = @gateway.credit(@amount * 2, response.authorization) assert_failure response end @@ -164,9 +164,9 @@ def test_failed_void def test_invalid_login gateway = IridiumGateway.new( - :login => '', - :password => '' - ) + login: '', + password: '' + ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response diff --git a/test/remote/gateways/remote_itransact_test.rb b/test/remote/gateways/remote_itransact_test.rb index 3ec0172e6b8..55fafbb0388 100644 --- a/test/remote/gateways/remote_itransact_test.rb +++ b/test/remote/gateways/remote_itransact_test.rb @@ -1,36 +1,34 @@ require 'test_helper' class RemoteItransactTest < Test::Unit::TestCase - - def setup @gateway = ItransactGateway.new(fixtures(:itransact)) - + @amount = 1065 @credit_card = credit_card('4000100011112224') @declined_card = credit_card('4000300011112220') - - @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase' + + @options = { + order_id: '1', + billing_address: address, + description: 'Store Purchase' } end - + def test_successful_purchase assert response = @gateway.purchase(@amount, @credit_card, @options) assert_success response assert_nil response.message end -# As of March 8, 2012, iTransact does not provide a way to generate unsuccessful transactions through use of a -# production gateway account in test mode. -# def test_unsuccessful_purchase -# assert response = @gateway.purchase(@amount, @credit_card, @options) -# assert_failure response -# assert_equal 'DECLINE', response.params['error_category'] -# assert_equal 'Code: NBE001 Your credit card was declined by the credit card processing network. Please use another card and resubmit your transaction.', response.message -# end + # As of March 8, 2012, iTransact does not provide a way to generate unsuccessful transactions through use of a + # production gateway account in test mode. + # def test_unsuccessful_purchase + # assert response = @gateway.purchase(@amount, @credit_card, @options) + # assert_failure response + # assert_equal 'DECLINE', response.params['error_category'] + # assert_equal 'Code: NBE001 Your credit card was declined by the credit card processing network. Please use another card and resubmit your transaction.', response.message + # end def test_authorize_and_capture amount = @amount @@ -42,13 +40,13 @@ def test_authorize_and_capture assert_success capture end -# As of March 8, 2012, iTransact does not provide a way to generate unsuccessful transactions through use of a -# production gateway account in test mode. -# def test_failed_capture -# assert response = @gateway.capture(@amount, '9999999999') -# assert_failure response -# assert_equal 'REPLACE WITH GATEWAY FAILURE MESSAGE', response.message -# end + # As of March 8, 2012, iTransact does not provide a way to generate unsuccessful transactions through use of a + # production gateway account in test mode. + # def test_failed_capture + # assert response = @gateway.capture(@amount, '9999999999') + # assert_failure response + # assert_equal 'REPLACE WITH GATEWAY FAILURE MESSAGE', response.message + # end def test_authorize_and_void amount = @amount @@ -65,11 +63,11 @@ def test_void assert_success void end -# As of Sep 19, 2012, iTransact REQUIRES the total amount for the refund. -# def test_refund -# assert refund = @gateway.refund(nil, '9999999999') -# assert_success refund -# end + # As of Sep 19, 2012, iTransact REQUIRES the total amount for the refund. + # def test_refund + # assert refund = @gateway.refund(nil, '9999999999') + # assert_success refund + # end def test_refund_partial assert refund = @gateway.refund(555, '9999999999') # $5.55 in cents @@ -78,10 +76,10 @@ def test_refund_partial def test_invalid_login gateway = ItransactGateway.new( - :login => 'x', - :password => 'x', - :gateway_id => 'x' - ) + login: 'x', + password: 'x', + gateway_id: 'x' + ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal 'Invalid login credentials', response.message diff --git a/test/remote/gateways/remote_iveri_test.rb b/test/remote/gateways/remote_iveri_test.rb index 11935d75858..d68147c2edd 100644 --- a/test/remote/gateways/remote_iveri_test.rb +++ b/test/remote/gateways/remote_iveri_test.rb @@ -24,6 +24,16 @@ def test_successful_purchase assert_equal '100', response.params['amount'] end + def test_successful_purchase_with_iveri_url + credentials = fixtures(:iveri_url).merge(url_override: 'iveri') + @gateway = IveriGateway.new(credentials) + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + assert_equal 'Succeeded', response.message + assert_equal '100', response.params['amount'] + end + def test_successful_purchase_with_more_options options = { ip: '127.0.0.1', @@ -48,7 +58,6 @@ def test_successful_purchase_with_3ds_params assert_equal 'Succeeded', response.message end - def test_failed_purchase response = @gateway.purchase(@amount, @bad_card, @options) assert_failure response @@ -74,7 +83,7 @@ def test_partial_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture end @@ -97,7 +106,7 @@ def test_partial_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount-1, purchase.authorization) + assert refund = @gateway.refund(@amount - 1, purchase.authorization) assert_success refund end @@ -124,13 +133,21 @@ def test_failed_void def test_successful_verify response = @gateway.verify(@credit_card, @options) + # authorization portion is successful since we use that as the main response assert_success response + assert_equal 'Authorisation', response.responses[0].params['transaction_command'] + assert_equal '0', response.responses[0].params['result_status'] + # authorizationreversal portion is successful + assert_success response.responses.last + assert_equal 'AuthorisationReversal', response.responses[1].params['transaction_command'] + assert_equal '0', response.responses[1].params['result_status'] assert_equal 'Succeeded', response.message end def test_failed_verify response = @gateway.verify(@bad_card, @options) - assert_failure response + assert_failure response # assert failure of authorization portion + assert_failure response.responses.last # assert failure of authorisationvoid portion assert_includes ['Denied', 'Hot card', 'Please call'], response.message end @@ -162,4 +179,35 @@ def test_transcript_scrubbing assert_scrubbed(@gateway.options[:cert_id], transcript) end + def test_successful_authorize_with_3ds_v1_options + @options[:three_d_secure] = { + version: '1.0', + cavv: 'FHhirTpN0Aefs4rIzTlheBByD77J', + eci: '02', + xid: SecureRandom.alphanumeric(28), + enrolled: 'true', + authentication_response_status: 'Y' + } + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_authorize_with_3ds_v2_options + @options[:three_d_secure] = { + version: '2.1.0', + cavv: 'FHhirTpN0Aefs4rIzTlheBByD77J', + eci: '02', + ds_transaction_id: 'ODUzNTYzOTcwODU5NzY3Qw==', + enrolled: 'Y', + authentication_response_status: 'Y' + } + + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + assert_equal 'Succeeded', response.message + assert_equal '100', response.params['amount'] + end end diff --git a/test/remote/gateways/remote_ixopay_test.rb b/test/remote/gateways/remote_ixopay_test.rb new file mode 100644 index 00000000000..b695de1a0e1 --- /dev/null +++ b/test/remote/gateways/remote_ixopay_test.rb @@ -0,0 +1,248 @@ +require 'test_helper' + +class RemoteIxopayTest < Test::Unit::TestCase + def setup + @gateway = IxopayGateway.new(fixtures(:ixopay)) + + @amount = 100 + @credit_card = credit_card('4111111111111111') + @declined_card = credit_card('4000300011112220') + + @options = { + billing_address: address, + shipping_address: address, + email: 'test@example.com', + description: 'Store Purchase', + ip: '192.168.1.1', + stored_credential: stored_credential(:initial) + } + + @extra_data = { extra_data: { customData1: 'some data', customData2: 'Can be anything really' } } + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + assert_equal 'FINISHED', response.message + assert_match(/[0-9a-zA-Z]+(|[0-9a-zA-Z]+)*/, response.authorization) + + assert_equal @credit_card.name, response.params.dig('return_data', 'creditcard_data', 'card_holder') + assert_equal '%02d' % @credit_card.month, response.params.dig('return_data', 'creditcard_data', 'expiry_month') + assert_equal @credit_card.year.to_s, response.params.dig('return_data', 'creditcard_data', 'expiry_year') + assert_equal @credit_card.number[0..5], response.params.dig('return_data', 'creditcard_data', 'first_six_digits') + assert_equal @credit_card.number.split(//).last(4).join, response.params.dig('return_data', 'creditcard_data', 'last_four_digits') + assert_equal 'FINISHED', response.params['return_type'] + + assert_not_nil response.params['purchase_id'] + assert_not_nil response.params['reference_id'] + end + + def test_successful_purchase_with_extra_data + response = @gateway.purchase(@amount, @credit_card, @options.merge(@extra_data)) + assert_success response + assert_equal 'FINISHED', response.message + assert_match(/[0-9a-zA-Z]+(|[0-9a-zA-Z]+)*/, response.authorization) + + assert_equal @credit_card.name, response.params.dig('return_data', 'creditcard_data', 'card_holder') + assert_equal '%02d' % @credit_card.month, response.params.dig('return_data', 'creditcard_data', 'expiry_month') + assert_equal @credit_card.year.to_s, response.params.dig('return_data', 'creditcard_data', 'expiry_year') + assert_equal @credit_card.number[0..5], response.params.dig('return_data', 'creditcard_data', 'first_six_digits') + assert_equal @credit_card.number.split(//).last(4).join, response.params.dig('return_data', 'creditcard_data', 'last_four_digits') + assert_equal 'FINISHED', response.params['return_type'] + + assert_not_nil response.params['purchase_id'] + assert_not_nil response.params['reference_id'] + end + + def test_failed_purchase + response = @gateway.purchase(@amount, @declined_card, {}) + + assert_failure response + assert_equal 'The transaction was declined', response.message + assert_equal '2003', response.error_code + end + + def test_failed_authentication + gateway = IxopayGateway.new( + username: 'baduser', + password: 'badpass', + secret: 'badsecret', + api_key: 'badapikey' + ) + + response = gateway.purchase(@amount, @credit_card, {}) + + assert_failure response + + assert_equal 'Invalid Signature', response.message + assert_equal '9999', response.error_code + end + + def test_successful_authorize_and_capture + auth = @gateway.authorize(@amount, @credit_card, @options) + + assert_success auth + assert_equal 'FINISHED', auth.message + assert_not_nil auth.params['purchase_id'] + assert_not_nil auth.params['reference_id'] + assert_not_nil auth.authorization + + assert capture = @gateway.capture(@amount, auth.authorization) + assert_success capture + assert_equal 'FINISHED', capture.message + end + + def test_successful_authorize_and_capture_with_extra_data + auth = @gateway.authorize(@amount, @credit_card, @options.merge(@extra_data)) + + assert_success auth + assert_equal 'FINISHED', auth.message + assert_not_nil auth.params['purchase_id'] + assert_not_nil auth.params['reference_id'] + assert_not_nil auth.authorization + + assert capture = @gateway.capture(@amount, auth.authorization) + assert_success capture + assert_equal 'FINISHED', capture.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 'FINISHED', capture.message + end + + def test_partial_capture_with_extra_data + auth = @gateway.authorize(@amount, @credit_card, @options.merge(@extra_data)) + assert_success auth + + assert capture = @gateway.capture(@amount - 1, auth.authorization) + assert_success capture + assert_equal 'FINISHED', capture.message + end + + def test_failed_authorize + response = @gateway.authorize(@amount, @declined_card, @options) + assert_failure response + assert_equal 'The transaction was declined', response.message + assert_equal 'ERROR', response.params['return_type'] + assert_equal response.error_code, '2003' + end + + def test_failed_capture + response = @gateway.capture(@amount, nil) + + assert_failure response + assert_equal 'Transaction of type "capture" requires a referenceTransactionId', response.message + end + + def test_successful_refund + options = @options.update(currency: 'USD') + + purchase = @gateway.purchase(@amount, @credit_card, options) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization, options) + assert_success refund + assert_equal 'FINISHED', refund.message + end + + def test_successful_refund_with_extra_data + options = @options.update(currency: 'USD') + + purchase = @gateway.purchase(@amount, @credit_card, options.merge(@extra_data)) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization, options) + assert_success refund + assert_equal 'FINISHED', refund.message + end + + def test_partial_refund + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount - 1, purchase.authorization) + assert_success refund + end + + def test_partial_refund_with_extra_data + purchase = @gateway.purchase(@amount, @credit_card, @options.merge(@extra_data)) + assert_success purchase + + assert refund = @gateway.refund(@amount - 1, purchase.authorization) + assert_success refund + end + + def test_failed_refund + response = @gateway.refund(@amount, nil) + assert_failure response + assert_equal 'Transaction of type "refund" requires a referenceTransactionId', response.message + end + + def test_successful_void + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert void = @gateway.void(auth.authorization) + assert_success void + assert_equal 'FINISHED', void.message + end + + def test_successful_void_with_extra_data + auth = @gateway.authorize(@amount, @credit_card, @options.merge(@extra_data)) + assert_success auth + + assert void = @gateway.void(auth.authorization) + assert_success void + assert_equal 'FINISHED', void.message + end + + def test_failed_void + response = @gateway.void('') + assert_failure response + assert_equal 'Transaction of type "void" requires a referenceTransactionId', response.message + end + + def test_successful_verify + response = @gateway.verify(@credit_card, @options) + assert_success response + assert_match %r{FINISHED}, response.message + end + + def test_successful_verify_with_extra_data + response = @gateway.verify(@credit_card, @options.merge(@extra_data)) + assert_success response + assert_match %r{FINISHED}, response.message + end + + def test_failed_verify + response = @gateway.verify(@declined_card, @options) + assert_failure response + assert_match %r{The transaction was declined}, response.message + end + + def test_invalid_login + gateway = IxopayGateway.new(username: '', password: '', secret: '', api_key: '') + + response = gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_match %r{Invalid Signature: Invalid authorization header}, 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[:password], transcript) + end +end diff --git a/test/remote/gateways/remote_jetpay_test.rb b/test/remote/gateways/remote_jetpay_test.rb index 29f0c40b01b..7f4a38c2539 100644 --- a/test/remote/gateways/remote_jetpay_test.rb +++ b/test/remote/gateways/remote_jetpay_test.rb @@ -1,7 +1,6 @@ require 'test_helper' class RemoteJetpayTest < Test::Unit::TestCase - def setup @gateway = JetpayGateway.new(fixtures(:jetpay)) @@ -9,12 +8,12 @@ def setup @declined_card = credit_card('4000300020001000') @options = { - :billing_address => address(:country => 'US', :zip => '75008'), - :shipping_address => address(:country => 'US'), - :email => 'test@test.com', - :ip => '127.0.0.1', - :order_id => '12345', - :tax => 7 + billing_address: address(country: 'US', zip: '75008'), + shipping_address: address(country: 'US'), + email: 'test@test.com', + ip: '127.0.0.1', + order_id: '12345', + tax: 7 } end @@ -33,7 +32,7 @@ def test_unsuccessful_purchase end def test_successful_purchase_with_origin - assert response = @gateway.purchase(9900, @credit_card, {:origin => 'RECURRING'}) + assert response = @gateway.purchase(9900, @credit_card, { origin: 'RECURRING' }) assert_success response assert_equal 'APPROVED', response.message assert_not_nil response.authorization @@ -75,7 +74,6 @@ def test_ud_fields_on_capture assert_success capture end - def test_void # must void a valid auth assert auth = @gateway.authorize(9900, @credit_card, @options) @@ -84,7 +82,6 @@ def test_void assert_not_nil auth.authorization assert_not_nil auth.params['approval'] - assert void = @gateway.void(auth.authorization) assert_success void end @@ -125,7 +122,7 @@ def test_capture_refund_with_token def test_refund_backwards_compatible # no need for csv - card = credit_card('4242424242424242', :verification_value => nil) + card = credit_card('4242424242424242', verification_value: nil) assert response = @gateway.purchase(9900, card, @options) assert_success response @@ -136,7 +133,7 @@ def test_refund_backwards_compatible old_authorization = [response.params['transaction_id'], response.params['approval'], 9900].join(';') # linked to a specific transaction_id - assert credit = @gateway.refund(9900, old_authorization, :credit_card => card) + assert credit = @gateway.refund(9900, old_authorization, credit_card: card) assert_success credit assert_not_nil(credit.authorization) assert_not_nil(response.params['approval']) @@ -145,7 +142,7 @@ def test_refund_backwards_compatible def test_credit # no need for csv - card = credit_card('4242424242424242', :verification_value => nil) + card = credit_card('4242424242424242', verification_value: nil) # no link to a specific transaction_id assert credit = @gateway.credit(9900, card) @@ -161,7 +158,7 @@ def test_failed_capture end def test_invalid_login - gateway = JetpayGateway.new(:login => 'bogus') + gateway = JetpayGateway.new(login: 'bogus') assert response = gateway.purchase(9900, @credit_card, @options) assert_failure response @@ -169,7 +166,7 @@ def test_invalid_login end def test_missing_login - gateway = JetpayGateway.new(:login => '') + gateway = JetpayGateway.new(login: '') assert response = gateway.purchase(9900, @credit_card, @options) assert_failure response diff --git a/test/remote/gateways/remote_jetpay_v2_certification_test.rb b/test/remote/gateways/remote_jetpay_v2_certification_test.rb index 4916dd08435..a213fc82691 100644 --- a/test/remote/gateways/remote_jetpay_v2_certification_test.rb +++ b/test/remote/gateways/remote_jetpay_v2_certification_test.rb @@ -1,20 +1,19 @@ require 'test_helper' class RemoteJetpayV2CertificationTest < Test::Unit::TestCase - def setup @gateway = JetpayV2Gateway.new(fixtures(:jetpay_v2)) @unique_id = '' @options = { - :device => 'spreedly', - :application => 'spreedly', - :developer_id => 'GenkID', - :billing_address => address(:address1 => '1234 Fifth Street', :address2 => '', :city => 'Beaumont', :state => 'TX', :country => 'US', :zip => '77708'), - :shipping_address => address(:address1 => '1234 Fifth Street', :address2 => '', :city => 'Beaumont', :state => 'TX', :country => 'US', :zip => '77708'), - :email => 'test@test.com', - :ip => '127.0.0.1' + device: 'spreedly', + application: 'spreedly', + developer_id: 'GenkID', + billing_address: address(address1: '1234 Fifth Street', address2: '', city: 'Beaumont', state: 'TX', country: 'US', zip: '77708'), + shipping_address: address(address1: '1234 Fifth Street', address2: '', city: 'Beaumont', state: 'TX', country: 'US', zip: '77708'), + email: 'test@test.com', + ip: '127.0.0.1' } end @@ -25,7 +24,7 @@ def teardown def test_certification_cnp1_authorize_mastercard @options[:order_id] = 'CNP1' amount = 1000 - master = credit_card('5111111111111118', :month => 12, :year => 2017, :brand => 'master', :verification_value => '121') + master = credit_card('5111111111111118', month: 12, year: 2017, brand: 'master', verification_value: '121') assert response = @gateway.authorize(amount, master, @options) assert_success response assert_equal 'APPROVED', response.message @@ -35,7 +34,7 @@ def test_certification_cnp1_authorize_mastercard def test_certification_cnp2_authorize_visa @options[:order_id] = 'CNP2' amount = 1105 - visa = credit_card('4111111111111111', :month => 12, :year => 2017, :brand => 'visa', :verification_value => '121') + visa = credit_card('4111111111111111', month: 12, year: 2017, brand: 'visa', verification_value: '121') assert response = @gateway.authorize(amount, visa, @options) assert_failure response assert_equal 'Do not honor.', response.message @@ -45,7 +44,7 @@ def test_certification_cnp2_authorize_visa def test_certification_cnp3_cnp4_authorize_and_capture_amex @options[:order_id] = 'CNP3' amount = 1200 - amex = credit_card('378282246310005', :month => 12, :year => 2017, :brand => 'american_express', :verification_value => '1221') + amex = credit_card('378282246310005', month: 12, year: 2017, brand: 'american_express', verification_value: '1221') assert response = @gateway.authorize(amount, amex, @options) assert_success response assert_equal 'APPROVED', response.message @@ -61,7 +60,7 @@ def test_certification_cnp3_cnp4_authorize_and_capture_amex def test_certification_cnp5_purchase_discover @options[:order_id] = 'CNP5' amount = 1300 - discover = credit_card('6011111111111117', :month => 12, :year => 2017, :brand => 'discover', :verification_value => '121') + discover = credit_card('6011111111111117', month: 12, year: 2017, brand: 'discover', verification_value: '121') assert response = @gateway.purchase(amount, discover, @options) assert_success response assert_equal 'APPROVED', response.message @@ -71,7 +70,7 @@ def test_certification_cnp5_purchase_discover def test_certification_cnp6_purchase_visa @options[:order_id] = 'CNP6' amount = 1405 - visa = credit_card('4111111111111111', :month => 12, :year => 2017, :brand => 'visa', :verification_value => '120') + visa = credit_card('4111111111111111', month: 12, year: 2017, brand: 'visa', verification_value: '120') assert response = @gateway.purchase(amount, visa, @options) assert_failure response assert_equal 'Do not honor.', response.message @@ -81,7 +80,7 @@ def test_certification_cnp6_purchase_visa def test_certification_cnp7_authorize_mastercard @options[:order_id] = 'CNP7' amount = 1500 - master = credit_card('5111111111111118', :month => 12, :year => 2017, :brand => 'master', :verification_value => '120') + master = credit_card('5111111111111118', month: 12, year: 2017, brand: 'master', verification_value: '120') assert response = @gateway.authorize(amount, master, @options) assert_success response assert_equal 'APPROVED', response.message @@ -91,7 +90,7 @@ def test_certification_cnp7_authorize_mastercard def test_certification_cnp8_authorize_visa @options[:order_id] = 'CNP8' amount = 1605 - visa = credit_card('4111111111111111', :month => 12, :year => 2017, :brand => 'visa', :verification_value => '120') + visa = credit_card('4111111111111111', month: 12, year: 2017, brand: 'visa', verification_value: '120') assert response = @gateway.authorize(amount, visa, @options) assert_failure response assert_equal 'Do not honor.', response.message @@ -101,7 +100,7 @@ def test_certification_cnp8_authorize_visa def test_certification_cnp9_cnp10_authorize_and_capture_amex @options[:order_id] = 'CNP9' amount = 1700 - amex = credit_card('378282246310005', :month => 12, :year => 2017, :brand => 'american_express', :verification_value => '1220') + amex = credit_card('378282246310005', month: 12, year: 2017, brand: 'american_express', verification_value: '1220') assert response = @gateway.authorize(amount, amex, @options) assert_success response assert_equal 'APPROVED', response.message @@ -117,7 +116,7 @@ def test_certification_cnp9_cnp10_authorize_and_capture_amex def test_certification_cnp11_purchase_discover @options[:order_id] = 'CNP11' amount = 1800 - discover = credit_card('6011111111111117', :month => 12, :year => 2017, :brand => 'discover', :verification_value => '120') + discover = credit_card('6011111111111117', month: 12, year: 2017, brand: 'discover', verification_value: '120') assert response = @gateway.purchase(amount, discover, @options) assert_success response assert_equal 'APPROVED', response.message @@ -130,7 +129,7 @@ def test_certification_rec01_recurring_mastercard @options[:billing_address] = nil @options[:shipping_address] = nil amount = 2000 - master = credit_card('5111111111111118', :month => 12, :year => 2017, :brand => 'master', :verification_value => '120') + master = credit_card('5111111111111118', month: 12, year: 2017, brand: 'master', verification_value: '120') assert response = @gateway.purchase(amount, master, @options) assert_success response assert_equal 'APPROVED', response.message @@ -141,7 +140,7 @@ def test_certification_rec02_recurring_visa @options[:order_id] = 'REC02' @options[:origin] = 'RECURRING' amount = 2100 - visa = credit_card('4111111111111111', :month => 12, :year => 2017, :brand => 'visa', :verification_value => '') + visa = credit_card('4111111111111111', month: 12, year: 2017, brand: 'visa', verification_value: '') assert response = @gateway.purchase(amount, visa, @options) assert_success response assert_equal 'APPROVED', response.message @@ -152,7 +151,7 @@ def test_certification_rec03_recurring_amex @options[:order_id] = 'REC03' @options[:origin] = 'RECURRING' amount = 2200 - amex = credit_card('378282246310005', :month => 12, :year => 2017, :brand => 'american_express', :verification_value => '1221') + amex = credit_card('378282246310005', month: 12, year: 2017, brand: 'american_express', verification_value: '1221') assert response = @gateway.purchase(amount, amex, @options) assert_success response assert_equal 'APPROVED', response.message @@ -162,7 +161,7 @@ def test_certification_rec03_recurring_amex def test_certification_corp07_corp08_authorize_and_capture_discover @options[:order_id] = 'CORP07' amount = 2500 - discover = credit_card('6011111111111117', :month => 12, :year => 2018, :brand => 'discover', :verification_value => '120') + discover = credit_card('6011111111111117', month: 12, year: 2018, brand: 'discover', verification_value: '120') assert response = @gateway.authorize(amount, discover, @options) assert_success response assert_equal 'APPROVED', response.message @@ -170,7 +169,7 @@ def test_certification_corp07_corp08_authorize_and_capture_discover puts "\n#{@options[:order_id]}: #{@unique_id}" @options[:order_id] = 'CORP08' - assert response = @gateway.capture(amount, response.authorization, @options.merge(:tax_amount => '200')) + assert response = @gateway.capture(amount, response.authorization, @options.merge(tax_amount: '200')) assert_success response @unique_id = response.params['unique_id'] end @@ -178,7 +177,7 @@ def test_certification_corp07_corp08_authorize_and_capture_discover def test_certification_corp09_corp10_authorize_and_capture_visa @options[:order_id] = 'CORP09' amount = 5000 - visa = credit_card('4111111111111111', :month => 12, :year => 2018, :brand => 'visa', :verification_value => '120') + visa = credit_card('4111111111111111', month: 12, year: 2018, brand: 'visa', verification_value: '120') assert response = @gateway.authorize(amount, visa, @options) assert_success response assert_equal 'APPROVED', response.message @@ -186,7 +185,7 @@ def test_certification_corp09_corp10_authorize_and_capture_visa puts "\n#{@options[:order_id]}: #{@unique_id}" @options[:order_id] = 'CORP10' - assert response = @gateway.capture(amount, response.authorization, @options.merge(:tax_amount => '0', :tax_exempt => 'true')) + assert response = @gateway.capture(amount, response.authorization, @options.merge(tax_amount: '0', tax_exempt: 'true')) assert_success response @unique_id = response.params['unique_id'] end @@ -194,7 +193,7 @@ def test_certification_corp09_corp10_authorize_and_capture_visa def test_certification_corp11_corp12_authorize_and_capture_mastercard @options[:order_id] = 'CORP11' amount = 7500 - master = credit_card('5111111111111118', :month => 12, :year => 2018, :brand => 'master', :verification_value => '120') + master = credit_card('5111111111111118', month: 12, year: 2018, brand: 'master', verification_value: '120') assert response = @gateway.authorize(amount, master, @options) assert_success response assert_equal 'APPROVED', response.message @@ -202,7 +201,7 @@ def test_certification_corp11_corp12_authorize_and_capture_mastercard puts "\n#{@options[:order_id]}: #{@unique_id}" @options[:order_id] = 'CORP12' - assert response = @gateway.capture(amount, response.authorization, @options.merge(:tax_amount => '0', :tax_exempt => 'false', :purchase_order => '456456')) + assert response = @gateway.capture(amount, response.authorization, @options.merge(tax_amount: '0', tax_exempt: 'false', purchase_order: '456456')) assert_success response @unique_id = response.params['unique_id'] end @@ -210,7 +209,7 @@ def test_certification_corp11_corp12_authorize_and_capture_mastercard def test_certification_cred02_credit_visa @options[:order_id] = 'CRED02' amount = 100 - visa = credit_card('4111111111111111', :month => 12, :year => 2017, :brand => 'visa', :verification_value => '120') + visa = credit_card('4111111111111111', month: 12, year: 2017, brand: 'visa', verification_value: '120') assert response = @gateway.credit(amount, visa, @options) assert_success response assert_equal 'APPROVED', response.message @@ -220,7 +219,7 @@ def test_certification_cred02_credit_visa def test_certification_cred03_credit_amex @options[:order_id] = 'CRED03' amount = 200 - amex = credit_card('378282246310005', :month => 12, :year => 2017, :brand => 'american_express', :verification_value => '1220') + amex = credit_card('378282246310005', month: 12, year: 2017, brand: 'american_express', verification_value: '1220') assert response = @gateway.credit(amount, amex, @options) assert_success response assert_equal 'APPROVED', response.message @@ -230,7 +229,7 @@ def test_certification_cred03_credit_amex def test_certification_void03_void04_purchase_void_visa @options[:order_id] = 'VOID03' amount = 300 - visa = credit_card('4111111111111111', :month => 12, :year => 2017, :brand => 'visa', :verification_value => '120') + visa = credit_card('4111111111111111', month: 12, year: 2017, brand: 'visa', verification_value: '120') assert response = @gateway.purchase(amount, visa, @options) assert_success response assert_equal 'APPROVED', response.message @@ -238,7 +237,7 @@ def test_certification_void03_void04_purchase_void_visa puts "\n#{@options[:order_id]}: #{@unique_id}" @options[:order_id] = 'VOID04' - transaction_id, approval, amount, token = response.authorization.split(';') + transaction_id, approval, _amount, token = response.authorization.split(';') amount = 500 authorization = [transaction_id, approval, amount, token].join(';') assert response = @gateway.void(authorization, @options) @@ -249,7 +248,7 @@ def test_certification_void03_void04_purchase_void_visa def test_certification_void07_void08_void09_authorize_capture_void_discover @options[:order_id] = 'VOID07' amount = 400 - discover = credit_card('6011111111111117', :month => 12, :year => 2017, :brand => 'discover', :verification_value => '120') + discover = credit_card('6011111111111117', month: 12, year: 2017, brand: 'discover', verification_value: '120') assert response = @gateway.authorize(amount, discover, @options) assert_success response assert_equal 'APPROVED', response.message @@ -272,7 +271,7 @@ def test_certification_void07_void08_void09_authorize_capture_void_discover def test_certification_void12_void13_credit_void_visa @options[:order_id] = 'VOID12' amount = 800 - visa = credit_card('4111111111111111', :month => 12, :year => 2017, :brand => 'visa', :verification_value => '120') + visa = credit_card('4111111111111111', month: 12, year: 2017, brand: 'visa', verification_value: '120') assert response = @gateway.credit(amount, visa, @options) assert_success response assert_equal 'APPROVED', response.message @@ -287,7 +286,7 @@ def test_certification_void12_void13_credit_void_visa def test_certification_tok15_tokenize_mastercard @options[:order_id] = 'TOK15' - master = credit_card('5111111111111118', :month => 12, :year => 2017, :brand => 'master', :verification_value => '101') + master = credit_card('5111111111111118', month: 12, year: 2017, brand: 'master', verification_value: '101') assert response = @gateway.store(master, @options) assert_success response assert_equal 'APPROVED', response.message @@ -298,11 +297,11 @@ def test_certification_tok15_tokenize_mastercard def test_certification_tok16_authorize_with_token_request_visa @options[:order_id] = 'TOK16' amount = 3100 - visa = credit_card('4111111111111111', :month => 12, :year => 2017, :brand => 'visa', :verification_value => '101') + visa = credit_card('4111111111111111', month: 12, year: 2017, brand: 'visa', verification_value: '101') assert response = @gateway.authorize(amount, visa, @options) assert_success response assert_equal 'APPROVED', response.message - transaction_id, approval, amount, token = response.authorization.split(';') + _transaction_id, _approval, _amount, token = response.authorization.split(';') assert_equal token, response.params['token'] @unique_id = response.params['unique_id'] end @@ -310,17 +309,17 @@ def test_certification_tok16_authorize_with_token_request_visa def test_certification_tok17_purchase_with_token_request_amex @options[:order_id] = 'TOK17' amount = 3200 - amex = credit_card('378282246310005', :month => 12, :year => 2017, :brand => 'american_express', :verification_value => '1001') + amex = credit_card('378282246310005', month: 12, year: 2017, brand: 'american_express', verification_value: '1001') assert response = @gateway.purchase(amount, amex, @options) assert_success response assert_equal 'APPROVED', response.message - transaction_id, approval, amount, token = response.authorization.split(';') + _transaction_id, _approval, _amount, token = response.authorization.split(';') assert_equal token, response.params['token'] @unique_id = response.params['unique_id'] end def test_certification_tok18_authorize_using_token_mastercard - master = credit_card('5111111111111118', :month => 12, :year => 2017, :brand => 'master', :verification_value => '101') + master = credit_card('5111111111111118', month: 12, year: 2017, brand: 'master', verification_value: '101') assert response = @gateway.store(master, @options) assert_success response @@ -333,7 +332,7 @@ def test_certification_tok18_authorize_using_token_mastercard end def test_certification_tok19_purchase_using_token_visa - visa = credit_card('4111111111111111', :month => 12, :year => 2017, :brand => 'visa', :verification_value => '101') + visa = credit_card('4111111111111111', month: 12, year: 2017, brand: 'visa', verification_value: '101') assert response = @gateway.store(visa, @options) assert_success response @@ -344,5 +343,4 @@ def test_certification_tok19_purchase_using_token_visa assert_equal 'APPROVED', response.message @unique_id = response.params['unique_id'] end - end diff --git a/test/remote/gateways/remote_jetpay_v2_test.rb b/test/remote/gateways/remote_jetpay_v2_test.rb index aa01fb19c6b..f2502bc7f32 100644 --- a/test/remote/gateways/remote_jetpay_v2_test.rb +++ b/test/remote/gateways/remote_jetpay_v2_test.rb @@ -1,7 +1,6 @@ require 'test_helper' class RemoteJetpayV2Test < Test::Unit::TestCase - def setup @gateway = JetpayV2Gateway.new(fixtures(:jetpay_v2)) @@ -11,14 +10,14 @@ def setup @amount_declined = 5205 @options = { - :device => 'spreedly', - :application => 'spreedly', - :developer_id => 'GenkID', - :billing_address => address(:city => 'Durham', :state => 'NC', :country => 'US', :zip => '27701'), - :shipping_address => address(:city => 'Durham', :state => 'NC', :country => 'US', :zip => '27701'), - :email => 'test@test.com', - :ip => '127.0.0.1', - :order_id => '12345' + device: 'spreedly', + application: 'spreedly', + developer_id: 'GenkID', + billing_address: address(city: 'Durham', state: 'NC', country: 'US', zip: '27701'), + shipping_address: address(city: 'Durham', state: 'NC', country: 'US', zip: '27701'), + email: 'test@test.com', + ip: '127.0.0.1', + order_id: '12345' } end @@ -37,7 +36,7 @@ def test_failed_purchase end def test_successful_purchase_with_minimal_options - assert response = @gateway.purchase(@amount_approved, @credit_card, {:device => 'spreedly', :application => 'spreedly'}) + assert response = @gateway.purchase(@amount_approved, @credit_card, { device: 'spreedly', application: 'spreedly' }) assert_success response assert_equal 'APPROVED', response.message assert_not_nil response.authorization @@ -49,7 +48,7 @@ def test_successful_purchase_with_additional_options ud_field_1: 'Value1', ud_field_2: 'Value2', ud_field_3: 'Value3' - ) + ) assert response = @gateway.purchase(@amount_approved, @credit_card, options) assert_success response end @@ -72,7 +71,7 @@ def test_successful_authorize_and_capture_with_tax assert_not_nil auth.authorization assert_not_nil auth.params['approval'] - assert capture = @gateway.capture(@amount_approved, auth.authorization, @options.merge(:tax_amount => '990', :purchase_order => 'ABC12345')) + assert capture = @gateway.capture(@amount_approved, auth.authorization, @options.merge(tax_amount: '990', purchase_order: 'ABC12345')) assert_success capture end @@ -100,7 +99,6 @@ def test_successful_void assert_not_nil auth.authorization assert_not_nil auth.params['approval'] - assert void = @gateway.void(auth.authorization, @options) assert_success void end @@ -148,7 +146,7 @@ def test_failed_refund end def test_successful_credit - card = credit_card('4242424242424242', :verification_value => nil) + card = credit_card('4242424242424242', verification_value: nil) assert credit = @gateway.credit(@amount_approved, card, @options) assert_success credit @@ -157,7 +155,7 @@ def test_successful_credit end def test_failed_credit - card = credit_card('2424242424242424', :verification_value => nil) + card = credit_card('2424242424242424', verification_value: nil) assert credit = @gateway.credit(@amount_approved, card, @options) assert_failure credit @@ -170,7 +168,7 @@ def test_successful_verify end def test_failed_verify - card = credit_card('2424242424242424', :verification_value => nil) + card = credit_card('2424242424242424', verification_value: nil) assert verify = @gateway.verify(card, @options) assert_failure verify @@ -178,7 +176,7 @@ def test_failed_verify end def test_invalid_login - gateway = JetpayV2Gateway.new(:login => 'bogus') + gateway = JetpayV2Gateway.new(login: 'bogus') assert response = gateway.purchase(@amount_approved, @credit_card, @options) assert_failure response @@ -186,7 +184,7 @@ def test_invalid_login end def test_missing_login - gateway = JetpayV2Gateway.new(:login => '') + gateway = JetpayV2Gateway.new(login: '') assert response = gateway.purchase(@amount_approved, @credit_card, @options) assert_failure response diff --git a/test/remote/gateways/remote_komoju_test.rb b/test/remote/gateways/remote_komoju_test.rb index 5214eb01fbf..9a5213fcd27 100644 --- a/test/remote/gateways/remote_komoju_test.rb +++ b/test/remote/gateways/remote_komoju_test.rb @@ -11,13 +11,13 @@ def setup @fraudulent_card = credit_card('4123111111111083') @options = { - :order_id => generate_unique_id, - :description => 'Store Purchase', - :tax => '10.0', - :ip => '192.168.0.1', - :email => 'valid@email.com', - :browser_language => 'en', - :browser_user_agent => 'user_agent' + order_id: generate_unique_id, + description: 'Store Purchase', + tax: '10.0', + ip: '192.168.0.1', + email: 'valid@email.com', + browser_language: 'en', + browser_user_agent: 'user_agent' } end @@ -65,7 +65,7 @@ def test_detected_fraud end def test_invalid_login - gateway = KomojuGateway.new(:login => 'abc') + gateway = KomojuGateway.new(login: 'abc') response = gateway.purchase(@amount, @credit_card, @options) assert_failure response end diff --git a/test/remote/gateways/remote_kushki_test.rb b/test/remote/gateways/remote_kushki_test.rb index b4eb060de00..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', @@ -23,7 +31,40 @@ def test_successful_purchase_with_options subtotal_iva: '10', iva: '1.54', ice: '3.50' - } + }, + contact_details: { + document_type: 'CC', + document_number: '123456', + email: 'who_dis@monkeys.tv', + first_name: 'Who', + last_name: 'Dis', + second_last_name: 'Buscemi', + phone_number: '+13125556789' + }, + 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 * ( @@ -39,6 +80,74 @@ def test_successful_purchase_with_options assert_match %r(^\d+$), response.authorization end + def test_successful_purchase_with_extra_taxes_cop + options = { + currency: 'COP', + amount: { + subtotal_iva_0: '4.95', + subtotal_iva: '10', + iva: '1.54', + ice: '3.50', + extra_taxes: { + propina: 0.1, + tasa_aeroportuaria: 0.2, + agencia_de_viaje: 0.3, + iac: 0.4 + } + } + } + + amount = 100 * ( + options[:amount][:subtotal_iva_0].to_f + + options[:amount][:subtotal_iva].to_f + + options[:amount][:iva].to_f + + options[:amount][:ice].to_f + + options[:amount][:extra_taxes][:propina].to_f + + options[:amount][:extra_taxes][:tasa_aeroportuaria].to_f + + options[:amount][:extra_taxes][:agencia_de_viaje].to_f + + options[:amount][:extra_taxes][:iac].to_f + ) + + response = @gateway.purchase(amount, @credit_card, options) + assert_success response + assert_equal 'Succeeded', response.message + assert_match %r(^\d+$), response.authorization + end + + def test_successful_purchase_with_extra_taxes_usd + options = { + currency: 'USD', + amount: { + subtotal_iva_0: '4.95', + subtotal_iva: '10', + iva: '1.54', + ice: '3.50', + extra_taxes: { + propina: 0.1, + tasa_aeroportuaria: 0.2, + agencia_de_viaje: 0.3, + iac: 0.4 + } + } + } + + amount = 100 * ( + options[:amount][:subtotal_iva_0].to_f + + options[:amount][:subtotal_iva].to_f + + options[:amount][:iva].to_f + + options[:amount][:ice].to_f + + options[:amount][:extra_taxes][:propina].to_f + + options[:amount][:extra_taxes][:tasa_aeroportuaria].to_f + + options[:amount][:extra_taxes][:agencia_de_viaje].to_f + + options[:amount][:extra_taxes][:iac].to_f + ) + + response = @gateway.purchase(amount, @credit_card, options) + assert_success response + assert_equal 'Succeeded', response.message + assert_match %r(^\d+$), response.authorization + end + def test_failed_purchase options = { amount: { @@ -51,6 +160,161 @@ def test_failed_purchase assert_equal 'Monto de la transacción es diferente al monto de la venta inicial', response.message end + def test_successful_authorize + 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 + end + + def test_approval_code_comes_back_when_passing_full_response + options = { + full_response: true + } + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_not_empty response.params.dig('details', 'approvalCode') + assert_equal 'Succeeded', response.message + end + + def test_failed_authorize + options = { + amount: { + subtotal_iva: '200' + } + } + response = @gateway.authorize(@amount, @credit_card, options) + assert_failure response + assert_equal 'K220', response.responses.last.error_code + 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 + + assert capture = @gateway.capture(@amount, auth.authorization) + assert_success capture + assert_equal 'Succeeded', capture.message + end + + def test_failed_capture + options = { + amount: { + subtotal_iva: '200' + } + } + auth = @gateway.authorize(@amount, @credit_card) + assert_success auth + + capture = @gateway.capture(@amount, auth.authorization, options) + assert_failure capture + assert_equal 'K012', capture.error_code + assert_equal 'Monto de captura inválido.', capture.message + end + def test_successful_refund purchase = @gateway.purchase(@amount, @credit_card) assert_success purchase @@ -69,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 @@ -81,7 +365,7 @@ def test_successful_void def test_failed_void response = @gateway.void('000') assert_failure response - assert_equal 'El monto de la transacción es requerido', response.message + assert_equal 'Cuerpo de la petición inválido.', response.message end def test_invalid_login @@ -89,7 +373,7 @@ def test_invalid_login response = gateway.purchase(@amount, @credit_card) assert_failure response - assert_match %r{ID de comercio no válido}, response.message + assert_match %r{Unauthorized}, response.message end def test_transcript_scrubbing diff --git a/test/remote/gateways/remote_linkpoint_test.rb b/test/remote/gateways/remote_linkpoint_test.rb index d7a14b214f7..efcffa07dec 100644 --- a/test/remote/gateways/remote_linkpoint_test.rb +++ b/test/remote/gateways/remote_linkpoint_test.rb @@ -36,7 +36,7 @@ def setup @amount = 100 @credit_card = credit_card('4111111111111111') - @options = { :order_id => generate_unique_id, :billing_address => address } + @options = { order_id: generate_unique_id, billing_address: address } end def test_successful_authorization @@ -90,22 +90,24 @@ def test_successfull_purchase_and_credit end def test_successfull_purchase_with_item_entity - @options.merge!({:line_items => [ - {:id => '123456', :description => 'Logo T-Shirt', :price => '12.00', :quantity => '1', :options => - [{:name => 'Color', :value => 'Red'}, {:name => 'Size', :value => 'XL'}]}, - {:id => '111', :description => 'keychain', :price => '3.00', :quantity => '1'}]}) + @options[:line_items] = [ + { id: '123456', description: 'Logo T-Shirt', price: '12.00', quantity: '1', + options: [{ name: 'Color', value: 'Red' }, { name: 'Size', value: 'XL' }] }, + { id: '111', description: 'keychain', price: '3.00', quantity: '1' } + ] assert purchase = @gateway.purchase(1500, @credit_card, @options) assert_success purchase - end def test_successful_recurring_payment - assert response = @gateway.recurring(2400, @credit_card, - :order_id => generate_unique_id, - :installments => 12, - :startdate => 'immediate', - :periodicity => :monthly, - :billing_address => address + assert response = @gateway.recurring( + 2400, + @credit_card, + order_id: generate_unique_id, + installments: 12, + startdate: 'immediate', + periodicity: :monthly, + billing_address: address ) assert_success response diff --git a/test/remote/gateways/remote_litle_certification_test.rb b/test/remote/gateways/remote_litle_certification_test.rb index 94ed48e24e9..995065dd443 100644 --- a/test/remote/gateways/remote_litle_certification_test.rb +++ b/test/remote/gateways/remote_litle_certification_test.rb @@ -9,146 +9,146 @@ def setup def test1 credit_card = CreditCard.new( - :number => '4457010000000009', - :month => '01', - :year => '2021', - :verification_value => '349', - :brand => 'visa' + number: '4457010000000009', + month: '01', + year: '2021', + verification_value: '349', + brand: 'visa' ) options = { - :order_id => '1', - :billing_address => { - :name => 'John & Mary Smith', - :address1 => '1 Main St.', - :city => 'Burlington', - :state => 'MA', - :zip => '01803-3747', - :country => 'US' + order_id: '1', + billing_address: { + name: 'John & Mary Smith', + address1: '1 Main St.', + city: 'Burlington', + state: 'MA', + zip: '01803-3747', + country: 'US' } } - auth_assertions(10100, credit_card, options, :avs => 'X', :cvv => 'M') + auth_assertions(10100, credit_card, options, avs: 'X', cvv: 'M') - authorize_avs_assertions(credit_card, options, :avs => 'X', :cvv => 'M') + authorize_avs_assertions(credit_card, options, avs: 'X', cvv: 'M') - sale_assertions(10100, credit_card, options, :avs => 'X', :cvv => 'M') + sale_assertions(10100, credit_card, options, avs: 'X', cvv: 'M') end def test2 - credit_card = CreditCard.new(:number => '5112010000000003', :month => '02', - :year => '2021', :brand => 'master', - :verification_value => '261', - :name => 'Mike J. Hammer') + credit_card = CreditCard.new(number: '5112010000000003', month: '02', + year: '2021', brand: 'master', + verification_value: '261', + name: 'Mike J. Hammer') options = { - :order_id => '2', - :billing_address => { - :address1 => '2 Main St.', - :address2 => 'Apt. 222', - :city => 'Riverside', - :state => 'RI', - :zip => '02915', - :country => 'US' + order_id: '2', + billing_address: { + address1: '2 Main St.', + address2: 'Apt. 222', + city: 'Riverside', + state: 'RI', + zip: '02915', + country: 'US' } } - auth_assertions(10100, credit_card, options, :avs => 'Z', :cvv => 'M') + auth_assertions(10100, credit_card, options, avs: 'Z', cvv: 'M') - authorize_avs_assertions(credit_card, options, :avs => 'Z', :cvv => 'M') + authorize_avs_assertions(credit_card, options, avs: 'Z', cvv: 'M') - sale_assertions(10100, credit_card, options, :avs => 'Z', :cvv => 'M') + sale_assertions(10100, credit_card, options, avs: 'Z', cvv: 'M') end def test3 credit_card = CreditCard.new( - :number => '6011010000000003', - :month => '03', - :year => '2021', - :verification_value => '758', - :brand => 'discover' + number: '6011010000000003', + month: '03', + year: '2021', + verification_value: '758', + brand: 'discover' ) options = { - :order_id => '3', - :billing_address => { - :name => 'Eileen Jones', - :address1 => '3 Main St.', - :city => 'Bloomfield', - :state => 'CT', - :zip => '06002', - :country => 'US' + order_id: '3', + billing_address: { + name: 'Eileen Jones', + address1: '3 Main St.', + city: 'Bloomfield', + state: 'CT', + zip: '06002', + country: 'US' } } - auth_assertions(10100, credit_card, options, :avs => 'Z', :cvv => 'M') + auth_assertions(10100, credit_card, options, avs: 'Z', cvv: 'M') - authorize_avs_assertions(credit_card, options, :avs => 'Z', :cvv => 'M') + authorize_avs_assertions(credit_card, options, avs: 'Z', cvv: 'M') - sale_assertions(10100, credit_card, options, :avs => 'Z', :cvv => 'M') + sale_assertions(10100, credit_card, options, avs: 'Z', cvv: 'M') end def test4 credit_card = CreditCard.new( - :number => '375001000000005', - :month => '04', - :year => '2021', - :brand => 'american_express' + number: '375001000000005', + month: '04', + year: '2021', + brand: 'american_express' ) options = { - :order_id => '4', - :billing_address => { - :name => 'Bob Black', - :address1 => '4 Main St.', - :city => 'Laurel', - :state => 'MD', - :zip => '20708', - :country => 'US' + order_id: '4', + billing_address: { + name: 'Bob Black', + address1: '4 Main St.', + city: 'Laurel', + state: 'MD', + zip: '20708', + country: 'US' } } - auth_assertions(10100, credit_card, options, :avs => 'A', :cvv => nil) + auth_assertions(10100, credit_card, options, avs: 'A', cvv: nil) - authorize_avs_assertions(credit_card, options, :avs => 'A') + authorize_avs_assertions(credit_card, options, avs: 'A') - sale_assertions(10100, credit_card, options, :avs => 'A', :cvv => nil) + sale_assertions(10100, credit_card, options, avs: 'A', cvv: nil) end def test5 credit_card = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new( - :number => '4100200300011001', - :month => '05', - :year => '2021', - :verification_value => '463', - :brand => 'visa', - :payment_cryptogram => 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=' + number: '4100200300011001', + month: '05', + year: '2021', + verification_value: '463', + brand: 'visa', + payment_cryptogram: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=' ) options = { - :order_id => '5' + order_id: '5' } - auth_assertions(10100, credit_card, options, :avs => 'U', :cvv => 'M') + auth_assertions(10100, credit_card, options, avs: 'U', cvv: 'M') - authorize_avs_assertions(credit_card, options, :avs => 'U', :cvv => 'M') + authorize_avs_assertions(credit_card, options, avs: 'U', cvv: 'M') - sale_assertions(10100, credit_card, options, :avs => 'U', :cvv => 'M') + sale_assertions(10100, credit_card, options, avs: 'U', cvv: 'M') end def test6 - credit_card = CreditCard.new(:number => '4457010100000008', :month => '06', - :year => '2021', :brand => 'visa', - :verification_value => '992') + credit_card = CreditCard.new(number: '4457010100000008', month: '06', + year: '2021', brand: 'visa', + verification_value: '992') options = { - :order_id => '6', - :billing_address => { - :name => 'Joe Green', - :address1 => '6 Main St.', - :city => 'Derry', - :state => 'NH', - :zip => '03038', - :country => 'US' + order_id: '6', + billing_address: { + name: 'Joe Green', + address1: '6 Main St.', + city: 'Derry', + state: 'NH', + zip: '03038', + country: 'US' } } @@ -170,29 +170,27 @@ def test6 assert_equal 'P', response.cvv_result['code'] puts "Test #{options[:order_id]} Sale: #{txn_id(response)}" - # 6A. void - assert response = @gateway.void(response.authorization, {:order_id => '6A'}) + assert response = @gateway.void(response.authorization, { order_id: '6A' }) assert_equal '360', response.params['response'] assert_equal 'No transaction found with specified transaction Id', response.message puts "Test #{options[:order_id]}A: #{txn_id(response)}" - end def test7 - credit_card = CreditCard.new(:number => '5112010100000002', :month => '07', - :year => '2021', :brand => 'master', - :verification_value => '251') + credit_card = CreditCard.new(number: '5112010100000002', month: '07', + year: '2021', brand: 'master', + verification_value: '251') options = { - :order_id => '7', - :billing_address => { - :name => 'Jane Murray', - :address1 => '7 Main St.', - :city => 'Amesbury', - :state => 'MA', - :zip => '01913', - :country => 'US' + order_id: '7', + billing_address: { + name: 'Jane Murray', + address1: '7 Main St.', + city: 'Amesbury', + state: 'MA', + zip: '01913', + country: 'US' } } @@ -206,7 +204,7 @@ def test7 puts "Test #{options[:order_id]} Authorize: #{txn_id(response)}" # 7: authorize avs - authorize_avs_assertions(credit_card, options, :avs => 'I', :cvv => 'N', :message => 'Invalid Account Number', :success => false) + authorize_avs_assertions(credit_card, options, avs: 'I', cvv: 'N', message: 'Invalid Account Number', success: false) # 7. sale assert response = @gateway.purchase(10100, credit_card, options) @@ -219,19 +217,19 @@ def test7 end def test8 - credit_card = CreditCard.new(:number => '6011010100000002', :month => '08', - :year => '2021', :brand => 'discover', - :verification_value => '184') + credit_card = CreditCard.new(number: '6011010100000002', month: '08', + year: '2021', brand: 'discover', + verification_value: '184') options = { - :order_id => '8', - :billing_address => { - :name => 'Mark Johnson', - :address1 => '8 Main St.', - :city => 'Manchester', - :state => 'NH', - :zip => '03101', - :country => 'US' + order_id: '8', + billing_address: { + name: 'Mark Johnson', + address1: '8 Main St.', + city: 'Manchester', + state: 'NH', + zip: '03101', + country: 'US' } } @@ -245,7 +243,7 @@ def test8 puts "Test #{options[:order_id]} Authorize: #{txn_id(response)}" # 8: authorize avs - authorize_avs_assertions(credit_card, options, :avs => 'I', :cvv => 'P', :message => 'Call Discover', :success => false) + authorize_avs_assertions(credit_card, options, avs: 'I', cvv: 'P', message: 'Call Discover', success: false) # 8: sale assert response = @gateway.purchase(80080, credit_card, options) @@ -258,19 +256,19 @@ def test8 end def test9 - credit_card = CreditCard.new(:number => '375001010000003', :month => '09', - :year => '2021', :brand => 'american_express', - :verification_value => '0421') + credit_card = CreditCard.new(number: '375001010000003', month: '09', + year: '2021', brand: 'american_express', + verification_value: '0421') options = { - :order_id => '9', - :billing_address => { - :name => 'James Miller', - :address1 => '9 Main St.', - :city => 'Boston', - :state => 'MA', - :zip => '02134', - :country => 'US' + order_id: '9', + billing_address: { + name: 'James Miller', + address1: '9 Main St.', + city: 'Boston', + state: 'MA', + zip: '02134', + country: 'US' } } @@ -283,7 +281,7 @@ def test9 puts "Test #{options[:order_id]} Authorize: #{txn_id(response)}" # 9: authorize avs - authorize_avs_assertions(credit_card, options, :avs => 'I', :message => 'Pick Up Card', :success => false) + authorize_avs_assertions(credit_card, options, avs: 'I', message: 'Pick Up Card', success: false) # 9: sale assert response = @gateway.purchase(10100, credit_card, options) @@ -296,19 +294,19 @@ def test9 # Authorization Reversal Tests def test32 - credit_card = CreditCard.new(:number => '4457010000000009', :month => '01', - :year => '2021', :brand => 'visa', - :verification_value => '349') + credit_card = CreditCard.new(number: '4457010000000009', month: '01', + year: '2021', brand: 'visa', + verification_value: '349') options = { - :order_id => '32', - :billing_address => { - :name => 'John Smith', - :address1 => '1 Main St.', - :city => 'Burlington', - :state => 'MA', - :zip => '01803-3747', - :country => 'US' + order_id: '32', + billing_address: { + name: 'John Smith', + address1: '1 Main St.', + city: 'Burlington', + state: 'MA', + zip: '01803-3747', + country: 'US' } } @@ -328,21 +326,21 @@ def test32 end def test33 - credit_card = CreditCard.new(:number => '5112010000000003', :month => '01', - :year => '2021', :brand => 'master', - :verification_value => '261') + credit_card = CreditCard.new(number: '5112010000000003', month: '01', + year: '2021', brand: 'master', + verification_value: '261') options = { - :order_id => '33', - :billing_address => { - :name => 'Mike J. Hammer', - :address1 => '2 Main St.', - :address2 => 'Apt. 222', - :city => 'Riverside', - :state => 'RI', - :zip => '02915', - :country => 'US', - :payment_cryptogram => 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=' + order_id: '33', + billing_address: { + name: 'Mike J. Hammer', + address1: '2 Main St.', + address2: 'Apt. 222', + city: 'Riverside', + state: 'RI', + zip: '02915', + country: 'US', + payment_cryptogram: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=' } } @@ -357,19 +355,19 @@ def test33 end def test34 - credit_card = CreditCard.new(:number => '6011010000000003', :month => '01', - :year => '2021', :brand => 'discover', - :verification_value => '758') + credit_card = CreditCard.new(number: '6011010000000003', month: '01', + year: '2021', brand: 'discover', + verification_value: '758') options = { - :order_id => '34', - :billing_address => { - :name => 'Eileen Jones', - :address1 => '3 Main St.', - :city => 'Bloomfield', - :state => 'CT', - :zip => '06002', - :country => 'US' + order_id: '34', + billing_address: { + name: 'Eileen Jones', + address1: '3 Main St.', + city: 'Bloomfield', + state: 'CT', + zip: '06002', + country: 'US' } } @@ -384,18 +382,18 @@ def test34 end def test35 - credit_card = CreditCard.new(:number => '375001000000005', :month => '01', - :year => '2021', :brand => 'american_express') + credit_card = CreditCard.new(number: '375001000000005', month: '01', + year: '2021', brand: 'american_express') options = { - :order_id => '35', - :billing_address => { - :name => 'Bob Black', - :address1 => '4 Main St.', - :city => 'Laurel', - :state => 'MD', - :zip => '20708', - :country => 'US' + order_id: '35', + billing_address: { + name: 'Bob Black', + address1: '4 Main St.', + city: 'Laurel', + state: 'MD', + zip: '20708', + country: 'US' } } @@ -416,12 +414,12 @@ def test35 end def test36 - credit_card = CreditCard.new(:number => '375000026600004', :month => '01', - :year => '2021', :brand => 'american_express') + credit_card = CreditCard.new(number: '375000026600004', month: '01', + year: '2021', brand: 'american_express') options = { - :order_id => '36' - } + order_id: '36' + } assert auth_response = @gateway.authorize(20500, credit_card, options) assert_success auth_response @@ -442,16 +440,16 @@ def test37 account_type: 'Checking' ) options = { - :order_id => '37', - :billing_address => { - :name => 'Tom Black', - :address1 => '8 Main St.', - :city => 'Manchester', - :state => 'NH', - :zip => '03101', - :country => 'US', - :email => 'test@test.com', - :phone => '2233334444' + order_id: '37', + billing_address: { + name: 'Tom Black', + address1: '8 Main St.', + city: 'Manchester', + state: 'NH', + zip: '03101', + country: 'US', + email: 'test@test.com', + phone: '2233334444' } } assert auth_response = @gateway.authorize(3001, check, options) @@ -469,16 +467,16 @@ def test38 account_type: 'Checking' ) options = { - :order_id => '38', - :billing_address => { - :name => 'John Smith', - :address1 => '8 Main St.', - :city => 'Manchester', - :state => 'NH', - :zip => '03101', - :country => 'US', - :email => 'test@test.com', - :phone => '2233334444' + order_id: '38', + billing_address: { + name: 'John Smith', + address1: '8 Main St.', + city: 'Manchester', + state: 'NH', + zip: '03101', + country: 'US', + email: 'test@test.com', + phone: '2233334444' } } assert auth_response = @gateway.authorize(3002, check, options) @@ -496,17 +494,17 @@ def test39 account_type: 'Corporate' ) options = { - :order_id => '39', - :billing_address => { - :name => 'John Smith', - :address1 => '8 Main St.', - :city => 'Manchester', - :state => 'NH', - :zip => '03101', - :country => 'US', - :company => 'Good Goods Inc', - :email => 'test@test.com', - :phone => '2233334444' + order_id: '39', + billing_address: { + name: 'John Smith', + address1: '8 Main St.', + city: 'Manchester', + state: 'NH', + zip: '03101', + country: 'US', + company: 'Good Goods Inc', + email: 'test@test.com', + phone: '2233334444' } } assert auth_response = @gateway.authorize(3003, check, options) @@ -524,17 +522,17 @@ def test40 account_type: 'Corporate' ) options = { - :order_id => '40', - :billing_address => { - :name => 'Peter Green', - :address1 => '8 Main St.', - :city => 'Manchester', - :state => 'NH', - :zip => '03101', - :country => 'US', - :company => 'Green Co', - :email => 'test@test.com', - :phone => '2233334444' + order_id: '40', + billing_address: { + name: 'Peter Green', + address1: '8 Main St.', + city: 'Manchester', + state: 'NH', + zip: '03101', + country: 'US', + company: 'Green Co', + email: 'test@test.com', + phone: '2233334444' } } assert auth_response = @gateway.authorize(3004, declined_authorize_check, options) @@ -552,16 +550,16 @@ def test41 account_type: 'Checking' ) options = { - :order_id => '41', - :billing_address => { - :name => 'Mike Hammer', - :address1 => '8 Main St.', - :city => 'Manchester', - :state => 'NH', - :zip => '03101', - :country => 'US', - :email => 'test@test.com', - :phone => '2233334444' + order_id: '41', + billing_address: { + name: 'Mike Hammer', + address1: '8 Main St.', + city: 'Manchester', + state: 'NH', + zip: '03101', + country: 'US', + email: 'test@test.com', + phone: '2233334444' } } assert purchase_response = @gateway.purchase(2008, check, options) @@ -579,16 +577,16 @@ def test42 account_type: 'Checking' ) options = { - :order_id => '42', - :billing_address => { - :name => 'Tom Black', - :address1 => '8 Main St.', - :city => 'Manchester', - :state => 'NH', - :zip => '03101', - :country => 'US', - :email => 'test@test.com', - :phone => '2233334444' + order_id: '42', + billing_address: { + name: 'Tom Black', + address1: '8 Main St.', + city: 'Manchester', + state: 'NH', + zip: '03101', + country: 'US', + email: 'test@test.com', + phone: '2233334444' } } assert purchase_response = @gateway.purchase(2004, check, options) @@ -606,17 +604,17 @@ def test43 account_type: 'Corporate' ) options = { - :order_id => '43', - :billing_address => { - :name => 'Peter Green', - :address1 => '8 Main St.', - :city => 'Manchester', - :state => 'NH', - :zip => '03101', - :country => 'US', - :company => 'Green Co', - :email => 'test@test.com', - :phone => '2233334444' + order_id: '43', + billing_address: { + name: 'Peter Green', + address1: '8 Main St.', + city: 'Manchester', + state: 'NH', + zip: '03101', + country: 'US', + company: 'Green Co', + email: 'test@test.com', + phone: '2233334444' } } assert purchase_response = @gateway.purchase(2007, check, options) @@ -634,17 +632,17 @@ def test44 account_type: 'Corporate' ) options = { - :order_id => '44', - :billing_address => { - :name => 'Peter Green', - :address1 => '8 Main St.', - :city => 'Manchester', - :state => 'NH', - :zip => '03101', - :country => 'US', - :company => 'Green Co', - :email => 'test@test.com', - :phone => '2233334444' + order_id: '44', + billing_address: { + name: 'Peter Green', + address1: '8 Main St.', + city: 'Manchester', + state: 'NH', + zip: '03101', + country: 'US', + company: 'Green Co', + email: 'test@test.com', + phone: '2233334444' } } assert purchase_response = @gateway.purchase(2009, check, options) @@ -662,16 +660,16 @@ def test45 account_type: 'Checking' ) options = { - :order_id => '45', - :billing_address => { - :name => 'John Smith', - :address1 => '8 Main St.', - :city => 'Manchester', - :state => 'NH', - :zip => '03101', - :country => 'US', - :email => 'test@test.com', - :phone => '2233334444' + order_id: '45', + billing_address: { + name: 'John Smith', + address1: '8 Main St.', + city: 'Manchester', + state: 'NH', + zip: '03101', + country: 'US', + email: 'test@test.com', + phone: '2233334444' } } assert refund_response = @gateway.refund(1001, check, options) @@ -689,18 +687,18 @@ def test46 account_type: 'Corporate' ) options = { - :order_id => '46', - :order_source => 'telephone', - :billing_address => { - :name => 'Robert Jones', - :address1 => '8 Main St.', - :city => 'Manchester', - :state => 'NH', - :zip => '03101', - :country => 'US', - :email => 'test@test.com', - :phone => '2233334444', - :company => 'Widget Inc' + order_id: '46', + order_source: 'telephone', + billing_address: { + name: 'Robert Jones', + address1: '8 Main St.', + city: 'Manchester', + state: 'NH', + zip: '03101', + country: 'US', + email: 'test@test.com', + phone: '2233334444', + company: 'Widget Inc' } } assert purchase_response = @gateway.purchase(1003, check, options) @@ -720,17 +718,17 @@ def test47 account_type: 'Corporate' ) options = { - :order_id => '47', - :billing_address => { - :name => 'Peter Green', - :address1 => '8 Main St.', - :city => 'Manchester', - :state => 'NH', - :zip => '03101', - :country => 'US', - :company => 'Green Co', - :email => 'test@test.com', - :phone => '2233334444' + order_id: '47', + billing_address: { + name: 'Peter Green', + address1: '8 Main St.', + city: 'Manchester', + state: 'NH', + zip: '03101', + country: 'US', + company: 'Green Co', + email: 'test@test.com', + phone: '2233334444' } } assert purchase_response = @gateway.purchase(1007, check, options) @@ -749,17 +747,17 @@ def test48 account_type: 'Corporate' ) options = { - :order_id => '43', - :billing_address => { - :name => 'Peter Green', - :address1 => '8 Main St.', - :city => 'Manchester', - :state => 'NH', - :zip => '03101', - :country => 'US', - :company => 'Green Co', - :email => 'test@test.com', - :phone => '2233334444' + order_id: '43', + billing_address: { + name: 'Peter Green', + address1: '8 Main St.', + city: 'Manchester', + state: 'NH', + zip: '03101', + country: 'US', + company: 'Green Co', + email: 'test@test.com', + phone: '2233334444' } } assert purchase_response = @gateway.purchase(2007, check, options) @@ -785,17 +783,17 @@ def test_echeck_void1 account_type: 'Checking' ) options = { - :order_id => '42', - :id => '236222', - :billing_address => { - :name => 'Tom Black', - :address1 => '8 Main St.', - :city => 'Manchester', - :state => 'NH', - :zip => '03101', - :country => 'US', - :email => 'test@test.com', - :phone => '2233334444' + order_id: '42', + id: '236222', + billing_address: { + name: 'Tom Black', + address1: '8 Main St.', + city: 'Manchester', + state: 'NH', + zip: '03101', + country: 'US', + email: 'test@test.com', + phone: '2233334444' } } assert purchase_response = @gateway.purchase(2004, check, options) @@ -814,17 +812,17 @@ def test_echeck_void2 account_type: 'Checking' ) options = { - :order_id => '46', - :id => '232222', - :billing_address => { - :name => 'Robert Jones', - :address1 => '8 Main St.', - :city => 'Manchester', - :state => 'NH', - :zip => '03101', - :country => 'US', - :email => 'test@test.com', - :phone => '2233334444' + order_id: '46', + id: '232222', + billing_address: { + name: 'Robert Jones', + address1: '8 Main St.', + city: 'Manchester', + state: 'NH', + zip: '03101', + country: 'US', + email: 'test@test.com', + phone: '2233334444' } } assert purchase_response = @gateway.purchase(1003, check, options) @@ -845,9 +843,9 @@ def test_echeck_void3 # Explicit Token Registration Tests def test50 - credit_card = CreditCard.new(:number => '4457119922390123') + credit_card = CreditCard.new(number: '4457119922390123') options = { - :order_id => '50' + order_id: '50' } # store @@ -856,16 +854,16 @@ def test50 assert_success store_response assert_equal '445711', store_response.params['bin'] assert_equal 'VI', store_response.params['type'] - assert_equal '0123', store_response.params['litleToken'][-4,4] + assert_equal '0123', store_response.params['litleToken'][-4, 4] assert_equal '801', store_response.params['response'] assert_equal 'Account number was successfully registered', store_response.message puts "Test #{options[:order_id]}: #{txn_id(response)}" end def test51 - credit_card = CreditCard.new(:number => '4457119999999999') + credit_card = CreditCard.new(number: '4457119999999999') options = { - :order_id => '51' + order_id: '51' } # store @@ -878,9 +876,9 @@ def test51 end def test52 - credit_card = CreditCard.new(:number => '4457119922390123') + credit_card = CreditCard.new(number: '4457119922390123') options = { - :order_id => '52' + order_id: '52' } # store @@ -891,7 +889,7 @@ def test52 assert_equal '445711', store_response.params['bin'] assert_equal 'VI', store_response.params['type'] assert_equal '802', store_response.params['response'] - assert_equal '0123', store_response.params['litleToken'][-4,4] + assert_equal '0123', store_response.params['litleToken'][-4, 4] puts "Test #{options[:order_id]}: #{txn_id(store_response)}" end @@ -900,8 +898,8 @@ def test53 routing_number: '011100012', account_number: '1099999998' ) - options = { - :order_id => '53' + options = { + order_id: '53' } store_response = @gateway.store(check, options) @@ -919,8 +917,8 @@ def test54 routing_number: '1145_7895', account_number: '1022222102' ) - options = { - :order_id => '54' + options = { + order_id: '54' } store_response = @gateway.store(check, options) @@ -933,20 +931,20 @@ def test54 # Implicit Token Registration Tests def test55 - credit_card = CreditCard.new(:number => '5435101234510196', - :month => '11', - :year => '2014', - :brand => 'master', - :verification_value => '987') + credit_card = CreditCard.new(number: '5435101234510196', + month: '11', + year: '2014', + brand: 'master', + verification_value: '987') options = { - :order_id => '55' + order_id: '55' } # authorize assert response = @gateway.authorize(15000, credit_card, options) assert_success response assert_equal 'Approved', response.message - assert_equal '0196', response.params['tokenResponse_litleToken'][-4,4] + assert_equal '0196', response.params['tokenResponse_litleToken'][-4, 4] assert %w(801 802).include? response.params['tokenResponse_tokenResponseCode'] assert_equal 'MC', response.params['tokenResponse_type'] assert_equal '543510', response.params['tokenResponse_bin'] @@ -954,13 +952,13 @@ def test55 end def test56 - credit_card = CreditCard.new(:number => '5435109999999999', - :month => '11', - :year => '2014', - :brand => 'master', - :verification_value => '987') + credit_card = CreditCard.new(number: '5435109999999999', + month: '11', + year: '2014', + brand: 'master', + verification_value: '987') options = { - :order_id => '56' + order_id: '56' } # authorize @@ -972,13 +970,13 @@ def test56 end def test57_58 - credit_card = CreditCard.new(:number => '5435101234510196', - :month => '11', - :year => '2014', - :brand => 'master', - :verification_value => '987') + credit_card = CreditCard.new(number: '5435101234510196', + month: '11', + year: '2014', + brand: 'master', + verification_value: '987') options = { - :order_id => '57' + order_id: '57' } # authorize card @@ -986,7 +984,7 @@ def test57_58 assert_success response assert_equal 'Approved', response.message - assert_equal '0196', response.params['tokenResponse_litleToken'][-4,4] + assert_equal '0196', response.params['tokenResponse_litleToken'][-4, 4] assert %w(801 802).include? response.params['tokenResponse_tokenResponseCode'] assert_equal 'MC', response.params['tokenResponse_type'] assert_equal '543510', response.params['tokenResponse_bin'] @@ -995,10 +993,10 @@ def test57_58 # authorize token token = response.params['tokenResponse_litleToken'] options = { - :order_id => '58', - :token => { - :month => credit_card.month, - :year => credit_card.year + order_id: '58', + token: { + month: credit_card.month, + year: credit_card.year } } @@ -1013,10 +1011,10 @@ def test57_58 def test59 token = '1111000100092332' options = { - :order_id => '59', - :token => { - :month => '11', - :year => '2021' + order_id: '59', + token: { + month: '11', + year: '2021' } } @@ -1032,10 +1030,10 @@ def test59 def test60 token = '171299999999999' options = { - :order_id => '60', - :token => { - :month => '11', - :year => '2014' + order_id: '60', + token: { + month: '11', + year: '2014' } } @@ -1050,7 +1048,7 @@ def test60 def test_apple_pay_purchase options = { - :order_id => transaction_id, + order_id: transaction_id } decrypted_apple_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new( { @@ -1059,7 +1057,8 @@ def test_apple_pay_purchase brand: 'visa', number: '4457000300000007', payment_cryptogram: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=' - }) + } + ) assert response = @gateway.purchase(10010, decrypted_apple_pay, options) assert_success response @@ -1068,7 +1067,7 @@ def test_apple_pay_purchase def test_android_pay_purchase options = { - :order_id => transaction_id, + order_id: transaction_id } decrypted_android_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new( { @@ -1078,7 +1077,8 @@ def test_android_pay_purchase brand: 'visa', number: '4457000300000007', payment_cryptogram: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=' - }) + } + ) assert response = @gateway.purchase(10010, decrypted_android_pay, options) assert_success response @@ -1106,22 +1106,22 @@ def test_three_d_secure def test_authorize_and_purchase_and_credit_with_token options = { - :order_id => transaction_id, - :billing_address => { - :name => 'John Smith', - :address1 => '1 Main St.', - :city => 'Burlington', - :state => 'MA', - :zip => '01803-3747', - :country => 'US' + order_id: transaction_id, + billing_address: { + name: 'John Smith', + address1: '1 Main St.', + city: 'Burlington', + state: 'MA', + zip: '01803-3747', + country: 'US' } } - credit_card = CreditCard.new(:number => '5435101234510196', - :month => '11', - :year => '2014', - :brand => 'master', - :verification_value => '987') + credit_card = CreditCard.new(number: '5435101234510196', + month: '11', + year: '2014', + brand: 'master', + verification_value: '987') # authorize assert auth_response = @gateway.authorize(0, credit_card, options) @@ -1134,12 +1134,12 @@ def test_authorize_and_purchase_and_credit_with_token # purchase purchase_options = options.merge({ - :order_id => transaction_id, - :token => { - :month => credit_card.month, - :year => credit_card.year - } - }) + order_id: transaction_id, + token: { + month: credit_card.month, + year: credit_card.year + } + }) assert purchase_response = @gateway.purchase(100, token, purchase_options) assert_success purchase_response @@ -1148,12 +1148,12 @@ def test_authorize_and_purchase_and_credit_with_token # credit credit_options = options.merge({ - :order_id => transaction_id, - :token => { - :month => credit_card.month, - :year => credit_card.year - } - }) + order_id: transaction_id, + token: { + month: credit_card.month, + year: credit_card.year + } + }) assert credit_response = @gateway.credit(500, token, credit_options) assert_success credit_response @@ -1163,64 +1163,7 @@ def test_authorize_and_purchase_and_credit_with_token private - def auth_assertions(amount, card, options, assertions={}) - # 1: authorize - assert response = @gateway.authorize(amount, card, options) - assert_success response - assert_equal 'Approved', response.message - assert_equal assertions[:avs], response.avs_result['code'] if assertions[:avs] - assert_equal assertions[:cvv], response.cvv_result['code'] if assertions[:cvv] - assert_equal auth_code(options[:order_id]), response.params['authCode'] - puts "Test #{options[:order_id]} Authorize: #{txn_id(response)}" - - # 1A: capture - assert response = @gateway.capture(amount, response.authorization, {:id => transaction_id}) - assert_equal 'Approved', response.message - puts "Test #{options[:order_id]}A: #{txn_id(response)}" - - # 1B: credit - assert response = @gateway.credit(amount, response.authorization, {:id => transaction_id}) - assert_equal 'Approved', response.message - puts "Test #{options[:order_id]}B: #{txn_id(response)}" - - # 1C: void - assert response = @gateway.void(response.authorization, {:id => transaction_id}) - assert_equal 'Approved', response.message - puts "Test #{options[:order_id]}C: #{txn_id(response)}" - end - - def authorize_avs_assertions(credit_card, options, assertions={}) - assert response = @gateway.authorize(000, credit_card, options) - assert_equal assertions.key?(:success) ? assertions[:success] : true, response.success? - assert_equal assertions[:message] || 'Approved', response.message - assert_equal assertions[:avs], response.avs_result['code'], caller.inspect - assert_equal assertions[:cvv], response.cvv_result['code'], caller.inspect if assertions[:cvv] - puts "Test #{options[:order_id]} AVS Only: #{txn_id(response)}" - end - - def sale_assertions(amount, card, options, assertions={}) - # 1: sale - assert response = @gateway.purchase(amount, card, options) - assert_success response - assert_equal 'Approved', response.message - assert_equal assertions[:avs], response.avs_result['code'] if assertions[:avs] - assert_equal assertions[:cvv], response.cvv_result['code'] if assertions[:cvv] - assert_equal auth_code(options[:order_id]), response.params['authCode'] - puts "Test #{options[:order_id]} Sale: #{txn_id(response)}" - - - # 1B: credit - assert response = @gateway.credit(amount, response.authorization, {:id => transaction_id}) - assert_equal 'Approved', response.message - puts "Test #{options[:order_id]}B Sale: #{txn_id(response)}" - - # 1C: void - assert response = @gateway.void(response.authorization, {:id => transaction_id}) - assert_equal 'Approved', response.message - puts "Test #{options[:order_id]}C Sale: #{txn_id(response)}" - end - - def auth_assertions(amount, card, options, assertions={}) + def auth_assertions(amount, card, options, assertions = {}) # 1: authorize assert response = @gateway.authorize(amount, card, options) assert_success response @@ -1230,19 +1173,19 @@ def auth_assertions(amount, card, options, assertions={}) assert_equal auth_code(options[:order_id]), response.params['authCode'] # 1A: capture - assert response = @gateway.capture(amount, response.authorization, {:id => transaction_id}) + assert response = @gateway.capture(amount, response.authorization, { id: transaction_id }) assert_equal 'Approved', response.message # 1B: credit - assert response = @gateway.credit(amount, response.authorization, {:id => transaction_id}) + assert response = @gateway.credit(amount, response.authorization, { id: transaction_id }) assert_equal 'Approved', response.message # 1C: void - assert response = @gateway.void(response.authorization, {:id => transaction_id}) + assert response = @gateway.void(response.authorization, { id: transaction_id }) assert_equal 'Approved', response.message end - def authorize_avs_assertions(credit_card, options, assertions={}) + def authorize_avs_assertions(credit_card, options, assertions = {}) assert response = @gateway.authorize(000, credit_card, options) assert_equal assertions.key?(:success) ? assertions[:success] : true, response.success? assert_equal assertions[:message] || 'Approved', response.message @@ -1250,7 +1193,7 @@ def authorize_avs_assertions(credit_card, options, assertions={}) assert_equal assertions[:cvv], response.cvv_result['code'], caller.inspect if assertions[:cvv] end - def sale_assertions(amount, card, options, assertions={}) + def sale_assertions(amount, card, options, assertions = {}) # 1: sale assert response = @gateway.purchase(amount, card, options) assert_success response @@ -1260,19 +1203,19 @@ def sale_assertions(amount, card, options, assertions={}) # assert_equal auth_code(options[:order_id]), response.params['authCode'] # 1B: credit - assert response = @gateway.credit(amount, response.authorization, {:id => transaction_id}) + assert response = @gateway.credit(amount, response.authorization, { id: transaction_id }) assert_equal 'Approved', response.message # 1C: void - assert response = @gateway.void(response.authorization, {:id => transaction_id}) + assert response = @gateway.void(response.authorization, { id: transaction_id }) assert_equal 'Approved', response.message end def three_d_secure_assertions(test_id, card, type, source, result) - credit_card = CreditCard.new(:number => card, :month => '01', - :year => '2021', :brand => type, - :verification_value => '261', - :name => 'Mike J. Hammer') + credit_card = CreditCard.new(number: card, month: '01', + year: '2021', brand: type, + verification_value: '261', + name: 'Mike J. Hammer') options = { order_id: test_id, @@ -1298,7 +1241,7 @@ def transaction_id end def auth_code(order_id) - order_id * 5 + ' ' + (order_id * 5) + ' ' end def txn_id(response) diff --git a/test/remote/gateways/remote_litle_test.rb b/test/remote/gateways/remote_litle_test.rb index 966acfc125e..1f2eadbbae0 100644 --- a/test/remote/gateways/remote_litle_test.rb +++ b/test/remote/gateways/remote_litle_test.rb @@ -7,7 +7,7 @@ def setup first_name: 'John', last_name: 'Smith', month: '01', - year: '2012', + year: '2024', brand: 'visa', number: '4457010000000009', verification_value: '349' @@ -53,7 +53,8 @@ def setup brand: 'visa', number: '44444444400009', payment_cryptogram: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=' - }) + } + ) @decrypted_android_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new( { source: :android_pay, @@ -62,23 +63,50 @@ def setup brand: 'visa', number: '4457000300000007', payment_cryptogram: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=' - }) + } + ) + + @decrypted_google_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new( + { + source: :google_pay, + month: '01', + year: '2021', + brand: 'visa', + number: '4457000300000007', + payment_cryptogram: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=' + } + ) + + @decrypted_network_token = NetworkTokenizationCreditCard.new( + { + source: :network_token, + month: '02', + year: '2050', + brand: 'master', + number: '5112010000000000', + payment_cryptogram: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=' + } + ) + @check = check( name: 'Tom Black', routing_number: '011075150', account_number: '4099999992', - account_type: 'Checking' + account_type: 'checking' ) @authorize_check = check( 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 @@ -93,7 +121,31 @@ def test_successful_authorization_with_merchant_data campaign: 'super-awesome-campaign', merchant_grouping_id: 'brilliant-group' ) - assert response = @gateway.authorize(10010, @credit_card1, options) + assert @gateway.authorize(10010, @credit_card1, options) + end + + def test_successful_capture_with_customer_id + options = @options.merge(customer_id: '8675309') + assert response = @gateway.authorize(1000, @credit_card1, options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_succesful_purchase_with_customer_id + options = @options.merge(customer_id: '8675309') + assert response = @gateway.purchase(1000, @credit_card1, options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_refund_with_customer_id + options = @options.merge(customer_id: '8675309') + + assert purchase = @gateway.purchase(100, @credit_card1, options) + + assert refund = @gateway.refund(444, purchase.authorization, options) + assert_success refund + assert_equal 'Approved', refund.message end def test_successful_authorization_with_echeck @@ -106,24 +158,34 @@ 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 'X', response.avs_result['code'] - assert_equal 'M', response.cvv_result['code'] + + 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 '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=>{ - :name => 'Joe Green', - :address1 => '6 Main St.', - :city => 'Derry', - :state => 'NH', - :zip => '03038', - :country => 'US' - }, + order_id: '6', + billing_address: { + name: 'Joe Green', + address1: '6 Main St.', + city: 'Derry', + state: 'NH', + zip: '03038', + country: 'US' + } } ) assert_failure response @@ -147,12 +209,40 @@ def test_successful_purchase_with_some_empty_address_parts assert_equal 'Approved', response.message end + def test_successful_purchase_with_truncated_billing_address + assert response = @gateway.purchase(10010, @credit_card1, { + order_id: '1', + email: 'test@example.com', + billing_address: { + address1: '1234 Supercalifragilisticexpialidocious', + address2: 'Unit 6', + city: '‎Lake Chargoggagoggmanchauggagoggchaubunagungamaugg', + state: 'ME', + zip: '09901', + country: 'US' + } + }) + assert_success response + assert_equal 'Approved', response.message + end + def test_successful_purchase_with_debt_repayment_flag assert response = @gateway.purchase(10010, @credit_card1, @options.merge(debt_repayment: true)) assert_success response assert_equal 'Approved', response.message end + def test_successful_purchase_with_fraud_filter_override_flag + assert response = @gateway.purchase(10010, @credit_card1, @options.merge(fraud_filter_override: true)) + assert_success response + assert_equal 'Approved', response.message + end + + def test_failed_purchase_when_fraud_filter_override_flag_not_sent_as_boolean + assert response = @gateway.purchase(10010, @credit_card1, @options.merge(fraud_filter_override: 'hey')) + assert_failure response + end + def test_successful_purchase_with_3ds_fields options = @options.merge({ order_source: '3dsAuthenticated', @@ -167,7 +257,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 @@ -176,6 +266,121 @@ def test_successful_purchase_with_android_pay assert_equal 'Approved', response.message end + def test_successful_purchase_with_google_pay + assert response = @gateway.purchase(10000, @decrypted_google_pay) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_network_token + assert response = @gateway.purchase(10100, @decrypted_network_token) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_level_two_data_visa + options = @options.merge( + level_2_data: { + sales_tax: 200 + } + ) + assert response = @gateway.purchase(10010, @credit_card1, options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_level_two_data_master + credit_card = CreditCard.new( + first_name: 'John', + last_name: 'Smith', + month: '01', + year: '2024', + brand: 'master', + number: '5555555555554444', + verification_value: '349' + ) + + options = @options.merge( + level_2_data: { + total_tax_amount: 200, + customer_code: 'PO12345', + card_acceptor_tax_id: '011234567', + tax_included_in_total: 'true', + tax_amount: 50 + } + ) + assert response = @gateway.purchase(10010, credit_card, options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_level_three_data_visa + options = @options.merge( + level_3_data: { + discount_amount: 50, + shipping_amount: 50, + duty_amount: 20, + tax_included_in_total: true, + tax_amount: 100, + tax_rate: 0.05, + tax_type_identifier: '01', + card_acceptor_tax_id: '361531321', + line_items: [{ + item_sequence_number: 1, + commodity_code: '041235', + item_description: 'ramdom-object', + product_code: 'TB123', + quantity: 2, + unit_of_measure: 'EACH', + unit_cost: 25, + discount_per_line_item: 5, + line_item_total: 300, + tax_included_in_total: true, + tax_amount: 100, + tax_rate: 0.05, + tax_type_identifier: '01', + card_acceptor_tax_id: '361531321' + }] + } + ) + assert response = @gateway.purchase(10010, @credit_card1, options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_level_three_data_master + credit_card = CreditCard.new( + first_name: 'John', + last_name: 'Smith', + month: '01', + year: '2024', + brand: 'master', + number: '5555555555554444', + verification_value: '349' + ) + + options = @options.merge( + level_3_data: { + total_tax_amount: 200, + 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', + quantity: 2, + unit_of_measure: 'EACH', + line_item_total: 300 + }] + } + ) + + assert response = @gateway.purchase(10010, credit_card, options) + assert_success response + assert_equal 'Approved', response.message + end + def test_successful_purchase_with_merchant_data options = @options.merge( affiliate: 'some-affiliate', @@ -198,23 +403,22 @@ def test_successful_purchase_with_echeck end def test_unsuccessful_purchase - assert response = @gateway.purchase(60060, @credit_card2, { - :order_id=>'6', - :billing_address=>{ - :name => 'Joe Green', - :address1 => '6 Main St.', - :city => 'Derry', - :state => 'NH', - :zip => '03038', - :country => 'US' - }, + assert response = @gateway.purchase(60060, @declined_card, { + order_id: '6', + billing_address: { + name: 'Joe Green', + address1: '6 Main St.', + city: 'Derry', + state: 'NH', + zip: '03038', + country: 'US' } - ) + }) assert_failure response assert_equal 'Insufficient Funds', response.message end - def test_authorization_capture_refund_void + def test_authorize_capture_refund_void assert auth = @gateway.authorize(10010, @credit_card1, @options) assert_success auth assert_equal 'Approved', auth.message @@ -227,11 +431,239 @@ def test_authorization_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 end + def test_authorize_and_capture_with_stored_credential_recurring + credit_card = CreditCard.new(@credit_card_hash.merge( + number: '4100200300011001', + month: '05', + year: '2021', + verification_value: '463' + )) + + initial_options = @options.merge( + order_id: 'Net_Id1', + stored_credential: { + initial_transaction: true, + reason_type: 'recurring', + initiator: 'merchant', + network_transaction_id: nil + } + ) + assert auth = @gateway.authorize(4999, credit_card, initial_options) + assert_success auth + 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) + assert_success capture + assert_equal 'Approved', capture.message + + used_options = @options.merge( + order_id: 'Net_Id1a', + stored_credential: { + initial_transaction: false, + reason_type: 'recurring', + initiator: 'merchant', + network_transaction_id: + } + ) + + assert auth = @gateway.authorize(4999, credit_card, used_options) + assert_success auth + 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 + assert_equal 'Approved', capture.message + end + + def test_authorize_and_capture_with_stored_credential_installment + credit_card = CreditCard.new(@credit_card_hash.merge( + number: '4457010000000009', + month: '01', + year: '2021', + verification_value: '349' + )) + + initial_options = @options.merge( + order_id: 'Net_Id2', + stored_credential: { + initial_transaction: true, + reason_type: 'installment', + initiator: 'merchant', + network_transaction_id: nil + } + ) + assert auth = @gateway.authorize(5500, credit_card, initial_options) + assert_success auth + assert_equal 'Approved', auth.message + assert network_transaction_id = auth.params['networkTransactionId'] + + assert capture = @gateway.capture(5500, auth.authorization) + assert_success capture + assert_equal 'Approved', capture.message + + used_options = @options.merge( + order_id: 'Net_Id2a', + stored_credential: { + initial_transaction: false, + reason_type: 'installment', + initiator: 'merchant', + network_transaction_id: + } + ) + assert auth = @gateway.authorize(5500, credit_card, used_options) + assert_success auth + assert_equal 'Approved', auth.message + + assert capture = @gateway.capture(5500, auth.authorization) + assert_success capture + assert_equal 'Approved', capture.message + end + + def test_authorize_and_capture_with_stored_credential_mit_card_on_file + credit_card = CreditCard.new(@credit_card_hash.merge( + number: '4457000800000002', + month: '01', + year: '2021', + verification_value: '349' + )) + + initial_options = @options.merge( + order_id: 'Net_Id3', + stored_credential: { + initial_transaction: true, + reason_type: 'unscheduled', + initiator: 'merchant', + network_transaction_id: nil + } + ) + assert auth = @gateway.authorize(5500, credit_card, initial_options) + assert_success auth + assert_equal 'Approved', auth.message + assert network_transaction_id = auth.params['networkTransactionId'] + + assert capture = @gateway.capture(5500, auth.authorization) + assert_success capture + assert_equal 'Approved', capture.message + + used_options = @options.merge( + order_id: 'Net_Id3a', + stored_credential: { + initial_transaction: false, + reason_type: 'unscheduled', + initiator: 'merchant', + network_transaction_id: + } + ) + assert auth = @gateway.authorize(2500, credit_card, used_options) + assert_success auth + assert_equal 'Approved', auth.message + + assert capture = @gateway.capture(2500, auth.authorization) + assert_success capture + assert_equal 'Approved', capture.message + end + + def test_authorize_and_capture_with_stored_credential_cit_card_on_file + credit_card = CreditCard.new(@credit_card_hash.merge( + number: '4457000800000002', + month: '01', + year: '2021', + verification_value: '349' + )) + + initial_options = @options.merge( + order_id: 'Net_Id3', + stored_credential: { + initial_transaction: true, + reason_type: 'unscheduled', + initiator: 'cardholder', + network_transaction_id: nil + } + ) + assert auth = @gateway.authorize(5500, credit_card, initial_options) + assert_success auth + assert_equal 'Approved', auth.message + assert network_transaction_id = auth.params['networkTransactionId'] + + assert capture = @gateway.capture(5500, auth.authorization) + assert_success capture + assert_equal 'Approved', capture.message + + used_options = @options.merge( + order_id: 'Net_Id3b', + stored_credential: { + initial_transaction: false, + reason_type: 'unscheduled', + initiator: 'cardholder', + network_transaction_id: + } + ) + assert auth = @gateway.authorize(4000, credit_card, used_options) + assert_success auth + assert_equal 'Approved', auth.message + + assert capture = @gateway.capture(4000, auth.authorization) + assert_success capture + assert_equal 'Approved', capture.message + end + + def test_authorize_with_network_token + assert response = @gateway.authorize(10100, @decrypted_network_token) + assert_success response + assert_equal 'Approved', response.message + end + + def test_purchase_with_stored_credential_cit_card_on_file_non_ecommerce + credit_card = CreditCard.new(@credit_card_hash.merge( + number: '4457000800000002', + month: '01', + year: '2021', + verification_value: '349' + )) + + initial_options = @options.merge( + order_id: 'Net_Id3', + order_source: '3dsAuthenticated', + xid: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=', + cavv: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=', + stored_credential: { + initial_transaction: true, + reason_type: 'unscheduled', + initiator: 'cardholder', + network_transaction_id: nil + } + ) + assert auth = @gateway.purchase(5500, credit_card, initial_options) + assert_success auth + assert_equal 'Approved', auth.message + assert network_transaction_id = auth.params['networkTransactionId'] + + used_options = @options.merge( + order_id: 'Net_Id3b', + order_source: '3dsAuthenticated', + xid: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=', + cavv: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=', + stored_credential: { + initial_transaction: false, + reason_type: 'unscheduled', + initiator: 'cardholder', + network_transaction_id: + } + ) + assert auth = @gateway.purchase(4000, credit_card, used_options) + + assert_success auth + assert_equal 'Approved', auth.message + end + def test_void_with_echeck options = @options.merge({ order_id: '42', @@ -253,9 +685,21 @@ 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 litleTxnId', void.message + assert_match(/^Error validating xml data against the schema/, void.message) + end + + def test_successful_credit + assert credit = @gateway.credit(123456, @credit_card1, @options) + assert_success credit + assert_equal 'Approved', credit.message + end + + def test_failed_credit + @credit_card1.number = '1234567890' + assert credit = @gateway.credit(1, @credit_card1, @options) + assert_failure credit end def test_partial_refund @@ -309,38 +753,36 @@ 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 litleTxnId', 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 litleTxnId', credit_response.message + assert_match(/^Error validating xml data against the schema/, credit_response.message) end def test_void_unsuccessful assert void_response = @gateway.void('123456789012345360') assert_failure void_response - assert_equal 'No transaction found with specified litleTxnId', void_response.message + assert_equal 'No transaction found with specified Transaction Id', void_response.message end def test_store_successful - credit_card = CreditCard.new(@credit_card_hash.merge(:number => '4457119922390123')) - assert store_response = @gateway.store(credit_card, :order_id => '50') + credit_card = CreditCard.new(@credit_card_hash.merge(number: '4457119922390123')) + assert store_response = @gateway.store(credit_card, order_id: '50') 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 paypage_registration_id = 'cDZJcmd1VjNlYXNaSlRMTGpocVZQY1NNlYE4ZW5UTko4NU9KK3p1L1p1VzE4ZWVPQVlSUHNITG1JN2I0NzlyTg=' - assert store_response = @gateway.store(paypage_registration_id, :order_id => '50') + assert store_response = @gateway.store(paypage_registration_id, order_id: '50') assert_success store_response assert_equal 'Account number was successfully registered', store_response.message @@ -349,17 +791,17 @@ def test_store_with_paypage_registration_id_successful end def test_store_unsuccessful - credit_card = CreditCard.new(@credit_card_hash.merge(:number => '4457119999999999')) - assert store_response = @gateway.store(credit_card, :order_id => '51') + 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 def test_store_and_purchase_with_token_successful - credit_card = CreditCard.new(@credit_card_hash.merge(:number => '4100280190123000')) - assert store_response = @gateway.store(credit_card, :order_id => '50') + credit_card = CreditCard.new(@credit_card_hash.merge(number: '4100280190123000')) + assert store_response = @gateway.store(credit_card, order_id: '50') assert_success store_response token = store_response.authorization @@ -367,7 +809,19 @@ 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 + assert store_response = @gateway.store(@credit_card1, order_id: '50') + assert_success store_response + + token = store_response.authorization + assert_equal store_response.params['litleToken'], token + + assert response = @gateway.purchase(10010, token, { basis_expiration_month: '01', basis_expiration_year: '2024' }) + assert_success response + assert_equal 'Partially Approved: The authorized amount is less than the requested amount.', response.message end def test_echeck_store_and_purchase @@ -380,7 +834,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 @@ -398,16 +852,16 @@ def test_unsuccessful_verify def test_successful_purchase_with_dynamic_descriptors assert response = @gateway.purchase(10010, @credit_card1, @options.merge( - descriptor_name: 'SuperCompany', - descriptor_phone: '9193341121', - )) + descriptor_name: 'SuperCompany', + descriptor_phone: '9193341121' + )) assert_success response assert_equal 'Approved', response.message end def test_unsuccessful_xml_schema_validation - credit_card = CreditCard.new(@credit_card_hash.merge(:number => '123456')) - assert store_response = @gateway.store(credit_card, :order_id => '51') + credit_card = CreditCard.new(@credit_card_hash.merge(number: '123456')) + assert store_response = @gateway.store(credit_card, order_id: '51') assert_failure store_response assert_match(/^Error validating xml data against the schema/, store_response.message) @@ -442,4 +896,15 @@ def test_echeck_scrubbing assert_scrubbed(@gateway.options[:login], transcript) assert_scrubbed(@gateway.options[:password], transcript) end + + def test_network_token_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(10010, @decrypted_network_token, @options) + end + transcript = @gateway.scrub(transcript) + assert_scrubbed(@decrypted_network_token.number, transcript) + assert_scrubbed(@decrypted_network_token.payment_cryptogram, transcript) + assert_scrubbed(@gateway.options[:login], transcript) + assert_scrubbed(@gateway.options[:password], transcript) + end end diff --git a/test/remote/gateways/remote_mercado_pago_test.rb b/test/remote/gateways/remote_mercado_pago_test.rb index aeda0a15172..274cb687775 100644 --- a/test/remote/gateways/remote_mercado_pago_test.rb +++ b/test/remote/gateways/remote_mercado_pago_test.rb @@ -2,17 +2,60 @@ class RemoteMercadoPagoTest < Test::Unit::TestCase def setup + exp_year = Time.now.year + 1 @gateway = MercadoPagoGateway.new(fixtures(:mercado_pago)) + @argentina_gateway = MercadoPagoGateway.new(fixtures(:mercado_pago_argentina)) + @colombian_gateway = MercadoPagoGateway.new(fixtures(:mercado_pago_colombia)) @amount = 500 - @credit_card = credit_card('4509953566233704') - @declined_card = credit_card('4000300011112220') + @credit_card = credit_card('5031433215406351') + @colombian_card = credit_card('4013540682746260') + @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', + month: 10, + year: exp_year, + first_name: 'John', + last_name: 'Smith', + 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') @options = { billing_address: address, shipping_address: address, - email: 'user+br@example.com', + email: 'test_user_1390220683@testuser.com', description: 'Store Purchase' } + @processing_options = { + binary_mode: false, + processing_mode: 'gateway', + merchant_account_id: fixtures(:mercado_pago)[:merchant_account_id], + fraud_scoring: true, + fraud_manual_review: true, + payment_method_option_id: '123abc' + } + @payer = { + entity_type: 'individual', + type: 'customer', + identification: {}, + first_name: 'Longbob', + last_name: 'Longsen' + } end def test_successful_purchase @@ -21,9 +64,34 @@ def test_successful_purchase assert_equal 'accredited', response.message end + def test_successful_purchase_with_elo + response = @gateway.purchase(@amount, @elo_credit_card, @options) + assert_success response + assert_equal 'accredited', response.message + end + + def test_successful_purchase_with_cabal + response = @argentina_gateway.purchase(@amount, @cabal_credit_card, @options) + assert_success response + assert_equal 'accredited', response.message + end + + def test_successful_purchase_with_naranja + response = @argentina_gateway.purchase(@amount, @naranja_credit_card, @options) + assert_success response + assert_equal 'accredited', response.message + end + def test_successful_purchase_with_binary_false @options.update(binary_mode: false) - response = @gateway.purchase(@amount, @credit_card, @options) + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + assert_equal 'pending_capture', response.message + end + + # Requires setup on merchant account + def test_successful_purchase_with_processing_mode_gateway + response = @gateway.purchase(@amount, @credit_card, @options.merge(@processing_options)) assert_success response assert_equal 'accredited', response.message end @@ -36,6 +104,48 @@ def test_successful_purchase_with_american_express assert_equal 'accredited', response.message end + def test_successful_purchase_with_taxes_and_net_amount + # Minimum transaction amount is 0.30 EUR or ~1112 $COL on 1/27/20. + # This value must exceed that + amount = 10000_00 + + # These values need to be represented as dollars, so divide them by 100 + tax_amount = amount * 0.19 + @options[:net_amount] = (amount - tax_amount) / 100 + @options[:taxes] = [{ value: tax_amount / 100, type: 'IVA' }] + + response = @colombian_gateway.purchase(amount, @colombian_card, @options) + assert_success response + assert_equal 'accredited', response.message + end + + def test_successful_purchase_with_notification_url + response = @gateway.purchase(@amount, @credit_card, @options.merge(notification_url: 'https://www.spreedly.com/')) + assert_success response + assert_equal 'https://www.spreedly.com/', response.params['notification_url'] + end + + def test_successful_purchase_with_idempotency_key + response = @gateway.purchase(@amount, @credit_card, @options.merge(idempotency_key: '0d5020ed-1af6-469c-ae06-c3bec19954bb')) + assert_success response + assert_equal 'accredited', response.message + end + + def test_successful_purchase_with_payer + response = @gateway.purchase(@amount, @credit_card, @options.merge({ payer: @payer })) + assert_success response + assert_equal 'accredited', response.message + end + + def test_successful_purchase_with_metadata_passthrough + metadata = { 'key_1' => 'value_1', + 'key_2' => 'value_2', + 'key_3' => { 'nested_key_1' => 'value_3' } } + response = @gateway.purchase(@amount, @credit_card, @options.merge({ metadata: })) + assert_success response + assert_equal metadata, response.params['metadata'] + end + def test_failed_purchase response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response @@ -53,6 +163,48 @@ def test_successful_authorize_and_capture assert_equal 'accredited', capture.message end + def test_successful_authorize_with_idempotency_key + response = @gateway.authorize(@amount, @credit_card, @options.merge(idempotency_key: '0d5020ed-1af6-469c-ae06-c3bec19954bb')) + assert_success response + assert_equal 'accredited', response.message + end + + def test_successful_authorize_and_capture_with_elo + auth = @gateway.authorize(@amount, @elo_credit_card, @options) + assert_success auth + assert_equal 'pending_capture', auth.message + + assert capture = @gateway.capture(@amount, auth.authorization) + assert_success capture + assert_equal 'accredited', capture.message + end + + def test_successful_authorize_and_capture_with_cabal + auth = @argentina_gateway.authorize(@amount, @cabal_credit_card, @options) + assert_success auth + assert_equal 'pending_capture', auth.message + + assert capture = @argentina_gateway.capture(@amount, auth.authorization) + assert_success capture + assert_equal 'accredited', capture.message + end + + def test_successful_authorize_and_capture_with_naranja + auth = @argentina_gateway.authorize(@amount, @naranja_credit_card, @options) + assert_success auth + assert_equal 'pending_capture', auth.message + + assert capture = @argentina_gateway.capture(@amount, auth.authorization) + assert_success capture + assert_equal 'accredited', capture.message + end + + def test_successful_authorize_with_capture_option + auth = @gateway.authorize(@amount, @credit_card, @options.merge(capture: true)) + assert_success auth + assert_equal 'accredited', auth.message + end + def test_failed_authorize response = @gateway.authorize(@amount, @declined_card, @options) assert_failure response @@ -60,10 +212,10 @@ def test_failed_authorize end def test_partial_capture - auth = @gateway.authorize(@amount, @credit_card, @options) + auth = @gateway.authorize(@amount + 1, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount, auth.authorization) assert_success capture assert_equal 'accredited', capture.message end @@ -71,7 +223,7 @@ def test_partial_capture def test_failed_capture response = @gateway.capture(@amount, '') assert_failure response - assert_equal 'Method not allowed', response.message + assert_equal 'json_parse_error', response.message end def test_successful_refund @@ -83,18 +235,45 @@ def test_successful_refund assert_equal nil, refund.message end + def test_successful_refund_with_elo + purchase = @gateway.purchase(@amount, @elo_credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization) + assert_success refund + assert_equal nil, refund.message + end + + def test_successful_refund_with_cabal + purchase = @argentina_gateway.purchase(@amount, @cabal_credit_card, @options) + assert_success purchase + + assert refund = @argentina_gateway.refund(@amount, purchase.authorization) + assert_success refund + assert_equal nil, refund.message + end + + def test_successful_refund_with_naranja + purchase = @argentina_gateway.purchase(@amount, @naranja_credit_card, @options) + assert_success purchase + + assert refund = @argentina_gateway.refund(@amount, purchase.authorization) + assert_success refund + assert_equal nil, refund.message + end + def test_partial_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount-1, purchase.authorization) + assert refund = @gateway.refund(@amount - 1, purchase.authorization) assert_success refund end def test_failed_refund response = @gateway.refund(@amount, '') assert_failure response - assert_equal 'Resource /payments/refunds not found.', response.message + assert_equal 'Not Found', response.message end def test_successful_void @@ -106,10 +285,37 @@ def test_successful_void assert_equal 'by_collector', void.message end + def test_successful_void_with_elo + auth = @gateway.authorize(@amount, @elo_credit_card, @options) + assert_success auth + + assert void = @gateway.void(auth.authorization) + assert_success void + assert_equal 'by_collector', void.message + end + + def test_successful_void_with_cabal + auth = @argentina_gateway.authorize(@amount, @cabal_credit_card, @options) + assert_success auth + + assert void = @argentina_gateway.void(auth.authorization) + assert_success void + assert_equal 'by_collector', void.message + end + + def test_successful_void_with_naranja + auth = @argentina_gateway.authorize(@amount, @naranja_credit_card, @options) + assert_success auth + + assert void = @argentina_gateway.void(auth.authorization) + assert_success void + assert_equal 'by_collector', void.message + end + def test_failed_void response = @gateway.void('') assert_failure response - assert_equal 'Method not allowed', response.message + assert_equal 'json_parse_error', response.message end def test_successful_verify @@ -118,12 +324,45 @@ def test_successful_verify assert_match %r{pending_capture}, response.message end + def test_successful_verify_with_idempotency_key + response = @gateway.verify(@credit_card, @options.merge(idempotency_key: '0d5020ed-1af6-469c-ae06-c3bec19954bb')) + assert_success response + assert_match %r{pending_capture}, response.message + end + + def test_successful_verify_with_amount + @options[:amount] = 200 + response = @gateway.verify(@credit_card, @options) + assert_success response + assert_match %r{pending_capture}, response.message + end + def test_failed_verify response = @gateway.verify(@declined_card, @options) assert_failure response assert_match %r{cc_rejected_other_reason}, response.message end + def test_successful_inquire_with_id + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + assert_equal 'pending_capture', auth.message + + assert inquire = @gateway.inquire(auth.authorization) + assert_success inquire + assert_equal auth.message, inquire.message + end + + def test_successful_inquire_with_external_reference + auth = @gateway.authorize(@amount, @credit_card, @options.merge(order_id: 'abcd1234')) + assert_success auth + assert auth.params['external_reference'] = 'abcd1234' + + assert inquire = @gateway.inquire(nil, { external_reference: 'abcd1234' }) + assert_success inquire + assert_equal auth.authorization, inquire.authorization + end + def test_invalid_login gateway = MercadoPagoGateway.new(access_token: '') @@ -143,4 +382,30 @@ def test_transcript_scrubbing assert_scrubbed(@gateway.options[:access_token], transcript) end + def test_successful_purchase_with_3ds + three_ds_cc = credit_card('5483928164574623', verification_value: '123', month: 11, year: 2025) + @options[:execute_threed] = true + + response = @gateway.purchase(290, three_ds_cc, @options) + + assert_success response + assert_equal 'pending_challenge', response.message + assert_include response.params, 'three_ds_info' + assert_equal response.params['three_ds_info']['external_resource_url'], 'https://api.mercadopago.com/cardholder_authenticator/v2/prod/browser-challenges' + assert_include response.params['three_ds_info'], 'creq' + end + + def test_successful_purchase_with_3ds_mandatory + three_ds_cc = credit_card('5031755734530604', verification_value: '123', month: 11, year: 2025) + @options[:execute_threed] = true + @options[:three_ds_mode] = 'mandatory' + + response = @gateway.purchase(290, three_ds_cc, @options) + + assert_success response + assert_equal 'pending_challenge', response.message + assert_include response.params, 'three_ds_info' + assert_equal response.params['three_ds_info']['external_resource_url'], 'https://api.mercadopago.com/cardholder_authenticator/v2/prod/browser-challenges' + assert_include response.params['three_ds_info'], 'creq' + end end diff --git a/test/remote/gateways/remote_merchant_e_solutions_test.rb b/test/remote/gateways/remote_merchant_e_solutions_test.rb index a9504dac516..c514aa6e3a2 100644 --- a/test/remote/gateways/remote_merchant_e_solutions_test.rb +++ b/test/remote/gateways/remote_merchant_e_solutions_test.rb @@ -11,18 +11,29 @@ def setup @declined_card = credit_card('4111111111111112') @options = { - :order_id => '123', - :billing_address => { - :name => 'John Doe', - :address1 => '123 State Street', - :address2 => 'Apartment 1', - :city => 'Nowhere', - :state => 'MT', - :country => 'US', - :zip => '55555', - :phone => '555-555-5555' + order_id: '123', + billing_address: { + name: 'John Doe', + address1: '123 State Street', + address2: 'Apartment 1', + city: 'Nowhere', + state: 'MT', + country: 'US', + zip: '55555', + phone: '555-555-5555', + recurring_pmt_num: 11, + recurring_pmt_count: 10 } } + @stored_credential_options = { + moto_ecommerce_ind: '7', + client_reference_number: '345892', + recurring_pmt_num: 11, + recurring_pmt_count: 10, + card_on_file: 'Y', + cit_mit_indicator: 'C101', + account_data_source: 'Y' + } end # MES has a race condition with immediately trying to operate on an @@ -37,6 +48,12 @@ def test_successful_purchase assert_equal 'This transaction has been approved', response.message end + def test_successful_purchase_with_moto_ecommerce_ind + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(@stored_credential_options)) + assert_success response + assert_equal 'This transaction has been approved', response.message + end + def test_unsuccessful_purchase assert response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response @@ -44,7 +61,7 @@ def test_unsuccessful_purchase end def test_purchase_with_long_order_id - options = {order_id: 'thisislongerthan17characters'} + options = { order_id: 'thisislongerthan17characters' } assert response = @gateway.purchase(@amount, @credit_card, options) assert_success response assert_equal 'This transaction has been approved', response.message @@ -71,7 +88,8 @@ def test_failed_capture def test_store_purchase_unstore assert store = @gateway.store(@credit_card) assert_success store - assert_equal 'This transaction has been approved', store.message + assert_equal 'Card Ok', store.message + assert_equal store.authorization, store.params['card_id'] assert purchase = @gateway.purchase(@amount, store.authorization, @options) assert_success purchase assert_equal 'This transaction has been approved', purchase.message @@ -116,15 +134,15 @@ def test_successful_avs_check def test_unsuccessful_avs_check_with_bad_street_address options = { - :billing_address => { - :name => 'John Doe', - :address1 => '124 State Street', - :address2 => 'Apartment 1', - :city => 'Nowhere', - :state => 'MT', - :country => 'US', - :zip => '55555', - :phone => '555-555-5555' + billing_address: { + name: 'John Doe', + address1: '124 State Street', + address2: 'Apartment 1', + city: 'Nowhere', + state: 'MT', + country: 'US', + zip: '55555', + phone: '555-555-5555' } } assert response = @gateway.purchase(@amount, @credit_card, options) @@ -136,20 +154,20 @@ def test_unsuccessful_avs_check_with_bad_street_address def test_unsuccessful_avs_check_with_bad_zip options = { - :billing_address => { - :name => 'John Doe', - :address1 => '123 State Street', - :address2 => 'Apartment 1', - :city => 'Nowhere', - :state => 'MT', - :country => 'US', - :zip => '55554', - :phone => '555-555-5555' + billing_address: { + name: 'John Doe', + address1: '123 State Street', + address2: 'Apartment 1', + city: 'Nowhere', + state: 'MT', + country: 'US', + zip: '55554', + phone: '555-555-5555' } } assert response = @gateway.purchase(@amount, @credit_card, options) assert_equal 'A', response.avs_result['code'] - assert_equal 'Street address matches, but 5-digit and 9-digit postal code do not match.', response.avs_result['message'] + assert_equal 'Street address matches, but postal code does not match.', response.avs_result['message'] assert_equal 'Y', response.avs_result['street_match'] assert_equal 'N', response.avs_result['postal_match'] end @@ -162,13 +180,13 @@ def test_successful_cvv_check def test_unsuccessful_cvv_check credit_card = ActiveMerchant::Billing::CreditCard.new({ - :first_name => 'John', - :last_name => 'Doe', - :number => '4111111111111111', - :month => '11', - :year => (Time.now.year + 1).to_s, - :verification_value => '555' - }) + first_name: 'John', + last_name: 'Doe', + number: '4111111111111111', + month: '11', + year: (Time.now.year + 1).to_s, + verification_value: '555' + }) assert response = @gateway.purchase(@amount, credit_card, @options) assert_equal 'N', response.cvv_result['code'] assert_equal 'CVV does not match', response.cvv_result['message'] @@ -176,29 +194,29 @@ def test_unsuccessful_cvv_check def test_invalid_login gateway = MerchantESolutionsGateway.new( - :login => '', - :password => '' - ) + login: '', + password: '' + ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response end - def test_connection_failure_404_notfound_with_purchase - @gateway.test_url = 'https://cert.merchante-solutions.com/mes-api/tridentApiasdasd' - assert response = @gateway.purchase(@amount, @credit_card, @options) - assert_failure response - assert_equal 'Failed with 404 Not Found', response.message - end - def test_successful_purchase_with_3dsecure_params - assert response = @gateway.purchase(@amount, @credit_card, @options.merge( - { :xid => 'ERERERERERERERERERERERERERE=', - :cavv => 'ERERERERERERERERERERERERERE=' - })) + options = @options.merge( + { xid: 'ERERERERERERERERERERERERERE=', + cavv: 'ERERERERERERERERERERERERERE=' } + ) + assert response = @gateway.purchase(@amount, @credit_card, options) assert_success response assert_equal 'This transaction has been approved', response.message end + def test_successful_verify + assert response = @gateway.verify(@credit_card, @options.merge({ verify_amount: 0 })) + assert_success response + assert_equal 'Card Ok', response.message + end + def test_transcript_scrubbing transcript = capture_transcript(@gateway) do @gateway.purchase(@amount, @credit_card, @options) diff --git a/test/remote/gateways/remote_merchant_one_test.rb b/test/remote/gateways/remote_merchant_one_test.rb index 936cb70235e..bad6b46aaae 100644 --- a/test/remote/gateways/remote_merchant_one_test.rb +++ b/test/remote/gateways/remote_merchant_one_test.rb @@ -1,7 +1,6 @@ require 'test_helper' class RemoteMerchantOneTest < Test::Unit::TestCase - def setup @gateway = MerchantOneGateway.new(fixtures(:merchant_one)) @@ -10,9 +9,9 @@ def setup @declined_card = credit_card('1111111111111111') @options = { - :order_id => '1', - :description => 'Store Purchase', - :billing_address => { + order_id: '1', + description: 'Store Purchase', + billing_address: { name: 'Jim Smith', address1: '1234 My Street', address2: 'Apt 1', @@ -31,34 +30,34 @@ def test_successful_purchase assert_equal 'SUCCESS', response.message end - def test_unsuccessful_purchase - assert response = @gateway.purchase(@amount, @declined_card, @options) - assert_failure response - assert response.message.include? 'Invalid Credit Card Number' - end + def test_unsuccessful_purchase + assert response = @gateway.purchase(@amount, @declined_card, @options) + assert_failure response + assert response.message.include? 'Invalid Credit Card Number' + end - def test_authorize_and_capture - amount = @amount - assert auth = @gateway.authorize(amount, @credit_card, @options) - assert_success auth - assert_equal 'SUCCESS', auth.message - assert auth.authorization, auth.to_yaml - assert capture = @gateway.capture(amount, auth.authorization) - assert_success capture - end + def test_authorize_and_capture + amount = @amount + assert auth = @gateway.authorize(amount, @credit_card, @options) + assert_success auth + assert_equal 'SUCCESS', auth.message + assert auth.authorization, auth.to_yaml + assert capture = @gateway.capture(amount, auth.authorization) + assert_success capture + end - def test_failed_capture + def test_failed_capture assert response = @gateway.capture(@amount, '') assert_failure response - end + end - def test_invalid_login - gateway = MerchantOneGateway.new( - :username => 'nnn', - :password => 'nnn' - ) - assert response = gateway.purchase(@amount, @credit_card, @options) - assert_failure response - assert_equal 'Authentication Failed', response.message - end + def test_invalid_login + gateway = MerchantOneGateway.new( + username: 'nnn', + password: 'nnn' + ) + assert response = gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_equal 'Authentication Failed', response.message + end end diff --git a/test/remote/gateways/remote_merchant_ware_test.rb b/test/remote/gateways/remote_merchant_ware_test.rb index 71c812e730a..1e46bcd8ccf 100644 --- a/test/remote/gateways/remote_merchant_ware_test.rb +++ b/test/remote/gateways/remote_merchant_ware_test.rb @@ -4,13 +4,13 @@ class RemoteMerchantWareTest < Test::Unit::TestCase def setup @gateway = MerchantWareGateway.new(fixtures(:merchant_ware)) - @amount = rand(1000) + 200 + @amount = rand(200..1199) - @credit_card = credit_card('5424180279791732', {:brand => 'master'}) + @credit_card = credit_card('5424180279791732', { brand: 'master' }) @options = { - :order_id => generate_unique_id, - :billing_address => address + order_id: generate_unique_id, + billing_address: address } end @@ -93,10 +93,10 @@ def test_failed_capture def test_invalid_login gateway = MerchantWareGateway.new( - :login => '', - :password => '', - :name => '' - ) + login: '', + password: '', + name: '' + ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal 'Server was unable to process request. ---> Invalid Credentials.', response.message 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 f1c538cc470..b553611b7e3 100644 --- a/test/remote/gateways/remote_merchant_ware_version_four_test.rb +++ b/test/remote/gateways/remote_merchant_ware_version_four_test.rb @@ -3,17 +3,17 @@ class RemoteMerchantWareVersionFourTest < Test::Unit::TestCase def setup @gateway = MerchantWareVersionFourGateway.new(fixtures(:merchant_ware_version_four)) - @amount = rand(1000) + 200 - @credit_card = credit_card('5424180279791732', {:brand => 'master'}) + @amount = rand(200..1199) + @credit_card = credit_card('5424180279791732', { brand: 'master' }) @declined_card = credit_card('1234567890123') @options = { - :order_id => generate_unique_id[0,8], - :billing_address => address + order_id: generate_unique_id[0, 8], + billing_address: address } @reference_purchase_options = { - :order_id => generate_unique_id[0,8] + order_id: generate_unique_id[0, 8] } end @@ -69,9 +69,11 @@ def test_purchase_and_reference_purchase assert_success purchase assert purchase.authorization - assert reference_purchase = @gateway.purchase(@amount, - purchase.authorization, - @reference_purchase_options) + assert reference_purchase = @gateway.purchase( + @amount, + purchase.authorization, + @reference_purchase_options + ) assert_success reference_purchase assert_not_nil reference_purchase.authorization end @@ -92,10 +94,10 @@ def test_failed_capture def test_invalid_login gateway = MerchantWareVersionFourGateway.new( - :login => '', - :password => '', - :name => '' - ) + login: '', + password: '', + name: '' + ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal 'Invalid Credentials.', response.message diff --git a/test/remote/gateways/remote_merchant_warrior_test.rb b/test/remote/gateways/remote_merchant_warrior_test.rb index d9c5e826c4a..852e79ce380 100644 --- a/test/remote/gateways/remote_merchant_warrior_test.rb +++ b/test/remote/gateways/remote_merchant_warrior_test.rb @@ -2,29 +2,37 @@ class RemoteMerchantWarriorTest < Test::Unit::TestCase def setup - @gateway = MerchantWarriorGateway.new(fixtures(:merchant_warrior).merge(:test => true)) + @gateway = MerchantWarriorGateway.new(fixtures(:merchant_warrior).merge(test: true)) @success_amount = 100 @failure_amount = 205 @credit_card = credit_card( - '5123456789012346', - :month => 5, - :year => Time.now.year + 2, - :verification_value => '123', - :brand => 'master' + '4564710000000004', + month: '2', + year: '29', + verification_value: '847', + brand: 'visa' + ) + + @expired_card = credit_card( + '4564710000000012', + month: '2', + year: '05', + verification_value: '963', + brand: 'visa' ) @options = { - :billing_address => { - :name => 'Longbob Longsen', - :country => 'AU', - :state => 'Queensland', - :city => 'Brisbane', - :address1 => '123 test st', - :zip => '4000' + billing_address: { + name: 'Longbob Longsen', + country: 'AU', + state: 'Queensland', + city: 'Brisbane', + address1: '123 test st', + zip: '4000' }, - :description => 'TestProduct' + description: 'TestProduct' } end @@ -44,15 +52,15 @@ def test_successful_authorize def test_successful_purchase assert purchase = @gateway.purchase(@success_amount, @credit_card, @options) - assert_equal 'Transaction approved', purchase.message assert_success purchase + assert_equal 'Transaction approved', purchase.message assert_not_nil purchase.params['transaction_id'] assert_equal purchase.params['transaction_id'], purchase.authorization end def test_failed_purchase - assert purchase = @gateway.purchase(@failure_amount, @credit_card, @options) - assert_equal 'Transaction declined', purchase.message + assert purchase = @gateway.purchase(@success_amount, @expired_card, @options) + assert_match 'Transaction declined', purchase.message assert_failure purchase assert_not_nil purchase.params['transaction_id'] assert_equal purchase.params['transaction_id'], purchase.authorization @@ -68,10 +76,25 @@ def test_successful_refund def test_failed_refund assert refund = @gateway.refund(@success_amount, 'invalid-transaction-id') - assert_match %r{Invalid transactionID}, refund.message + assert_match %r{MW - 011:Invalid transactionID}, refund.message assert_failure refund end + def test_successful_void + assert purchase = @gateway.purchase(@success_amount, @credit_card, @options) + assert_success purchase + + assert void = @gateway.void(purchase.authorization, amount: @success_amount) + assert_success void + assert_equal 'Transaction approved', void.message + end + + def test_failed_void + assert void = @gateway.void('invalid-transaction-id', amount: @success_amount) + assert_match %r{MW - 011:Invalid transactionID}, void.message + assert_failure void + end + def test_capture_too_much assert auth = @gateway.authorize(300, @credit_card, @options) assert_success auth @@ -114,6 +137,37 @@ def test_successful_purchase_with_funky_names assert_success purchase end + def test_successful_purchase_with_recurring_flag + @options[:recurring_flag] = 1 + test_successful_purchase + end + + def test_successful_authorize_with_recurring_flag + @options[:recurring_flag] = 1 + test_successful_authorize + end + + def test_successful_authorize_with_soft_descriptors + @options[:descriptor_name] = 'FOO*Test' + @options[:descriptor_city] = 'Melbourne' + @options[:descriptor_state] = 'VIC' + test_successful_authorize + end + + def test_successful_purchase_with_soft_descriptors + @options[:descriptor_name] = 'FOO*Test' + @options[:descriptor_city] = 'Melbourne' + @options[:descriptor_state] = 'VIC' + test_successful_purchase + end + + def test_successful_refund_with_soft_descriptors + @options[:descriptor_name] = 'FOO*Test' + @options[:descriptor_city] = 'Melbourne' + @options[:descriptor_state] = 'VIC' + test_successful_refund + end + def test_transcript_scrubbing transcript = capture_transcript(@gateway) do @gateway.purchase(@success_amount, @credit_card, @options) @@ -138,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_mercury_certification_test.rb b/test/remote/gateways/remote_mercury_certification_test.rb index f019b3ad045..4f87d61df99 100644 --- a/test/remote/gateways/remote_mercury_certification_test.rb +++ b/test/remote/gateways/remote_mercury_certification_test.rb @@ -13,7 +13,7 @@ def test_sale_and_reversal assert_success sale assert_equal 'AP', sale.params['text_response'] - reversal = tokenization_gateway.void(sale.authorization, options.merge(:try_reversal => true)) + reversal = tokenization_gateway.void(sale.authorization, options.merge(try_reversal: true)) assert_success reversal assert_equal 'REVERSED', reversal.params['text_response'] end @@ -30,30 +30,6 @@ def test_sale_and_void assert_equal 'AP', void.params['text_response'] end - def test_preauth_capture_and_reversal - close_batch(tokenization_gateway) - - cc = credit_card( - '4005550000000480', - :brand => 'visa', - :month => '12', - :year => '15', - :verification_value => '123' - ) - - preauth = tokenization_gateway.authorize(106, cc, options('1')) - assert_success preauth - assert_equal 'AP', preauth.params['text_response'] - - capture = tokenization_gateway.capture(106, preauth.authorization, options) - assert_success capture - assert_equal 'AP', capture.params['text_response'] - - reversal = tokenization_gateway.void(capture.authorization, options.merge(:try_reversal => true)) - assert_success reversal - assert_equal 'REVERSED', reversal.params['text_response'] - end - def test_return close_batch(tokenization_gateway) @@ -69,7 +45,7 @@ def test_preauth_and_reversal assert_success preauth assert_equal 'AP', preauth.params['text_response'] - reversal = tokenization_gateway.void(preauth.authorization, options.merge(:try_reversal => true)) + reversal = tokenization_gateway.void(preauth.authorization, options.merge(try_reversal: true)) assert_success reversal assert_equal 'REVERSED', reversal.params['text_response'] end @@ -94,45 +70,45 @@ def test_preauth_capture_and_reversal def tokenization_gateway @tokenization_gateway ||= MercuryGateway.new( - :login => '023358150511666', - :password => 'xyz' + login: '023358150511666', + password: 'xyz' ) end def visa @visa ||= credit_card( '4003000123456781', - :brand => 'visa', - :month => '12', - :year => '15', - :verification_value => '123' + brand: 'visa', + month: '12', + year: '15', + verification_value: '123' ) end def disc @disc ||= credit_card( '6011000997235373', - :brand => 'discover', - :month => '12', - :year => '15', - :verification_value => '362' + brand: 'discover', + month: '12', + year: '15', + verification_value: '362' ) end def mc @mc ||= credit_card( '5439750001500248', - :brand => 'master', - :month => '12', - :year => '15', - :verification_value => '123' + brand: 'master', + month: '12', + year: '15', + verification_value: '123' ) end - def options(order_id=nil, other={}) + def options(order_id = nil, other = {}) { - :order_id => order_id, - :description => 'ActiveMerchant', + order_id:, + description: 'ActiveMerchant' }.merge(other) end end diff --git a/test/remote/gateways/remote_mercury_test.rb b/test/remote/gateways/remote_mercury_test.rb index 4dd1a95b80c..60bbdd01f3c 100644 --- a/test/remote/gateways/remote_mercury_test.rb +++ b/test/remote/gateways/remote_mercury_test.rb @@ -8,28 +8,29 @@ def setup @gateway = MercuryGateway.new(fixtures(:mercury)) @amount = 100 + @decline_amount = 257 - @credit_card = credit_card('4003000123456781', :brand => 'visa', :month => '12', :year => '18') + @credit_card = credit_card('4895281000000006', brand: 'visa', month: '12', year: Time.now.year) - @track_1_data = '%B4003000123456781^LONGSEN/L. ^18121200000000000000**123******?*' - @track_2_data = ';5413330089010608=2512101097750213?' + @track_1_data = "%B#{@credit_card.number}^LONGSEN/L. ^18121200000000000000**111******?*" + @track_2_data = ";#{@credit_card.number}=18121200000000000000?" @options = { - :order_id => 'c111111111.1', - :description => 'ActiveMerchant' + order_id: 'c111111111.1', + description: 'ActiveMerchant' } @options_with_billing = @options.merge( - :merchant => '999', - :billing_address => { - :address1 => '4 Corporate SQ', - :zip => '30329' + merchant: '999', + billing_address: { + address1: '123 Main Street', + zip: '45209' } ) @full_options = @options_with_billing.merge( - :ip => '123.123.123.123', - :merchant => 'Open Dining', - :customer => 'Tim', - :tax => '5' + ip: '123.123.123.123', + merchant: 'Open Dining', + customer: 'Tim', + tax: '5' ) close_batch @@ -46,13 +47,13 @@ def test_successful_authorize_and_capture end def test_failed_authorize - response = @gateway.authorize(1100, @credit_card, @options) + response = @gateway.authorize(@decline_amount, @credit_card, @options) assert_failure response assert_equal 'DECLINE', response.message end def test_purchase_and_void - response = @gateway.purchase(102, @credit_card, @options) + response = @gateway.purchase(@amount, @credit_card, @options) assert_success response void = @gateway.void(response.authorization) @@ -82,13 +83,14 @@ def test_credit end def test_failed_purchase - response = @gateway.purchase(1100, @credit_card, @options) + response = @gateway.purchase(@decline_amount, @credit_card, @options) assert_failure response assert_equal 'DECLINE', response.message assert_equal Gateway::STANDARD_ERROR_CODE[:card_declined], response.error_code end def test_avs_and_cvv_results + @credit_card.verification_value = '222' response = @gateway.authorize(333, @credit_card, @options_with_billing) assert_success response @@ -101,7 +103,7 @@ def test_avs_and_cvv_results }, response.avs_result ) - assert_equal({'code'=>'M', 'message'=>'CVV matches'}, response.cvv_result) + assert_equal({ 'code' => 'M', 'message' => 'CVV matches' }, response.cvv_result) end def test_avs_and_cvv_results_with_track_data @@ -118,7 +120,7 @@ def test_avs_and_cvv_results_with_track_data }, response.avs_result ) - assert_equal({'code'=>'P', 'message'=>'CVV not processed'}, response.cvv_result) + assert_equal({ 'code' => 'P', 'message' => 'CVV not processed' }, response.cvv_result) end def test_partial_capture @@ -136,15 +138,13 @@ def test_partial_capture end def test_authorize_with_bad_expiration_date - @credit_card.month = 13 - @credit_card.year = 2001 - response = @gateway.authorize(575, @credit_card, @options_with_billing) + response = @gateway.authorize(267, @credit_card, @options_with_billing) assert_failure response assert_equal 'INVLD EXP DATE', response.message end def test_mastercard_authorize_and_capture_with_refund - mc = credit_card('5499990123456781', :brand => 'master') + mc = credit_card('5499990123456781', brand: 'master') response = @gateway.authorize(200, mc, @options) assert_success response @@ -161,7 +161,7 @@ def test_mastercard_authorize_and_capture_with_refund end def test_amex_authorize_and_capture_with_refund - amex = credit_card('373953244361001', :brand => 'american_express', :verification_value => '1234') + amex = credit_card('373953244361001', brand: 'american_express', verification_value: '1234') response = @gateway.authorize(201, amex, @options) assert_success response @@ -177,7 +177,7 @@ def test_amex_authorize_and_capture_with_refund end def test_discover_authorize_and_capture - discover = credit_card('6011000997235373', :brand => 'discover') + discover = credit_card('6011000997235373', brand: 'discover') response = @gateway.authorize(225, discover, @options_with_billing) assert_success response @@ -206,14 +206,14 @@ def test_authorize_and_capture_without_tokenization assert_success response assert_equal '1.00', response.params['authorize'] - capture = gateway.capture(nil, response.authorization, :credit_card => @credit_card) + capture = gateway.capture(nil, response.authorization, credit_card: @credit_card) assert_success capture assert_equal '1.00', capture.params['authorize'] end def test_successful_authorize_and_capture_with_track_1_data @credit_card.track_data = @track_1_data - response = @gateway.authorize(100, @credit_card, @options) + response = @gateway.authorize(@amount, @credit_card, @options) assert_success response assert_equal '1.00', response.params['authorize'] diff --git a/test/remote/gateways/remote_metrics_global_test.rb b/test/remote/gateways/remote_metrics_global_test.rb index b1d19387559..329c735622f 100644 --- a/test/remote/gateways/remote_metrics_global_test.rb +++ b/test/remote/gateways/remote_metrics_global_test.rb @@ -3,17 +3,17 @@ class MetricsGlobalTest < Test::Unit::TestCase def setup Base.mode = :test - + @gateway = MetricsGlobalGateway.new(fixtures(:metrics_global)) @amount = 100 - @credit_card = credit_card('4111111111111111', :verification_value => '999') + @credit_card = credit_card('4111111111111111', verification_value: '999') @options = { - :order_id => generate_unique_id, - :billing_address => address(:address1 => '888 Test Street', :zip => '77777'), - :description => 'Store purchase' + order_id: generate_unique_id, + billing_address: address(address1: '888 Test Street', zip: '77777'), + description: 'Store purchase' } end - + def test_successful_purchase assert response = @gateway.purchase(@amount, @credit_card, @options) assert_success response @@ -21,7 +21,7 @@ def test_successful_purchase assert_equal 'This transaction has been approved', response.message assert response.authorization end - + def test_declined_authorization @amount = 10 assert response = @gateway.purchase(@amount, @credit_card, @options) @@ -29,71 +29,71 @@ def test_declined_authorization assert response.test? assert_equal 'This transaction has been declined', response.message end - + def test_successful_authorization assert response = @gateway.authorize(@amount, @credit_card, @options) assert_success response assert_equal 'This transaction has been approved', response.message assert response.authorization end - + def test_authorization_and_capture assert authorization = @gateway.authorize(@amount, @credit_card, @options) assert_success authorization - + assert capture = @gateway.capture(@amount, authorization.authorization) assert_success capture assert_equal 'This transaction has been approved', capture.message end - + def test_authorization_and_void assert authorization = @gateway.authorize(@amount, @credit_card, @options) assert_success authorization - + assert void = @gateway.void(authorization.authorization) assert_success void assert_equal 'This transaction has been approved', void.message end - + def test_bad_login gateway = MetricsGlobalGateway.new( - :login => 'X', - :password => 'Y' + login: 'X', + password: 'Y' ) - + assert response = gateway.purchase(@amount, @credit_card) - + assert_equal Response, response.class - assert_equal ['avs_result_code', - 'card_code', - 'response_code', - 'response_reason_code', - 'response_reason_text', - 'transaction_id'], response.params.keys.sort + assert_equal %w[avs_result_code + card_code + response_code + response_reason_code + response_reason_text + transaction_id], response.params.keys.sort assert_match(/Authentication Failed/, response.message) - + assert_equal false, response.success? end - + def test_using_test_request gateway = MetricsGlobalGateway.new( - :login => 'X', - :password => 'Y' + login: 'X', + password: 'Y' ) - + assert response = gateway.purchase(@amount, @credit_card) - + assert_equal Response, response.class - assert_equal ['avs_result_code', - 'card_code', - 'response_code', - 'response_reason_code', - 'response_reason_text', - 'transaction_id'], response.params.keys.sort - + assert_equal %w[avs_result_code + card_code + response_code + response_reason_code + response_reason_text + transaction_id], response.params.keys.sort + assert_match(/Authentication Failed/, response.message) - - assert_equal false, response.success? + + assert_equal false, response.success? end end diff --git a/test/remote/gateways/remote_micropayment_test.rb b/test/remote/gateways/remote_micropayment_test.rb index 29b3a3fdcd1..d5c49cf3787 100644 --- a/test/remote/gateways/remote_micropayment_test.rb +++ b/test/remote/gateways/remote_micropayment_test.rb @@ -16,7 +16,7 @@ def setup end def test_invalid_login - gateway = MicropaymentGateway.new(access_key: 'invalid', api_key:'invalid') + gateway = MicropaymentGateway.new(access_key: 'invalid', api_key: 'invalid') response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal 'Authorization failed - Reason: api accesskey wrong', response.message @@ -89,7 +89,7 @@ def test_successful_void_for_purchase end def test_successful_authorize_and_capture_and_refund - response = @gateway.authorize(@amount, @credit_card, @options.merge(recurring: false)) + response = @gateway.authorize(@amount, @credit_card, @options.merge(recurring: false)) assert_success response assert_equal 'Succeeded', response.message assert_match %r(^\w+\|.+$), response.authorization diff --git a/test/remote/gateways/remote_migs_test.rb b/test/remote/gateways/remote_migs_test.rb index 3e6f3ae43f6..b71f36917b8 100644 --- a/test/remote/gateways/remote_migs_test.rb +++ b/test/remote/gateways/remote_migs_test.rb @@ -10,15 +10,18 @@ def setup @amount = 100 @declined_amount = 105 - @visa = credit_card('4987654321098769', :month => 5, :year => 2021, :brand => 'visa') - @master = credit_card('5123456789012346', :month => 5, :year => 2021, :brand => 'master') - @amex = credit_card('371449635311004', :month => 5, :year => 2021, :brand => 'american_express') - @diners = credit_card('30123456789019', :month => 5, :year => 2021, :brand => 'diners_club') + @visa = credit_card('4987654321098769', month: 5, year: 2021, brand: 'visa') + @master = credit_card('5123456789012346', month: 5, year: 2021, brand: 'master') + @amex = credit_card('371449635311004', month: 5, year: 2021, brand: 'american_express') + @diners = credit_card('30123456789019', month: 5, year: 2021, brand: 'diners_club') @credit_card = @visa + @valid_tx_source = 'MOTO' + @invalid_tx_source = 'penguin' + @options = { - :order_id => '1', - :currency => 'SAR' + order_id: '1', + currency: 'SAR' } @three_ds_options = { @@ -33,15 +36,15 @@ def setup def test_server_purchase_url options = { - :order_id => 1, - :unique_id => 9, - :return_url => 'http://localhost:8080/payments/return', - :currency => 'SAR' + order_id: 1, + unique_id: 9, + return_url: 'http://localhost:8080/payments/return', + currency: 'SAR' } choice_url = @gateway.purchase_offsite_url(@amount, options) - assert_response_match /Pay securely .* by clicking on the card logo below/, choice_url + assert_response_match(/Pay securely .* by clicking on the card logo below/, choice_url) responses = { 'visa' => /You have chosen .*VISA.*/, @@ -49,7 +52,7 @@ def test_server_purchase_url } responses.each_pair do |card_type, response_text| - url = @gateway.purchase_offsite_url(@amount, options.merge(:card_type => card_type)) + url = @gateway.purchase_offsite_url(@amount, options.merge(card_type:)) assert_response_match response_text, url end end @@ -104,6 +107,48 @@ def test_refund # assert_equal 'Approved', response.message end + def test_purchase_passes_tx_source + # returns a successful response when a valid tx_source parameter is sent + assert good_response = @gateway.purchase(@amount, @credit_card, @options.merge(tx_source: @valid_tx_source)) + assert_success good_response + assert_equal 'Approved', good_response.message + + # returns a failed response when an invalid tx_source parameter is sent + assert bad_response = @gateway.purchase(@amount, @credit_card, @options.merge(tx_source: @invalid_tx_source)) + assert_failure bad_response + end + + def test_capture_passes_tx_source + # authorize the credit card in order to then run capture + assert auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + assert_equal 'Approved', auth.message + + # returns a successful response when a valid tx_source paramater is sent + assert good_response = @gateway.capture(@amount, auth.authorization, @options.merge(tx_source: @valid_tx_source)) + assert_success good_response + + # returns a failed response when an invalid tx_source parameter is sent + assert bad_response = @gateway.capture(@amount, auth.authorization, @options.merge(tx_source: @invalid_tx_source)) + assert_failure bad_response + end + + def test_void_passes_tx_source + # authorize the credit card in order to then run capture + assert auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + assert_equal 'Approved', auth.message + + # returns a successful response when a valid tx_source paramater is sent + assert good_response = @gateway.void(auth.authorization, @options.merge(tx_source: @valid_tx_source)) + assert_success good_response + assert_equal 'Approved', good_response.message + + # returns a failed response when an invalid tx_source parameter is sent + assert bad_response = @gateway.void(auth.authorization, @options.merge(tx_source: @invalid_tx_source)) + assert_failure bad_response + end + def test_status purchase_response = @gateway.purchase(@declined_amount, @credit_card, @options) assert response = @gateway.status(purchase_response.params['MerchTxnRef']) @@ -112,7 +157,7 @@ def test_status end def test_invalid_login - gateway = MigsGateway.new(:login => '', :password => '') + gateway = MigsGateway.new(login: '', password: '') assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal 'Required field vpc_Merchant was not present in the request', response.message @@ -166,7 +211,7 @@ def assert_response_match(regexp, url) def https_response(url, cookie = nil) retry_exceptions do - headers = cookie ? {'Cookie' => cookie} : {} + headers = cookie ? { 'Cookie' => cookie } : {} response = raw_ssl_request(:get, url, nil, headers) if response.is_a?(Net::HTTPRedirection) new_cookie = [cookie, response['Set-Cookie']].compact.join(';') diff --git a/test/remote/gateways/remote_mit_test.rb b/test/remote/gateways/remote_mit_test.rb new file mode 100644 index 00000000000..db58072b039 --- /dev/null +++ b/test/remote/gateways/remote_mit_test.rb @@ -0,0 +1,129 @@ +require 'test_helper' + +class RemoteMitTest < Test::Unit::TestCase + def setup + @gateway = MitGateway.new(fixtures(:mit)) + + @amount = 1115 + @amount_fail = 11165 + + @credit_card = ActiveMerchant::Billing::CreditCard.new( + number: '5555555555555557', + verification_value: '261', + month: '09', + year: '2025', + first_name: 'Pedro', + last_name: 'Flores Valdes' + ) + + @declined_card = ActiveMerchant::Billing::CreditCard.new( + number: '4111111111111111', + verification_value: '318', + month: '09', + year: '2025', + first_name: 'Pedro', + last_name: 'Flores Valdes' + ) + + @options_success = { + order_id: '721', + transaction_id: '721', # unique id for every transaction, needs to be generated for every test + billing_address: address, + description: 'Store Purchase' + } + + @options = { + order_id: '721', + transaction_id: '721', # unique id for every transaction, needs to be generated for every test + billing_address: address, + description: 'Store Purchase', + api_key: fixtures(:mit)[:apikey] + } + end + + def test_successful_purchase + # ############################################################### + # create unique id based on timestamp for testing purposes + # Each order / transaction passed to the gateway must be unique + time = Time.now.to_i.to_s + @options_success[:order_id] = 'TID|' + time + response = @gateway.purchase(@amount, @credit_card, @options_success) + assert_success response + assert_equal 'approved', response.message + end + + def test_failed_purchase + response = @gateway.purchase(@amount_fail, @declined_card, @options) + assert_failure response + assert_not_equal 'approved', response.message + end + + def test_successful_authorize_and_capture + # ############################################################### + # create unique id based on timestamp for testing purposes + # Each order / transaction passed to the gateway must be unique + time = Time.now.to_i.to_s + @options_success[:order_id] = 'TID|' + time + auth = @gateway.authorize(@amount, @credit_card, @options_success) + assert_success auth + + assert capture = @gateway.capture(@amount, auth.authorization, @options_success) + assert_success capture + assert_equal 'approved', capture.message + end + + def test_failed_authorize + response = @gateway.authorize(@amount_fail, @declined_card, @options) + assert_failure response + assert_not_equal 'approved', response.message + end + + def test_failed_capture + # ############################################################### + # create unique id based on timestamp for testing purposes + # Each order / transaction passed to the gateway must be unique + time = Time.now.to_i.to_s + @options[:order_id] = 'TID|' + time + response = @gateway.capture(@amount_fail, 'requiredauth', @options) + assert_failure response + assert_not_equal 'approved', response.message + end + + def test_successful_refund + # ############################################################### + # create unique id based on timestamp for testing purposes + # Each order / transaction passed to the gateway must be unique + time = Time.now.to_i.to_s + @options_success[:order_id] = 'TID|' + time + purchase = @gateway.purchase(@amount, @credit_card, @options_success) + assert_success purchase + + # authorization is required + assert refund = @gateway.refund(@amount, purchase.authorization, @options_success) + assert_success refund + assert_equal 'approved', refund.message + end + + def test_failed_refund + # ############################################################### + # create unique id based on timestamp for testing purposes + # Each order / transaction passed to the gateway must be unique + time = Time.now.to_i.to_s + @options[:order_id] = 'TID|' + time + response = @gateway.refund(@amount, 'invalidauth', @options) + assert_failure response + assert_not_equal 'approved', response.message + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @credit_card, @options_success) + end + + clean_transcript = @gateway.scrub(transcript) + assert_scrubbed(@credit_card.number, clean_transcript) + assert_scrubbed(@credit_card.verification_value, clean_transcript) + assert_scrubbed(@gateway.options[:api_key], clean_transcript) + assert_scrubbed(@gateway.options[:key_session], clean_transcript) + end +end diff --git a/test/remote/gateways/remote_modern_payments_cim_test.rb b/test/remote/gateways/remote_modern_payments_cim_test.rb index a6afc2b97fe..ed480d5fa65 100644 --- a/test/remote/gateways/remote_modern_payments_cim_test.rb +++ b/test/remote/gateways/remote_modern_payments_cim_test.rb @@ -1,56 +1,54 @@ require 'test_helper' class RemoteModernPaymentsCimTest < Test::Unit::TestCase - - def setup @gateway = ModernPaymentsCimGateway.new(fixtures(:modern_payments)) - + @amount = 100 @credit_card = credit_card('4111111111111111') @declined_card = credit_card('4000000000000000') - - @options = { - :billing_address => address, - :customer => 'JIMSMITH2000' + + @options = { + billing_address: address, + customer: 'JIMSMITH2000' } end - + def test_successful_create_customer response = @gateway.create_customer(@options) assert_success response assert !response.params['create_customer_result'].blank? end - + def test_successful_modify_customer_credit_card customer = @gateway.create_customer(@options) assert_success customer - + customer_id = customer.params['create_customer_result'] - + credit_card = @gateway.modify_customer_credit_card(customer_id, @credit_card) assert_success credit_card assert !credit_card.params['modify_customer_credit_card_result'].blank? end - + def test_succsessful_authorize_credit_card_payment customer = @gateway.create_customer(@options) assert_success customer - + customer_id = customer.params['create_customer_result'] - + credit_card = @gateway.modify_customer_credit_card(customer_id, @credit_card) assert_success credit_card - + payment = @gateway.authorize_credit_card_payment(customer_id, @amount) assert_success payment end def test_invalid_login gateway = ModernPaymentsCimGateway.new( - :login => '', - :password => '' - ) + login: '', + password: '' + ) assert response = gateway.create_customer(@options) assert_failure response assert_equal ModernPaymentsCimGateway::ERROR_MESSAGE, response.message diff --git a/test/remote/gateways/remote_modern_payments_test.rb b/test/remote/gateways/remote_modern_payments_test.rb index c0ec1040d63..a5467e42fc0 100644 --- a/test/remote/gateways/remote_modern_payments_test.rb +++ b/test/remote/gateways/remote_modern_payments_test.rb @@ -1,7 +1,6 @@ require 'test_helper' class RemoteModernPaymentTest < Test::Unit::TestCase - def setup @gateway = ModernPaymentsGateway.new(fixtures(:modern_payments)) @@ -10,11 +9,10 @@ def setup @declined_card = credit_card('4000000000000000') @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase' + order_id: '1', + billing_address: address, + description: 'Store Purchase' } - end def test_successful_purchase @@ -35,23 +33,12 @@ def test_unsuccessful_purchase def test_invalid_login gateway = ModernPaymentsGateway.new( - :login => '5000', - :password => 'password' - ) - assert response = gateway.purchase(@amount, @credit_card, @options) - assert_failure response - assert_equal ModernPaymentsCimGateway::FAILURE_MESSAGE, response.message - end - - def test_invalid_login - gateway = ModernPaymentsGateway.new( - :login => '', - :password => '' - ) + login: '', + password: '' + ) assert_raises(ActiveMerchant::ResponseError) do gateway.purchase(@amount, @credit_card, @options) end end - end diff --git a/test/remote/gateways/remote_moka_test.rb b/test/remote/gateways/remote_moka_test.rb new file mode 100644 index 00000000000..8dbac306cf3 --- /dev/null +++ b/test/remote/gateways/remote_moka_test.rb @@ -0,0 +1,274 @@ +require 'test_helper' + +class RemoteMokaTest < Test::Unit::TestCase + def setup + @gateway = MokaGateway.new(fixtures(:moka)) + + @amount = 100 + @credit_card = credit_card('5269111122223332') + @declined_card = credit_card('4000300011112220') + @options = { + description: 'Store Purchase' + } + @three_ds_options = @options.merge({ + execute_threed: true, + redirect_type: 1, + redirect_url: 'www.example.com' + }) + end + + def test_invalid_login + gateway = MokaGateway.new(dealer_code: '', username: '', password: '') + + response = gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_match 'PaymentDealer.CheckPaymentDealerAuthentication.InvalidAccount', response.message + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Success', response.message + end + + def test_successful_purchase_with_single_digit_exp_month + @credit_card.month = 1 + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Success', response.message + end + + def test_successful_purchase_with_more_options + options = { + order_id: '1', + ip: '127.0.0.1', + sub_merchant_name: 'Example Co.', + is_pool_payment: 1 + } + + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Success', response.message + end + + def test_successful_purchase_with_buyer_information + options = { + billing_address: address, + email: 'safiye.ali@example.com' + } + + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Success', response.message + end + + def test_successful_purchase_with_basket_products + # Basket Products must be on the list of Merchant Products for your Moka account. + # To see this list or add products to it, log in to your Moka Dashboard + options = { + basket_product: [ + { + product_id: 333, + product_code: '0173', + unit_price: 19900, + quantity: 1 + }, + { + product_id: 281, + product_code: '38', + unit_price: 5000, + quantity: 1 + } + ] + } + + response = @gateway.purchase(24900, @credit_card, options) + assert_success response + assert_equal 'Success', response.message + end + + def test_successful_purchase_with_nil_cvv + test_card = credit_card('5269111122223332') + test_card.verification_value = nil + + response = @gateway.purchase(@amount, test_card, @options) + assert_success response + assert_equal 'Success', response.message + end + + def test_successful_purchase_with_installments + options = @options.merge(installment_number: 12) + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Success', response.message + end + + def test_failed_purchase + response = @gateway.purchase(@amount, @declined_card, @options) + assert_failure response + + assert_equal 'PaymentDealer.DoDirectPayment.VirtualPosNotAvailable', 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 'Success', capture.message + end + + def test_successful_authorize_and_capture_using_non_default_currency + options = @options.merge(currency: 'USD') + auth = @gateway.authorize(@amount, @credit_card, options) + assert_success auth + + assert capture = @gateway.capture(@amount, auth.authorization, currency: 'USD') + assert_success capture + assert_equal 'Success', capture.message + end + + def test_failed_authorize + response = @gateway.authorize(@amount, @declined_card, @options) + assert_failure response + assert_equal 'PaymentDealer.DoDirectPayment.VirtualPosNotAvailable', response.error_code + end + + def test_partial_capture + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert capture = @gateway.capture(@amount - 0.1, auth.authorization) + assert_success capture + end + + def test_failed_capture + response = @gateway.capture(@amount, '') + assert_failure response + assert_equal 'PaymentDealer.DoCapture.OtherTrxCodeOrVirtualPosOrderIdMustGiven', response.message + end + + # # Moka does not allow a same-day refund on a purchase/capture. In order to test refund, + # # you must pass a reference that has 'matured' at least one day. + # def test_successful_refund + # my_matured_reference = 'REPLACE ME' + # assert refund = @gateway.refund(0, my_matured_reference) + # assert_success refund + # assert_equal 'Success', refund.message + # end + + # # Moka does not allow a same-day refund on a purchase/capture. In order to test refund, + # # you must pass a reference that has 'matured' at least one day. For the purposes of testing + # # a partial refund, make sure the original transaction being referenced was for an amount + # # greater than the 'partial_amount' supplied in the test. + # def test_partial_refund + # my_matured_reference = 'REPLACE ME' + # partial_amount = 50 + # assert refund = @gateway.refund(partial_amount, my_matured_reference) + # assert_success refund + # assert_equal 'Success', refund.message + # end + + def test_failed_refund + response = @gateway.refund(@amount, '') + assert_failure response + assert_equal 'PaymentDealer.DoCreateRefundRequest.OtherTrxCodeOrVirtualPosOrderIdMustGiven', response.message + end + + def test_successful_void + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + 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 'PaymentDealer.DoVoid.InvalidRequest', response.message + end + + def test_successful_verify + response = @gateway.verify(@credit_card, @options) + assert_success response + assert_match 'Success', response.message + end + + def test_failed_verify + response = @gateway.verify(@declined_card, @options) + assert_failure response + assert_match 'PaymentDealer.DoDirectPayment.VirtualPosNotAvailable', response.message + end + + # 3ds Tests + + def test_successful_initiation_of_3ds_authorize + response = @gateway.authorize(@amount, @credit_card, @three_ds_options) + + assert_success response + assert_equal 'Success', response.message + assert response.params['Data']['Url'].present? + assert response.params['Data']['CodeForHash'].present? + end + + def test_failed_3ds_authorize + response = @gateway.authorize(@amount, @declined_card, @three_ds_options) + + assert_failure response + assert_equal 'PaymentDealer.DoDirectPayment3dRequest.VirtualPosNotAvailable', response.message + end + + def test_successful_initiation_of_3ds_purchase + response = @gateway.purchase(@amount, @credit_card, @three_ds_options) + + assert_success response + assert_equal 'Success', response.message + assert response.params['Data']['Url'].present? + assert response.params['Data']['CodeForHash'].present? + end + + def test_failed_3ds_purchase + response = @gateway.purchase(@amount, @declined_card, @three_ds_options) + + assert_failure response + assert_equal 'PaymentDealer.DoDirectPayment3dRequest.VirtualPosNotAvailable', response.message + end + + # Scrubbing Tests + + def test_transcript_scrubbing_with_string_dealer_code + gateway = MokaGateway.new(fixtures(:moka)) + gateway.options[:dealer_code] = gateway.options[:dealer_code].to_s + + capture_transcript_and_assert_scrubbed(gateway) + end + + def test_transcript_scrubbing_with_integer_dealer_code + gateway = MokaGateway.new(fixtures(:moka)) + gateway.options[:dealer_code] = gateway.options[:dealer_code].to_i + + capture_transcript_and_assert_scrubbed(gateway) + end + + def capture_transcript_and_assert_scrubbed(gateway) + 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[:dealer_code], transcript) + assert_scrubbed(gateway.options[:username], transcript) + assert_scrubbed(gateway.options[:password], transcript) + assert_scrubbed(check_key, transcript) + end + + def check_key + str = "#{@gateway.options[:dealer_code]}MK#{@gateway.options[:username]}PD#{@gateway.options[:password]}" + Digest::SHA256.hexdigest(str) + end +end diff --git a/test/remote/gateways/remote_monei_test.rb b/test/remote/gateways/remote_monei_test.rb index 474f317c76a..9d49ade34f9 100755 --- a/test/remote/gateways/remote_monei_test.rb +++ b/test/remote/gateways/remote_monei_test.rb @@ -7,31 +7,175 @@ def setup ) @amount = 100 - @credit_card = credit_card('4000100011112224') - @declined_card = credit_card('5453010000059675') + @credit_card = credit_card('4548812049400004', month: 12, year: 2034, verification_value: '123') + @declined_card = credit_card('5453010000059675', month: 12, year: 2034, verification_value: '123') + + @three_ds_declined_card = credit_card('4444444444444505', month: 12, year: 2034, verification_value: '123') + @three_ds_2_enrolled_card = credit_card('4444444444444406', month: 12, year: 2034, verification_value: '123') @options = { - order_id: '1', billing_address: address, description: 'Store Purchase' } end + def random_order_id + SecureRandom.hex(16) + end + def test_successful_purchase - response = @gateway.purchase(@amount, @credit_card, @options) + options = @options.merge({ order_id: random_order_id }) + response = @gateway.purchase(@amount, @credit_card, options) + + assert_success response + assert_equal 'Transaction approved', response.message + end + + def test_successful_purchase_with_no_billing_address + options = { + order_id: random_order_id, + description: 'Store Purchase' + } + response = @gateway.purchase(@amount, @credit_card, options) assert_success response - assert_equal 'Request successfully processed in \'Merchant in Connector Test Mode\'', response.message + assert_equal 'Transaction approved', response.message + end + + def test_successful_purchase_with_partial_billing_address + partial_address = { + name: 'Jim Smith', + address1: '456 My Street', + city: 'MIlan', + zip: 'K1C2N6' + } + options = { + billing_address: partial_address, + order_id: random_order_id, + description: 'Store Purchase' + } + response = @gateway.purchase(@amount, @credit_card, options) + + assert_success response + assert_equal 'Transaction approved', response.message + end + + def test_successful_purchase_with_3ds + options = @options.merge!({ + order_id: random_order_id, + three_d_secure: { + eci: '05', + cavv: 'AAACAgSRBklmQCFgMpEGAAAAAAA=', + xid: 'CAACCVVUlwCXUyhQNlSXAAAAAAA=' + }, + ip: '77.110.174.153', + user_agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36', + lang: 'en' + }) + response = @gateway.purchase(@amount, @credit_card, options) + + assert_success response + assert_equal 'Transaction approved', response.message + end + + def test_successful_purchase_with_3ds_v2 + options = @options.merge!({ + order_id: random_order_id, + three_d_secure: { + eci: '05', + cavv: 'AAACAgSRBklmQCFgMpEGAAAAAAA=', + ds_transaction_id: '7eac9571-3533-4c38-addd-00cf34af6a52' + }, + ip: '77.110.174.153', + user_agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36', + lang: 'en' + }) + response = @gateway.purchase(@amount, @three_ds_2_enrolled_card, options) + + assert_success response + assert_equal 'Transaction approved', response.message end def test_failed_purchase - response = @gateway.purchase(@amount, @declined_card, @options) + options = @options.merge({ order_id: random_order_id }) + response = @gateway.purchase(@amount, @declined_card, options) + assert_failure response + assert_equal 'Card rejected: invalid card number', response.message + end + + def test_failed_purchase_with_3ds + options = @options.merge!({ + order_id: random_order_id, + three_d_secure: { + eci: '05', + cavv: 'INVALID_Verification_ID', + xid: 'CAACCVVUlwCXUyhQNlSXAAAAAAA=' + }, + ip: '77.110.174.153', + user_agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36', + lang: 'en' + }) + response = @gateway.purchase(@amount, @three_ds_declined_card, options) assert_failure response - assert_equal 'invalid cc number/brand combination', response.message + assert_equal 'Invalid 3DSecure Verification ID', response.message + end + + def test_successful_store + options = @options.merge({ order_id: random_order_id }) + response = @gateway.store(@credit_card, options) + + assert_success response + assert(!response.params['paymentToken'].nil?) + assert_equal response.authorization, response.params['paymentToken'] + assert_equal 'Transaction approved', response.message + end + + def test_successful_store_with_3ds_v2 + options = @options.merge!({ + order_id: random_order_id, + three_d_secure: { + eci: '05', + cavv: 'AAACAgSRBklmQCFgMpEGAAAAAAA=', + ds_transaction_id: '7eac9571-3533-4c38-addd-00cf34af6a52' + }, + ip: '77.110.174.153', + user_agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36', + lang: 'en' + }) + response = @gateway.store(@three_ds_2_enrolled_card, options) + + assert_success response + assert(!response.params['paymentToken'].nil?) + assert_equal response.authorization, response.params['paymentToken'] + assert_equal 'Transaction approved', response.message + end + + def test_successful_store_and_purchase + options = @options.merge({ order_id: random_order_id }) + stored = @gateway.store(@credit_card, options) + assert_success stored + + options = @options.merge({ order_id: random_order_id }) + assert purchase = @gateway.purchase(@amount, stored.authorization, options) + assert_success purchase + end + + def test_successful_store_authorize_and_capture + options = @options.merge({ order_id: random_order_id }) + stored = @gateway.store(@credit_card, options) + assert_success stored + + options = @options.merge({ order_id: random_order_id }) + authorize = @gateway.authorize(@amount, stored.authorization, options) + assert_success authorize + + assert capture = @gateway.capture(@amount, authorize.authorization) + assert_success capture end def test_successful_authorize_and_capture - auth = @gateway.authorize(@amount, @credit_card, @options) + options = @options.merge({ order_id: random_order_id }) + auth = @gateway.authorize(@amount, @credit_card, options) assert_success auth assert capture = @gateway.capture(@amount, auth.authorization) @@ -39,26 +183,29 @@ def test_successful_authorize_and_capture end def test_failed_authorize - response = @gateway.authorize(@amount, @declined_card, @options) + options = @options.merge({ order_id: random_order_id }) + response = @gateway.authorize(@amount, @declined_card, options) assert_failure response end def test_partial_capture - auth = @gateway.authorize(@amount, @credit_card, @options) + options = @options.merge({ order_id: random_order_id }) + auth = @gateway.authorize(@amount, @credit_card, options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture end def test_multi_partial_capture - auth = @gateway.authorize(@amount, @credit_card, @options) + options = @options.merge({ order_id: random_order_id }) + auth = @gateway.authorize(@amount, @credit_card, options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_failure capture end @@ -68,7 +215,8 @@ def test_failed_capture end def test_successful_refund - purchase = @gateway.purchase(@amount, @credit_card, @options) + options = @options.merge({ order_id: random_order_id }) + purchase = @gateway.purchase(@amount, @credit_card, options) assert_success purchase assert refund = @gateway.refund(@amount, purchase.authorization) @@ -76,21 +224,23 @@ def test_successful_refund end def test_partial_refund - purchase = @gateway.purchase(@amount, @credit_card, @options) + options = @options.merge({ order_id: random_order_id }) + purchase = @gateway.purchase(@amount, @credit_card, options) assert_success purchase - assert refund = @gateway.refund(@amount-1, purchase.authorization) + assert refund = @gateway.refund(@amount - 1, purchase.authorization) assert_success refund end def test_multi_partial_refund - purchase = @gateway.purchase(@amount, @credit_card, @options) + options = @options.merge({ order_id: random_order_id }) + purchase = @gateway.purchase(@amount, @credit_card, options) assert_success purchase - assert refund = @gateway.refund(@amount-1, purchase.authorization) + assert refund = @gateway.refund(@amount - 1, purchase.authorization) assert_success refund - assert refund = @gateway.refund(@amount-1, purchase.authorization) + assert refund = @gateway.refund(@amount - 1, purchase.authorization) assert_failure refund end @@ -100,7 +250,8 @@ def test_failed_refund end def test_successful_void - auth = @gateway.authorize(@amount, @credit_card, @options) + options = @options.merge({ order_id: random_order_id }) + auth = @gateway.authorize(@amount, @credit_card, options) assert_success auth assert void = @gateway.void(auth.authorization) @@ -113,27 +264,26 @@ def test_failed_void end def test_successful_verify - response = @gateway.verify(@credit_card, @options) + options = @options.merge({ order_id: random_order_id }) + response = @gateway.verify(@credit_card, options) assert_success response - assert_equal 'Request successfully processed in \'Merchant in Connector Test Mode\'', response.message + assert_equal 'Transaction approved', response.message end def test_failed_verify - response = @gateway.verify(@declined_card, @options) + options = @options.merge({ order_id: random_order_id }) + response = @gateway.verify(@declined_card, options) assert_failure response - assert_equal 'invalid cc number/brand combination', response.message + assert_equal 'Card rejected: invalid card number', response.message end def test_invalid_login gateway = MoneiGateway.new( - :sender_id => 'mother', - :channel_id => 'there is no other', - :login => 'like mother', - :pwd => 'so treat Her right' + api_key: 'invalid' ) - response = gateway.purchase(@amount, @credit_card, @options) + options = @options.merge({ order_id: random_order_id }) + response = gateway.purchase(@amount, @credit_card, options) assert_failure response end - end diff --git a/test/remote/gateways/remote_moneris_test.rb b/test/remote/gateways/remote_moneris_test.rb index 6cd0cd8e993..e3f1829cbfa 100644 --- a/test/remote/gateways/remote_moneris_test.rb +++ b/test/remote/gateways/remote_moneris_test.rb @@ -5,12 +5,30 @@ def setup Base.mode = :test @gateway = MonerisGateway.new(fixtures(:moneris)) + + # https://developer.moneris.com/More/Testing/Penny%20Value%20Simulator @amount = 100 - @credit_card = credit_card('4242424242424242') + @fail_amount = 105 + + # https://developer.moneris.com/livedemo/3ds2/reference/guide/php + @fully_authenticated_eci = 5 + @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, - :customer => generate_unique_id, - :billing_address => address + order_id: generate_unique_id, + customer: generate_unique_id, + billing_address: address } end @@ -21,24 +39,159 @@ def test_successful_purchase assert_false response.authorization.blank? end - def test_successful_purchase_with_network_tokenization - @credit_card = network_tokenization_credit_card('4242424242424242', - payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', - verification_value: nil + 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, + @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 response = @gateway.purchase(@amount, @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 + def test_successful_first_purchase_with_credential_on_file + gateway = MonerisGateway.new(fixtures(:moneris)) + assert response = gateway.purchase(@amount, @credit_card, @options.merge(issuer_id: '', payment_indicator: 'C', payment_information: '0')) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + 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) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_successful_non_cof_purchase_with_cof_enabled_and_only_issuer_id_sent + gateway = MonerisGateway.new(fixtures(:moneris)) + assert response = gateway.purchase(@amount, @credit_card, @options.merge(issuer_id: '')) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + assert_nil response.params['issuer_id'] + end + + def test_successful_subsequent_purchase_with_credential_on_file + gateway = MonerisGateway.new(fixtures(:moneris)) + assert response = gateway.authorize( + @amount, + @credit_card, + @options.merge( + issuer_id: '', + payment_indicator: 'C', + payment_information: '0' + ) ) - assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + + assert response2 = gateway.purchase( + @amount, + @credit_card, + @options.merge( + order_id: response.authorization, + issuer_id: response.params['issuer_id'], + payment_indicator: 'U', + payment_information: '2' + ) + ) + assert_success response2 + assert_equal 'Approved', response2.message + assert_false response2.authorization.blank? + end + + def test_successful_purchase_with_network_tokenization + 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 + assert response = @gateway.purchase(@amount, @apple_pay_credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_successful_purchase_with_network_tokenization_apple_pay_source_with_nil_order_id + @options[:order_id] = nil + assert response = @gateway.purchase(@amount, @apple_pay_credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_successful_purchase_with_network_tokenization_google_pay_source + assert response = @gateway.purchase(@amount, @google_pay_credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_successful_purchase_with_network_tokenization_google_pay_source_with_nil_order_id + @options[:order_id] = nil + assert response = @gateway.purchase(@amount, @google_pay_credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_successful_authorize_with_network_tokenization + assert response = @gateway.authorize(@amount, @network_tokenization_credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_successful_authorize_with_network_tokenization_apple_pay_source + assert response = @gateway.authorize(@amount, @apple_pay_credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_successful_authorize_with_network_tokenization_apple_pay_source_with_nil_order_id + @options[:order_id] = nil + assert response = @gateway.authorize(@amount, @apple_pay_credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_successful_authorize_with_network_tokenization_google_pay_source + assert response = @gateway.authorize(@amount, @google_pay_credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_successful_authorize_with_network_tokenization_google_pay_source_with_nil_order_id + @options[:order_id] = nil + assert response = @gateway.authorize(@amount, @google_pay_credit_card, @options) assert_success response assert_equal 'Approved', response.message assert_false response.authorization.blank? @@ -51,7 +204,7 @@ def test_successful_authorization end def test_failed_authorization - response = @gateway.authorize(105, @credit_card, @options) + response = @gateway.authorize(@fail_amount, @credit_card, @options) assert_failure response end @@ -72,7 +225,7 @@ def test_successful_authorization_and_capture_and_void response = @gateway.capture(@amount, response.authorization) assert_success response - void = @gateway.void(response.authorization, :purchasecorrection => true) + void = @gateway.void(response.authorization, purchasecorrection: true) assert_success void end @@ -85,8 +238,75 @@ def test_successful_authorization_and_void assert_success void end + 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, + @options.merge( + three_d_secure: { + version: '2', + cavv: 'AAABBJg0VhI0VniQEjRWAAAAAAA=', + eci: '7', + 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? + end + + 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, + @options.merge( + three_d_secure: { + version: '2', + cavv: 'AAABBJg0VhI0VniQEjRWAAAAAAA=', + eci: @fully_authenticated_eci, + 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? + + response = @gateway.capture(@amount, response.authorization) + assert_success response + end + + 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, + @options.merge( + three_d_secure: { + version: '2', + cavv: 'AAABBJg0VhI0VniQEjRWAAAAAAA=', + eci: @no_liability_shift_eci, + three_ds_server_trans_id: 'e11d4985-8d25-40ed-99d6-c3803fe5e68f', + ds_transaction_id: '12345' + } + ) + ) + + assert_failure response + end + def test_successful_authorization_with_network_tokenization - @credit_card = network_tokenization_credit_card('4242424242424242', + @credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', verification_value: nil ) @@ -100,7 +320,7 @@ def test_successful_purchase_and_void purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - void = @gateway.void(purchase.authorization, :purchasecorrection => true) + void = @gateway.void(purchase.authorization, purchasecorrection: true) assert_success void end @@ -123,7 +343,7 @@ def test_successful_purchase_and_refund def test_failed_purchase_from_error assert response = @gateway.purchase(150, @credit_card, @options) assert_failure response - assert_equal 'Declined', response.message + assert_equal 'Card declined do not retry card declined do not retry', response.message end def test_successful_verify @@ -140,6 +360,45 @@ def test_successful_store @data_key = response.params['data_key'] end + def test_successful_store_with_duration + assert response = @gateway.store(@credit_card, duration: 600) + assert_success response + assert_equal 'Successfully registered cc details', response.message + assert response.params['data_key'].present? + end + + # AVS result fields are stored in the vault and returned as part of the + # XML response under (which isn't parsed by ActiveMerchant so + # we can't test for it). + # + # Actual AVS results aren't returned processed until an actual transaction is made + # so we make a second purchase request. + def test_successful_store_and_purchase_with_avs + gateway = MonerisGateway.new(fixtures(:moneris).merge(avs_enabled: true)) + + # card number triggers AVS match + @credit_card = credit_card('4761739012345637', verification_value: '012') + assert response = gateway.store(@credit_card, @options) + assert_success response + assert_equal 'Successfully registered cc details', response.message + assert response.params['data_key'].present? + data_key = response.params['data_key'] + + options_without_address = @options.dup + options_without_address.delete(:address) + assert response = gateway.purchase(@amount, data_key, options_without_address) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + + assert_equal(response.avs_result, { + 'code' => 'Y', + 'message' => 'Street address and 5-digit postal code match.', + 'street_match' => 'Y', + 'postal_match' => 'Y' + }) + end + def test_successful_unstore test_successful_store assert response = @gateway.unstore(@data_key) @@ -173,27 +432,27 @@ def test_successful_authorization_with_vault def test_failed_authorization_with_vault test_successful_store - response = @gateway.authorize(105, @data_key, @options) + response = @gateway.authorize(@fail_amount, @data_key, @options) assert_failure response end def test_cvv_match_when_not_enabled assert response = @gateway.purchase(1039, @credit_card, @options) assert_success response - assert_equal({'code' => nil, 'message' => nil}, response.cvv_result) + assert_equal({ 'code' => nil, 'message' => nil }, response.cvv_result) end def test_cvv_no_match_when_not_enabled assert response = @gateway.purchase(1053, @credit_card, @options) assert_success response - assert_equal({'code' => nil, 'message' => nil}, response.cvv_result) + assert_equal({ 'code' => nil, 'message' => nil }, response.cvv_result) end def test_cvv_match_when_enabled gateway = MonerisGateway.new(fixtures(:moneris).merge(cvv_enabled: true)) assert response = gateway.purchase(1039, @credit_card, @options) assert_success response - assert_equal({'code' => 'M', 'message' => 'CVV matches'}, response.cvv_result) + assert_equal({ 'code' => 'M', 'message' => 'CVV matches' }, response.cvv_result) end def test_avs_result_valid_when_enabled @@ -203,7 +462,7 @@ def test_avs_result_valid_when_enabled assert_success response assert_equal(response.avs_result, { 'code' => 'A', - 'message' => 'Street address matches, but 5-digit and 9-digit postal code do not match.', + 'message' => 'Street address matches, but postal code does not match.', 'street_match' => 'Y', 'postal_match' => 'N' }) @@ -233,14 +492,141 @@ def test_avs_result_nil_when_efraud_disabled }) end - def test_purchase_scrubbing + def test_purchase_using_stored_credential_recurring_cit + 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['issuer_id'] + assert_equal 'Approved', purchase.message + assert_false purchase.authorization.blank? + assert_not_empty purchase.params['issuer_id'] + + used_options = stored_credential_options(:recurring, :cardholder, id: network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + assert_equal 'Approved', purchase.message + assert_false purchase.authorization.blank? + assert_not_empty purchase.params['issuer_id'] + end + + def test_purchase_using_stored_credential_recurring_mit + initial_options = stored_credential_options(:merchant, :recurring, :initial) + assert purchase = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success purchase + assert network_transaction_id = purchase.params['issuer_id'] + assert_equal 'Approved', purchase.message + assert_false purchase.authorization.blank? + assert_not_empty purchase.params['issuer_id'] + + used_options = stored_credential_options(:merchant, :recurring, id: network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + assert_equal 'Approved', purchase.message + assert_false purchase.authorization.blank? + assert_not_empty purchase.params['issuer_id'] + 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) + assert_success purchase + assert network_transaction_id = purchase.params['issuer_id'] + assert_equal 'Approved', purchase.message + assert_false purchase.authorization.blank? + assert_not_empty purchase.params['issuer_id'] + + used_options = stored_credential_options(:installment, :cardholder, id: network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + assert_equal 'Approved', purchase.message + assert_false purchase.authorization.blank? + assert_not_empty purchase.params['issuer_id'] + end + + def test_purchase_using_stored_credential_installment_mit + initial_options = stored_credential_options(:merchant, :installment, :initial) + assert purchase = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success purchase + assert network_transaction_id = purchase.params['issuer_id'] + assert_equal 'Approved', purchase.message + assert_false purchase.authorization.blank? + assert_not_empty purchase.params['issuer_id'] + + used_options = stored_credential_options(:merchant, :installment, id: network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + assert_equal 'Approved', purchase.message + assert_false purchase.authorization.blank? + assert_not_empty purchase.params['issuer_id'] + end + + def test_purchase_using_stored_credential_unscheduled_cit + initial_options = stored_credential_options(:cardholder, :unscheduled, :initial) + assert purchase = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success purchase + assert network_transaction_id = purchase.params['issuer_id'] + assert_equal 'Approved', purchase.message + assert_false purchase.authorization.blank? + assert_not_empty purchase.params['issuer_id'] + + used_options = stored_credential_options(:unscheduled, :cardholder, id: network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + assert_equal 'Approved', purchase.message + assert_false purchase.authorization.blank? + assert_not_empty purchase.params['issuer_id'] + end + + def test_purchase_using_stored_credential_unscheduled_mit + initial_options = stored_credential_options(:merchant, :unscheduled, :initial) + assert purchase = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success purchase + assert network_transaction_id = purchase.params['issuer_id'] + assert_equal 'Approved', purchase.message + assert_false purchase.authorization.blank? + assert_not_empty purchase.params['issuer_id'] + + used_options = stored_credential_options(:merchant, :unscheduled, id: network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + assert_equal 'Approved', purchase.message + assert_false purchase.authorization.blank? + assert_not_empty purchase.params['issuer_id'] + end + + def test_authorize_and_capture_with_stored_credential + initial_options = stored_credential_options(:cardholder, :recurring, :initial) + assert authorization = @gateway.authorize(@amount, @credit_card, initial_options) + assert_success authorization + assert network_transaction_id = authorization.params['issuer_id'] + assert_equal 'Approved', authorization.message + assert_not_empty authorization.params['issuer_id'] + + assert capture = @gateway.capture(@amount, authorization.authorization) + assert_success capture + + used_options = stored_credential_options(:cardholder, :recurring, id: network_transaction_id) + assert authorization = @gateway.authorize(@amount, @credit_card, used_options) + assert_success authorization + assert @gateway.capture(@amount, authorization.authorization) + end + + def test_purchase_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(@credit_card.number, transcript) + assert_scrubbed(@credit_card.verification_value, transcript) assert_scrubbed(@gateway.options[:password], transcript) end + + private + + def stored_credential_options(*args, id: nil) + @options.merge(order_id: generate_unique_id, + stored_credential: stored_credential(*args, id:), + issuer_id: '') + end end diff --git a/test/remote/gateways/remote_moneris_us_test.rb b/test/remote/gateways/remote_moneris_us_test.rb deleted file mode 100644 index 75bbf7cf52d..00000000000 --- a/test/remote/gateways/remote_moneris_us_test.rb +++ /dev/null @@ -1,261 +0,0 @@ -require 'test_helper' - -class MonerisUsRemoteTest < Test::Unit::TestCase - def setup - Base.mode = :test - - @gateway = MonerisUsGateway.new(fixtures(:moneris_us)) - @amount = 100 - @credit_card = credit_card('4242424242424242') - @options = { - :order_id => generate_unique_id, - :billing_address => address, - :description => 'Store Purchase' - } - @check = check({ - routing_number: '011000015', - account_number: '1234455', - number: 123 - }) - end - - def test_successful_purchase - assert response = @gateway.purchase(@amount, @credit_card, @options) - assert_success response - assert_equal 'Approved', response.message - assert_false response.authorization.blank? - end - - def test_successful_echeck_purchase - response = @gateway.purchase(@amount, @check, @options) - assert_success response - assert response.test? - assert_equal 'Registered', response.message - assert response.authorization - end - - def test_failed_echeck_purchase - response = @gateway.purchase(105, check(routing_number: 5), @options) - assert_failure response - assert response.test? - assert_equal 'Unspecified error', response.message - end - - def test_successful_authorization - response = @gateway.authorize(@amount, @credit_card, @options) - assert_success response - assert_false response.authorization.blank? - end - - def test_successful_verify - response = @gateway.verify(@credit_card, @options) - assert_success response - assert_equal 'Approved', response.message - assert response.authorization - end - - def test_failed_verify - response = @gateway.verify(credit_card(nil), @options) - assert_failure response - assert_match %r{Invalid pan parameter}, response.message - end - - def test_failed_authorization - response = @gateway.authorize(105, @credit_card, @options) - assert_failure response - end - - def test_successful_authorization_and_capture - response = @gateway.authorize(@amount, @credit_card, @options) - assert_success response - assert response.authorization - - response = @gateway.capture(@amount, response.authorization) - assert_success response - end - - def test_successful_authorization_and_void - response = @gateway.authorize(@amount, @credit_card, @options) - assert_success response - assert response.authorization - - # Moneris cannot void a preauthorization - # You must capture the auth transaction with an amount of $0.00 - void = @gateway.capture(0, response.authorization) - assert_success void - end - - def test_successful_purchase_and_void - purchase = @gateway.purchase(@amount, @credit_card, @options) - assert_success purchase - - void = @gateway.void(purchase.authorization) - assert_success void - end - - def test_failed_purchase_and_void - purchase = @gateway.purchase(101, @credit_card, @options) - assert_failure purchase - - void = @gateway.void(purchase.authorization) - assert_failure void - end - - def test_successful_purchase_and_refund - purchase = @gateway.purchase(@amount, @credit_card, @options) - assert_success purchase - - refund = @gateway.refund(@amount, purchase.authorization) - assert_success refund - end - - def test_failed_purchase_from_error - assert response = @gateway.purchase(150, @credit_card, @options) - assert_failure response - assert_equal 'Declined', response.message - end - - def test_successful_store - assert response = @gateway.store(@credit_card) - assert_success response - assert_equal 'Successfully registered cc details', response.message - assert response.params['data_key'].present? - @data_key = response.params['data_key'] - end - - def test_successful_echeck_store - assert response = @gateway.store(@check) - assert_success response - assert_equal 'Successfully registered ach details', response.message - assert response.params['data_key'].present? - @data_key_echeck = response.params['data_key'] - end - - def test_successful_unstore - test_successful_store - assert response = @gateway.unstore(@data_key) - assert_success response - assert_equal 'Successfully deleted cc details', response.message - assert response.params['data_key'].present? - end - - def test_successful_echeck_unstore - test_successful_echeck_store - assert response = @gateway.unstore(@data_key_echeck) - assert_success response - assert_equal 'Successfully deleted ach details', response.message - assert response.params['data_key'].present? - end - - def test_update - test_successful_store - assert response = @gateway.update(@data_key, @credit_card) - assert_success response - assert_equal 'Successfully updated cc details', response.message - assert response.params['data_key'].present? - end - - def test_echeck_update - test_successful_echeck_store - assert response = @gateway.update(@data_key_echeck, @check) - assert_success response - assert_equal 'Successfully updated ach details', response.message - assert response.params['data_key'].present? - end - - def test_successful_purchase_with_vault - test_successful_store - assert response = @gateway.purchase(@amount, @data_key, @options) - assert_success response - assert_equal 'Approved', response.message - assert_false response.authorization.blank? - end - - def test_successful_authorization_with_vault - test_successful_store - assert response = @gateway.authorize(@amount, @data_key, @options) - assert_success response - assert_false response.authorization.blank? - end - - def test_failed_authorization_with_vault - test_successful_store - response = @gateway.authorize(105, @data_key, @options) - assert_failure response - end - - def test_cvv_match_when_not_enabled - assert response = @gateway.purchase(1039, @credit_card, @options) - assert_success response - assert_equal({'code' => nil, 'message' => nil}, response.cvv_result) - end - - def test_cvv_no_match_when_not_enabled - assert response = @gateway.purchase(1053, @credit_card, @options) - assert_success response - assert_equal({'code' => nil, 'message' => nil}, response.cvv_result) - end - - def test_cvv_match_when_enabled - gateway = MonerisGateway.new(fixtures(:moneris).merge(cvv_enabled: true)) - assert response = gateway.purchase(1039, @credit_card, @options) - assert_success response - assert_equal({'code' => 'M', 'message' => 'CVV matches'}, response.cvv_result) - end - - def test_cvv_no_match_when_enabled - gateway = MonerisGateway.new(fixtures(:moneris).merge(cvv_enabled: true)) - assert response = gateway.purchase(1053, @credit_card, @options) - assert_success response - assert_equal({'code' => 'N', 'message' => 'CVV does not match'}, response.cvv_result) - end - - def test_avs_result_valid_when_enabled - gateway = MonerisGateway.new(fixtures(:moneris).merge(avs_enabled: true)) - - assert response = gateway.purchase(1010, @credit_card, @options) - assert_success response - assert_equal(response.avs_result, { - 'code' => 'A', - 'message' => 'Street address matches, but 5-digit and 9-digit postal code do not match.', - 'street_match' => 'Y', - 'postal_match' => 'N' - }) - end - - def test_avs_result_nil_when_address_absent - gateway = MonerisGateway.new(fixtures(:moneris).merge(avs_enabled: true)) - - assert response = gateway.purchase(1010, @credit_card, @options.tap { |x| x.delete(:billing_address) }) - assert_success response - assert_equal(response.avs_result, { - 'code' => nil, - 'message' => nil, - 'street_match' => nil, - 'postal_match' => nil - }) - end - - def test_avs_result_nil_when_efraud_disabled - assert response = @gateway.purchase(@amount, @credit_card, @options) - assert_success response - assert_equal(response.avs_result, { - 'code' => nil, - 'message' => nil, - 'street_match' => nil, - 'postal_match' => nil - }) - 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[:password], transcript) - end - -end diff --git a/test/remote/gateways/remote_money_movers_test.rb b/test/remote/gateways/remote_money_movers_test.rb index 64165cb50d8..ddbc976fdb0 100644 --- a/test/remote/gateways/remote_money_movers_test.rb +++ b/test/remote/gateways/remote_money_movers_test.rb @@ -10,9 +10,9 @@ def setup @credit_card = credit_card('4111111111111111') @options = { - :order_id => generate_unique_id, - :billing_address => address, - :description => 'Active Merchant Remote Test Purchase' + order_id: generate_unique_id, + billing_address: address, + description: 'Active Merchant Remote Test Purchase' } end @@ -72,9 +72,9 @@ def test_authorize_and_capture def test_invalid_login gateway = MoneyMoversGateway.new( - :login => '', - :password => '' - ) + login: '', + password: '' + ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal 'Error in transaction data or system error', response.message diff --git a/test/remote/gateways/remote_mundipagg_test.rb b/test/remote/gateways/remote_mundipagg_test.rb index 417cc547102..a3b5606fef1 100644 --- a/test/remote/gateways/remote_mundipagg_test.rb +++ b/test/remote/gateways/remote_mundipagg_test.rb @@ -5,26 +5,68 @@ def setup @gateway = MundipaggGateway.new(fixtures(:mundipagg)) @amount = 100 - @credit_card = credit_card('4000100011112224') + @credit_card = credit_card('4000000000000010') @declined_card = credit_card('4000300011112220') - @voucher = credit_card('60607044957644', brand: 'sodexo') + @sodexo_voucher = credit_card('6060704495764400', brand: 'sodexo') + + # Mundipagg only allows certain card numbers for success and failure scenarios. + # As such, we cannot use card numbers with BINs belonging to VR or Alelo. + # See https://docs.mundipagg.com/docs/simulador-de-voucher. + @vr_voucher = credit_card('4000000000000010', brand: 'vr') + @alelo_voucher = credit_card('4000000000000010', brand: 'alelo') + @declined_alelo_voucher = credit_card('4000000000000028', brand: 'alelo') + @options = { - billing_address: address(options = { neighborhood: 'Sesame Street' }), + gateway_affiliation_id: fixtures(:mundipagg)[:gateway_affiliation_id], + billing_address: address({ neighborhood: 'Sesame Street' }), description: 'Store Purchase' } + + @authorization_secret_options = { authorization_secret_key: fixtures(:mundipagg)[:api_key] } + + @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' + }, + address: { + street: 'Malibu Point', + number: '10880', + complement: 'A', + neighborhood: 'Central Malibu', + city: 'Malibu', + state: 'CA', + country: 'US', + zip_code: '24210-460' + } + } + } + + @excess_length_neighborhood = address({ neighborhood: 'Super Long Neighborhood Name' * 5 }) + @neighborhood_length_error = 'Invalid parameters; The request is invalid. | The field neighborhood must be a string with a maximum length of 64.' end def test_successful_purchase - response = @gateway.purchase(@amount, @credit_card, @options) - assert_success response - assert_equal 'Simulator|Transação de simulação autorizada com sucesso', response.message + test_successful_purchase_with(@credit_card) + end + + def test_successful_purchase_with_alelo_card + test_successful_purchase_with(@alelo_voucher) end def test_successful_purchase_no_address @options.delete(:billing_address) response = @gateway.purchase(@amount, @credit_card, @options) assert_success response - assert_equal 'Simulator|Transação de simulação autorizada com sucesso', response.message + assert_equal 'Transação capturada com sucesso', response.message end def test_successful_purchase_with_more_options @@ -39,40 +81,82 @@ def test_successful_purchase_with_more_options assert_success response end - def test_successful_purchase_with_voucher + def test_successful_purchase_with_sodexo_voucher @options.update(holder_document: '93095135270') - response = @gateway.purchase(@amount, @voucher, @options) + response = @gateway.purchase(@amount, @sodexo_voucher, @options) + assert_success response + assert_equal 'Transação capturada com sucesso', response.message + end + + def test_successful_purchase_with_vr_voucher + @options.update(holder_document: '93095135270') + response = @gateway.purchase(@amount, @vr_voucher, @options) + assert_success response + assert_equal 'Transação capturada com sucesso', response.message + end + + def test_successful_purchase_with_submerchant + options = @options.update(@submerchant_options) + response = @gateway.purchase(@amount, @credit_card, options) assert_success response - assert_equal 'Simulator|Transação de simulação autorizada com sucesso', response.message + assert_equal 'Transação capturada com sucesso', response.message end def test_failed_purchase - response = @gateway.purchase(105200, @declined_card, @options) + test_failed_purchase_with(@declined_card) + end + + def test_failed_purchase_with_top_level_errors + @options[:billing_address] = @excess_length_neighborhood + + response = @gateway.purchase(105200, @credit_card, @options) + assert_failure response - assert_equal 'Simulator|Transação de simulada negada por falta de crédito, utilizado para realizar simulação de autorização parcial.', response.message + assert_equal @neighborhood_length_error, response.message + end + + def test_failed_purchase_with_alelo_card + test_failed_purchase_with(@declined_alelo_voucher) end def test_successful_authorize_and_capture - auth = @gateway.authorize(@amount, @credit_card, @options) - assert_success auth + test_successful_authorize_and_capture_with(@credit_card) + end - assert capture = @gateway.capture(@amount, auth.authorization) - assert_success capture - assert_equal 'Simulator|Transação de simulação capturada com sucesso', capture.message + def test_successful_authorize_and_capture_with_alelo_card + test_successful_authorize_and_capture_with(@alelo_voucher) + end + + def test_successful_authorize_with_submerchant + options = @options.update(@submerchant_options) + response = @gateway.authorize(@amount, @credit_card, options) + assert_success response + assert_equal 'Transação authorizada com sucesso', response.message end def test_failed_authorize - response = @gateway.authorize(105200, @declined_card, @options) + test_failed_authorize_with(@declined_card) + end + + def test_failed_authorize_with_alelo_card + test_failed_authorize_with(@declined_alelo_voucher) + end + + def test_failed_authorize_with_top_level_errors + @options[:billing_address] = @excess_length_neighborhood + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response - assert_equal 'Simulator|Transação de simulada negada por falta de crédito, utilizado para realizar simulação de autorização parcial.', response.message + assert_equal @neighborhood_length_error, response.message end def test_partial_capture - auth = @gateway.authorize(@amount, @credit_card, @options) - assert_success auth + test_partial_capture_with(@credit_card) + end - assert capture = @gateway.capture(@amount-1, auth.authorization) - assert_success capture + def test_partial_capture_with_alelo_card + test_partial_capture_with(@alelo_voucher) end def test_failed_capture @@ -82,19 +166,19 @@ def test_failed_capture end def test_successful_refund - purchase = @gateway.purchase(@amount, @credit_card, @options) - assert_success purchase + test_successful_refund_with(@credit_card) + end - assert refund = @gateway.refund(@amount, purchase.authorization) - assert_success refund + def test_successful_refund_with_alelo_card + test_successful_refund_with(@alelo_voucher) end def test_partial_refund - purchase = @gateway.purchase(@amount, @credit_card, @options) - assert_success purchase + test_partial_refund_with(@credit_card) + end - assert refund = @gateway.refund(@amount - 1, purchase.authorization) - assert_success refund + def test_partial_refund_with_alelo_card + test_partial_refund_with(@alelo_voucher) end def test_failed_refund @@ -104,25 +188,43 @@ def test_failed_refund end def test_successful_void - auth = @gateway.authorize(@amount, @credit_card, @options) + test_successful_void_with(@credit_card) + end + + def test_successful_void_with_alelo_card + test_successful_void_with(@alelo_voucher) + end + + def test_successful_void_with_sodexo_voucher + @options.update(holder_document: '93095135270') + auth = @gateway.purchase(@amount, @sodexo_voucher, @options) assert_success auth assert void = @gateway.void(auth.authorization) assert_success void end - def test_successful_void_with_voucher + def test_successful_void_with_vr_voucher @options.update(holder_document: '93095135270') - auth = @gateway.purchase(@amount, @voucher, @options) + auth = @gateway.purchase(@amount, @vr_voucher, @options) assert_success auth assert void = @gateway.void(auth.authorization) assert_success void end - def test_successful_refund_with_voucher + def test_successful_refund_with_sodexo_voucher + @options.update(holder_document: '93095135270') + auth = @gateway.purchase(@amount, @sodexo_voucher, @options) + assert_success auth + + assert void = @gateway.refund(1, auth.authorization) + assert_success void + end + + def test_successful_refund_with_vr_voucher @options.update(holder_document: '93095135270') - auth = @gateway.purchase(@amount, @voucher, @options) + auth = @gateway.purchase(@amount, @vr_voucher, @options) assert_success auth assert void = @gateway.refund(1, auth.authorization) @@ -136,18 +238,40 @@ def test_failed_void end def test_successful_verify - response = @gateway.verify(@credit_card, @options) - assert_success response - assert_match %r{Simulator|Transação de simulação autorizada com sucesso}, response.message + test_successful_verify_with(@credit_card) + end + + def test_successful_verify_with_alelo_card + test_successful_verify_with(@alelo_voucher) end def test_successful_store_and_purchase - store = @gateway.store(@credit_card, @options) - assert_success store + test_successful_store_and_purchase_with(@credit_card) + end - assert purchase = @gateway.purchase(@amount, store.authorization, @options) - assert_success purchase - assert_equal 'Simulator|Transação de simulação autorizada com sucesso', purchase.message + def test_successful_store_and_purchase_with_alelo_card + test_successful_store_and_purchase_with(@alelo_voucher) + end + + def test_invalid_login_with_bad_api_key_overwrite + response = @gateway.purchase(@amount, @credit_card, @options.merge({ authorization_secret_key: 'bad_key' })) + assert_failure response + assert_match %r{Invalid API key; Authorization has been denied for this request.}, response.message + end + + def test_successful_purchase_with_api_key_overwrite + response = @gateway.purchase(@amount, @credit_card, @options.merge(@authorization_secret_options)) + assert_success response + assert_equal 'Transação capturada com sucesso', response.message + end + + def test_failed_store_with_top_level_errors + @options[:billing_address] = @excess_length_neighborhood + + response = @gateway.store(@credit_card, @options) + + assert_failure response + assert_equal @neighborhood_length_error, response.message end def test_invalid_login @@ -158,6 +282,16 @@ def test_invalid_login assert_match %r{Invalid API key; Authorization has been denied for this request.}, response.message end + def test_gateway_id_fallback + gateway = MundipaggGateway.new(api_key: fixtures(:mundipagg)[:api_key], gateway_id: fixtures(:mundipagg)[:gateway_id]) + options = { + billing_address: address({ neighborhood: 'Sesame Street' }), + description: 'Store Purchase' + } + response = gateway.purchase(@amount, @credit_card, options) + assert_success response + end + def test_transcript_scrubbing transcript = capture_transcript(@gateway) do @gateway.purchase(@amount, @credit_card, @options) @@ -168,4 +302,80 @@ def test_transcript_scrubbing assert_scrubbed(@credit_card.verification_value, transcript) assert_scrubbed(@gateway.options[:api_key], transcript) end + + private + + def test_successful_purchase_with(card) + response = @gateway.purchase(@amount, card, @options) + assert_success response + assert_equal 'Transação capturada com sucesso', response.message + end + + def test_failed_purchase_with(card) + response = @gateway.purchase(105200, card, @options) + assert_failure response + assert_equal 'Transação não autorizada', response.message + end + + def test_successful_authorize_and_capture_with(card) + auth = @gateway.authorize(@amount, card, @options) + assert_success auth + + assert capture = @gateway.capture(@amount, auth.authorization) + assert_success capture + assert_equal 'Transação capturada com sucesso', capture.message + end + + def test_failed_authorize_with(card) + response = @gateway.authorize(105200, card, @options) + assert_failure response + assert_equal 'Transação não autorizada', response.message + end + + def test_partial_capture_with(card) + auth = @gateway.authorize(@amount, card, @options) + assert_success auth + + assert capture = @gateway.capture(@amount - 1, auth.authorization) + assert_success capture + end + + def test_successful_refund_with(card) + purchase = @gateway.purchase(@amount, card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization) + assert_success refund + end + + def test_partial_refund_with(card) + purchase = @gateway.purchase(@amount, card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount - 1, purchase.authorization) + assert_success refund + end + + def test_successful_void_with(card) + auth = @gateway.authorize(@amount, card, @options) + assert_success auth + + assert void = @gateway.void(auth.authorization) + assert_success void + end + + def test_successful_verify_with(card) + response = @gateway.verify(card, @options) + assert_success response + assert_match %r{Transação authorizada com sucesso}, response.message + end + + def test_successful_store_and_purchase_with(card) + store = @gateway.store(card, @options) + assert_success store + + assert purchase = @gateway.purchase(@amount, store.authorization, @options) + assert_success purchase + assert_equal 'Transação capturada com sucesso', purchase.message + end end diff --git a/test/remote/gateways/remote_nab_transact_test.rb b/test/remote/gateways/remote_nab_transact_test.rb index 71cfe58cdb2..19052fe001c 100644 --- a/test/remote/gateways/remote_nab_transact_test.rb +++ b/test/remote/gateways/remote_nab_transact_test.rb @@ -1,7 +1,6 @@ require 'test_helper' class RemoteNabTransactTest < Test::Unit::TestCase - def setup @gateway = NabTransactGateway.new(fixtures(:nab_transact)) @privileged_gateway = NabTransactGateway.new(fixtures(:nab_transact_privileged)) @@ -12,9 +11,9 @@ def setup @declined_card = credit_card('4111111111111234') @options = { - :order_id => '1', - :billing_address => address, - :description => 'NAB Transact Purchase' + order_id: '1', + billing_address: address, + description: 'NAB Transact Purchase' } end @@ -31,16 +30,16 @@ def test_successful_purchase end def test_unsuccessful_purchase_insufficient_funds - #Any total not ending in 00/08/11/16 - failing_amount = 151 #Specifically tests 'Insufficient Funds' + # Any total not ending in 00/08/11/16 + failing_amount = 151 # Specifically tests 'Insufficient Funds' assert response = @gateway.purchase(failing_amount, @credit_card, @options) assert_failure response assert_equal 'Insufficient Funds', response.message end def test_unsuccessful_purchase_do_not_honour - #Any total not ending in 00/08/11/16 - failing_amount = 105 #Specifically tests 'do not honour' + # Any total not ending in 00/08/11/16 + failing_amount = 105 # Specifically tests 'do not honour' assert response = @gateway.purchase(failing_amount, @credit_card, @options) assert_failure response assert_equal 'Do Not Honour', response.message @@ -64,11 +63,11 @@ def test_unsuccessful_purchase_bad_credit_card # ensure we get the error. def test_successful_purchase_with_card_acceptor card_acceptor_options = { - :merchant_name => 'ActiveMerchant', - :merchant_location => 'Melbourne' + merchant_name: 'ActiveMerchant', + merchant_location: 'Melbourne' } card_acceptor_options.each do |key, value| - options = @options.merge({key => value}) + options = @options.merge({ key => value }) assert response = @gateway.purchase(@amount, @credit_card, options) assert_failure response assert_equal 'Permission denied', response.message @@ -122,18 +121,18 @@ def test_unsuccessful_capture_amount_greater_than_authorized authorization = auth.authorization - assert capture = @gateway.capture(@amount+100, authorization) + assert capture = @gateway.capture(@amount + 100, authorization) assert_failure capture assert_equal 'Preauth was done for smaller amount', capture.message end def test_authorize_and_capture_with_card_acceptor card_acceptor_options = { - :merchant_name => 'ActiveMerchant', - :merchant_location => 'Melbourne' + merchant_name: 'ActiveMerchant', + merchant_location: 'Melbourne' } card_acceptor_options.each do |key, value| - options = @options.merge({key => value}) + options = @options.merge({ key => value }) assert response = @gateway.authorize(@amount, @credit_card, options) assert_failure response assert_equal 'Permission denied', response.message @@ -162,11 +161,11 @@ def test_successful_refund # You need to speak to NAB Transact to have this feature enabled on # your account otherwise you will receive a "Permission denied" error def test_credit - assert response = @gateway.credit(@amount, @credit_card, {:order_id => '1'}) + assert response = @gateway.credit(@amount, @credit_card, { order_id: '1' }) assert_failure response assert_equal 'Permission denied', response.message - assert response = @privileged_gateway.credit(@amount, @credit_card, {:order_id => '1'}) + assert response = @privileged_gateway.credit(@amount, @credit_card, { order_id: '1' }) assert_success response assert_equal 'Approved', response.message end @@ -175,16 +174,16 @@ def test_failed_refund assert response = @gateway.purchase(@amount, @credit_card, @options) assert_success response authorization = response.authorization - assert response = @gateway.refund(@amount+1, authorization) + assert response = @gateway.refund(@amount + 1, authorization) assert_failure response assert_equal 'Only 2.00 AUD available for refund', response.message end def test_invalid_login gateway = NabTransactGateway.new( - :login => 'ABCFAKE', - :password => 'changeit' - ) + login: 'ABCFAKE', + password: 'changeit' + ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal 'Invalid merchant ID', response.message @@ -205,11 +204,11 @@ def test_unsuccessful_store def test_duplicate_store @gateway.unstore(1236) - assert response = @gateway.store(@credit_card, {:billing_id => 1236}) + assert response = @gateway.store(@credit_card, { billing_id: 1236 }) assert_success response assert_equal 'Successful', response.message - assert response = @gateway.store(@credit_card, {:billing_id => 1236}) + assert response = @gateway.store(@credit_card, { billing_id: 1236 }) assert_failure response assert_equal 'Duplicate CRN Found', response.message end @@ -240,13 +239,13 @@ def test_failure_trigger_purchase trigger_amount = 0 @gateway.unstore(gateway_id) - assert response = @gateway.store(@credit_card, {:billing_id => gateway_id, :amount => 150}) + assert response = @gateway.store(@credit_card, { billing_id: gateway_id, amount: 150 }) assert_success response assert_equal 'Successful', response.message purchase_response = @gateway.purchase(trigger_amount, gateway_id) - assert gateway_id = purchase_response.params['crn'] + assert purchase_response.params['crn'] assert_failure purchase_response assert_equal 'Invalid Amount', purchase_response.message end @@ -260,5 +259,4 @@ def test_transcript_scrubbing assert_scrubbed(@credit_card.number, clean_transcript) assert_scrubbed(@credit_card.verification_value.to_s, clean_transcript) end - end diff --git a/test/remote/gateways/remote_ncr_secure_pay_test.rb b/test/remote/gateways/remote_ncr_secure_pay_test.rb index 239063f06e8..ddd21d8cb85 100644 --- a/test/remote/gateways/remote_ncr_secure_pay_test.rb +++ b/test/remote/gateways/remote_ncr_secure_pay_test.rb @@ -44,7 +44,7 @@ def test_partial_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture end @@ -118,5 +118,4 @@ def test_transcript_scrubbing assert_scrubbed(@credit_card.verification_value, transcript) assert_scrubbed(@gateway.options[:password], transcript) end - end diff --git a/test/remote/gateways/remote_net_registry_test.rb b/test/remote/gateways/remote_net_registry_test.rb index c97ddc7dd91..dfbb80c9ac1 100644 --- a/test/remote/gateways/remote_net_registry_test.rb +++ b/test/remote/gateways/remote_net_registry_test.rb @@ -10,15 +10,14 @@ # All purchases made in these tests are $1, so hopefully you won't be # sent broke if you forget... class NetRegistryTest < Test::Unit::TestCase - def setup @gateway = NetRegistryGateway.new(fixtures(:net_registry)) @amount = 100 @valid_creditcard = credit_card @invalid_creditcard = credit_card('41111111111111111') - @expired_creditcard = credit_card('4111111111111111', :year => '2000') - @invalid_month_creditcard = credit_card('4111111111111111', :month => '13') + @expired_creditcard = credit_card('4111111111111111', year: '2000') + @invalid_month_creditcard = credit_card('4111111111111111', month: '13') end def test_successful_purchase_and_credit @@ -57,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 @@ -88,9 +85,9 @@ def test_purchase_with_invalid_month def test_bad_login gateway = NetRegistryGateway.new( - :login => 'bad-login', - :password => 'bad-login' - ) + login: 'bad-login', + password: 'bad-login' + ) response = gateway.purchase(@amount, @valid_creditcard) assert_equal 'failed', response.params['status'] assert_failure response diff --git a/test/remote/gateways/remote_netaxept_test.rb b/test/remote/gateways/remote_netaxept_test.rb index 5219b4144bc..b4dd8771771 100644 --- a/test/remote/gateways/remote_netaxept_test.rb +++ b/test/remote/gateways/remote_netaxept_test.rb @@ -9,7 +9,7 @@ def setup @declined_card = credit_card('4925000000000087') @options = { - :order_id => generate_unique_id + order_id: generate_unique_id } end @@ -20,24 +20,24 @@ def test_successful_purchase end def test_failed_purchase - assert response = @gateway.purchase(@amount, @declined_card, @options) - assert_failure response - assert_match(/failure/i, response.message) + assert response = @gateway.purchase(@amount, @declined_card, @options) + assert_failure response + assert_match(/failure/i, response.message) end def test_authorize_and_capture - assert auth = @gateway.authorize(@amount, @credit_card, @options) - assert_success auth - assert_equal 'OK', auth.message - assert auth.authorization - assert capture = @gateway.capture(@amount, auth.authorization) - assert_success capture + assert auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + assert_equal 'OK', auth.message + assert auth.authorization + assert capture = @gateway.capture(@amount, auth.authorization) + assert_success capture end def test_failed_capture - assert response = @gateway.capture(@amount, '') - assert_failure response - assert_equal 'Unable to find transaction', response.message + assert response = @gateway.capture(@amount, '') + assert_failure response + assert_equal 'Unable to find transaction', response.message end def test_successful_refund @@ -49,12 +49,12 @@ def test_successful_refund end def test_failed_refund - assert response = @gateway.purchase(@amount, @credit_card, @options) - assert_success response + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response - response = @gateway.refund(@amount+100, response.authorization) - assert_failure response - assert_equal 'Unable to credit more than captured amount', response.message + response = @gateway.refund(@amount + 100, response.authorization) + assert_failure response + assert_equal 'Unable to credit more than captured amount', response.message end def test_successful_void @@ -66,60 +66,46 @@ def test_successful_void end def test_failed_void - assert response = @gateway.purchase(@amount, @credit_card, @options) - assert_success response + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + + response = @gateway.void(response.authorization) + assert_failure response + assert_equal 'Unable to annul, wrong state', response.message + end - response = @gateway.void(response.authorization) - assert_failure response - assert_equal 'Unable to annul, wrong state', response.message + def test_error_in_transaction_setup + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(currency: 'BOGG')) + assert_failure response + assert_match(/currency code/, response.message) end def test_successful_amex_purchase - credit_card = credit_card('378282246310005', :brand => 'american_express') + credit_card = credit_card('378282246310005', brand: 'american_express') assert response = @gateway.purchase(@amount, credit_card, @options) assert_success response assert_equal 'OK', response.message end def test_successful_master_purchase - credit_card = credit_card('5413000000000000', :brand => 'master') + credit_card = credit_card('5413000000000000', brand: 'master') assert response = @gateway.purchase(@amount, credit_card, @options) assert_success response assert_equal 'OK', response.message end - def test_error_in_transaction_setup - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(:currency => 'BOGG')) - assert_failure response - assert_match(/currency code/, response.message) - end - - def test_successful_amex_purchase - credit_card = credit_card('378282246310005', :brand => 'american_express') - assert response = @gateway.purchase(@amount, credit_card, @options) - assert_success response - assert_equal 'OK', response.message - end - - def test_successful_master_purchase - credit_card = credit_card('5413000000000000', :brand => 'master') - assert response = @gateway.purchase(@amount, credit_card, @options) - assert_success response - assert_equal 'OK', response.message - end - def test_error_in_payment_details - assert response = @gateway.purchase(@amount, credit_card(''), @options) - assert_failure response - assert_equal 'Cardnumber:Required', response.message + assert response = @gateway.purchase(@amount, credit_card(''), @options) + assert_failure response + assert_equal 'Cardnumber:Required', response.message end def test_amount_is_not_required_again_when_capturing_authorization - assert response = @gateway.authorize(@amount, @credit_card, @options) - assert_success response + assert response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response - assert response = @gateway.capture(nil, response.authorization) - assert_equal 'OK', response.message + assert response = @gateway.capture(nil, response.authorization) + assert_equal 'OK', response.message end def test_query_fails @@ -129,12 +115,12 @@ def test_query_fails end def test_invalid_login - gateway = NetaxeptGateway.new( - :login => '', - :password => '' - ) - assert response = gateway.purchase(@amount, @credit_card, @options) - assert_failure response - assert_match(/Unable to authenticate merchant/, response.message) + gateway = NetaxeptGateway.new( + login: '', + password: '' + ) + assert response = gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_match(/Unable to authenticate merchant/, response.message) end end diff --git a/test/remote/gateways/remote_netbanx_test.rb b/test/remote/gateways/remote_netbanx_test.rb index 19b4faddd5c..2024df2ee5e 100644 --- a/test/remote/gateways/remote_netbanx_test.rb +++ b/test/remote/gateways/remote_netbanx_test.rb @@ -5,12 +5,24 @@ def setup @gateway = NetbanxGateway.new(fixtures(:netbanx)) @amount = 100 @credit_card = credit_card('4530910000012345') + @credit_card_no_match_cvv = credit_card('4530910000012345', { verification_value: 666 }) @declined_amount = 11 @options = { billing_address: address, description: 'Store Purchase', currency: 'CAD' } + + @options_3ds2 = @options.merge( + three_d_secure: { + version: '2.1.0', + eci: '05', + cavv: 'AAABCIEjYgAAAAAAlCNiENiWiV+=', + ds_transaction_id: 'a3a721f3-b6fa-4cb5-84ea-c7b5c39890a2', + xid: 'OU9rcTRCY1VJTFlDWTFESXFtTHU=', + directory_response_status: 'Y' + } + ) end def test_successful_purchase @@ -18,6 +30,24 @@ def test_successful_purchase assert_success response assert_equal 'OK', response.message assert_equal response.authorization, response.params['id'] + assert_equal 'MATCH', response.params['cvvVerification'] + assert_equal 'MATCH', response.params['avsResponse'] + end + + def test_successful_purchase_avs_no_match_cvv + response = @gateway.purchase(@amount, @credit_card_no_match_cvv, @options) + assert_success response + assert_equal 'X', response.avs_result['code'] + assert_equal 'N', response.cvv_result['code'] + end + + def split_names(full_name) + names = (full_name || '').split + return [nil, nil] if names.size == 0 + + last_name = names.pop + first_name = names.join(' ') + [first_name, last_name] end def test_successful_purchase_with_more_options @@ -28,9 +58,22 @@ def test_successful_purchase_with_more_options email: 'joe@example.com' } + first_name, last_name = split_names(address[:name]) + response = @gateway.purchase(@amount, @credit_card, options) assert_equal 'OK', response.message assert_equal response.authorization, response.params['id'] + assert_equal first_name, response.params['profile']['firstName'] + assert_equal last_name, response.params['profile']['lastName'] + assert_equal options[:email], response.params['profile']['email'] + assert_equal options[:ip], response.params['customerIp'] + end + + def test_successful_purchase_with_3ds2_auth + assert response = @gateway.purchase(@amount, @credit_card, @options_3ds2) + assert_success response + assert_equal 'OK', response.message + assert_equal response.authorization, response.params['id'] end def test_failed_purchase @@ -39,6 +82,15 @@ def test_failed_purchase assert_equal 'The card has been declined due to insufficient funds.', response.message end + def test_failed_verify_before_purchase + options = { + verification_value: '' + } + response = @gateway.purchase(@amount, @credit_card, options) + assert_failure response + assert_equal 'The zip/postal code must be provided for an AVS check request.', response.message + end + def test_successful_authorize auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth @@ -63,7 +115,7 @@ def test_partial_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization, @options) + assert capture = @gateway.capture(@amount - 1, auth.authorization, @options) assert_success capture end @@ -73,6 +125,15 @@ def test_failed_capture assert_equal 'The authorization ID included in this settlement request could not be found.', response.message end + def test_successful_authorize_and_capture_with_3ds2_auth + auth = @gateway.authorize(@amount, @credit_card, @options_3ds2) + assert_success auth + + assert capture = @gateway.capture(@amount, auth.authorization, @options_3ds2) + assert_success capture + assert_equal 'OK', capture.message + end + # def test_successful_refund # # Unfortunately when testing a refund, you need to wait until the transaction # # if batch settled by the test system, this can take up to 2h. @@ -122,7 +183,9 @@ def test_failed_capture # assert_equal 'OK', refund.message # end - def test_failed_refund + # Changed test_failed_refund to test_cancelled_refund + # Because We added the checking status. If the transactions that are pending, API call needs to be Cancellation + def test_cancelled_refund # Read comment in `test_successful_refund` method. auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth @@ -130,11 +193,23 @@ def test_failed_refund assert capture = @gateway.capture(@amount, auth.authorization, @options) assert_success capture - # the following shall fail if you run it immediately after the capture - # as noted in the comment from `test_successful_refund` - assert refund = @gateway.refund(@amount, capture.authorization) - assert_failure refund - assert_equal 'The settlement you are attempting to refund has not been batched yet. There are no settled funds available to refund.', refund.message + # The settlement you are attempting to refund has not been batched yet. There are no settled funds available to refund. + # So the following refund shall be cancelled if you run it immediately after the capture + assert cancelled_response = @gateway.refund(@amount, capture.authorization) + assert_success cancelled_response + assert_equal 'CANCELLED', cancelled_response.params['status'] + end + + def test_reject_partial_refund_on_pending_status + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert capture = @gateway.capture(@amount, auth.authorization, @options) + assert_success capture + + assert rejected_response = @gateway.refund(90, capture.authorization) + assert_failure rejected_response + assert_equal 'Transaction not settled. Either do a full refund or try partial refund after settlement.', rejected_response.message end def test_successful_void @@ -172,7 +247,7 @@ def test_transcript_scrubbing def test_successful_store merchant_customer_id = SecureRandom.hex - assert response = @gateway.store(@credit_card, locale: 'en_GB', merchant_customer_id: merchant_customer_id, email: 'email@example.com') + assert response = @gateway.store(@credit_card, locale: 'en_GB', merchant_customer_id:, email: 'email@example.com') assert_success response assert_equal merchant_customer_id, response.params['merchantCustomerId'] first_card = response.params['cards'].first @@ -181,7 +256,7 @@ def test_successful_store def test_successful_unstore merchant_customer_id = SecureRandom.hex - assert response = @gateway.store(@credit_card, locale: 'en_GB', merchant_customer_id: merchant_customer_id, email: 'email@example.com') + assert response = @gateway.store(@credit_card, locale: 'en_GB', merchant_customer_id:, email: 'email@example.com') assert_success response assert_equal merchant_customer_id, response.params['merchantCustomerId'] first_card = response.params['cards'].first @@ -195,11 +270,34 @@ def test_successful_unstore def test_successful_purchase_using_stored_card merchant_customer_id = SecureRandom.hex - assert store = @gateway.store(@credit_card, @options.merge({locale: 'en_GB', merchant_customer_id: merchant_customer_id, email: 'email@example.com'})) + assert store = @gateway.store(@credit_card, @options.merge({ locale: 'en_GB', merchant_customer_id:, email: 'email@example.com' })) assert_success store assert response = @gateway.purchase(@amount, store.authorization.split('|').last) assert_success response assert_equal 'OK', response.message end + + def test_successful_verify + verify = @gateway.verify(@credit_card, @options) + assert_success verify + end + + def test_failed_verify + options = { + verification_value: '' + } + verify = @gateway.verify(@credit_card, options) + assert_failure verify + assert_equal 'The zip/postal code must be provided for an AVS check request.', verify.message + end + + def test_successful_cancel_settlement + response = @gateway.purchase(@amount, @credit_card, @options) + authorization = response.authorization + + assert cancelled_response = @gateway.refund(@amount, authorization) + assert_success cancelled_response + assert_equal 'CANCELLED', cancelled_response.params['status'] + end end diff --git a/test/remote/gateways/remote_netbilling_test.rb b/test/remote/gateways/remote_netbilling_test.rb index 7004e213627..46cafebd62b 100644 --- a/test/remote/gateways/remote_netbilling_test.rb +++ b/test/remote/gateways/remote_netbilling_test.rb @@ -6,18 +6,17 @@ def setup @credit_card = credit_card('4444111111111119') - @address = { :address1 => '1600 Amphitheatre Parkway', - :city => 'Mountain View', - :state => 'CA', - :country => 'US', - :zip => '94043', - :phone => '650-253-0001' - } + @address = { address1: '1600 Amphitheatre Parkway', + city: 'Mountain View', + state: 'CA', + country: 'US', + zip: '94043', + phone: '650-253-0001' } @options = { - :billing_address => @address, - :description => 'Internet purchase', - :order_id => 987654321 + billing_address: @address, + description: 'Internet purchase', + order_id: 987654321 } @amount = 100 @@ -89,9 +88,9 @@ def test_failed_capture def test_invalid_login gateway = NetbillingGateway.new( - :login => '', - :password => '' - ) + login: '', + password: '' + ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_match(/missing/i, response.message) assert_failure response diff --git a/test/remote/gateways/remote_netpay_test.rb b/test/remote/gateways/remote_netpay_test.rb index cb87c5ca3f7..148773c4f28 100644 --- a/test/remote/gateways/remote_netpay_test.rb +++ b/test/remote/gateways/remote_netpay_test.rb @@ -9,7 +9,7 @@ def setup @declined_card = credit_card('4000300011112220') @options = { - :description => 'Store Purchase' + description: 'Store Purchase' } end @@ -44,47 +44,45 @@ def test_successful_purchase_and_refund assert_equal 'Aprobada', refund.message end -=begin - # Netpay are currently adding support for authorize and capture. - # When this is complete, the following remote calls should work. - - def test_successful_authorize - assert response = @gateway.authorize(@amount, @credit_card, @options) - assert_success response - assert_equal 'Aprobada', response.message - end - - def test_unsuccessful_authorize - # We have to force a decline using the mode option - opts = @options.clone - opts[:mode] = 'D' - assert response = @gateway.authorize(@amount, @declined_card, opts) - assert_failure response - assert_match %r{Declinada}, response.message - end - - def test_successful_authorize_and_capture - assert purchase = @gateway.authorize(@amount, @credit_card, @options) - assert_success purchase - assert capture = @gateway.capture(@amount, purchase.authorization) - assert_success capture - assert_equal 'Aprobada', capture.message - end - - def test_failed_capture - assert response = @gateway.capture(@amount, '') - assert_failure response - assert_equal 'REPLACE WITH GATEWAY FAILURE MESSAGE', response.message - end - - def test_invalid_login - gateway = NetpayGateway.new( - :login => '', - :password => '' - ) - assert response = gateway.purchase(@amount, @credit_card, @options) - assert_failure response - assert_equal 'REPLACE WITH FAILURE MESSAGE', response.message - end -=end + # # Netpay are currently adding support for authorize and capture. + # # When this is complete, the following remote calls should work. + # + # def test_successful_authorize + # assert response = @gateway.authorize(@amount, @credit_card, @options) + # assert_success response + # assert_equal 'Aprobada', response.message + # end + # + # def test_unsuccessful_authorize + # # We have to force a decline using the mode option + # opts = @options.clone + # opts[:mode] = 'D' + # assert response = @gateway.authorize(@amount, @declined_card, opts) + # assert_failure response + # assert_match %r{Declinada}, response.message + # end + # + # def test_successful_authorize_and_capture + # assert purchase = @gateway.authorize(@amount, @credit_card, @options) + # assert_success purchase + # assert capture = @gateway.capture(@amount, purchase.authorization) + # assert_success capture + # assert_equal 'Aprobada', capture.message + # end + # + # def test_failed_capture + # assert response = @gateway.capture(@amount, '') + # assert_failure response + # assert_equal 'REPLACE WITH GATEWAY FAILURE MESSAGE', response.message + # end + # + # def test_invalid_login + # gateway = NetpayGateway.new( + # :login => '', + # :password => '' + # ) + # assert response = gateway.purchase(@amount, @credit_card, @options) + # assert_failure response + # assert_equal 'REPLACE WITH FAILURE MESSAGE', response.message + # end end diff --git a/test/remote/gateways/remote_network_merchants_test.rb b/test/remote/gateways/remote_network_merchants_test.rb index 290a0cad4c6..c4a43d5661c 100644 --- a/test/remote/gateways/remote_network_merchants_test.rb +++ b/test/remote/gateways/remote_network_merchants_test.rb @@ -11,9 +11,9 @@ def setup @check = check @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase' + order_id: '1', + billing_address: address, + description: 'Store Purchase' } end @@ -54,7 +54,7 @@ def test_unsuccessful_purchase_with_track_data end def test_purchase_and_store - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(:store => true)) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(store: true)) assert_success response assert_equal response.params['transactionid'], response.authorization assert response.params['customer_vault_id'] @@ -153,9 +153,9 @@ def test_purchase_on_stored_card def test_invalid_login gateway = NetworkMerchantsGateway.new( - :login => '', - :password => '' - ) + login: '', + password: '' + ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal 'Invalid Username', response.message @@ -163,16 +163,16 @@ def test_invalid_login def test_successful_purchase_without_state @options[:billing_address] = { - :name => 'Jim Smith', - :address1 => 'Gullhauggrenda 30', - :address2 => 'Apt 1', - :company => 'Widgets Inc', - :city => 'Baerums Verk', - :state => nil, - :zip => '1354', - :country => 'NO', - :phone => '(555)555-5555', - :fax => '(555)555-6666' + name: 'Jim Smith', + address1: 'Gullhauggrenda 30', + address2: 'Apt 1', + company: 'Widgets Inc', + city: 'Baerums Verk', + state: nil, + zip: '1354', + country: 'NO', + phone: '(555)555-5555', + fax: '(555)555-6666' } assert response = @gateway.purchase(@amount, @credit_card, @options) diff --git a/test/remote/gateways/remote_nmi_test.rb b/test/remote/gateways/remote_nmi_test.rb index c98b29dcaac..50caa47d595 100644 --- a/test/remote/gateways/remote_nmi_test.rb +++ b/test/remote/gateways/remote_nmi_test.rb @@ -3,24 +3,52 @@ class RemoteNmiTest < Test::Unit::TestCase def setup @gateway = NmiGateway.new(fixtures(:nmi)) + @gateway_secure = NmiGateway.new(fixtures(:nmi_secure)) @amount = Random.rand(100...1000) @credit_card = credit_card('4111111111111111', verification_value: 917) @check = check( - :routing_number => '123123123', - :account_number => '123123123' + routing_number: '123123123', + account_number: '123123123' ) - @apple_pay_card = network_tokenization_credit_card('4111111111111111', - :payment_cryptogram => 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', - :month => '01', - :year => '2024', - :source => :apple_pay, - :eci => '5', - :transaction_id => '123456789' + @apple_pay = network_tokenization_credit_card( + '4111111111111111', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + month: '01', + year: Time.new.year + 2, + source: :apple_pay, + eci: '5', + transaction_id: '123456789' ) + + @google_pay = network_tokenization_credit_card( + '4111111111111111', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + month: '01', + year: Time.new.year + 2, + source: :google_pay, + transaction_id: '123456789', + eci: '05' + ) + @options = { - :order_id => generate_unique_id, - :billing_address => address, - :description => 'Store purchase' + order_id: generate_unique_id, + billing_address: address, + description: 'Store purchase' + } + @level3_options = { + tax: 5.25, shipping: 10.51, ponumber: 1002 + } + @descriptor_options = { + descriptor: 'test', + descriptor_phone: '123', + descriptor_address: 'address', + descriptor_city: 'city', + descriptor_state: 'state', + descriptor_postal: 'postal', + descriptor_country: 'country', + descriptor_mcc: 'mcc', + descriptor_merchant_id: '120', + descriptor_url: 'url' } end @@ -31,11 +59,82 @@ def test_invalid_login assert_equal 'Authentication Failed', response.message end - def test_successful_purchase + def test_invalid_login_security_key_empty + gateway_secure = NmiGateway.new(security_key: '') + assert response = gateway_secure.purchase(@amount, @credit_card, @options) + assert_failure response + assert_equal 'Authentication Failed', response.message + end + + def test_valid_login_username_password + @gateway = NmiGateway.new(login: 'demo', password: 'password') assert response = @gateway.purchase(@amount, @credit_card, @options) assert_success response + end + + def test_valid_login_security_key + gateway_secure = NmiGateway.new(fixtures(:nmi_secure)) + assert response = gateway_secure.purchase(@amount, @credit_card, @options) + assert_success response + end + + def test_successful_authorization_security_key + assert response = @gateway_secure.authorize(@amount, @credit_card, @options) + assert_success response + assert_equal 'Succeeded', response.message + assert response.authorization + end + + def test_successful_purchase_using_security_key + assert response = @gateway_secure.purchase(@amount, @credit_card, @options) + assert_success response + assert response.test? + assert_equal 'Succeeded', response.message + assert response.authorization + end + + def test_transcript_scrubbing_using_security_key + transcript = capture_transcript(@gateway_secure) do + @gateway_secure.purchase(@amount, @credit_card, @options) + end + transcript = @gateway_secure.scrub(transcript) + assert_scrubbed(@gateway_secure.options[:security_key], transcript) + end + + def test_successful_purchase + options = @options.merge(@level3_options) + 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_customer_vault_data + vault_id = SecureRandom.hex(16) + + options = { + order_id: generate_unique_id, + billing_address: address, + description: 'Store purchase', + customer_vault: 'add_customer' + } + + assert response = @gateway.purchase(@amount, @credit_card, options.merge(customer_vault_id: vault_id)) + assert_success response + assert response.test? + assert_equal 'Succeeded', response.message + assert_equal vault_id, response.params['customer_vault_id'] + assert response.authorization + end + + def test_successful_purchase_with_customer_vault_and_auto_generate_customer_vault_id + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(customer_vault: 'add_customer')) + assert_success response assert response.test? + assert_equal 'Succeeded', response.message + assert response.params.include?('customer_vault_id') assert response.authorization end @@ -70,25 +169,134 @@ def test_failed_purchase_with_echeck assert_equal 'FAILED', response.message end - def test_successful_purchase_with_apple_pay_card - assert @gateway.supports_network_tokenization? - assert response = @gateway.purchase(@amount, @apple_pay_card, @options) + def test_successful_purchase_with_apple_pay + assert @gateway_secure.supports_network_tokenization? + assert response = @gateway_secure.purchase(@amount, @apple_pay, @options) assert_success response assert response.test? assert_equal 'Succeeded', response.message assert response.authorization end - def test_failed_purchase_with_apple_pay_card - assert response = @gateway.purchase(99, @apple_pay_card, @options) + def test_successful_purchase_with_apple_pay_and_industry_field + assert @gateway_secure.supports_network_tokenization? + assert response = @gateway_secure.purchase(@amount, @apple_pay, @options.merge(industry_indicator: 'ecommerce')) + assert_success response + assert response.test? + assert_equal 'Succeeded', response.message + assert response.authorization + end + + def test_successful_purchase_with_google_pay + assert @gateway_secure.supports_network_tokenization? + assert response = @gateway_secure.purchase(@amount, @google_pay, @options) + assert_success response + assert response.test? + assert_equal 'Succeeded', response.message + assert response.authorization + end + + def test_successful_purchase_google_pay_without_billing_address + assert @gateway_secure.supports_network_tokenization? + @options.delete(:billing_address) + + assert response = @gateway_secure.purchase(@amount, @google_pay, @options) + assert_success response + assert response.test? + assert_equal 'Succeeded', response.message + assert response.authorization + end + + def test_successful_purchase_apple_pay_without_billing_address + assert @gateway_secure.supports_network_tokenization? + @options.delete(:billing_address) + + assert response = @gateway_secure.purchase(@amount, @apple_pay, @options) + assert_success response + assert response.test? + assert_equal 'Succeeded', response.message + assert response.authorization + end + + def test_failed_purchase_with_apple_pay + assert response = @gateway_secure.purchase(1, @apple_pay, @options) + assert_failure response + assert response.test? + assert_equal 'DECLINE', response.message + end + + def test_failed_purchase_with_google_pay + assert response = @gateway_secure.purchase(1, @google_pay, @options) assert_failure response assert response.test? assert_equal 'DECLINE', response.message end + def test_successful_purchase_with_additional_options + options = @options.merge({ + customer_id: '234', + vendor_id: '456', + recurring: true + }) + 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_three_d_secure + three_d_secure_options = @options.merge({ + three_d_secure: { + version: '2.1.0', + authentication_response_status: 'Y', + cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA', + ds_transaction_id: '97267598-FAE6-48F2-8083-C23433990FBC' + } + }) + + assert response = @gateway.purchase(@amount, @credit_card, three_d_secure_options) + assert_success response + assert response.test? + assert_equal 'Succeeded', response.message + assert response.authorization + end + def test_successful_authorization - assert response = @gateway.authorize(@amount, @credit_card, @options) + options = @options.merge(@level3_options) + + assert response = @gateway.authorize(@amount, @credit_card, options) + assert_success response + assert_equal 'Succeeded', response.message + assert response.authorization + end + + def test_successful_purchase_with_descriptors + options = @options.merge({ descriptors: @descriptor_options }) + + 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_shipping_fields + options = @options.merge({ 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 @@ -160,9 +368,10 @@ def test_successful_refund_with_echeck assert_equal 'Succeeded', response.message end - def test_successful_credit - response = @gateway.credit(@amount, @credit_card, @options) + options = @options.merge(@level3_options) + + response = @gateway.credit(@amount, @credit_card, options) assert_success response assert_equal 'Succeeded', response.message end @@ -174,11 +383,41 @@ def test_failed_credit end def test_successful_verify - response = @gateway.verify(@credit_card, @options) + options = @options.merge(@level3_options) + + response = @gateway.verify(@credit_card, options) assert_success response assert_match 'Succeeded', response.message end + def test_successful_verify_with_customer_vault_data + vault_id = SecureRandom.hex(16) + + options = { + order_id: generate_unique_id, + billing_address: address, + description: 'Store purchase', + customer_vault: 'add_customer' + } + + assert response = @gateway.verify(@credit_card, options.merge(customer_vault_id: vault_id)) + assert_success response + assert response.test? + assert_equal 'Succeeded', response.message + assert_equal vault_id, response.params['customer_vault_id'] + assert response.authorization + end + + def test_successful_verify_with_customer_vault_and_auto_generate_customer_vault_id + assert response = @gateway.verify(@credit_card, @options.merge(customer_vault: 'add_customer')) + assert_success response + assert response.test? + + assert_equal 'Succeeded', response.message + assert response.params.include?('customer_vault_id') + assert response.authorization + end + def test_failed_verify card = credit_card(year: 2010) response = @gateway.verify(card, @options) @@ -190,39 +429,38 @@ def test_successful_store response = @gateway.store(@credit_card, @options) assert_success response assert_equal 'Succeeded', response.message - assert response.params['customer_vault_id'] + assert response.authorization.include?(response.params['customer_vault_id']) end def test_failed_store card = credit_card(year: 2010) response = @gateway.store(card, @options) assert_failure response - assert_nil response.params['customer_vault_id'] end def test_successful_store_with_echeck response = @gateway.store(@check, @options) assert_success response assert_equal 'Succeeded', response.message - assert response.params['customer_vault_id'] + assert response.authorization.include?(response.params['customer_vault_id']) end def test_successful_store_and_purchase - vault_id = @gateway.store(@credit_card, @options).params['customer_vault_id'] + vault_id = @gateway.store(@credit_card, @options).authorization purchase = @gateway.purchase(@amount, vault_id, @options) assert_success purchase assert_equal 'Succeeded', purchase.message end def test_successful_store_and_auth - vault_id = @gateway.store(@credit_card, @options).params['customer_vault_id'] + vault_id = @gateway.store(@credit_card, @options).authorization auth = @gateway.authorize(@amount, vault_id, @options) assert_success auth assert_equal 'Succeeded', auth.message end def test_successful_store_and_credit - vault_id = @gateway.store(@credit_card, @options).params['customer_vault_id'] + vault_id = @gateway.store(@credit_card, @options).authorization credit = @gateway.credit(@amount, vault_id, @options) assert_success credit assert_equal 'Succeeded', credit.message @@ -242,17 +480,107 @@ def test_verify_credentials assert !gateway.verify_credentials end + def test_purchase_using_stored_credential_recurring_cit + 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'] + + used_options = stored_credential_options(:recurring, :cardholder, id: network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + end + + def test_purchase_using_stored_credential_recurring_mit + initial_options = stored_credential_options(:merchant, :recurring, :initial) + assert purchase = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success purchase + assert network_transaction_id = purchase.params['transactionid'] + + used_options = stored_credential_options(:merchant, :recurring, id: network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + 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) + assert_success purchase + assert network_transaction_id = purchase.params['transactionid'] + + used_options = stored_credential_options(:cardholder, :installment, id: network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + end + + def test_purchase_using_stored_credential_installment_mit + initial_options = stored_credential_options(:merchant, :installment, :initial) + assert purchase = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success purchase + assert network_transaction_id = purchase.params['transactionid'] + + used_options = stored_credential_options(:merchant, :installment, id: network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + end + + def test_purchase_using_stored_credential_unscheduled_cit + initial_options = stored_credential_options(:cardholder, :unscheduled, :initial) + assert purchase = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success purchase + assert network_transaction_id = purchase.params['transactionid'] + + used_options = stored_credential_options(:cardholder, :unscheduled, id: network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + end + + def test_purchase_using_stored_credential_unscheduled_mit + initial_options = stored_credential_options(:merchant, :unscheduled, :initial) + assert purchase = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success purchase + assert network_transaction_id = purchase.params['transactionid'] + + used_options = stored_credential_options(:merchant, :unscheduled, id: network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + end + + def test_authorize_and_capture_with_stored_credential + initial_options = stored_credential_options(:cardholder, :recurring, :initial) + assert authorization = @gateway.authorize(@amount, @credit_card, initial_options) + assert_success authorization + assert network_transaction_id = authorization.params['transactionid'] + + assert capture = @gateway.capture(@amount, authorization.authorization) + assert_success capture + + used_options = stored_credential_options(:cardholder, :recurring, id: network_transaction_id) + assert authorization = @gateway.authorize(@amount, @credit_card, used_options) + assert_success authorization + assert @gateway.capture(@amount, authorization.authorization) + end + def test_card_transcript_scrubbing transcript = capture_transcript(@gateway) do @gateway.purchase(@amount, @credit_card, @options) end clean_transcript = @gateway.scrub(transcript) - assert_scrubbed(@credit_card.number, clean_transcript) - assert_scrubbed(@credit_card.verification_value.to_s, clean_transcript) - - # "password=password is filtered, but can't be tested b/c of key match" - # assert_scrubbed(@gateway.options[:password], clean_transcript) + assert_cvv_scrubbed(clean_transcript) + assert_password_scrubbed(clean_transcript) end def test_check_transcript_scrubbing @@ -263,21 +591,52 @@ def test_check_transcript_scrubbing assert_scrubbed(@check.account_number, clean_transcript) assert_scrubbed(@check.routing_number, clean_transcript) - - # "password=password is filtered, but can't be tested b/c of key match" - # assert_scrubbed(@gateway.options[:password], clean_transcript) + assert_password_scrubbed(clean_transcript) end def test_network_tokenization_transcript_scrubbing transcript = capture_transcript(@gateway) do - @gateway.purchase(@amount, @apple_pay_card, @options) + @gateway.purchase(@amount, @apple_pay, @options) end clean_transcript = @gateway.scrub(transcript) - assert_scrubbed(@apple_pay_card.number, clean_transcript) - assert_scrubbed(@apple_pay_card.payment_cryptogram, clean_transcript) + assert_scrubbed(@apple_pay.number, clean_transcript) + assert_scrubbed(@apple_pay.payment_cryptogram, clean_transcript) + assert_password_scrubbed(clean_transcript) + end + + def test_transcript_scrubbing_with_google_pay + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @google_pay, @options) + end + + clean_transcript = @gateway.scrub(transcript) + assert_scrubbed(@apple_pay.number, clean_transcript) + assert_scrubbed(@apple_pay.payment_cryptogram, clean_transcript) + assert_password_scrubbed(clean_transcript) + end + + private + + # "password=password is filtered, but can't be tested via normal + # `assert_scrubbed` b/c of key match" + def assert_password_scrubbed(transcript) + assert_match(/password=\[FILTERED\]/, transcript) + end + + # Because the cvv is a simple three digit number, sometimes there are random + # failures using `assert_scrubbed` because of natural collisions with a + # substring within orderid in transcript; e.g. + # + # Expected the value to be scrubbed out of the transcript. + # was expected to not match + # <"opening connection to secure.nmi.com:443...\nopened\nstarting SSL for secure.nmi.com:443...\nSSL established\n<- \"POST /api/transact.php HTTP/1.1\\r\\nContent-Type: application/x-www-form-urlencoded;charset=UTF-8\\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.nmi.com\\r\\nContent-Length: 394\\r\\n\\r\\n\"\n<- \"amount=7.96&orderid=9bb4c3bf6fbb26b91796ae9442cb1941&orderdescription=Store+purchase¤cy=USD&payment=creditcard&firstname=Longbob&lastname=Longsen&ccnumber=[FILTERED]&cvv=[FILTERED]&ccexp=0920&email=&ipaddress=&customer_id=&company=Widgets+Inc&address1=456+My+Street&address2=Apt+1&city=Ottawa&state=ON&country=CA&zip=K1C2N6&phone=%28555%29555-5555&type=sale&username=demo&password=[FILTERED]\"\n-> \"HTTP/1.1 200 OK\\r\\n\"\n-> \"Date: Wed, 12 Jun 2019 21:10:29 GMT\\r\\n\"\n-> \"Server: Apache\\r\\n\"\n-> \"Content-Length: 169\\r\\n\"\n-> \"Connection: close\\r\\n\"\n-> \"Content-Type: text/html; charset=UTF-8\\r\\n\"\n-> \"\\r\\n\"\nreading 169 bytes...\n-> \"response=1&responsetext=SUCCESS&authcode=123456&transactionid=4743046890&avsresponse=N&cvvresponse=N&orderid=9bb4c3bf6fbb26b91796ae9442cb1941&type=sale&response_code=100\"\nread 169 bytes\nConn close\n">. + def assert_cvv_scrubbed(transcript) + assert_match(/cvv=\[FILTERED\]/, transcript) + end - # "password=password is filtered, but can't be tested b/c of key match" - # assert_scrubbed(@gateway.options[:password], clean_transcript) + def stored_credential_options(*args, id: nil) + @options.merge(order_id: generate_unique_id, + stored_credential: stored_credential(*args, id:)) end end diff --git a/test/remote/gateways/remote_nuvei_test.rb b/test/remote/gateways/remote_nuvei_test.rb new file mode 100644 index 00000000000..68557044b2e --- /dev/null +++ b/test/remote/gateways/remote_nuvei_test.rb @@ -0,0 +1,458 @@ +require 'test_helper' +require 'timecop' + +class RemoteNuveiTest < Test::Unit::TestCase + def setup + @gateway = NuveiGateway.new(fixtures(:nuvei)) + + @amount = 10000 + @credit_card = credit_card('4761344136141390', verification_value: '999', first_name: 'Cure', last_name: 'Tester') + @declined_card = credit_card('4000128449498204') + @challenge_credit_card = credit_card('2221008123677736', first_name: 'CL-BRW2', last_name: '') + @three_ds_amount = 151 # for challenge = 151, for frictionless >= 150 + @frictionless_credit_card = credit_card('4000020951595032', first_name: 'FL-BRW2', last_name: '') + @credit_card_3ds = credit_card('4000020951595032') + + @options = { + email: 'test@gmail.com', + billing_address: address.merge(name: 'Cure Tester'), + ip: '127.0.0.1', + order_id: '123456' + } + + @three_ds_options = { + 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' + } + } + } + + @bank_account = check(account_number: '111111111', routing_number: '999999992') + + @three_d_secure_options = @options.merge({ + three_d_secure: { + cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA=', + ds_transaction_id: '97267598-FAE6-48F2-8083-C23433990FBC', + eci: '05' + } + }) + + @apple_pay_card = network_tokenization_credit_card( + '5204245250460049', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + month: '12', + year: Time.new.year + 2, + source: :apple_pay, + verification_value: 111, + eci: '5' + ) + + @google_pay_card = network_tokenization_credit_card( + '4761344136141390', + payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', + month: '12', + year: Time.new.year + 2, + source: :google_pay, + eci: '5' + ) + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.authorize(@amount, @credit_card, @options) + end + + @gateway.scrub(transcript) + end + + def test_successful_session_token_generation + response = @gateway.send(:fetch_session_token, @options) + assert_success response + assert_not_nil response.params[:sessionToken] + end + + def test_failed_session_token_generation + @gateway.options[:merchant_site_id] = 123 + response = @gateway.send(:fetch_session_token, {}) + assert_failure response + assert_match 'Invalid merchant site id', response.message + end + + def test_successful_authorize + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + assert_equal response.params[:clientUniqueId], @options[:order_id] + assert_not_nil response.params[:orderId] + assert_not_nil response.params[:transactionId] + assert_match 'APPROVED', response.message + end + + def test_successful_authorize_without_order_id + @options.delete(:order_id) + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + assert_not_nil response.params[:clientUniqueId] + assert_not_nil response.params[:transactionId] + assert_match 'APPROVED', response.message + end + + def test_failed_authorize + response = @gateway.authorize(@amount, @declined_card, @options) + assert_failure response + assert_match 'DECLINED', response.params['transactionStatus'] + end + + def test_successful_authorize_and_capture + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + + capture_response = @gateway.capture(round_down(@amount).to_i, response.authorization) + + assert_success capture_response + assert_match 'APPROVED', capture_response.message + end + + def test_successful_zero_auth + response = @gateway.authorize(0, @credit_card, @options) + assert_success response + assert_match 'APPROVED', response.message + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal response.params[:clientUniqueId], @options[:order_id] + assert_not_nil response.params[:orderId] + assert_not_nil response.params[:transactionId] + assert_match 'APPROVED', response.message + assert_match 'SUCCESS', response.params['status'] + end + + def test_successful_purchase_with_3ds_frictionless + response = @gateway.purchase(@three_ds_amount, @frictionless_credit_card, @options.merge(@three_ds_options)) + assert_success response + assert_not_nil response.params[:transactionId] + assert_match 'SUCCESS', response.params['status'] + assert_match 'APPROVED', response.message + end + + def test_successful_purchase_with_3ds_challenge + response = @gateway.purchase(@three_ds_amount, @challenge_credit_card, @options.merge(@three_ds_options)) + assert_success response + assert_not_nil response.params[:transactionId] + assert_match 'SUCCESS', response.params['status'] + assert_match 'APPROVED', response.message + end + + def test_successful_purchase_with_not_enrolled_card + response = @gateway.purchase(@three_ds_amount, @credit_card, @options.merge(@three_ds_options)) + assert_success response + assert_not_nil response.params[:transactionId] + assert_match 'SUCCESS', response.params['status'] + assert_match 'APPROVED', response.message + end + + def test_successful_purchase_with_3ds_frictionless_and_forced_3ds + response = @gateway.purchase(@three_ds_amount, @frictionless_credit_card, @options.merge(@three_ds_options.merge({ force_3d_secure: true }))) + assert_success response + assert_not_nil response.params[:transactionId] + assert_match 'SUCCESS', response.params['status'] + assert_match 'APPROVED', response.message + end + + def test_successful_purchase_with_not_enrolled_card_and_forced_3ds + response = @gateway.purchase(@three_ds_amount, @credit_card, @options.merge(@three_ds_options.merge({ force_3d_secure: true }))) + assert_failure response + assert_equal response.message, '3D Secure is required but not supported' + end + + def test_failed_purchase + response = @gateway.purchase(@amount, @declined_card, @options) + assert_failure response + assert_match 'DECLINED', response.params['transactionStatus'] + end + + def test_failed_purchase_with_invalid_cvv + @credit_card.verification_value = '' + response = @gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_match 'ERROR', response.params['status'] + assert_match 'cardData.CVV is invalid', response.message + end + + def test_failed_capture_invalid_transaction_id + response = @gateway.capture(@amount, '123') + assert_failure response + assert_match 'ERROR', response.params['status'] + assert_match 'Invalid relatedTransactionId', response.message + end + + def test_successful_void + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + + void_response = @gateway.void(response.authorization) + assert_success void_response + assert_match 'SUCCESS', void_response.params['status'] + assert_match 'APPROVED', void_response.message + end + + def test_failed_void_invalid_transaction_id + response = @gateway.void('123') + assert_failure response + assert_match 'ERROR', response.params['status'] + assert_match 'Invalid relatedTransactionId', response.message + end + + def test_successful_refund + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + + refund_response = @gateway.refund(@amount, response.authorization) + assert_success refund_response + assert_match 'SUCCESS', refund_response.params['status'] + assert_match 'APPROVED', refund_response.message + end + + def test_successful_verify + response = @gateway.verify(@credit_card, @options) + assert_success response + assert_match 'SUCCESS', response.params['status'] + assert_match 'APPROVED', response.message + end + + def test_successful_save_payment_method_override + response = @gateway.purchase(@amount, @credit_card, @options.merge(save_payment_method: false)) + assert_success response + assert_match 'SUCCESS', response.params['status'] + assert_match 'APPROVED', response.message + assert_not_nil response.params[:paymentOption][:userPaymentOptionId] + end + + def test_successful_verify_with_authentication_only_type + response = @gateway.verify(@credit_card, @options.merge({ authentication_only_type: 'MAINTAINCARD' })) + assert_match 'SUCCESS', response.params['status'] + assert_match 'APPROVED', response.message + end + + def test_successful_unreferenced_refund + refund_response = @gateway.credit(@amount, @credit_card, @options.merge(user_token_id: '12345678')) + assert_success refund_response + assert_match 'SUCCESS', refund_response.params['status'] + assert_match 'APPROVED', refund_response.message + end + + def test_successful_unreferenced_refund_with_user_option_id + # getting the user_option_id from prevouse purchase + purchase_response = @gateway.purchase(@amount, @credit_card, @options.merge(user_token_id: '12345678')) + assert_success purchase_response + + user_payment_id = purchase_response.params[:paymentOption][:userPaymentOptionId] + + refund_response = @gateway.credit(@amount, @credit_card, @options.merge(user_token_id: '12345678', user_payment_option_id: user_payment_id)) + assert_success refund_response + assert_match 'SUCCESS', refund_response.params['status'] + assert_match 'APPROVED', refund_response.message + end + + def test_successful_payout + payout_response = @gateway.credit(@amount, @credit_card, @options.merge(user_token_id: '12345678', is_payout: true)) + assert_success payout_response + assert_match 'SUCCESS', payout_response.params['status'] + assert_match 'APPROVED', payout_response.message + end + + def test_successful_payout_with_google_pay + purchase_response = @gateway.purchase(@amount, @credit_card, @options.merge(user_token_id: '12345678')) + assert_success purchase_response + user_payment_id = purchase_response.params[:paymentOption][:userPaymentOptionId] + + options = @options.merge( + user_payment_option_id: user_payment_id, + user_token_id: '12345678', + is_payout: true, + notification_url: 'https://example.com/notification' + ) + payout_response = @gateway.credit(@amount, @google_pay_card, options) + assert_success payout_response + assert_match 'SUCCESS', payout_response.params['status'] + assert_match 'APPROVED', payout_response.message + end + + def test_failed_unreferenced_refund + refund_response = @gateway.credit(@amount, @declined_card, @options.merge(user_token_id: '12345678')) + assert_failure refund_response + + assert_match 'DECLINED', refund_response.params['transactionStatus'] + assert_match 'External Error in Processing', refund_response.message + end + + def test_failed_payout + payout_response = @gateway.credit(@amount, @declined_card, @options.merge(user_token_id: '12345678')) + assert_failure payout_response + + assert_match 'DECLINED', payout_response.params['transactionStatus'] + assert_match 'External Error in Processing', payout_response.message + end + + def test_successful_store + response = @gateway.store(@credit_card, @options.merge(user_token_id: '12345678')) + assert_success response + assert_match 'SUCCESS', response.params['status'] + assert_match 'APPROVED', response.message + end + + def test_purchase_using_stored_credentials_cit + options = @options.merge!(user_token_id: '12345678', stored_credential: stored_credential(:cardholder, :unscheduled, :initial)) + assert response = @gateway.authorize(@amount, @credit_card, options) + assert_success response + + assert capture = @gateway.capture(round_down(@amount).to_i, response.authorization, @options) + assert_success capture + + options_stored_credentials = @options.merge!(stored_credential: stored_credential(:cardholder, :recurring, id: response.network_transaction_id)) + assert purchase_response = @gateway.purchase(@amount, @credit_card, options_stored_credentials) + assert_success purchase_response + end + + def test_purchase_using_stored_credentials_recurring_cit + # Initial transaction with stored credentials + initial_options = @options.merge(user_token_id: '12345678', stored_credential: stored_credential(:cardholder, :unscheduled, :initial)) + initial_response = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success initial_response + + assert_not_nil initial_response.authorization + assert_match 'SUCCESS', initial_response.params['status'] + + stored_credential_options = @options.merge( + user_token_id: '12345678', + stored_credential: stored_credential(:merchant, :recurring, network_transaction_id: initial_response.network_transaction_id) + ) + + recurring_response = @gateway.purchase(@amount, @credit_card, stored_credential_options) + assert_success recurring_response + assert_match 'SUCCESS', recurring_response.params['status'] + end + + def test_purchase_using_stored_credentials_merchant_installments_cit + initial_options = @options.merge(user_token_id: '12345678', stored_credential: stored_credential(:cardholder, :unscheduled, :initial)) + initial_response = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success initial_response + + assert_not_nil initial_response.authorization + assert_match 'SUCCESS', initial_response.params['status'] + + stored_credential_options = @options.merge( + user_token_id: '12345678', + stored_credential: stored_credential(:merchant, :installments, network_transaction_id: initial_response.network_transaction_id) + ) + + recurring_response = @gateway.purchase(@amount, @credit_card, stored_credential_options) + assert_success recurring_response + assert_match 'SUCCESS', recurring_response.params['status'] + end + + def test_purchase_subsequent_mit + initial_options = @options.merge(user_token_id: '12345678', stored_credential: stored_credential(:merchant, :unscheduled, :initial)) + initial_response = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success initial_response + + subsequent_options = @options.merge(user_token_id: '12345678', stored_credential: stored_credential(:merchant, :recurring, network_transaction_id: initial_response.authorization)) + subsequent_response = @gateway.purchase(@amount, @credit_card, subsequent_options) + assert_success subsequent_response + assert_match 'SUCCESS', subsequent_response.params['status'] + end + + def test_successful_partial_approval + response = @gateway.authorize(55, @credit_card, @options.merge(is_partial_approval: true)) + assert_success response + assert_equal '0.55', response.params['partialApproval']['requestedAmount'] + assert_equal '0.55', response.params['partialApproval']['processedAmount'] + assert_match 'APPROVED', response.message + end + + def test_successful_authorize_with_bank_account + @options.update(billing_address: address.merge(country: 'US', state: 'MA')) + response = @gateway.authorize(1.25, @bank_account, @options) + assert_success response + assert_match 'PENDING', response.message + end + + def test_failing_purchase_three_d_secure + @three_d_secure_options[:three_d_secure][:cavv] = 'wrong_cavv_value' + assert response = @gateway.purchase(@amount, @credit_card_3ds, @three_d_secure_options) + assert_failure response + assert_equal 'UNEXPECTED SYSTEM ERROR - PLEASE RETRY LATER', response.message + assert_match 'ERROR', response.params['transactionStatus'] + end + + def test_successful_purchase_with_three_d_secure + assert response = @gateway.purchase(@amount, @credit_card_3ds, @three_d_secure_options) + assert_success response + assert response.authorization + assert_equal 'APPROVED', response.message + assert_match 'SUCCESS', response.params['status'] + end + + def test_successful_purchase_three_d_secure_challenge_preference + assert response = @gateway.purchase(@amount, @credit_card_3ds, @three_d_secure_options.merge(challenge_preference: 'ExemptionRequest', exemption_request_reason: 'AccountVerification')) + assert_success response + assert_equal 'APPROVED', response.message + assert_match 'SUCCESS', response.params['status'] + assert response.authorization + end + + def test_successful_purchase_with_apple_pay + response = @gateway.purchase(@amount, @apple_pay_card, @options) + assert_success response + assert_equal 'APPROVED', response.message + assert_not_nil response.params[:paymentOption][:userPaymentOptionId] + end + + def test_successful_purchase_with_google_pay + response = @gateway.purchase(@amount, @google_pay_card, @options) + assert_success response + assert_equal 'APPROVED', response.message + assert_not_nil response.params[:paymentOption][:userPaymentOptionId] + end + + def test_purchase_account_funding_transaction + response = @gateway.purchase(@amount, @credit_card, @options.merge(is_aft: true, aft_recipient_first_name: 'John', aft_recipient_last_name: 'Doe')) + assert_success response + assert_equal 'APPROVED', response.message + end + + def test_refund_account_funding_transaction + purchase_response = @gateway.purchase(@amount, @credit_card, @options.merge(is_aft: true, aft_recipient_first_name: 'John', aft_recipient_last_name: 'Doe')) + assert_success purchase_response + + refund_response = @gateway.refund(@amount, purchase_response.authorization) + assert_success refund_response + assert_equal 'APPROVED', refund_response.message + end + + def test_successful_authorize_with_cardholder_name_verification + response = @gateway.authorize(0, @credit_card, @options.merge({ perform_name_verification: true })) + assert_success response + assert_match 'APPROVED', response.message + end + + def round_down(value, decimals = 1) + value = value.to_f / 2 + factor = 10**decimals + ((value * factor).floor / factor.to_f).to_s + end +end diff --git a/test/remote/gateways/remote_ogone_test.rb b/test/remote/gateways/remote_ogone_test.rb index 6f62cd8f0ce..202c17002f7 100644 --- a/test/remote/gateways/remote_ogone_test.rb +++ b/test/remote/gateways/remote_ogone_test.rb @@ -1,39 +1,61 @@ # coding: utf-8 + require 'test_helper' 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') + @mastercard = credit_card('5399999999999999', brand: 'mastercard') @declined_card = credit_card('1111111111111111') - @credit_card_d3d = credit_card('4000000000000002', :verification_value => '111') + @credit_card_d3d = credit_card('4000000000000002', verification_value: '111') + @credit_card_d3d_2_challenge = credit_card('5130257474533310', verification_value: '123') + @credit_card_d3d_2_frictionless = credit_card('4186455175836497', verification_value: '123') @options = { - :order_id => generate_unique_id[0...30], - :billing_address => address, - :description => 'Store Purchase', - :currency => fixtures(:ogone)[:currency] || 'EUR', - :origin => 'STORE' + order_id: generate_unique_id[0...30], + billing_address: address, + description: 'Store Purchase', + currency: fixtures(:ogone)[:currency] || 'EUR', + origin: 'STORE' + } + @options_browser_info = { + three_ds_2: { + browser_info: { + width: 390, + height: 400, + depth: 24, + timezone: 300, + user_agent: 'Spreedly Agent', + java: false, + javascript: true, + language: 'en-US', + browser_size: '05', + accept_header: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8' + } + } } end def test_successful_purchase - assert response = @gateway.purchase(@amount, @credit_card, @options) + assert response = @gateway_3ds.purchase(@amount, @credit_card, @options) assert_success response assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message assert_equal @options[:order_id], response.order_id end def test_successful_purchase_with_utf8_encoding_1 - assert response = @gateway.purchase(@amount, credit_card('4000100011112224', :first_name => 'Rémy', :last_name => 'Fröåïør'), @options) + assert response = @gateway_3ds.purchase(@amount, credit_card('4000100011112224', first_name: 'Rémy', last_name: 'Fröåïør'), @options) assert_success response assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message end def test_successful_purchase_with_utf8_encoding_2 - assert response = @gateway.purchase(@amount, credit_card('4000100011112224', :first_name => 'ワタシ', :last_name => 'ёжзийклмнопрсуфхцч'), @options) + assert response = @gateway_3ds.purchase(@amount, credit_card('4000100011112224', first_name: 'ワタシ', last_name: 'ёжзийклмнопрсуфхцч'), @options) assert_success response assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message end @@ -61,37 +83,96 @@ def test_successful_purchase_with_utf8_encoding_2 # NOTE: You have to set the "Hash algorithm" to "SHA-512" in the "Technical information"->"Global security parameters" # section of your account admin on https://secure.ogone.com/ncol/test/frame_ogone.asp before running this test def test_successful_purchase_with_signature_encryptor_to_sha512 - gateway = OgoneGateway.new(fixtures(:ogone).merge(:signature_encryptor => 'sha512')) + gateway = OgoneGateway.new(fixtures(:ogone).merge(signature_encryptor: 'sha512')) assert response = gateway.purchase(@amount, @credit_card, @options) assert_success response assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message end # NOTE: You have to contact Ogone to make sure your test account allow 3D Secure transactions before running this test - def test_successful_purchase_with_3d_secure - assert response = @gateway.purchase(@amount, @credit_card_d3d, @options.merge(:d3d => true)) + def test_successful_purchase_with_3d_secure_v1 + assert response = @gateway_3ds.purchase(@amount, @credit_card_d3d, @options.merge(@options_browser_info, d3d: true)) + assert_success response + assert_equal '46', response.params['STATUS'] + assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message + assert response.params['HTML_ANSWER'] + assert Base64.decode64(response.params['HTML_ANSWER']) + end + + def test_successful_purchase_with_3d_secure_v2 + assert response = @gateway_3ds.purchase(@amount, @credit_card_d3d_2_challenge, @options_browser_info.merge(d3d: true)) assert_success response assert_equal '46', response.params['STATUS'] assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message assert response.params['HTML_ANSWER'] + assert Base64.decode64(response.params['HTML_ANSWER']) + end + + def test_successful_purchase_with_3d_secure_v2_flag_updated + options = @options_browser_info.merge(three_d_secure: { required: true }) + assert response = @gateway_3ds.purchase(@amount, @credit_card_d3d, options) + assert_success response + assert_equal '46', response.params['STATUS'] + assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message + assert response.params['HTML_ANSWER'] + assert Base64.decode64(response.params['HTML_ANSWER']) + end + + def test_successful_purchase_with_3d_secure_v2_frictionless + assert response = @gateway_3ds.purchase(@amount, @credit_card_d3d_2_frictionless, @options_browser_info.merge(d3d: true)) + assert_success response + assert_includes response.params, 'PAYID' + assert_equal '0', response.params['NCERROR'] + assert_equal '9', response.params['STATUS'] + assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message + end + + def test_successful_purchase_with_3d_secure_v2_recomended_parameters + options = @options.merge(@options_browser_info) + assert response = @gateway_3ds.authorize(@amount, @credit_card_d3d_2_challenge, options.merge(d3d: true)) + assert_success response + assert_equal '46', response.params['STATUS'] + assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message + assert response.params['HTML_ANSWER'] + assert Base64.decode64(response.params['HTML_ANSWER']) + end + + def test_successful_purchase_with_3d_secure_v2_optional_parameters + options = @options.merge(@options_browser_info).merge(mpi: { threeDSRequestorChallengeIndicator: '04' }) + assert response = @gateway_3ds.authorize(@amount, @credit_card_d3d_2_challenge, options.merge(d3d: true)) + assert_success response + assert_equal '46', response.params['STATUS'] + assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message + assert response.params['HTML_ANSWER'] + assert Base64.decode64(response.params['HTML_ANSWER']) + end + + def test_unsuccessful_purchase_with_3d_secure_v2 + @credit_card_d3d_2_challenge.number = '4419177274955460' + assert response = @gateway_3ds.purchase(@amount, @credit_card_d3d_2_challenge, @options_browser_info.merge(d3d: true)) + assert_failure response + assert_includes response.params, 'PAYID' + assert_equal response.params['NCERROR'], '40001134' + assert_equal response.params['STATUS'], '2' + assert_equal response.params['NCERRORPLUS'], 'Authentication failed. Please retry or cancel.' end def test_successful_with_non_numeric_order_id @options[:order_id] = "##{@options[:order_id][0...26]}.12" - assert response = @gateway.purchase(@amount, @credit_card, @options) + assert response = @gateway_3ds.purchase(@amount, @credit_card, @options) assert_success response assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message end def test_successful_purchase_without_explicit_order_id @options.delete(:order_id) - assert response = @gateway.purchase(@amount, @credit_card, @options) + assert response = @gateway_3ds.purchase(@amount, @credit_card, @options) assert_success response assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message end def test_successful_purchase_with_custom_eci - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(:eci => 4)) + assert response = @gateway_3ds.purchase(@amount, @credit_card, @options.merge(eci: 4)) assert_success response assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message end @@ -99,8 +180,7 @@ def test_successful_purchase_with_custom_eci # NOTE: You have to allow USD as a supported currency in the "Account"->"Currencies" # section of your account admin on https://secure.ogone.com/ncol/test/frame_ogone.asp before running this test def test_successful_purchase_with_custom_currency_at_the_gateway_level - gateway = OgoneGateway.new(fixtures(:ogone).merge(:currency => 'USD')) - assert response = gateway.purchase(@amount, @credit_card) + assert response = @gateway_3ds.purchase(@amount, @credit_card) assert_success response assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message end @@ -108,92 +188,90 @@ def test_successful_purchase_with_custom_currency_at_the_gateway_level # NOTE: You have to allow USD as a supported currency in the "Account"->"Currencies" # section of your account admin on https://secure.ogone.com/ncol/test/frame_ogone.asp before running this test def test_successful_purchase_with_custom_currency - gateway = OgoneGateway.new(fixtures(:ogone).merge(:currency => 'EUR')) - assert response = gateway.purchase(@amount, @credit_card, @options.merge(:currency => 'USD')) + assert response = @gateway_3ds.purchase(@amount, @credit_card, @options.merge(currency: 'USD')) assert_success response assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message end def test_unsuccessful_purchase - assert response = @gateway.purchase(@amount, @declined_card, @options) + assert response = @gateway_3ds.purchase(@amount, @declined_card, @options) assert_failure response - assert_equal 'No brand', response.message + assert_equal 'No brand or invalid card number', response.message end def test_successful_authorize_with_mastercard - assert auth = @gateway.authorize(@amount, @mastercard, @options) + assert auth = @gateway_3ds.authorize(@amount, @mastercard, @options) assert_success auth assert_equal BarclaysEpdqExtraPlusGateway::SUCCESS_MESSAGE, auth.message end def test_authorize_and_capture - assert auth = @gateway.authorize(@amount, @credit_card, @options) + assert auth = @gateway_3ds.authorize(@amount, @credit_card, @options) assert_success auth assert_equal OgoneGateway::SUCCESS_MESSAGE, auth.message assert auth.authorization - assert capture = @gateway.capture(@amount, auth.authorization) + assert capture = @gateway_3ds.capture(@amount, auth.authorization) assert_success capture end def test_authorize_and_capture_with_custom_eci - assert auth = @gateway.authorize(@amount, @credit_card, @options.merge(:eci => 4)) + assert auth = @gateway_3ds.authorize(@amount, @credit_card, @options.merge(eci: 4)) assert_success auth assert_equal OgoneGateway::SUCCESS_MESSAGE, auth.message assert auth.authorization - assert capture = @gateway.capture(@amount, auth.authorization, @options) + assert capture = @gateway_3ds.capture(@amount, auth.authorization, @options) assert_success capture end def test_unsuccessful_capture - assert response = @gateway.capture(@amount, '') + assert response = @gateway_3ds.capture(@amount, '') assert_failure response - assert_equal 'No card no, no exp date, no brand', response.message + assert_equal 'No card no, no exp date, no brand or invalid card number', response.message end def test_successful_void - assert auth = @gateway.authorize(@amount, @credit_card, @options) + assert auth = @gateway_3ds.authorize(@amount, @credit_card, @options) assert_success auth assert auth.authorization - assert void = @gateway.void(auth.authorization) + assert void = @gateway_3ds.void(auth.authorization) assert_equal OgoneGateway::SUCCESS_MESSAGE, auth.message assert_success void end def test_successful_store - assert response = @gateway.store(@credit_card, :billing_id => 'test_alias') + assert response = @gateway_3ds.store(@credit_card, billing_id: 'test_alias') assert_success response - assert purchase = @gateway.purchase(@amount, 'test_alias') + assert purchase = @gateway_3ds.purchase(@amount, 'test_alias') assert_success purchase end def test_successful_store_with_store_amount_at_the_gateway_level - gateway = OgoneGateway.new(fixtures(:ogone).merge(:store_amount => 100)) - assert response = gateway.store(@credit_card, :billing_id => 'test_alias') + assert response = @gateway_3ds.store(@credit_card, billing_id: 'test_alias') assert_success response - assert purchase = gateway.purchase(@amount, 'test_alias') + assert purchase = @gateway_3ds.purchase(@amount, 'test_alias') assert_success purchase end def test_successful_store_generated_alias - assert response = @gateway.store(@credit_card) + assert response = @gateway_3ds.store(@credit_card) assert_success response - assert purchase = @gateway.purchase(@amount, response.billing_id) + assert purchase = @gateway_3ds.purchase(@amount, response.billing_id) assert_success purchase end def test_successful_refund - assert purchase = @gateway.purchase(@amount, @credit_card, @options) + assert purchase = @gateway_3ds.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount, purchase.authorization, @options) + assert refund = @gateway_3ds.refund(@amount, purchase.authorization, @options) assert_success refund assert refund.authorization assert_equal OgoneGateway::SUCCESS_MESSAGE, refund.message end def test_unsuccessful_refund - assert purchase = @gateway.purchase(@amount, @credit_card, @options) + assert purchase = @gateway_3ds.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount+1, purchase.authorization, @options) # too much refund requested + assert refund = @gateway_3ds.refund(@amount + 1, purchase.authorization, @options) # too much refund requested assert_failure refund assert refund.authorization assert_equal 'Overflow in refunds requests', refund.message @@ -207,36 +285,36 @@ def test_successful_credit end def test_successful_verify - response = @gateway.verify(@credit_card, @options) + response = @gateway_3ds.verify(@credit_card, @options) assert_success response assert_equal 'The transaction was successful', response.message end def test_failed_verify - response = @gateway.verify(@declined_card, @options) + response = @gateway_3ds.verify(@declined_card, @options) assert_failure response - assert_equal 'No brand', response.message + assert_equal 'No brand or invalid card number', response.message end def test_reference_transactions # Setting an alias - assert response = @gateway.purchase(@amount, credit_card('4000100011112224'), @options.merge(:billing_id => 'awesomeman', :order_id=>Time.now.to_i.to_s+'1')) + assert response = @gateway_3ds.purchase(@amount, credit_card('4000100011112224'), @options.merge(billing_id: 'awesomeman', order_id: Time.now.to_i.to_s + '1')) assert_success response # Updating an alias - assert response = @gateway.purchase(@amount, credit_card('4111111111111111'), @options.merge(:billing_id => 'awesomeman', :order_id=>Time.now.to_i.to_s+'2')) + assert response = @gateway_3ds.purchase(@amount, credit_card('4111111111111111'), @options.merge(billing_id: 'awesomeman', order_id: Time.now.to_i.to_s + '2')) assert_success response # Using an alias (i.e. don't provide the credit card) - assert response = @gateway.purchase(@amount, 'awesomeman', @options.merge(:order_id => Time.now.to_i.to_s + '3')) + assert response = @gateway_3ds.purchase(@amount, 'awesomeman', @options.merge(order_id: Time.now.to_i.to_s + '3')) assert_success response end def test_invalid_login gateway = OgoneGateway.new( - login: 'login', - user: 'user', - password: 'password', - signature: 'signature' - ) + login: 'login', + user: 'user', + password: 'password', + signature: 'signature' + ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response end diff --git a/test/remote/gateways/remote_omise_test.rb b/test/remote/gateways/remote_omise_test.rb index 4b5be3064b0..95ae0d30b66 100644 --- a/test/remote/gateways/remote_omise_test.rb +++ b/test/remote/gateways/remote_omise_test.rb @@ -6,7 +6,7 @@ def setup @amount = 8888 @credit_card = credit_card('4242424242424242') @declined_card = credit_card('4255555555555555') - @invalid_cvc = credit_card('4111111111160001', {verification_value: ''}) + @invalid_cvc = credit_card('4111111111160001', { verification_value: '' }) @options = { description: 'Active Merchant', email: 'active.merchant@testing.test', @@ -54,7 +54,7 @@ def test_successful_purchase_after_store end def test_failed_purchase_with_token - response = @gateway.purchase(@amount, nil, {token_id: 'tokn_invalid_12345'}) + response = @gateway.purchase(@amount, nil, { token_id: 'tokn_invalid_12345' }) assert_failure response end @@ -95,9 +95,8 @@ def test_successful_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase assert_equal purchase.params['amount'], @amount - response = @gateway.refund(@amount-1000, purchase.authorization) + response = @gateway.refund(@amount - 1000, purchase.authorization) assert_success response - assert_equal @amount-1000, response.params['amount'] + assert_equal @amount - 1000, response.params['amount'] end - end diff --git a/test/remote/gateways/remote_openpay_test.rb b/test/remote/gateways/remote_openpay_test.rb index 54db72973a2..b7d336d78d6 100644 --- a/test/remote/gateways/remote_openpay_test.rb +++ b/test/remote/gateways/remote_openpay_test.rb @@ -16,6 +16,19 @@ def setup end def test_successful_purchase + @options[:email] = '%d@example.org' % Time.now + @options[:name] = 'Customer name' + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_nil response.message + end + + def test_successful_purchase_with_mexico_url + @options[:email] = '%d@example.org' % Time.now + @options[:name] = 'Customer name' + credentials = fixtures(:openpay).merge(merchant_country: 'MX') + @gateway = OpenpayGateway.new(credentials) + assert response = @gateway.purchase(@amount, @credit_card, @options) assert_success response assert_nil response.message @@ -31,7 +44,7 @@ def test_successful_purchase_with_email def test_unsuccessful_purchase assert response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response - assert_equal 'The card was declined', response.message + assert_equal 'The card was declined by the bank', response.message end def test_successful_refund @@ -48,7 +61,7 @@ def test_successful_refund end def test_unsuccessful_refund - assert response = @gateway.refund(@amount, '1', @options) + assert response = @gateway.refund(@amount, '1', @options) assert_failure response assert_not_nil response.message end @@ -69,7 +82,7 @@ def test_successful_authorize_with_email def test_unsuccessful_authorize assert response = @gateway.authorize(@amount, @declined_card, @options) assert_failure response - assert_equal 'The card was declined', response.message + assert_equal 'The card was declined by the bank', response.message end def test_successful_capture @@ -165,7 +178,7 @@ def test_successful_verify def test_unsuccessful_verify response = @gateway.verify(@declined_card, @options) assert_failure response - assert_match /The card was declined/, response.message + assert_match(/The card was declined/, response.message) end def test_invalid_login diff --git a/test/remote/gateways/remote_opp_test.rb b/test/remote/gateways/remote_opp_test.rb index 2652c756ad4..6e5a9dacdbf 100644 --- a/test/remote/gateways/remote_opp_test.rb +++ b/test/remote/gateways/remote_opp_test.rb @@ -1,14 +1,13 @@ require 'test_helper' class RemoteOppTest < Test::Unit::TestCase - def setup @gateway = OppGateway.new(fixtures(:opp)) @amount = 100 - @valid_card = credit_card('4200000000000000', month: 05, year: 2018) - @invalid_card = credit_card('4444444444444444', month: 05, year: 2018) - @amex_card = credit_card('377777777777770 ', month: 05, year: 2018, brand: 'amex', verification_value: '1234') + @valid_card = credit_card('4200000000000000', month: 05, year: Date.today.year + 2) + @invalid_card = credit_card('4444444444444444', month: 05, year: Date.today.year + 2) + @amex_card = credit_card('377777777777770 ', month: 05, year: Date.today.year + 2, brand: 'amex', verification_value: '1234') request_type = 'complete' # 'minimal' || 'complete' time = Time.now.to_i @@ -16,44 +15,44 @@ def setup @complete_request_options = { order_id: "Order #{time}", merchant_transaction_id: "active_merchant_test_complete #{time}", - address: address, + address:, description: 'Store Purchase - Books', -# riskWorkflow: true, -# testMode: 'EXTERNAL' # or 'INTERNAL', valid only for test system - - billing_address: { - address1: '123 Test Street', - city: 'Test', - state: 'TE', - zip: 'AB12CD', - country: 'GB', - }, - shipping_address: { - name: 'Muton DeMicelis', - address1: 'My Street On Upiter, Apt 3.14/2.78', - city: 'Munich', - state: 'Bov', - zip: '81675', - country: 'DE', - }, - customer: { - merchant_customer_id: 'your merchant/customer id', - givenName: 'Jane', - surname: 'Jones', - birthDate: '1965-05-01', - phone: '(?!?)555-5555', - mobile: '(?!?)234-23423', - email: 'jane@jones.com', - company_name: 'JJ Ltd.', - identification_doctype: 'PASSPORT', - identification_docid: 'FakeID2342431234123', - ip: ip, - }, + # riskWorkflow: true, + # testMode: 'EXTERNAL' # or 'INTERNAL', valid only for test system + + billing_address: { + address1: '123 Test Street', + city: 'Test', + state: 'TE', + zip: 'AB12CD', + country: 'GB' + }, + shipping_address: { + name: 'Muton DeMicelis', + address1: 'My Street On Upiter, Apt 3.14/2.78', + city: 'Munich', + state: 'Bov', + zip: '81675', + country: 'DE' + }, + customer: { + merchant_customer_id: 'your merchant/customer id', + givenName: 'Jane', + surname: 'Jones', + birthDate: '1965-05-01', + phone: '(?!?)555-5555', + mobile: '(?!?)234-23423', + email: 'jane@jones.com', + company_name: 'JJ Ltd.', + identification_doctype: 'PASSPORT', + identification_docid: 'FakeID2342431234123', + ip: + } } @minimal_request_options = { order_id: "Order #{time}", - description: 'Store Purchase - Books', + description: 'Store Purchase - Books' } @complete_request_options['customParameters[SHOPPER_test124TestName009]'] = 'customParameters_test' @@ -67,7 +66,7 @@ def setup @options = @complete_request_options if request_type == 'complete' end -# ****************************************** SUCCESSFUL TESTS ****************************************** + # ****************************************** SUCCESSFUL TESTS ****************************************** def test_successful_purchase @options[:description] = __method__ @@ -140,7 +139,7 @@ def test_successful_partial_capture auth = @gateway.authorize(@amount, @valid_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture assert_match %r{Request successfully processed}, capture.message end @@ -150,7 +149,7 @@ def test_successful_partial_refund purchase = @gateway.purchase(@amount, @valid_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount-1, purchase.authorization) + assert refund = @gateway.refund(@amount - 1, purchase.authorization) assert_success refund assert_match %r{Request successfully processed}, refund.message end @@ -162,7 +161,7 @@ def test_successful_verify assert_match %r{Request successfully processed}, response.message end -# ****************************************** FAILURE TESTS ****************************************** + # ****************************************** FAILURE TESTS ****************************************** def test_failed_purchase @options[:description] = __method__ @@ -199,7 +198,7 @@ def test_failed_void assert_match %r{reversal needs at least one successful transaction}, response.message end -# ************************************** TRANSCRIPT SCRUB ****************************************** + # ************************************** TRANSCRIPT SCRUB ****************************************** def test_transcript_scrubbing assert @gateway.supports_scrubbing? @@ -211,6 +210,6 @@ def test_transcript_scrubbing assert_scrubbed(@valid_card.number, transcript) assert_scrubbed(@valid_card.verification_value, transcript) - assert_scrubbed(@gateway.options[:password], transcript) + assert_scrubbed(@gateway.options[:access_token], transcript) end end diff --git a/test/remote/gateways/remote_optimal_payment_test.rb b/test/remote/gateways/remote_optimal_payment_test.rb index f42849579d9..045f9e22938 100644 --- a/test/remote/gateways/remote_optimal_payment_test.rb +++ b/test/remote/gateways/remote_optimal_payment_test.rb @@ -7,13 +7,14 @@ def setup @amount = 100 @declined_amount = 5 @credit_card = credit_card('4387751111011') + @expired_card = credit_card('4387751111011', month: 12, year: 2019) @options = { - :order_id => '1', - :billing_address => address, - :description => 'Basic Subscription', - :email => 'email@example.com', - :ip => '1.2.3.4' + order_id: '1', + billing_address: address, + description: 'Basic Subscription', + email: 'email@example.com', + ip: '1.2.3.4' } end @@ -24,7 +25,7 @@ def test_successful_purchase end def test_unsuccessful_purchase_with_shipping_address - @options.merge!(:shipping_address => address) + @options[:shipping_address] = address assert response = @gateway.purchase(@amount, @credit_card, @options) assert_success response assert_equal 'no_error', response.message @@ -51,6 +52,57 @@ def test_purchase_with_no_cvv assert_equal 'no_error', response.message end + def test_successful_verify + response = @gateway.verify(@credit_card, @options) + assert_success response + assert_equal 'no_error', response.message + end + + def test_stored_data_auth_and_capture_after_store + response = @gateway.store(@credit_card, @options) + assert_success response + assert_equal 'no_error', response.message + + assert auth = @gateway.stored_authorize(@amount, response.authorization) + assert_success auth + assert_equal 'no_error', auth.message + assert auth.authorization + + assert capture = @gateway.capture(@amount, auth.authorization) + assert_success capture + end + + def test_stored_data_purchase_after_store + response = @gateway.store(@credit_card, @options) + assert_success response + assert_equal 'no_error', response.message + + assert stored_purchase = @gateway.stored_purchase(@amount, response.authorization) + assert_success stored_purchase + end + + def test_stored_data_auth_after_failed_store + response = @gateway.store(@expired_card, @options) + assert_failure response + assert_not_nil response.authorization + assert_equal 'ERROR', response.params['decision'] + + assert auth = @gateway.stored_authorize(@amount, response.authorization) + assert_failure auth + assert_equal 'ERROR', auth.params['decision'] + end + + def test_stored_data_purchase_after_failed_store + response = @gateway.store(@expired_card, @options) + assert_failure response + assert_not_nil response.authorization + assert_equal 'ERROR', response.params['decision'] + + assert stored_purchase = @gateway.stored_purchase(@amount, response.authorization) + assert_failure stored_purchase + assert_equal 'ERROR', stored_purchase.params['decision'] + end + def test_authorize_and_capture assert auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth @@ -135,10 +187,10 @@ def test_overloaded_stored_data_authorize_and_capture def test_invalid_login gateway = OptimalPaymentGateway.new( - :account_number => '1', - :store_id => 'bad', - :password => 'bad' - ) + account_number: '1', + store_id: 'bad', + password: 'bad' + ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal 'invalid merchant account', response.message diff --git a/test/remote/gateways/remote_orbital_test.rb b/test/remote/gateways/remote_orbital_test.rb index 20b1b018431..dfa6012d26b 100644 --- a/test/remote/gateways/remote_orbital_test.rb +++ b/test/remote/gateways/remote_orbital_test.rb @@ -1,27 +1,42 @@ -require 'test_helper.rb' +require 'test_helper' class RemoteOrbitalGatewayTest < Test::Unit::TestCase def setup Base.mode = :test @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 - @credit_card = credit_card('4112344112344113') + @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') + @google_pay_card = network_tokenization_credit_card( + '4777777777777778', + payment_cryptogram: 'BwAQCFVQdwEAABNZI1B3EGLyGC8=', + verification_value: '987', + source: :google_pay, + brand: 'visa', + eci: '5' + ) @options = { - :order_id => generate_unique_id, - :address => address, - :merchant_id => 'merchant1234' + order_id: generate_unique_id, + address:, + merchant_id: 'merchant1234' } @cards = { - :visa => '4788250000028291', - :mc => '5454545454545454', - :amex => '371449635398431', - :ds => '6011000995500000', - :diners => '36438999960016', - :jcb => '3566002020140006'} + visa: '4556761029983886', + mc: '5454545454545454', + amex: '371449635398431', + ds: '6011000995500000', + diners: '36438999960016', + jcb: '3566002020140006' + } @level_2_options = { tax_indicator: '1', @@ -37,18 +52,83 @@ def setup city: address[:city], state: address[:state], 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 = { + freight_amount: 1, + duty_amount: 1, + ship_from_zip: 27604, + dest_country: 'USA', + discount_amount: 1, + vat_tax: 1, + vat_rate: 25, + invoice_discount_treatment: 1, + tax_treatment: 1, + ship_vat_rate: 10, + unique_vat_invoice_ref: 'ABC123' + } + + @level_2_options_master = { + freight_amount: 1, + duty_amount: 1, + ship_from_zip: 27604, + dest_country: 'USA', + alt_tax: 1, + alt_ind: 25 } + @line_items_visa = [ + { + desc: 'another item', + prod_cd: generate_unique_id[0, 11], + qty: 1, + u_o_m: 'LBR', + tax_amt: 250, + tax_rate: 10000, + line_tot: 2500, + disc: 250, + comm_cd: '00584', + unit_cost: 2500, + gross_net: 'Y', + tax_type: 'sale', + debit_ind: 'C' + }, + { + desc: 'something else', + prod_cd: generate_unique_id[0, 11], + qty: 1, + u_o_m: 'LBR', + tax_amt: 125, + tax_rate: 5000, + line_tot: 2500, + disc: 250, + comm_cd: '00584', + unit_cost: 250000, + gross_net: 'Y', + tax_type: 'sale', + debit_ind: 'C' + } + ] + @test_suite = [ - {:card => :visa, :AVSzip => 11111, :CVD => 111, :amount => 3000}, - {:card => :visa, :AVSzip => 33333, :CVD => nil, :amount => 3801}, - {:card => :mc, :AVSzip => 44444, :CVD => nil, :amount => 4100}, - {:card => :mc, :AVSzip => 88888, :CVD => 666, :amount => 1102}, - {:card => :amex, :AVSzip => 55555, :CVD => nil, :amount => 105500}, - {:card => :amex, :AVSzip => 66666, :CVD => 2222, :amount => 7500}, - {:card => :ds, :AVSzip => 77777, :CVD => nil, :amount => 1000}, - {:card => :ds, :AVSzip => 88888, :CVD => 444, :amount => 6303}, - {:card => :jcb, :AVSzip => 33333, :CVD => nil, :amount => 2900}] + { card: :visa, AVSzip: 11111, CVD: 111, amount: 3000 }, + { card: :visa, AVSzip: 33333, CVD: nil, amount: 3801 }, + { card: :mc, AVSzip: 44444, CVD: nil, amount: 4100 }, + { card: :mc, AVSzip: 88888, CVD: 666, amount: 1102 }, + { card: :amex, AVSzip: 55555, CVD: nil, amount: 105500 }, + { card: :amex, AVSzip: 66666, CVD: 2222, amount: 7500 }, + { card: :ds, AVSzip: 77777, CVD: nil, amount: 1000 }, + { card: :ds, AVSzip: 88888, CVD: 444, amount: 6303 }, + { card: :jcb, AVSzip: 33333, CVD: nil, amount: 2900 } + ] end def test_successful_purchase @@ -58,15 +138,33 @@ def test_successful_purchase end def test_successful_purchase_with_soft_descriptor_hash - assert response = @gateway.purchase( - @amount, @credit_card, @options.merge( - soft_descriptors: { - merchant_name: 'Merch', - product_description: 'Description', - merchant_email: 'email@example', - } - ) + options = @options.merge( + soft_descriptors: { + merchant_name: 'Merch', + product_description: 'Description', + merchant_email: 'email@example' + } + ) + assert response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_card_indicators + options = @options.merge( + card_indicators: 'y' ) + assert response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_card_indicators_and_line_items + options = @options.merge( + line_items: @line_items, + card_indicators: 'y' + ) + assert response = @gateway.purchase(@amount, @credit_card, options) assert_success response assert_equal 'Approved', response.message end @@ -78,22 +176,40 @@ def test_successful_purchase_with_level_2_data assert_equal 'Approved', response.message end + def test_successful_purchase_with_level_3_data + response = @gateway.purchase(@amount, @credit_card, @options.merge(level_2_data: @level_2_options, level_3_data: @level_3_options_visa, line_items: @line_items_visa)) + + assert_success response + assert_equal 'Approved', response.message + end + def test_successful_purchase_with_visa_network_tokenization_credit_card_with_eci - network_card = network_tokenization_credit_card('4788250000028291', + network_card = network_tokenization_credit_card( + '4788250000028291', payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', transaction_id: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', verification_value: '111', brand: 'visa', eci: '5' ) - assert response = @gateway.purchase(3000, network_card, @options) + # Ensure that soft descriptor fields don't conflict with network token data in schema + options = @options.merge( + soft_descriptors: { + merchant_name: 'Merch', + product_description: 'Description', + merchant_email: 'email@example' + } + ) + + assert response = @gateway.purchase(3000, network_card, options) assert_success response assert_equal 'Approved', response.message assert_false response.authorization.blank? 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', @@ -105,8 +221,51 @@ def test_successful_purchase_with_master_card_network_tokenization_credit_card assert_false response.authorization.blank? end + def test_successful_purchase_with_sca_recurring_master_card + cc = credit_card('5555555555554444', first_name: 'Joe', last_name: 'Smith', + month: '12', year: '2022', brand: 'master', verification_value: '999') + options_local = { + three_d_secure: { + eci: '7', + xid: 'TESTXID', + cavv: 'AAAEEEDDDSSSAAA2243234', + ds_transaction_id: '97267598FAE648F28083C23433990FBC', + version: '2.2.0' + }, + sca_recurring: 'Y' + } + + assert response = @three_ds_gateway.purchase(100, cc, @options.merge(options_local)) + + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_successful_purchase_with_sca_merchant_initiated_master_card + cc = credit_card('5555555555554444', first_name: 'Joe', last_name: 'Smith', + month: '12', year: '2022', brand: 'master', verification_value: '999') + options_local = { + three_d_secure: { + eci: '7', + xid: 'TESTXID', + cavv: 'AAAEEEDDDSSSAAA2243234', + ds_transaction_id: '97267598FAE648F28083C23433990FBC', + version: '2.2.0' + }, + sca_merchant_initiated: 'Y' + } + + assert response = @three_ds_gateway.purchase(100, cc, @options.merge(options_local)) + + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + 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', @@ -119,7 +278,8 @@ 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', @@ -131,6 +291,202 @@ def test_successful_purchase_with_discover_network_tokenization_credit_card assert_false response.authorization.blank? end + def test_successful_purchase_with_echeck + assert response = @echeck_gateway.purchase(20, @echeck, @options) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_successful_purchase_with_echeck_having_written_authorization + @options[:auth_method] = 'W' + assert response = @echeck_gateway.purchase(20, @echeck, @options) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_successful_purchase_with_echeck_having_internet_authorization + @options[:auth_method] = 'I' + assert response = @echeck_gateway.purchase(20, @echeck, @options) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_successful_purchase_with_echeck_having_telephonic_authorization + @options[:auth_method] = 'T' + assert response = @echeck_gateway.purchase(20, @echeck, @options) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_successful_purchase_with_echeck_having_arc_authorization + test_check = check(account_number: '000000000', account_type: 'checking', routing_number: '072403004') + assert response = @echeck_gateway.purchase(20, test_check, @options.merge({ auth_method: 'A' })) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_failed_missing_serial_for_arc_with_echeck + assert_raise do + test_check = { account_type: 'savings', routing_number: '072403004' } + @echeck_gateway.purchase(20, test_check, @options.merge({ auth_method: 'A' })) + end + end + + def test_successful_purchase_with_echeck_having_pop_authorization + test_check = check(account_number: '000000000', account_type: 'savings', routing_number: '072403004') + assert response = @echeck_gateway.purchase(20, test_check, @options.merge({ auth_method: 'P', terminal_city: 'CO', terminal_state: 'IL', image_reference_number: '00000' })) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_failed_missing_serial_for_pop_with_echeck + assert_raise do + test_check = { account_type: 'savings', routing_number: '072403004' } + @echeck_gateway.purchase(20, test_check, @options.merge({ auth_method: 'P' })) + end + end + + def test_successful_purchase_with_echeck_on_same_day + @options[:same_day] = 'Y' + assert response = @echeck_gateway.purchase(20, @echeck, @options) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_successful_purchase_with_echeck_on_next_day + @options[:same_day] = 'N' + assert response = @echeck_gateway.purchase(20, @echeck, @options) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_successful_purchase_with_commercial_echeck + commercial_echeck = check(account_number: '072403004', account_type: 'checking', account_holder_type: 'business', routing_number: '072403004') + + assert response = @echeck_gateway.purchase(20, commercial_echeck, @options) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_successful_purchase_with_mit_stored_credentials + mit_stored_credentials = { + mit_msg_type: 'MUSE', + mit_stored_credential_ind: 'Y', + mit_submitted_transaction_id: 'abcdefg12345678' + } + + response = @gateway.purchase(@amount, @credit_card, @options.merge(mit_stored_credentials)) + + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_cit_stored_credentials + cit_options = { + mit_msg_type: 'CUSE', + mit_stored_credential_ind: 'Y' + } + + response = @gateway.purchase(@amount, @credit_card, @options.merge(cit_options)) + + assert_success response + assert_equal 'Approved', response.message + end + + def test_purchase_using_stored_credential_recurring_cit + initial_options = stored_credential_options(:cardholder, :recurring, :initial) + assert purchase = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success purchase + assert_equal 'Approved', purchase.message + assert network_transaction_id = purchase.params['mit_received_transaction_id'] + + used_options = stored_credential_options(:recurring, :cardholder, id: network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + assert_equal 'Approved', purchase.message + end + + def test_purchase_using_stored_credential_recurring_mit + initial_options = stored_credential_options(:merchant, :recurring, :initial) + assert purchase = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success purchase + assert_equal 'Approved', purchase.message + assert network_transaction_id = purchase.params['mit_received_transaction_id'] + + used_options = stored_credential_options(:recurring, :merchant, id: network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + assert_equal 'Approved', purchase.message + end + + def test_successful_purchase_with_overridden_normalized_stored_credentials + stored_credential = { + stored_credential: { + initial_transaction: false, + initiator: 'merchant', + reason_type: 'unscheduled', + network_transaction_id: 'abcdefg12345678' + }, + mit_msg_type: 'MRSB' + } + + response = @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential)) + + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_google_pay + response = @gateway.purchase(@google_pay_amount, @google_pay_card, @options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_force_capture_with_echeck + @options[:force_capture] = true + assert response = @echeck_gateway.purchase(@amount, @echeck, @options) + assert_success response + assert_match 'APPROVAL', response.message + assert_equal 'Approved and Completed', response.params['status_msg'] + assert_false response.authorization.blank? + end + + def test_failed_force_capture_with_echeck_due_to_invalid_amount + @options[:force_capture] = true + assert capture = @echeck_gateway.purchase(-1, @echeck, @options.merge(order_id: '2')) + assert_failure capture + assert_equal '801', capture.params['proc_status'] + assert_equal 'Error validating amount. Must be numerical and greater than 0 [-1]', capture.message + end + + def test_successful_force_capture_with_echeck_prenote_valid_action_code + @options[:force_capture] = true + @options[:action_code] = 'W8' + assert response = @echeck_gateway.authorize(0, @echeck, @options) + assert_success response + assert_match 'APPROVAL', response.message + assert_equal 'Approved and Completed', response.params['status_msg'] + assert_false response.authorization.blank? + end + + def test_failed_force_capture_with_echeck_prenote_invalid_action_code + @options[:force_capture] = true + @options[:action_code] = 'W7' + assert authorize = @echeck_gateway.authorize(0, @echeck, @options) + assert_failure authorize + assert_equal '19784', authorize.params['proc_status'] + assert_equal ' EWS: Invalid Action Code [W7], For Transaction Type [A].', authorize.message + end + # Amounts of x.01 will fail def test_unsuccessful_purchase assert response = @gateway.purchase(101, @declined_card, @options) @@ -140,11 +496,11 @@ def test_unsuccessful_purchase def test_authorize_and_capture amount = @amount - assert auth = @gateway.authorize(amount, @credit_card, @options.merge(:order_id => '2')) + assert auth = @gateway.authorize(amount, @credit_card, @options.merge(order_id: '2')) assert_success auth assert_equal 'Approved', auth.message assert auth.authorization - assert capture = @gateway.capture(amount, auth.authorization, :order_id => '2') + assert capture = @gateway.capture(amount, auth.authorization, order_id: '2') assert_success capture end @@ -157,16 +513,78 @@ def test_successful_authorize_and_capture_with_level_2_data assert_success capture end + def test_successful_authorize_and_capture_with_level_3_data + auth = @gateway.authorize(@amount, @credit_card, @options.merge(level_3_data: @level_3_options)) + assert_success auth + assert_equal 'Approved', auth.message + + capture = @gateway.capture(@amount, auth.authorization, @options.merge(level_3_data: @level_3_options)) + assert_success capture + end + + def test_successful_authorize_and_capture_with_echeck + assert auth = @echeck_gateway.authorize(@amount, @echeck, @options.merge(order_id: '2')) + assert_success auth + assert_equal 'Approved', auth.message + assert auth.authorization + assert capture = @echeck_gateway.capture(@amount, auth.authorization, order_id: '2') + assert_success capture + end + + def test_successful_authorize_and_capture_with_line_items + auth = @gateway.authorize(@amount, @credit_card, @options.merge(level_2_data: @level_2_options, level_3_data: @level_3_options_visa, line_items: @line_items_visa)) + assert_success auth + assert_equal 'Approved', auth.message + + capture = @gateway.capture(@amount, auth.authorization, @options.merge(level_2_data: @level_2_options, level_3_data: @level_3_options_visa, line_items: @line_items_visa)) + assert_success capture + end + + def test_successful_authorize_and_capture_with_google_pay + auth = @gateway.authorize(@amount, @google_pay_card, @options) + assert_success auth + assert_equal 'Approved', auth.message + + capture = @gateway.capture(@amount, auth.authorization, @options) + assert_success capture + end + + def test_failed_authorize_with_echeck_due_to_invalid_amount + assert auth = @echeck_gateway.authorize(-1, @echeck, @options.merge(order_id: '2')) + assert_failure auth + assert_equal '885', auth.params['proc_status'] + assert_equal 'Error validating amount. Must be numeric, equal to zero or greater [-1]', auth.message + end + def test_authorize_and_void - assert auth = @gateway.authorize(@amount, @credit_card, @options.merge(:order_id => '2')) + assert auth = @gateway.authorize(@amount, @credit_card, @options.merge(order_id: '2')) + assert_success auth + assert_equal 'Approved', auth.message + assert auth.authorization + assert void = @gateway.void(auth.authorization, order_id: '2') + assert_success void + end + + def test_successful_authorize_and_void_with_echeck + assert auth = @echeck_gateway.authorize(@amount, @echeck, @options.merge(order_id: '2')) + assert_success auth + assert_equal 'Approved', auth.message + assert auth.authorization + assert void = @echeck_gateway.void(auth.authorization, order_id: '2') + assert_success void + end + + def test_authorize_and_void_using_google_pay + assert auth = @gateway.authorize(@amount, @google_pay_card, @options) assert_success auth assert_equal 'Approved', auth.message + assert auth.authorization - assert void = @gateway.void(auth.authorization, :order_id => '2') + assert void = @gateway.void(auth.authorization) assert_success void end - def test_refund + def test_successful_refund amount = @amount assert response = @gateway.purchase(amount, @credit_card, @options) assert_success response @@ -175,6 +593,50 @@ def test_refund assert_success refund end + def test_successful_refund_with_payment_source + amount = @amount + assert response = @gateway.purchase(amount, @credit_card, @options) + assert_success response + assert response.authorization + + assert refund = @gateway.refund(amount, '', @options.merge({ payment_method: @credit_card })) + assert_success refund + end + + def test_failed_refund + assert refund = @gateway.refund(@amount, '123;123', @options) + assert_failure refund + assert_equal '881', refund.params['proc_status'] + end + + def test_successful_refund_with_google_pay + auth = @gateway.authorize(@amount, @google_pay_card, @options) + assert_success auth + assert_equal 'Approved', auth.message + + capture = @gateway.capture(@amount, auth.authorization, @options) + assert_success capture + + assert capture.authorization + assert refund = @gateway.refund(@amount, capture.authorization, @options) + assert_success refund + end + + def test_successful_refund_with_echeck + assert response = @echeck_gateway.purchase(@amount, @echeck, @options) + assert_success response + assert response.authorization + assert refund = @echeck_gateway.refund(@amount, response.authorization, @options) + assert_success refund + end + + def test_failed_refund_with_echeck_due_to_invalid_authorization + assert refund = @echeck_gateway.refund(@amount, '123;123', @options) + assert_failure refund + assert_equal 'The LIDM you supplied (3F3F3F) does not match with any existing transaction', refund.message + assert_equal '881', refund.params['proc_status'] + end + def test_successful_refund_with_level_2_data amount = @amount assert response = @gateway.purchase(amount, @credit_card, @options.merge(level_2_data: @level_2_options)) @@ -184,20 +646,118 @@ def test_successful_refund_with_level_2_data assert_success refund end + def test_successful_credit + payment_method = credit_card('5454545454545454') + assert response = @gateway.credit(@amount, payment_method, @options) + assert_success response + end + def test_failed_capture assert response = @gateway.capture(@amount, '') assert_failure response assert_equal 'Bad data error', response.message end + def test_authorize_sends_with_retry + assert auth = @echeck_gateway.authorize(@amount, @credit_card, @options.merge(order_id: '4', retry_logic: 'true', trace_number: '989898')) + assert_success auth + assert_equal 'Approved', auth.message + end + + def test_authorize_sends_with_payment_delivery + assert auth = @echeck_gateway.authorize(@amount, @echeck, @options.merge(order_id: '4', payment_delivery: 'A')) + assert_success auth + assert_equal 'Approved', auth.message + end + + def test_default_payment_delivery_with_no_payment_delivery_sent + transcript = capture_transcript(@echeck_gateway) do + response = @echeck_gateway.authorize(@amount, @echeck, @options.merge(order_id: '4')) + assert_equal '1', response.params['approval_status'] + assert_equal '00', response.params['resp_code'] + end + + assert_match(/B/, transcript) + assert_match(/A/, transcript) + end + + def test_sending_echeck_adds_ecp_details_for_refund + assert auth = @echeck_gateway.authorize(@amount, @echeck, @options.merge(order_id: '2')) + assert_success auth + assert_equal 'Approved', auth.message + + capture = @echeck_gateway.capture(@amount, auth.authorization, @options) + assert_success capture + + transcript = capture_transcript(@echeck_gateway) do + refund = @echeck_gateway.refund(@amount, capture.authorization, @options.merge(payment_method: @echeck, action_code: 'W6', auth_method: 'I')) + assert_success refund + assert_equal '1', refund.params['approval_status'] + end + + assert_match(/W6/, transcript) + assert_match(/I/, transcript) + assert_match(/R/, transcript) + end + + def test_sending_credit_card_performs_correct_refund + assert auth = @echeck_gateway.authorize(@amount, @credit_card, @options.merge(order_id: '2')) + assert_success auth + assert_equal 'Approved', auth.message + + capture = @echeck_gateway.capture(@amount, auth.authorization, @options) + assert_success capture + + refund = @echeck_gateway.refund(@amount, capture.authorization, @options) + assert_success refund + end + + def test_echeck_purchase_with_address_responds_with_name + transcript = capture_transcript(@echeck_gateway) do + response = @echeck_gateway.authorize(@amount, @echeck, @options.merge(order_id: '2')) + assert_equal '00', response.params['resp_code'] + assert_equal 'Approved', response.params['status_msg'] + end + + assert_match(/Jim Smith/, transcript) + end + + def test_echeck_purchase_with_no_address_responds_with_name + test_check_no_address = check(name: 'Test McTest') + + transcript = capture_transcript(@echeck_gateway) do + response = @echeck_gateway.authorize(@amount, test_check_no_address, @options.merge(order_id: '2', address: nil, billing_address: nil)) + assert_equal '00', response.params['resp_code'] + assert_equal 'Approved', response.params['status_msg'] + end + + assert_match(/Test McTest/, transcript) + end + + def test_credit_purchase_with_address_responds_with_name + transcript = capture_transcript(@gateway) do + response = @gateway.authorize(@amount, @credit_card, @options.merge(order_id: '2')) + assert_equal '00', response.params['resp_code'] + assert_equal 'Approved', response.params['status_msg'] + end + + assert_match(/Longbob Longsen/, transcript) + end + + def test_credit_purchase_with_no_address_responds_with_no_name + response = @gateway.authorize(@amount, @credit_card, @options.merge(order_id: '2', address: nil, billing_address: nil)) + assert_equal '00', response.params['resp_code'] + assert_equal 'Approved', response.params['status_msg'] + end + # == Certification Tests # ==== Section A def test_auth_only_transactions for suite in @test_suite do amount = suite[:amount] - card = credit_card(@cards[suite[:card]], :verification_value => suite[:CVD]) - @options[:address].merge!(:zip => suite[:AVSzip]) + card = credit_card(@cards[suite[:card]], verification_value: suite[:CVD]) + @options[:address][:zip] = suite[:AVSzip] assert response = @gateway.authorize(amount, card, @options) assert_kind_of Response, response @@ -214,8 +774,8 @@ def test_auth_only_transactions def test_auth_capture_transactions for suite in @test_suite do amount = suite[:amount] - card = credit_card(@cards[suite[:card]], :verification_value => suite[:CVD]) - options = @options; options[:address].merge!(:zip => suite[:AVSzip]) + card = credit_card(@cards[suite[:card]], verification_value: suite[:CVD]) + options = @options; options[:address][:zip] = suite[:AVSzip] assert response = @gateway.purchase(amount, card, options) assert_kind_of Response, response @@ -230,7 +790,7 @@ def test_auth_capture_transactions # ==== Section C def test_mark_for_capture_transactions - [[:visa, 3000],[:mc, 4100],[:amex, 105500],[:ds, 1000],[:jcb, 2900]].each do |suite| + [[:visa, 3000], [:mc, 4100], [:amex, 105500], [:ds, 1000], [:jcb, 2900]].each do |suite| amount = suite[1] card = credit_card(@cards[suite[0]]) assert auth_response = @gateway.authorize(amount, card, @options) @@ -246,7 +806,7 @@ def test_mark_for_capture_transactions # ==== Section D def test_refund_transactions - [[:visa, 1200],[:mc, 1100],[:amex, 105500],[:ds, 1000],[:jcb, 2900]].each do |suite| + [[:visa, 1200], [:mc, 1100], [:amex, 105500], [:ds, 1000], [:jcb, 2900]].each do |suite| amount = suite[1] card = credit_card(@cards[suite[0]]) assert purchase_response = @gateway.purchase(amount, card, @options) @@ -264,7 +824,7 @@ def test_refund_transactions def test_void_transactions [3000, 105500, 2900].each do |amount| assert auth_response = @gateway.authorize(amount, @credit_card, @options) - assert void_response = @gateway.void(auth_response.authorization, @options.merge(:transaction_index => 1)) + assert void_response = @gateway.void(auth_response.authorization, @options.merge(transaction_index: 1)) assert_kind_of Response, void_response # Makes it easier to fill in cert sheet if you print these to the command line @@ -276,12 +836,125 @@ def test_void_transactions def test_successful_verify response = @gateway.verify(@credit_card, @options) assert_success response - assert_equal 'Approved', response.message + assert_equal 'No reason to decline', response.message end - def test_failed_verify - response = @gateway.verify(@declined_card, @options) - assert_failure response + 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) + assert_success response + assert_equal 'No reason to decline', response.message + end + + def test_successful_verify_with_discover_brand + @credit_card.brand = 'discover' + response = @gateway.verify(@credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_unsuccessful_verify_with_invalid_discover_card + @declined_card.brand = 'discover' + response = @gateway.verify(@declined_card, @options) + assert_failure response + assert_equal 'Invalid CC Number', response.message + end + + def test_failed_verify + response = @gateway.verify(@declined_card, @options) + assert_failure response assert_equal 'Invalid CC Number', response.message end @@ -310,4 +983,714 @@ def test_transcript_scrubbing_profile assert_scrubbed(@gateway.options[:login], transcript) assert_scrubbed(@gateway.options[:merchant_id], transcript) end + + def test_transcript_scrubbing_echeck + transcript = capture_transcript(@echeck_gateway) do + @echeck_gateway.purchase(20, @echeck, @options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@echeck.account_number, transcript) + assert_scrubbed(@echeck_gateway.options[:password], transcript) + assert_scrubbed(@echeck_gateway.options[:login], transcript) + assert_scrubbed(@echeck_gateway.options[:merchant_id], transcript) + end + + def test_transcript_scrubbing_network_card + network_card = network_tokenization_credit_card( + '4788250000028291', + payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', + transaction_id: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', + verification_value: '111', + brand: 'visa', + eci: '5' + ) + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, network_card, @options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(network_card.payment_cryptogram, transcript) + end + + private + + def stored_credential_options(*args, id: nil) + @options.merge(order_id: generate_unique_id, + stored_credential: stored_credential(*args, id:)) + end +end + +class BrandSpecificOrbitalTests < RemoteOrbitalGatewayTest + # Additional class for a subset of tests that share setup logic. + # This will run automatically with the rest of the tests in this file, + # or you can specify individual tests by name as you usually would. + def setup + super + + @brand_specific_fixtures = { + visa: { + card: { + number: '4112344112344113', + verification_value: '411', + brand: 'visa' + }, + three_d_secure: { + eci: '5', + cavv: 'AAABAIcJIoQDIzAgVAkiAAAAAAA=', + xid: 'AAABAIcJIoQDIzAgVAkiAAAAAAA=' + }, + address: { + address1: '55 Forever Ave', + address2: '', + city: 'Concord', + state: 'NH', + zip: '03301', + country: 'US' + } + }, + master: { + card: { + number: '5112345112345114', + verification_value: '823', + brand: 'master' + }, + three_d_secure: { + eci: '5', + cavv: 'AAAEEEDDDSSSAAA2243234', + xid: 'Asju1ljfl86bAAAAAACm9zU6aqY=', + version: '2.2.0', + ds_transaction_id: '8dh4htokdf84jrnxyemfiosheuyfjt82jiek' + }, + address: { + address1: 'Byway Street', + address2: '', + city: 'Portsmouth', + state: 'MA', + zip: '67890', + country: 'US', + phone: '5555555555' + } + }, + american_express: { + card: { + number: '371144371144376', + verification_value: '1234', + brand: 'american_express' + }, + three_d_secure: { + eci: '5', + cavv: 'AAABBWcSNIdjeUZThmNHAAAAAAA=', + xid: 'AAABBWcSNIdjeUZThmNHAAAAAAA=' + }, + address: { + address1: '4 Northeastern Blvd', + address2: '', + city: 'Salem', + state: 'NH', + zip: '03105', + country: 'US' + } + }, + discover: { + card: { + number: '6011016011016011', + verification_value: '613', + brand: 'discover' + }, + three_d_secure: { + eci: '6', + cavv: 'Asju1ljfl86bAAAAAACm9zU6aqY=', + ds_transaction_id: '32b274ee-582d-4232-b20a-363f2acafa5a' + }, + address: { + address1: '1 Northeastern Blvd', + address2: '', + city: 'Bedford', + state: 'NH', + zip: '03109', + country: 'US' + } + } + } + end + + def test_successful_3ds_authorization_with_visa + cc = brand_specific_card(@brand_specific_fixtures[:visa][:card]) + options = brand_specific_3ds_options(@brand_specific_fixtures[:visa]) + + assert response = @three_ds_gateway.authorize(100, cc, options) + assert_success_with_authorization(response) + end + + def test_successful_3ds_purchase_with_visa + cc = brand_specific_card(@brand_specific_fixtures[:visa][:card]) + options = brand_specific_3ds_options(@brand_specific_fixtures[:visa]) + + assert response = @three_ds_gateway.purchase(100, cc, options) + assert_success_with_authorization(response) + end + + def test_successful_3ds_authorization_with_mastercard + cc = brand_specific_card(@brand_specific_fixtures[:master][:card]) + options = brand_specific_3ds_options(@brand_specific_fixtures[:master]) + + assert response = @three_ds_gateway.authorize(100, cc, options) + assert_success_with_authorization(response) + end + + def test_succesful_3ds_purchase_with_mastercard + cc = brand_specific_card(@brand_specific_fixtures[:master][:card]) + options = brand_specific_3ds_options(@brand_specific_fixtures[:master]) + + assert response = @three_ds_gateway.purchase(100, cc, options) + assert_success_with_authorization(response) + end + + def test_successful_3ds_authorization_with_american_express + cc = brand_specific_card(@brand_specific_fixtures[:american_express][:card]) + options = brand_specific_3ds_options(@brand_specific_fixtures[:american_express]) + + assert response = @three_ds_gateway.authorize(100, cc, options) + assert_success_with_authorization(response) + end + + def test_successful_3ds_purchase_with_american_express + cc = brand_specific_card(@brand_specific_fixtures[:american_express][:card]) + options = brand_specific_3ds_options(@brand_specific_fixtures[:american_express]) + + assert response = @three_ds_gateway.purchase(100, cc, options) + assert_success_with_authorization(response) + end + + def test_successful_3ds_authorization_with_discover + cc = brand_specific_card(@brand_specific_fixtures[:discover][:card]) + options = brand_specific_3ds_options(@brand_specific_fixtures[:discover]) + + assert response = @three_ds_gateway.authorize(100, cc, options) + assert_success_with_authorization(response) + end + + def test_successful_3ds_purchase_with_discover + cc = brand_specific_card(@brand_specific_fixtures[:discover][:card]) + options = brand_specific_3ds_options(@brand_specific_fixtures[:discover]) + + assert response = @three_ds_gateway.purchase(100, cc, options) + assert_success_with_authorization(response) + end + + private + + def assert_success_with_authorization(response) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def brand_specific_3ds_options(data) + @options.merge( + order_id: '2', + currency: 'USD', + three_d_secure: data[:three_d_secure], + address: data[:address], + soft_descriptors: { + merchant_name: 'Merch', + product_description: 'Description', + merchant_email: 'email@example' + } + ) + end + + def brand_specific_card(card_data) + credit_card( + card_data[:number], + { + verification_value: card_data[:verification_value], + brand: card_data[:brand] + } + ) + end +end + +class TandemOrbitalTests < Test::Unit::TestCase + # Additional test cases to verify tandem integration + def setup + Base.mode = :test + @tandem_gateway = ActiveMerchant::Billing::OrbitalGateway.new(fixtures(:orbital_tandem_gateway)) + + @amount = 100 + @google_pay_amount = 10000 + @credit_card = credit_card('4556761029983886') + @declined_card = credit_card('4011361100000012') + @google_pay_card = network_tokenization_credit_card( + '4777777777777778', + payment_cryptogram: 'BwAQCFVQdwEAABNZI1B3EGLyGC8=', + verification_value: '987', + source: :google_pay, + brand: 'visa', + eci: '5' + ) + + @options = { + order_id: generate_unique_id, + address:, + merchant_id: 'merchant1234' + } + + @level_2_options = { + tax_indicator: '1', + tax: '75', + purchase_order: '123abc', + 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 = { + freight_amount: 1, + duty_amount: 1, + ship_from_zip: 27604, + dest_country: 'USA', + discount_amount: 1, + vat_tax: 1, + vat_rate: 25, + invoice_discount_treatment: 1, + tax_treatment: 1, + ship_vat_rate: 10, + unique_vat_invoice_ref: 'ABC123' + } + + @line_items = [ + { + desc: 'another item', + prod_cd: generate_unique_id[0, 11], + qty: 1, + u_o_m: 'LBR', + tax_amt: 250, + tax_rate: 10000, + comm_cd: '00584', + unit_cost: 2500, + gross_net: 'Y', + tax_type: 'sale', + debit_ind: 'C' + }, + { + desc: 'something else', + prod_cd: generate_unique_id[0, 11], + qty: 1, + u_o_m: 'LBR', + tax_amt: 125, + tax_rate: 5000, + comm_cd: '00584', + unit_cost: 1000, + gross_net: 'Y', + tax_type: 'sale', + debit_ind: 'C' + } + ] + end + + def test_successful_purchase + assert response = @tandem_gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_inr_currency + assert response = @tandem_gateway.purchase(@amount, @credit_card, @options.merge(currency: 'INR')) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_soft_descriptor + options = @options.merge( + soft_descriptors: { + merchant_name: 'Merch', + product_description: 'Description', + merchant_email: 'email@example' + } + ) + assert response = @tandem_gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_level_2_data + response = @tandem_gateway.purchase(@amount, @credit_card, @options.merge(level_2_data: @level_2_options)) + + assert_success response + 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)) + + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_visa_network_tokenization_credit_card_with_eci + network_card = network_tokenization_credit_card( + '4788250000028291', + payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', + transaction_id: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', + verification_value: '111', + brand: 'visa', + eci: '5' + ) + + assert response = @tandem_gateway.purchase(3000, network_card, @options) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_successful_purchase_with_master_card_network_tokenization_credit_card + network_card = network_tokenization_credit_card( + '4788250000028291', + payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', + transaction_id: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', + verification_value: '111', + brand: 'master' + ) + assert response = @tandem_gateway.purchase(3000, network_card, @options) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_successful_purchase_with_american_express_network_tokenization_credit_card + network_card = network_tokenization_credit_card( + '4788250000028291', + payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', + transaction_id: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', + verification_value: '111', + brand: 'american_express' + ) + assert response = @tandem_gateway.purchase(3000, network_card, @options) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_successful_purchase_with_discover_network_tokenization_credit_card + network_card = network_tokenization_credit_card( + '4788250000028291', + payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', + transaction_id: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', + verification_value: '111', + brand: 'discover' + ) + assert response = @tandem_gateway.purchase(3000, network_card, @options) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + # verify stored credential flows in tandem support + + def test_successful_purchase_with_mit_stored_credentials + mit_stored_credentials = { + mit_msg_type: 'MUSE', + mit_stored_credential_ind: 'Y', + mit_submitted_transaction_id: '111222333444555' + } + + response = @tandem_gateway.purchase(@amount, @credit_card, @options.merge(mit_stored_credentials)) + + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_cit_stored_credentials + cit_options = { + mit_msg_type: 'CUSE', + mit_stored_credential_ind: 'Y' + } + + response = @tandem_gateway.purchase(@amount, @credit_card, @options.merge(cit_options)) + + assert_success response + assert_equal 'Approved', response.message + end + + def test_purchase_using_stored_credential_recurring_cit + initial_options = stored_credential_options(:cardholder, :recurring, :initial) + assert purchase = @tandem_gateway.purchase(@amount, @credit_card, initial_options) + assert_success purchase + assert_equal 'Approved', purchase.message + assert network_transaction_id = purchase.params['mit_received_transaction_id'] + + used_options = stored_credential_options(:recurring, :cardholder, id: network_transaction_id) + assert purchase = @tandem_gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + assert_equal 'Approved', purchase.message + end + + def test_purchase_using_stored_credential_recurring_mit + initial_options = stored_credential_options(:merchant, :recurring, :initial) + assert purchase = @tandem_gateway.purchase(@amount, @credit_card, initial_options) + assert_success purchase + assert_equal 'Approved', purchase.message + assert network_transaction_id = purchase.params['mit_received_transaction_id'] + + used_options = stored_credential_options(:recurring, :merchant, id: network_transaction_id) + assert purchase = @tandem_gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + assert_equal 'Approved', purchase.message + end + + def test_successful_purchase_with_overridden_normalized_stored_credentials + stored_credential = { + stored_credential: { + initial_transaction: false, + initiator: 'merchant', + reason_type: 'unscheduled', + network_transaction_id: '111222333444555' + }, + mit_msg_type: 'MRSB' + } + + response = @tandem_gateway.purchase(@amount, @credit_card, @options.merge(stored_credential)) + + assert_success response + assert_equal 'Approved', response.message + end + + # verify google pay transactions on tandem account + + def test_successful_purchase_with_google_pay + response = @tandem_gateway.purchase(@google_pay_amount, @google_pay_card, @options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_unsuccessful_purchase + assert response = @tandem_gateway.purchase(101, @declined_card, @options) + assert_failure response + assert_match 'AUTH DECLINED', response.message + end + + def test_authorize_and_capture + amount = @amount + assert auth = @tandem_gateway.authorize(amount, @credit_card, @options.merge(order_id: '2')) + assert_success auth + assert_equal 'Approved', auth.message + assert auth.authorization + assert capture = @tandem_gateway.capture(amount, auth.authorization, order_id: '2') + assert_success capture + end + + def test_successful_authorize_and_capture_with_level_2_data + auth = @tandem_gateway.authorize(@amount, @credit_card, @options.merge(level_2_data: @level_2_options)) + assert_success auth + assert_equal 'Approved', auth.message + + capture = @tandem_gateway.capture(@amount, auth.authorization, @options.merge(level_2_data: @level_2_options)) + assert_success capture + end + + def test_successful_authorize_and_capture_with_line_items + auth = @tandem_gateway.authorize(@amount, @credit_card, @options.merge(level_2_data: @level_2_options, level_3_data: @level_3_options, line_items: @line_items)) + assert_success auth + assert_equal 'Approved', auth.message + + capture = @tandem_gateway.capture(@amount, auth.authorization, @options.merge(level_2_data: @level_2_options, level_3_data: @level_3_options, line_items: @line_items)) + assert_success capture + end + + def test_successful_authorize_and_capture_with_google_pay + auth = @tandem_gateway.authorize(@amount, @google_pay_card, @options) + assert_success auth + assert_equal 'Approved', auth.message + + capture = @tandem_gateway.capture(@amount, auth.authorization, @options) + assert_success capture + end + + def test_authorize_and_void + assert auth = @tandem_gateway.authorize(@amount, @credit_card, @options.merge(order_id: '2')) + assert_success auth + assert_equal 'Approved', auth.message + assert auth.authorization + assert void = @tandem_gateway.void(auth.authorization, order_id: '2') + assert_success void + end + + def test_authorize_and_void_using_google_pay + assert auth = @tandem_gateway.authorize(@amount, @google_pay_card, @options) + assert_success auth + assert_equal 'Approved', auth.message + + assert auth.authorization + assert void = @tandem_gateway.void(auth.authorization) + assert_success void + end + + def test_successful_refund + amount = @amount + assert response = @tandem_gateway.purchase(amount, @credit_card, @options) + assert_success response + assert response.authorization + assert refund = @tandem_gateway.refund(amount, response.authorization, @options) + assert_success refund + end + + def test_failed_refund + assert refund = @tandem_gateway.refund(@amount, '123;123', @options) + assert_failure refund + assert_equal '881', refund.params['proc_status'] + end + + def test_successful_refund_with_google_pay + auth = @tandem_gateway.authorize(@amount, @google_pay_card, @options) + assert_success auth + assert_equal 'Approved', auth.message + + capture = @tandem_gateway.capture(@amount, auth.authorization, @options) + assert_success capture + + assert capture.authorization + assert refund = @tandem_gateway.refund(@amount, capture.authorization, @options) + assert_success refund + end + + def test_successful_refund_with_level_2_data + amount = @amount + assert response = @tandem_gateway.purchase(amount, @credit_card, @options.merge(level_2_data: @level_2_options)) + assert_success response + assert response.authorization + assert refund = @tandem_gateway.refund(amount, response.authorization, @options.merge(level_2_data: @level_2_options)) + assert_success refund + end + + def test_successful_credit + payment_method = credit_card('5454545454545454') + assert response = @tandem_gateway.credit(@amount, payment_method, @options) + assert_success response + end + + def test_failed_capture + assert response = @tandem_gateway.capture(@amount, '') + assert_failure response + assert_equal 'Bad data error', response.message + end + + def test_credit_purchase_with_address_responds_with_name + transcript = capture_transcript(@tandem_gateway) do + response = @tandem_gateway.authorize(@amount, @credit_card, @options.merge(order_id: '2')) + assert_equal '00', response.params['resp_code'] + assert_equal 'Approved', response.params['status_msg'] + end + + assert_match(/Longbob Longsen/, transcript) + end + + def test_credit_purchase_with_no_address_responds_with_no_name + response = @tandem_gateway.authorize(@amount, @credit_card, @options.merge(order_id: '2', address: nil, billing_address: nil)) + assert_equal '00', response.params['resp_code'] + assert_equal 'Approved', response.params['status_msg'] + end + + def test_void_transactions + [3000, 105500, 2900].each do |amount| + assert auth_response = @tandem_gateway.authorize(amount, @credit_card, @options) + assert void_response = @tandem_gateway.void(auth_response.authorization, @options.merge(transaction_index: 1)) + assert_kind_of Response, void_response + end + end + + def test_successful_verify + response = @tandem_gateway.verify(@credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_different_cards + @credit_card.brand = 'master' + response = @tandem_gateway.verify(@credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_verify_with_discover_brand + @credit_card.brand = 'discover' + response = @tandem_gateway.verify(@credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_unsuccessful_verify_with_invalid_discover_card + @declined_card.brand = 'discover' + response = @tandem_gateway.verify(@declined_card, @options.merge({ verify_amount: '101' })) + assert_failure response + assert_match 'AUTH DECLINED', response.message + end + + def test_failed_verify + response = @tandem_gateway.verify(@declined_card, @options.merge({ verify_amount: '101' })) + assert_failure response + assert_match 'AUTH DECLINED', response.message + end + + def test_transcript_scrubbing + transcript = capture_transcript(@tandem_gateway) do + @tandem_gateway.purchase(@amount, @credit_card, @options) + end + transcript = @tandem_gateway.scrub(transcript) + + assert_scrubbed(@credit_card.number, transcript) + assert_scrubbed(@credit_card.verification_value, transcript) + assert_scrubbed(@tandem_gateway.options[:password], transcript) + assert_scrubbed(@tandem_gateway.options[:login], transcript) + assert_scrubbed(@tandem_gateway.options[:merchant_id], transcript) + end + + def test_transcript_scrubbing_profile + transcript = capture_transcript(@tandem_gateway) do + @tandem_gateway.add_customer_profile(@credit_card, @options) + end + transcript = @tandem_gateway.scrub(transcript) + + assert_scrubbed(@credit_card.number, transcript) + assert_scrubbed(@credit_card.verification_value, transcript) + assert_scrubbed(@tandem_gateway.options[:password], transcript) + assert_scrubbed(@tandem_gateway.options[:login], transcript) + assert_scrubbed(@tandem_gateway.options[:merchant_id], transcript) + end + + def test_transcript_scrubbing_network_card + network_card = network_tokenization_credit_card( + '4788250000028291', + payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', + transaction_id: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', + verification_value: '111', + brand: 'visa', + eci: '5' + ) + transcript = capture_transcript(@tandem_gateway) do + @tandem_gateway.purchase(@tandem_gateway, network_card, @options) + end + transcript = @tandem_gateway.scrub(transcript) + + assert_scrubbed(network_card.payment_cryptogram, transcript) + end + + private + + def stored_credential_options(*args, id: nil) + @options.merge(order_id: generate_unique_id, + stored_credential: stored_credential(*args, id:)) + end end diff --git a/test/remote/gateways/remote_pagarme_test.rb b/test/remote/gateways/remote_pagarme_test.rb index 868ff0a48c3..0679cbd43c6 100644 --- a/test/remote/gateways/remote_pagarme_test.rb +++ b/test/remote/gateways/remote_pagarme_test.rb @@ -14,7 +14,7 @@ def setup @declined_card = credit_card('4242424242424242', { first_name: 'Richard', last_name: 'Deschamps', - :verification_value => '688' + verification_value: '688' }) @options = { @@ -153,5 +153,4 @@ def test_transcript_scrubbing assert_scrubbed(@credit_card.verification_value, transcript) assert_scrubbed(@gateway.options[:api_key], transcript) end - end diff --git a/test/remote/gateways/remote_pago_facil_test.rb b/test/remote/gateways/remote_pago_facil_test.rb index 254ebd33dec..b672f9e0e06 100644 --- a/test/remote/gateways/remote_pago_facil_test.rb +++ b/test/remote/gateways/remote_pago_facil_test.rb @@ -86,7 +86,7 @@ def successful_response_to random_response = yield if random_response.success? return random_response - elsif(attempts > 2) + elsif attempts > 2 raise 'Unable to get a successful response' else assert_equal 'Declined_(General).', random_response.params.fetch('error') diff --git a/test/remote/gateways/remote_pay_arc_test.rb b/test/remote/gateways/remote_pay_arc_test.rb new file mode 100644 index 00000000000..bc98c11e7e9 --- /dev/null +++ b/test/remote/gateways/remote_pay_arc_test.rb @@ -0,0 +1,262 @@ +require 'test_helper' + +class RemotePayArcTest < Test::Unit::TestCase + def setup + @gateway = PayArcGateway.new(fixtures(:pay_arc)) + credit_card_options = { + month: '12', + year: '2022', + first_name: 'Rex Joseph', + last_name: '', + verification_value: '999' + } + @credit_card = credit_card('4111111111111111', credit_card_options) + @invalid_credit_card = credit_card('3111111111111111', credit_card_options) + @invalid_cvv_card = credit_card('4111111111111111', credit_card_options.update(verification_value: '123')) + + @amount = 100 + + @options = { + description: 'Store Purchase', + card_source: 'INTERNET', + billing_address: address.update(phone: '8772036624'), + email: 'testy@test.com', + phone: '8772036624' + } + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_block do + PayArcGateway::SUCCESS_STATUS.include? response.message + end + end + + def test_successful_purchase_with_more_options + extra_options = { + order_id: '1', + ip: '127.0.0.1', + email: 'joe@example.com', + tip_amount: 10 + } + response = @gateway.purchase(1500, @credit_card, @options.merge(extra_options)) + assert_success response + assert_block do + PayArcGateway::SUCCESS_STATUS.include? response.message + end + end + + def test_successful_two_digit_card_exp_month + credit_card_options = { + month: '02', + year: '2022', + first_name: 'Rex Joseph', + last_name: '', + verification_value: '999' + } + credit_card = credit_card('4111111111111111', credit_card_options) + + response = @gateway.purchase(1022, credit_card, @options) + assert_success response + + assert_block do + PayArcGateway::SUCCESS_STATUS.include? response.message + end + end + + def test_successful_one_digit_card_exp_month + credit_card_options = { + month: '2', + year: '2022', + first_name: 'Rex Joseph', + last_name: '', + verification_value: '999' + } + credit_card = credit_card('4111111111111111', credit_card_options) + + response = @gateway.purchase(1022, credit_card, @options) + assert_success response + + assert_block do + PayArcGateway::SUCCESS_STATUS.include? response.message + end + end + + def test_failed_three_digit_card_exp_month + credit_card_options = { + month: '200', + year: '2022', + first_name: 'Rex Joseph', + last_name: '', + verification_value: '999' + } + credit_card = credit_card('4111111111111111', credit_card_options) + + response = @gateway.purchase(1022, credit_card, @options) + assert_failure response + assert_equal 'error', response.params['status'] + end + + def test_successful_adds_phone_number_for_purchase + response = @gateway.purchase(250, @credit_card, @options) + assert_success response + + assert_block do + PayArcGateway::SUCCESS_STATUS.include? response.message + end + + assert_equal '8772036624', response.params['data']['phone_number'] + end + + def test_successful_purchase_without_billing_address + @options.delete(:billing_address) + response = @gateway.purchase(250, @credit_card, @options) + + assert_nil response.params['data']['card']['data']['address1'] + assert_success response + end + + def test_failed_purchase + response = @gateway.purchase(1300, @invalid_credit_card, @options) + assert_failure response + assert_equal 'error', response.params['status'] + end + + def test_successful_authorize + response = @gateway.authorize(1100, @credit_card, @options) + assert_success response + assert_equal 'authorized', response.message + end + + # Failed due to invalid CVV + def test_failed_authorize + response = @gateway.authorize(500, @invalid_cvv_card, @options) + assert_failure response + assert_equal 'error', response.params['status'] + end + + def test_successful_authorize_and_capture + authorize_response = @gateway.authorize(2000, @credit_card, @options) + assert_success authorize_response + response = @gateway.capture(2000, authorize_response.authorization, @options) + assert_success response + assert_block do + PayArcGateway::SUCCESS_STATUS.include? response.message + end + end + + def test_failed_capture + response = @gateway.capture(2000, 'invalid_txn_refernece', @options) + assert_failure response + assert_equal 'error', response.params['status'] + end + + def test_successful_void + @options.update(reason: 'duplicate') + charge_response = @gateway.purchase(1200, @credit_card, @options) + assert_success charge_response + + assert void = @gateway.void(charge_response.authorization, @options) + assert_success void + assert_block do + PayArcGateway::SUCCESS_STATUS.include? void.message + end + end + + def test_failed_void + response = @gateway.void('invalid_txn_reference', @options) + assert_failure response + assert_equal 'error', response.params['status'] + end + + def test_partial_capture + authorize_response = @gateway.authorize(@amount, @credit_card, @options) + assert_success authorize_response + + response = @gateway.capture(@amount - 1, authorize_response.authorization, @options) + assert_success response + assert_block do + PayArcGateway::SUCCESS_STATUS.include? response.message + end + end + + def test_successful_refund + purchase = @gateway.purchase(900, @credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(900, purchase.authorization) + assert_success refund + assert_block do + PayArcGateway::SUCCESS_STATUS.include? refund.message + end + assert_equal 'refunded', refund.message + end + + def test_partial_refund + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount - 1, purchase.authorization) + assert_success refund + assert_block do + PayArcGateway::SUCCESS_STATUS.include? refund.message + end + assert_equal 'partial_refund', refund.message + end + + def test_failed_refund + response = @gateway.refund(1200, '') + assert_failure response + assert_equal 'error', response.params['status'] + end + + def test_successful_credit + response = @gateway.credit(250, @credit_card, @options) + assert_success response + assert_block do + PayArcGateway::SUCCESS_STATUS.include? response.message + end + assert_equal 'refunded', response.message + end + + def test_failed_credit + response = @gateway.credit('', @invalid_credit_card, @options) + assert_failure response + assert_equal 'error', response.params['status'] + end + + def test_successful_verify + response = @gateway.verify(@credit_card, @options) + assert_success response + end + + def test_failed_verify + response = @gateway.verify(@invalid_credit_card, @options) + assert_failure response + assert_block do + !(200..299).cover? response.error_code + end + end + + def test_invalid_login + gateway = PayArcGateway.new(api_key: '') + + response = gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_equal 'error', response.params['status'] + assert_block do + !(200..299).cover? response.error_code + end + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @credit_card, @options) + end + transcript = @gateway.scrub(transcript) + assert_scrubbed(fixtures(:pay_arc), transcript) + assert_scrubbed(/card_number=#{@credit_card.number}/, transcript) + assert_scrubbed(/cvv=#{@credit_card.verification_value}/, transcript) + end +end diff --git a/test/remote/gateways/remote_pay_conex_test.rb b/test/remote/gateways/remote_pay_conex_test.rb index 0299a43c0f0..a87790f8068 100644 --- a/test/remote/gateways/remote_pay_conex_test.rb +++ b/test/remote/gateways/remote_pay_conex_test.rb @@ -27,7 +27,18 @@ def test_transcript_scrubbing assert_scrubbed(@credit_card.number, transcript) assert_scrubbed(@credit_card.verification_value, transcript) assert_scrubbed(@gateway.options[:api_accesskey], transcript) - end + end + + def test_transcript_scrubbing_with_check + 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[:api_accesskey], transcript) + end def test_successful_purchase response = @gateway.purchase(@amount, @credit_card, @options) @@ -61,7 +72,7 @@ def test_partial_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture assert_equal 'CAPTURED', capture.message end @@ -85,7 +96,7 @@ def test_partial_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount-1, purchase.authorization) + assert refund = @gateway.refund(@amount - 1, purchase.authorization) assert_success refund assert_equal 'REFUND', refund.message end diff --git a/test/remote/gateways/remote_pay_gate_xml_test.rb b/test/remote/gateways/remote_pay_gate_xml_test.rb index b16cedb0678..02d23f0e8b7 100644 --- a/test/remote/gateways/remote_pay_gate_xml_test.rb +++ b/test/remote/gateways/remote_pay_gate_xml_test.rb @@ -9,11 +9,11 @@ def setup @declined_card = credit_card('4000000000000036') @options = { - :order_id => generate_unique_id, - :billing_address => address, - :email => 'john.doe@example.com', - :ip => '127.0.0.1', - :description => 'Store Purchase', + order_id: generate_unique_id, + billing_address: address, + email: 'john.doe@example.com', + ip: '127.0.0.1', + description: 'Store Purchase' } end @@ -47,8 +47,8 @@ def test_failed_capture def test_invalid_login gateway = PayGateXmlGateway.new( - :login => '', - :password => '' + login: '', + password: '' ) assert response = gateway.authorize(@amount, @credit_card, @options) assert_failure response @@ -59,7 +59,7 @@ def test_successful_purchase_and_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - credit = @gateway.refund(@amount, purchase.authorization, :note => 'Sorry') + credit = @gateway.refund(@amount, purchase.authorization, note: 'Sorry') assert_success credit assert credit.test? end diff --git a/test/remote/gateways/remote_pay_hub_test.rb b/test/remote/gateways/remote_pay_hub_test.rb index e57fe048e4a..ae79455b5a2 100644 --- a/test/remote/gateways/remote_pay_hub_test.rb +++ b/test/remote/gateways/remote_pay_hub_test.rb @@ -7,14 +7,14 @@ def setup @credit_card = credit_card('5466410004374507', verification_value: '998') @invalid_card = credit_card('371449635398431', verification_value: '9997') @options = { - :first_name => 'Garrya', - :last_name => 'Barrya', - :email => 'payhubtest@mailinator.com', - :address => { - :address1 => '123a ahappy St.', - :city => 'Happya City', - :state => 'CA', - :zip => '94901' + first_name: 'Garrya', + last_name: 'Barrya', + email: 'payhubtest@mailinator.com', + address: { + address1: '123a ahappy St.', + city: 'Happya City', + state: 'CA', + zip: '94901' } } end diff --git a/test/remote/gateways/remote_pay_junction_test.rb b/test/remote/gateways/remote_pay_junction_test.rb index bd0ff583733..dcb32c71211 100644 --- a/test/remote/gateways/remote_pay_junction_test.rb +++ b/test/remote/gateways/remote_pay_junction_test.rb @@ -10,28 +10,28 @@ class PayJunctionTest < Test::Unit::TestCase def setup @gateway = PayJunctionGateway.new(fixtures(:pay_junction)) - @credit_card = credit_card('4444333322221111', :verification_value => '999') + @credit_card = credit_card('4444333322221111', verification_value: '999') @valid_verification_value = '999' @invalid_verification_value = '1234' @valid_address = { - :address1 => '123 Test St.', - :address2 => nil, - :city => 'Somewhere', - :state => 'CA', - :zip => '90001' + address1: '123 Test St.', + address2: nil, + city: 'Somewhere', + state: 'CA', + zip: '90001' } @invalid_address = { - :address1 => '187 Apple Tree Lane.', - :address2 => nil, - :city => 'Woodside', - :state => 'CA', - :zip => '94062' + address1: '187 Apple Tree Lane.', + address2: nil, + city: 'Woodside', + state: 'CA', + zip: '94062' } - @options = { :billing_address => @valid_address, :order_id => generate_unique_id } + @options = { billing_address: @valid_address, order_id: generate_unique_id } end def test_successful_purchase @@ -55,7 +55,7 @@ def test_successful_purchase_with_cvv end def test_successful_authorize - assert response = @gateway.authorize( AMOUNT, @credit_card, @options) + assert response = @gateway.authorize(AMOUNT, @credit_card, @options) assert_equal PayJunctionGateway::SUCCESS_MESSAGE, response.message assert_equal 'hold', response.params['posture'], 'Should be a held charge' @@ -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 @@ -90,11 +89,10 @@ def test_successful_void purchase = @gateway.purchase(AMOUNT, @credit_card, @options) assert_success purchase - assert response = @gateway.void(purchase.authorization, :order_id => order_id) + assert response = @gateway.void(purchase.authorization, 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 @@ -102,26 +100,27 @@ def test_successful_instant_purchase # transaction can be executed if you have the transaction ID of a # previous successful transaction. - purchase = @gateway.purchase( AMOUNT, @credit_card, @options) + purchase = @gateway.purchase(AMOUNT, @credit_card, @options) assert_success purchase - assert response = @gateway.purchase(AMOUNT, purchase.authorization, :order_id => generate_unique_id) + assert response = @gateway.purchase(AMOUNT, purchase.authorization, order_id: generate_unique_id) 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, - :periodicity => :monthly, - :payments => 12, - :order_id => generate_unique_id[0..15] - ) + assert response = @gateway.recurring( + AMOUNT, + @credit_card, + periodicity: :monthly, + payments: 12, + order_id: generate_unique_id[0..15] + ) assert_equal PayJunctionGateway::SUCCESS_MESSAGE, response.message assert_equal 'charge', response.params['transaction_action'] @@ -136,7 +135,8 @@ def test_should_send_invoice end private + def success_price - 200 + rand(200) + rand(200..399) end end diff --git a/test/remote/gateways/remote_pay_junction_v2_test.rb b/test/remote/gateways/remote_pay_junction_v2_test.rb index 62954ff4496..8d6ae6987bf 100644 --- a/test/remote/gateways/remote_pay_junction_v2_test.rb +++ b/test/remote/gateways/remote_pay_junction_v2_test.rb @@ -5,9 +5,10 @@ def setup @gateway = PayJunctionV2Gateway.new(fixtures(:pay_junction_v2)) @amount = 99 - @credit_card = credit_card('4444333322221111', month: 01, year: 2020, verification_value: 999) + @credit_card = credit_card('4444333322221111', month: 01, year: 2021, verification_value: 999) @options = { - order_id: generate_unique_id + order_id: generate_unique_id, + billing_address: address() } end @@ -24,6 +25,13 @@ def test_successful_purchase assert_success response assert_equal 'Approved', response.message assert response.test? + + assert_match @options[:billing_address][:company], response.params['billing']['companyName'] + assert_match @options[:billing_address][:address1], response.params['billing']['address']['address'] + assert_match @options[:billing_address][:city], response.params['billing']['address']['city'] + assert_match @options[:billing_address][:state], response.params['billing']['address']['state'] + assert_match @options[:billing_address][:country], response.params['billing']['address']['country'] + assert_match @options[:billing_address][:zip], response.params['billing']['address']['zip'] end def test_successful_purchase_sans_options @@ -60,9 +68,9 @@ def test_partial_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture - assert_equal sprintf('%.2f', (@amount-1).to_f / 100), capture.params['amountTotal'] + assert_equal sprintf('%.2f', (@amount - 1).to_f / 100), capture.params['amountTotal'] end def test_failed_capture @@ -81,13 +89,13 @@ def test_successful_refund end def test_partial_refund - purchase = @gateway.purchase(@amount, @credit_card, @options) + purchase = @gateway.purchase(@amount + 50, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount-50, purchase.authorization) + assert refund = @gateway.refund(@amount, purchase.authorization) assert_success refund assert_equal 'Approved', refund.message - assert_equal sprintf('%.2f', (@amount-50).to_f / 100), refund.params['amountTotal'] + assert_equal sprintf('%.2f', @amount.to_f / 100), refund.params['amountTotal'] end def test_failed_refund diff --git a/test/remote/gateways/remote_pay_secure_test.rb b/test/remote/gateways/remote_pay_secure_test.rb index 8dd8837adc1..4e82df22a9a 100644 --- a/test/remote/gateways/remote_pay_secure_test.rb +++ b/test/remote/gateways/remote_pay_secure_test.rb @@ -1,18 +1,17 @@ require 'test_helper' class RemotePaySecureTest < Test::Unit::TestCase - def setup @gateway = PaySecureGateway.new(fixtures(:pay_secure)) - + @credit_card = credit_card('4000100011112224') - @options = { - :billing_address => address, - :order_id => generate_unique_id + @options = { + billing_address: address, + order_id: generate_unique_id } @amount = 100 end - + def test_successful_purchase assert response = @gateway.purchase(@amount, @credit_card, @options) assert_success response @@ -26,12 +25,12 @@ def test_unsuccessful_purchase assert_equal 'Declined, card expired', response.message assert_failure response end - + def test_invalid_login gateway = PaySecureGateway.new( - :login => '', - :password => '' - ) + login: '', + password: '' + ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_equal "MissingField: 'MERCHANT_ID'", response.message assert_failure response diff --git a/test/remote/gateways/remote_pay_trace_test.rb b/test/remote/gateways/remote_pay_trace_test.rb new file mode 100644 index 00000000000..7b5aa7ab0a7 --- /dev/null +++ b/test/remote/gateways/remote_pay_trace_test.rb @@ -0,0 +1,415 @@ +require 'test_helper' + +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class PayTraceGateway < Gateway + def settle + post = {} + response = commit('transactions/settle', post) + check_token_response(response, 'transactions/settle', post, options) + end + end + end +end + +class RemotePayTraceTest < Test::Unit::TestCase + def setup + @gateway = PayTraceGateway.new(fixtures(:pay_trace)) + + @amount = 100 + @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', verification_value: '996') + @amex = credit_card('371449635392376', verification_value: '9997') + @echeck = check(account_number: '123456', routing_number: '325070760') + @options = { + billing_address: { + address1: '8320 This Way Lane', + city: 'Placeville', + state: 'CA', + zip: '85284' + }, + description: 'Store Purchase' + } + end + + def test_acquire_token + response = @gateway.acquire_access_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 + assert_equal 'Your transaction was successfully approved.', response.message + end + + def test_successful_purchase_with_customer_id + create = @gateway.store(@mastercard, @options) + customer_id = create.params['customer_id'] + response = @gateway.purchase(500, customer_id, @options) + assert_success response + assert_equal 'Your transaction was successfully approved.', response.message + end + + def test_successful_purchase_with_ach + @echeck.account_number = rand.to_s[2..7] + response = @gateway.purchase(1000, @echeck, @options) + assert_success response + assert_equal response.message, 'Your check was successfully processed.' + end + + def test_successful_purchase_by_customer_with_ach + @echeck.account_number = rand.to_s[2..7] + create = @gateway.store(@echeck, @options) + assert_success create + customer_id = create.params['customer_id'] + response = @gateway.purchase(500, customer_id, @options.merge({ check_transaction: 'true' })) + assert_success response + end + + def test_successful_purchase_with_more_options + options = { + email: 'joe@example.com' + } + + response = @gateway.purchase(200, @discover, options) + assert_success response + assert_equal 'Your transaction was successfully approved.', response.message + end + + def test_successful_purchase_with_level_3_data_visa + options = { + visa_or_mastercard: 'visa', + invoice_id: 'inv12345', + customer_reference_id: '123abcd', + tax_amount: 499, + national_tax_amount: 172, + merchant_tax_id: '3456defg', + customer_tax_id: '3456test', + commodity_code: '4321', + discount_amount: 99, + freight_amount: 75, + duty_amount: 32, + source_address: { + zip: '94947' + }, + shipping_address: { + zip: '94948', + country: 'US' + }, + additional_tax_amount: 4, + additional_tax_rate: 1, + line_items: [ + { + additional_tax_amount: 0, + additional_tax_rate: 8, + amount: 1999, + commodity_code: '123commodity', + description: 'plumbing', + discount_amount: 327, + product_id: 'skucode123', + quantity: 4, + unit_of_measure: 'EACH', + unit_cost: 424 + } + ] + } + + response = @gateway.purchase(3000, @credit_card, options) + assert_success response + assert_equal 101, response.params['response_code'] + assert_not_nil response.authorization + end + + def test_successful_purchase_with_level_3_data_mastercard + options = { + visa_or_mastercard: 'mastercard', + invoice_id: 'inv1234', + customer_reference_id: 'PO123456', + tax_amount: 810, + source_address: { + zip: '99201' + }, + shipping_address: { + zip: '85284', + country: 'US' + }, + additional_tax_amount: 40, + additional_tax_included: true, + line_items: [ + { + additional_tax_amount: 40, + additional_tax_included: true, + additional_tax_rate: 8, + amount: 1999, + debit_or_credit: 'D', + description: 'business services', + discount_amount: 327, + discount_rate: 1, + discount_included: true, + merchant_tax_id: '12-123456', + product_id: 'sku1245', + quantity: 4, + tax_included: true, + unit_of_measure: 'EACH', + unit_cost: 524 + } + ] + } + + response = @gateway.purchase(250, @mastercard, options) + assert_success response + assert_equal 101, response.params['response_code'] + end + + # Level three data can only be added to approved sale transactions done with visa or mastercard. + # This test is to show that if a transaction were to come through with a different card type, + # the gateway integration would ignore the attempt to add level three data, but could still approve the sale transaction. + def test_successful_purchase_with_attempted_level_3 + options = { + visa_or_mastercard: 'discover', + invoice_id: 'inv1234', + customer_reference_id: 'PO123456' + } + + response = @gateway.purchase(300, @discover, @options.merge(options)) + assert_success response + end + + def test_failed_purchase + response = @gateway.purchase(29, @amex, @options) + assert_failure response + assert_equal false, response.success? + end + + def test_failed_purchase_with_multiple_errors + response = @gateway.purchase(25000, @invalid_card, @options) + assert_failure response + assert_equal 'Errors- code:35, message:["Please provide a valid Credit Card Number."] code:43, message:["Please provide a valid Expiration Month."]', response.message + end + + def test_successful_authorize_and_full_capture + auth = @gateway.authorize(4000, @credit_card, @options) + assert_success auth + + assert capture = @gateway.capture(4000, auth.authorization, @options) + assert_success capture + assert_equal 'Your transaction was successfully captured.', capture.message + end + + def test_successful_authorize_with_customer_id + store = @gateway.store(@mastercard, @options) + assert_success store + customer_id = store.params['customer_id'] + + response = @gateway.authorize(200, customer_id, @options) + assert_success response + assert_equal 'Your transaction was successfully approved.', response.message + end + + def test_successful_authorize_with_ach + @echeck.account_number = rand.to_s[2..7] + response = @gateway.authorize(1000, @echeck, @options) + assert_success response + assert_equal response.message, 'Your check was successfully processed.' + end + + def test_successful_authorize_by_customer_with_ach + @echeck.account_number = rand.to_s[2..7] + store = @gateway.store(@echeck, @options) + assert_success store + customer_id = store.params['customer_id'] + + response = @gateway.authorize(200, customer_id, @options.merge({ check_transaction: 'true' })) + assert_success response + end + + def test_successful_authorize_and_capture_with_level_3_data + options = { + visa_or_mastercard: 'mastercard', + address: { + zip: '99201' + }, + shipping_address: { + zip: '85284', + country: 'US' + }, + line_items: [ + { + description: 'office supplies', + product_id: 'sku9876' + }, + { + description: 'business services', + product_id: 'sku3456' + } + ] + } + auth = @gateway.authorize(@amount, @mastercard, options) + assert_success auth + + assert capture = @gateway.capture(@amount, auth.authorization, options) + assert_success capture + + transaction_id = auth.authorization + assert_equal capture.authorization, transaction_id + assert_equal 'Your transaction was successfully captured.', capture.message + end + + def test_failed_authorize + response = @gateway.authorize(29, @mastercard, @options) + assert_failure response + assert_equal 'Your transaction was not approved. EXPIRED CARD - Expired card', response.message + end + + def test_partial_capture + auth = @gateway.authorize(500, @amex, @options) + assert_success auth + + assert capture = @gateway.capture(200, auth.authorization, @options) + assert_success capture + end + + def test_authorize_and_capture_with_ach + @echeck.account_number = rand.to_s[2..7] + auth = @gateway.authorize(500, @echeck, @options) + assert_success auth + + assert capture = @gateway.capture(500, auth.authorization, @options.merge({ check_transaction: 'true' })) + assert_success capture + end + + def test_failed_capture + response = @gateway.capture(@amount, '') + assert_failure response + assert_equal 'Errors- code:58, message:["Please provide a valid Transaction ID."]', response.message + end + + def test_successful_refund + purchase = @gateway.purchase(200, @credit_card, @options) + assert_success purchase + authorization = purchase.authorization + + settle = @gateway.settle() + assert_success settle + + refund = @gateway.refund(200, authorization, @options) + assert_success refund + assert_equal 'Your transaction was successfully refunded.', refund.message + end + + def test_partial_refund + purchase = @gateway.purchase(400, @mastercard, @options) + assert_success purchase + authorization = purchase.authorization + + settle = @gateway.settle() + assert_success settle + + refund = @gateway.refund(300, authorization, @options) + assert_success refund + assert_equal 'Your transaction was successfully refunded.', refund.message + end + + def test_refund_without_amount + purchase = @gateway.purchase(@amount, @discover, @options) + assert_success purchase + authorization = purchase.authorization + + settle = @gateway.settle() + assert_success settle + + refund = @gateway.refund(@amount, authorization) + assert_success refund + assert_equal 'Your transaction was successfully refunded.', refund.message + end + + def test_failed_refund + purchase = @gateway.purchase(2000, @credit_card, @options) + assert_success purchase + authorization = purchase.authorization + + response = @gateway.refund(2000, authorization, @options) + assert_failure response + assert_equal 'Errors- code:817, message:["The Transaction ID that you provided could not be refunded. Only settled transactions can be refunded. Please try to void the transaction instead."]', response.message + end + + def test_successful_void + auth = @gateway.authorize(@amount, @amex, @options) + assert_success auth + + assert void = @gateway.void(auth.authorization) + assert_success void + assert_equal 'Your transaction was successfully voided.', void.message + end + + def test_successful_void_with_ach + @echeck.account_number = rand.to_s[2..7] + auth = @gateway.authorize(@amount, @echeck, @options) + assert_success auth + + assert void = @gateway.void(auth.authorization, { check_transaction: 'true' }) + assert_success void + end + + def test_failed_void + response = @gateway.void('') + assert_failure response + assert_equal 'Errors- code:58, message:["Please provide a valid Transaction ID."]', response.message + end + + def test_successful_verify + response = @gateway.verify(@mastercard, @options) + assert_success response + assert_equal 'Your transaction was successfully approved.', response.message + end + + def test_successful_store + response = @gateway.store(@credit_card, @options) + assert_success response + end + + def test_successful_store_and_redact_customer_profile + response = @gateway.store(@mastercard, @options) + assert_success response + customer_id = response.params['customer_id'] + redact = @gateway.unstore(customer_id) + assert_success redact + assert_equal true, redact.success? + end + + def test_duplicate_customer_creation + create = @gateway.store(@discover, @options) + customer_id = create.params['customer_id'] + response = @gateway.store(@discover, @options.merge(customer_id:)) + assert_failure response + end + + # Not including a test_failed_verify since the only way to force a failure on this + # 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_transcript_scrubbing + 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[:password], transcript) + assert_scrubbed(@gateway.options[:username], transcript) + assert_scrubbed(@gateway.options[:integrator_id], transcript) + end +end diff --git a/test/remote/gateways/remote_paybox_direct_3ds_test.rb b/test/remote/gateways/remote_paybox_direct_3ds_test.rb new file mode 100644 index 00000000000..a8fa0589d8f --- /dev/null +++ b/test/remote/gateways/remote_paybox_direct_3ds_test.rb @@ -0,0 +1,140 @@ +# encoding: utf-8 + +require 'test_helper' + +class RemotePayboxDirect3DSTest < Test::Unit::TestCase + def setup + fixtures = fixtures(:paybox_direct) + @gateway = PayboxDirectGateway.new(fixtures) + + @amount = 100 + @credit_card = credit_card(fixtures[:credit_card_ok_3ds]) + @declined_card = credit_card(fixtures[:credit_card_nok_3ds]) + @unenrolled_card = credit_card(fixtures[:credit_card_ok_3ds_not_enrolled]) + + @options = { + order_id: '1', + billing_address: address, + description: 'Store Purchase', + three_d_secure: { + eci: '02', + cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA=', + xid: '00000000000000000501', + cavv_algorithm: '1' + } + } + end + + def test_successful_purchase + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'The transaction was approved', response.message + end + + def test_successful_purchase_other_eci + options = @options + options[:three_d_secure][:eci] = '05' + + assert response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'The transaction was approved', response.message + end + + def test_unsuccessful_purchase + assert response = @gateway.purchase(@amount, @declined_card, @options) + assert_failure response + assert_equal "PAYBOX : Num\xE9ro de porteur invalide".force_encoding('ASCII-8BIT'), response.message + end + + def test_successful_unenrolled_3ds_purchase + assert response = @gateway.purchase(@amount, @unenrolled_card, @options) + assert_success response + assert_equal 'The transaction was approved', response.message + end + + def test_authorize_and_capture + amount = @amount + assert auth = @gateway.authorize(amount, @credit_card, @options) + assert_success auth + assert_equal 'The transaction was approved', auth.message + assert auth.authorization + assert capture = @gateway.capture(amount, auth.authorization, order_id: '1') + assert_success capture + end + + def test_purchase_and_void + assert purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + assert_equal 'The transaction was approved', purchase.message + assert purchase.authorization + # Paybox requires you to remember the expiration date + assert void = @gateway.void(purchase.authorization, order_id: '1', amount: @amount) + assert_equal 'The transaction was approved', void.message + assert_success void + end + + def test_failed_capture + assert response = @gateway.capture(@amount, '', order_id: '1') + assert_failure response + assert_equal 'Mandatory values missing keyword:13 Type:1', response.message + end + + def test_purchase_and_partial_credit + assert purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + assert_equal 'The transaction was approved', purchase.message + assert purchase.authorization + assert credit = @gateway.refund(@amount / 2, purchase.authorization, order_id: '1') + assert_equal 'The transaction was approved', credit.message + assert_success credit + end + + def test_successful_refund + assert purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization, order_id: '1') + assert_success refund + end + + def test_partial_refund + assert purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount / 2, purchase.authorization, order_id: '1') + assert_success refund + end + + def test_failed_refund + refund = @gateway.refund(@amount, '', order_id: '2') + assert_failure refund + assert_equal 'Mandatory values missing keyword:13 Type:13', refund.message + end + + def test_failed_purchase_invalid_eci + options = @options + options[:three_d_secure][:eci] = '00' + + assert purchase = @gateway.purchase(@amount, @credit_card, options) + assert_failure purchase + assert_equal "PAYBOX : Transaction refus\xE9e".force_encoding('ASCII-8BIT'), purchase.message + end + + def test_failed_purchase_invalid_cavv + options = @options + options[:three_d_secure][:cavv] = 'jJ81HADVRtXfCBATEp01CJUAAAAVZQGY=' + + assert purchase = @gateway.purchase(@amount, @credit_card, options) + assert_failure purchase + assert_equal 'Some values exceed max length', purchase.message + end + + def test_failed_purchase_invalid_xid + options = @options + options[:three_d_secure][:xid] = '00000000000000000510123123123456789' + + assert purchase = @gateway.purchase(@amount, @credit_card, options) + assert_failure purchase + assert_equal 'Some values exceed max length', purchase.message + end +end diff --git a/test/remote/gateways/remote_paybox_direct_test.rb b/test/remote/gateways/remote_paybox_direct_test.rb index e1d718056bb..6650a2f6a1f 100644 --- a/test/remote/gateways/remote_paybox_direct_test.rb +++ b/test/remote/gateways/remote_paybox_direct_test.rb @@ -3,21 +3,21 @@ require 'test_helper' class RemotePayboxDirectTest < Test::Unit::TestCase - def setup - @gateway = PayboxDirectGateway.new(fixtures(:paybox_direct)) - + fixtures = fixtures(:paybox_direct) + @gateway = PayboxDirectGateway.new(fixtures) + @amount = 100 - @credit_card = credit_card('1111222233334444') - @declined_card = credit_card('1111222233334445') - - @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase' + @credit_card = credit_card(fixtures[:credit_card_ok]) + @declined_card = credit_card(fixtures[:credit_card_nok]) + + @options = { + order_id: '1', + billing_address: address, + description: 'Store Purchase' } end - + def test_successful_purchase assert response = @gateway.purchase(@amount, @credit_card, @options) assert_success response @@ -36,37 +36,37 @@ def test_authorize_and_capture assert_success auth assert_equal 'The transaction was approved', auth.message assert auth.authorization - assert capture = @gateway.capture(amount, auth.authorization, :order_id => '1') + assert capture = @gateway.capture(amount, auth.authorization, order_id: '1') assert_success capture end - + def test_purchase_and_void assert purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase assert_equal 'The transaction was approved', purchase.message assert purchase.authorization # Paybox requires you to remember the expiration date - assert void = @gateway.void(purchase.authorization, :order_id => '1', :amount => @amount) + assert void = @gateway.void(purchase.authorization, order_id: '1', amount: @amount) assert_equal 'The transaction was approved', void.message assert_success void end def test_failed_capture - assert response = @gateway.capture(@amount, '', :order_id => '1') + assert response = @gateway.capture(@amount, '', order_id: '1') assert_failure response - assert_equal 'Invalid data', response.message + assert_equal 'Mandatory values missing keyword:13 Type:1', response.message end - + def test_purchase_and_partial_credit assert purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase assert_equal 'The transaction was approved', purchase.message assert purchase.authorization - assert credit = @gateway.credit(@amount / 2, purchase.authorization, :order_id => '1') + assert credit = @gateway.refund(@amount / 2, purchase.authorization, order_id: '1') assert_equal 'The transaction was approved', credit.message assert_success credit end - + def test_successful_refund assert purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase @@ -79,34 +79,34 @@ def test_partial_refund assert purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount/2, purchase.authorization, order_id: '1') + assert refund = @gateway.refund(@amount / 2, purchase.authorization, order_id: '1') assert_success refund end def test_failed_refund - refund = @gateway.refund(@amount, '', order_id: '2') + refund = @gateway.refund(@amount, '', order_id: '2') assert_failure refund - assert_equal 'Invalid data', refund.message + assert_equal 'Mandatory values missing keyword:13 Type:13', refund.message end def test_invalid_login gateway = PayboxDirectGateway.new( - login: '199988899', - password: '1999888F', - rang: 100 - ) + login: '199988899', + password: '1999888F', + rang: 100 + ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response - assert_equal 'Non autorise', response.message + assert_equal 'HMAC requis', response.message end def test_invalid_login_without_rang gateway = PayboxDirectGateway.new( - login: '199988899', - password: '1999888F', - ) + login: '199988899', + password: '1999888F' + ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response - assert_equal 'Non autorise', response.message + assert_equal "PAYBOX : Acc\xE8s refus\xE9 ou site/rang/cl\xE9 invalide".force_encoding('ASCII-8BIT'), response.message end end diff --git a/test/remote/gateways/remote_payeezy_test.rb b/test/remote/gateways/remote_payeezy_test.rb index 497332317ac..da2fd0fae93 100644 --- a/test/remote/gateways/remote_payeezy_test.rb +++ b/test/remote/gateways/remote_payeezy_test.rb @@ -7,10 +7,11 @@ def setup @bad_credit_card = credit_card('4111111111111113') @check = check @amount = 100 + @reversal_id = "REV-#{SecureRandom.random_number(1000000)}" @options = { - :billing_address => address, - :merchant_ref => 'Store Purchase', - :ta_token => 'NOIW' + billing_address: address, + merchant_ref: 'Store Purchase', + ta_token: 'NOIW' } @options_mdd = { soft_descriptors: { @@ -25,6 +26,40 @@ def setup merchant_contact_info: '8885551212' } } + @options_stored_credentials = { + cardbrand_original_transaction_id: 'abc123', + sequence: 'FIRST', + is_scheduled: true, + initiator: 'MERCHANT', + auth_type_override: 'A' + } + @options_standardized_stored_credentials = { + stored_credential: { + network_transaction_id: 'abc123', # Not checked if initial_transaction == true; not valid if initial_transaction == false. + initial_transaction: true, + reason_type: 'recurring', + initiator: 'cardholder' + } + } + @apple_pay_card = network_tokenization_credit_card( + '4761209980011439', + payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', + month: '11', + year: Time.now.year + 1, + eci: 5, + source: :apple_pay, + verification_value: 569 + ) + @apple_pay_card_amex = network_tokenization_credit_card( + '373953192351004', + brand: 'american_express', + payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', + month: '11', + year: Time.now.year + 1, + eci: 5, + source: :apple_pay, + verification_value: 569 + ) end def test_successful_store @@ -51,11 +86,31 @@ def test_unsuccessful_store def test_successful_purchase 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_successful_purchase_with_apple_pay + assert response = @gateway.purchase(@amount, @apple_pay_card, @options) + assert_success response + end + + def test_successful_purchase_with_apple_pay_amex + assert response = @gateway.purchase(@amount, @apple_pay_card_amex, @options) assert_success response end + def test_successful_authorize_and_capture_with_apple_pay + assert auth = @gateway.authorize(@amount, @apple_pay_card, @options) + assert_success auth + + assert capture = @gateway.capture(@amount, auth.authorization) + assert_success capture + end + def test_successful_purchase_with_echeck - options = @options.merge({customer_id_type: '1', customer_id_number: '1', client_email: 'test@example.com'}) + options = @options.merge({ customer_id_type: '1', customer_id_number: '1', client_email: 'test@example.com' }) assert response = @gateway.purchase(@amount, @check, options) assert_match(/Transaction Normal/, response.message) assert_success response @@ -67,13 +122,117 @@ 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) + assert_success response + end + + def test_successful_purchase_with_stored_credentials + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(@options_stored_credentials)) + assert_match(/Transaction Normal/, response.message) + assert_success response + end + + def test_successful_purchase_with_standardized_stored_credentials + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(@options_standardized_stored_credentials)) + assert_match(/Transaction Normal/, response.message) + assert_success response + end + + def test_successful_purchase_with_apple_pay_name_from_billing_address + @apple_pay_card.first_name = nil + @apple_pay_card.last_name = nil + assert response = @gateway.purchase(@amount, @apple_pay_card, @options) + assert_success response + assert_equal 'Jim Smith', response.params['card']['cardholder_name'] + end + + def test_failed_purchase_with_apple_pay_no_name + @options[:billing_address] = nil + @apple_pay_card.first_name = nil + @apple_pay_card.last_name = nil + assert response = @gateway.purchase(@amount, @apple_pay_card, @options) + assert_failure response + assert_equal 'Bad Request (27) - Invalid Card Holder', response.message + end + def test_failed_purchase @amount = 501300 - assert response = @gateway.purchase(@amount, @credit_card, @options ) + assert response = @gateway.purchase(@amount, @credit_card, @options) assert_match(/Transaction not approved/, response.message) assert_failure response end + def test_failed_purchase_with_insufficient_funds + assert response = @gateway.purchase(530200, @credit_card, @options) + assert_failure response + assert_equal '302', response.error_code + 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 @@ -105,7 +264,7 @@ def test_partial_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture end @@ -125,6 +284,28 @@ def test_successful_refund assert response.authorization end + def test_successful_refund_with_soft_descriptors + assert purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_match(/Transaction Normal/, purchase.message) + assert_success purchase + + assert response = @gateway.refund(50, purchase.authorization, @options.merge(@options_mdd)) + assert_success response + assert_match(/Transaction Normal/, response.message) + assert response.authorization + end + + def test_successful_refund_with_order_id + assert purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_match(/Transaction Normal/, purchase.message) + assert_success purchase + + assert response = @gateway.refund(50, purchase.authorization, @options.merge(order_id: '1234')) + assert_success response + assert_match(/Transaction Normal/, response.message) + assert response.authorization + end + def test_successful_refund_with_echeck assert purchase = @gateway.purchase(@amount, @check, @options) assert_match(/Transaction Normal/, purchase.message) @@ -154,7 +335,7 @@ def test_partial_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount-1, purchase.authorization) + assert refund = @gateway.refund(@amount - 1, purchase.authorization) assert_success refund end @@ -169,6 +350,22 @@ def test_failed_refund assert response.authorization end + def test_successful_general_credit + assert response = @gateway.credit(@amount, @credit_card, @options.merge(@options_mdd)) + 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_successful_general_credit_with_order_id + assert response = @gateway.credit(@amount, @credit_card, @options.merge(order_id: '1234')) + 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_successful_void auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth @@ -178,6 +375,36 @@ def test_successful_void assert_equal 'Transaction Normal - Approved', void.message end + def test_successful_auth_void_with_reversal_id + auth = @gateway.authorize(@amount, @credit_card, @options.merge(reversal_id: @reversal_id)) + assert_success auth + + assert void = @gateway.void(auth.authorization, reversal_id: @reversal_id) + assert_success void + assert_equal 'Transaction Normal - Approved', void.message + end + + def test_successful_void_purchase_with_reversal_id + response = @gateway.purchase(@amount, @credit_card, @options.merge(reversal_id: @reversal_id)) + assert_success response + + assert void = @gateway.void(response.authorization, reversal_id: @reversal_id) + assert_success void + assert_equal 'Transaction Normal - Approved', void.message + end + + def test_successful_void_with_stored_card_and_reversal_id + response = @gateway.store(@credit_card, @options) + assert_success response + + auth = @gateway.authorize(@amount, response.authorization, @options.merge(reversal_id: @reversal_id)) + assert_success auth + + assert void = @gateway.void(auth.authorization, reversal_id: @reversal_id) + assert_success void + assert_equal 'Transaction Normal - Approved', void.message + end + def test_successful_void_with_stored_card response = @gateway.store(@credit_card, @options) assert_success response @@ -231,10 +458,10 @@ def test_response_contains_cvv_and_avs_results def test_trans_error # ask for error 42 (unable to send trans) as the cents bit... @amount = 500042 - assert response = @gateway.purchase(@amount, @credit_card, @options ) + 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 @@ -283,4 +510,14 @@ def test_transcript_scrubbing_echeck assert_scrubbed(@check.routing_number, transcript) assert_scrubbed(@gateway.options[:token], transcript) end + + def test_transcript_scrubbing_network_token + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @apple_pay_card, @options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@apple_pay_card.payment_cryptogram, transcript) + assert_scrubbed(@apple_pay_card.verification_value, transcript) + end end diff --git a/test/remote/gateways/remote_payex_test.rb b/test/remote/gateways/remote_payex_test.rb index fd37ddb6164..0eb8903fbce 100644 --- a/test/remote/gateways/remote_payex_test.rb +++ b/test/remote/gateways/remote_payex_test.rb @@ -1,7 +1,6 @@ require 'test_helper' class RemotePayexTest < Test::Unit::TestCase - def setup @gateway = PayexGateway.new(fixtures(:payex)) @@ -10,7 +9,7 @@ def setup @declined_card = credit_card('4000300011112220') @options = { - :order_id => '1234', + order_id: '1234' } end @@ -80,7 +79,7 @@ def test_successful_store_and_purchase assert_success response assert_equal 'OK', response.message - assert response = @gateway.purchase(@amount, response.authorization, @options.merge({order_id: '5678'})) + assert response = @gateway.purchase(@amount, response.authorization, @options.merge({ order_id: '5678' })) assert_success response assert_equal 'OK', response.message end @@ -90,7 +89,7 @@ def test_successful_store_and_authorize_and_capture assert_success response assert_equal 'OK', response.message - assert response = @gateway.authorize(@amount, response.authorization, @options.merge({order_id: '5678'})) + assert response = @gateway.authorize(@amount, response.authorization, @options.merge({ order_id: '5678' })) assert_success response assert_equal 'OK', response.message assert response.authorization @@ -109,9 +108,9 @@ def test_successful_unstore def test_invalid_login gateway = PayexGateway.new( - :account => '1', - :encryption_key => '1' - ) + account: '1', + encryption_key: '1' + ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_not_equal 'OK', response.message diff --git a/test/remote/gateways/remote_payflow_express_test.rb b/test/remote/gateways/remote_payflow_express_test.rb index c2a2cbfe2c0..7a0377e0087 100644 --- a/test/remote/gateways/remote_payflow_express_test.rb +++ b/test/remote/gateways/remote_payflow_express_test.rb @@ -3,22 +3,23 @@ class RemotePayflowExpressTest < Test::Unit::TestCase def setup Base.mode = :test - + @gateway = PayflowExpressGateway.new(fixtures(:payflow)) - @options = { :billing_address => { - :name => 'Cody Fauser', - :address1 => '1234 Shady Brook Lane', - :city => 'Ottawa', - :state => 'ON', - :country => 'CA', - :zip => '90210', - :phone => '555-555-5555' - }, - :email => 'cody@example.com' - } + @options = { + billing_address: { + name: 'Cody Fauser', + address1: '1234 Shady Brook Lane', + city: 'Ottawa', + state: 'ON', + country: 'CA', + zip: '90210', + phone: '555-555-5555' + }, + email: 'cody@example.com' + } end - + # Only works with a Payflow 2.0 account or by requesting the addition # of Express checkout to an existing Payflow Pro account. This can be done # by contacting Payflow sales. The PayPal account used must be a business @@ -26,21 +27,21 @@ def setup # the tests to work correctly def test_set_express_authorization @options.update( - :return_url => 'http://example.com', - :cancel_return_url => 'http://example.com', - :email => 'Buyer1@paypal.com' + return_url: 'http://example.com', + cancel_return_url: 'http://example.com', + email: 'Buyer1@paypal.com' ) response = @gateway.setup_authorization(500, @options) assert response.success? assert response.test? assert !response.params['token'].blank? end - + def test_set_express_purchase @options.update( - :return_url => 'http://example.com', - :cancel_return_url => 'http://example.com', - :email => 'Buyer1@paypal.com' + return_url: 'http://example.com', + cancel_return_url: 'http://example.com', + email: 'Buyer1@paypal.com' ) response = @gateway.setup_purchase(500, @options) assert response.success? @@ -50,142 +51,168 @@ def test_set_express_purchase def test_setup_authorization_discount_taxes_included_free_shipping amount = 2518 - options = {:ip=>'127.0.0.1', - :return_url=>'http://localhost:3000/orders/1/8e06ea26f8add7608671d433f13c2193/commit_paypal?buyer_accepts_marketing=true&utm_nooverride=1', - :cancel_return_url=>'http://localhost:3000/orders/1/8e06ea26f8add7608671d433f13c2193/commit_paypal?utm_nooverride=1', - :customer=>'test6@test.com', - :email=>'test6@test.com', - :order_id=>'#1092', - :currency=>'USD', - :subtotal=>2798, - :items => [ - {:name => 'test4', - :description => 'test4', - :quantity=>2 , - :amount=> 1399 , - :url=>'http://localhost:3000/products/test4'}], - :discount=>280, - :no_shipping=>true} + options = { + ip: '127.0.0.1', + return_url: 'http://localhost:3000/orders/1/8e06ea26f8add7608671d433f13c2193/commit_paypal?buyer_accepts_marketing=true&utm_nooverride=1', + cancel_return_url: 'http://localhost:3000/orders/1/8e06ea26f8add7608671d433f13c2193/commit_paypal?utm_nooverride=1', + customer: 'test6@test.com', + email: 'test6@test.com', + order_id: '#1092', + currency: 'USD', + subtotal: 2798, + items: [ + { + name: 'test4', + description: 'test4', + quantity: 2, + amount: 1399, + url: 'http://localhost:3000/products/test4' + } + ], + discount: 280, + no_shipping: true + } response = @gateway.setup_authorization(amount, options) assert response.success?, response.message end def test_setup_authorization_with_discount_taxes_additional amount = 2518 - options = {:ip=>'127.0.0.1', - :return_url=>'http://localhost:3000/orders/1/8e06ea26f8add7608671d433f13c2193/commit_paypal?buyer_accepts_marketing=true&utm_nooverride=1', - :cancel_return_url=>'http://localhost:3000/orders/1/8e06ea26f8add7608671d433f13c2193/commit_paypal?utm_nooverride=1', - :customer=>'test6@test.com', - :email=>'test6@test.com', - :order_id=>'#1092', - :currency=>'USD', - :subtotal=>2798, - :items => [ - {:name => 'test4', - :description => 'test4', - :quantity=>2 , - :amount=> 1399 , - :url=>'http://localhost:3000/products/test4'}], - :discount=>280, - :no_shipping=>true} + options = { + ip: '127.0.0.1', + return_url: 'http://localhost:3000/orders/1/8e06ea26f8add7608671d433f13c2193/commit_paypal?buyer_accepts_marketing=true&utm_nooverride=1', + cancel_return_url: 'http://localhost:3000/orders/1/8e06ea26f8add7608671d433f13c2193/commit_paypal?utm_nooverride=1', + customer: 'test6@test.com', + email: 'test6@test.com', + order_id: '#1092', + currency: 'USD', + subtotal: 2798, + items: [ + { + name: 'test4', + description: 'test4', + quantity: 2, + amount: 1399, + url: 'http://localhost:3000/products/test4' + } + ], + discount: 280, + no_shipping: true + } response = @gateway.setup_authorization(amount, options) assert response.success?, response.message end def test_setup_authorization_with_discount_taxes_and_shipping_addtiional amount = 2518 - options = {:ip=>'127.0.0.1', - :return_url=>'http://localhost:3000/orders/1/8e06ea26f8add7608671d433f13c2193/commit_paypal?buyer_accepts_marketing=true&utm_nooverride=1', - :cancel_return_url=>'http://localhost:3000/orders/1/8e06ea26f8add7608671d433f13c2193/commit_paypal?utm_nooverride=1', - :customer=>'test6@test.com', - :email=>'test6@test.com', - :order_id=>'#1092', - :currency=>'USD', - :subtotal=>2798, - :items => [ - {:name => 'test4', - :description => 'test4', - :quantity=>2 , - :amount=> 1399 , - :url=>'http://localhost:3000/products/test4'}], - :discount=>280, - :no_shipping=>false} + options = { + ip: '127.0.0.1', + return_url: 'http://localhost:3000/orders/1/8e06ea26f8add7608671d433f13c2193/commit_paypal?buyer_accepts_marketing=true&utm_nooverride=1', + cancel_return_url: 'http://localhost:3000/orders/1/8e06ea26f8add7608671d433f13c2193/commit_paypal?utm_nooverride=1', + customer: 'test6@test.com', + email: 'test6@test.com', + order_id: '#1092', + currency: 'USD', + subtotal: 2798, + items: [ + { + name: 'test4', + description: 'test4', + quantity: 2, + amount: 1399, + url: 'http://localhost:3000/products/test4' + } + ], + discount: 280, + no_shipping: false + } response = @gateway.setup_authorization(amount, options) assert response.success?, response.message end end class RemotePayflowExpressUkTest < Test::Unit::TestCase - def setup @gateway = PayflowExpressUkGateway.new(fixtures(:payflow_uk)) end def test_setup_authorization_discount_taxes_included_free_shipping amount = 2518 - options = {:ip=>'127.0.0.1', - :return_url=>'http://localhost:3000/orders/1/8e06ea26f8add7608671d433f13c2193/commit_paypal?buyer_accepts_marketing=true&utm_nooverride=1', - :cancel_return_url=>'http://localhost:3000/orders/1/8e06ea26f8add7608671d433f13c2193/commit_paypal?utm_nooverride=1', - :customer=>'test6@test.com', - :email=>'test6@test.com', - :order_id=>'#1092', - :currency=>'GBP', - :subtotal=>2798, - :items=> [ - {:name=>'test4', - :description=>'test4', - :quantity=>2, - :amount=>1399, - :url=>'http://localhost:3000/products/test4'}, - ], - :discount=>280, - :no_shipping=>true} + options = { + ip: '127.0.0.1', + return_url: 'http://localhost:3000/orders/1/8e06ea26f8add7608671d433f13c2193/commit_paypal?buyer_accepts_marketing=true&utm_nooverride=1', + cancel_return_url: 'http://localhost:3000/orders/1/8e06ea26f8add7608671d433f13c2193/commit_paypal?utm_nooverride=1', + customer: 'test6@test.com', + email: 'test6@test.com', + order_id: '#1092', + currency: 'GBP', + subtotal: 2798, + items: [ + { + name: 'test4', + description: 'test4', + quantity: 2, + amount: 1399, + url: 'http://localhost:3000/products/test4' + } + ], + discount: 280, + no_shipping: true + } response = @gateway.setup_authorization(amount, options) assert response.success?, response.message end def test_setup_authorization_with_discount_taxes_additional amount = 2518 - options = {:ip=>'127.0.0.1', - :return_url=>'http://localhost:3000/orders/1/8e06ea26f8add7608671d433f13c2193/commit_paypal?buyer_accepts_marketing=true&utm_nooverride=1', - :cancel_return_url=>'http://localhost:3000/orders/1/8e06ea26f8add7608671d433f13c2193/commit_paypal?utm_nooverride=1', - :customer=>'test6@test.com', - :email=>'test6@test.com', - :order_id=>'#1092', - :currency=>'GBP', - :subtotal=>2798, - :items=> [ - {:name=>'test4', - :description=>'test4', - :quantity=>2, - :amount=>1399, - :url=>'http://localhost:3000/products/test4'}, - ], - :discount=>280, - :no_shipping=>true} + options = { + ip: '127.0.0.1', + return_url: 'http://localhost:3000/orders/1/8e06ea26f8add7608671d433f13c2193/commit_paypal?buyer_accepts_marketing=true&utm_nooverride=1', + cancel_return_url: 'http://localhost:3000/orders/1/8e06ea26f8add7608671d433f13c2193/commit_paypal?utm_nooverride=1', + customer: 'test6@test.com', + email: 'test6@test.com', + order_id: '#1092', + currency: 'GBP', + subtotal: 2798, + items: [ + { + name: 'test4', + description: 'test4', + quantity: 2, + amount: 1399, + url: 'http://localhost:3000/products/test4' + } + ], + discount: 280, + no_shipping: true + } response = @gateway.setup_authorization(amount, options) assert response.success?, response.message end def test_setup_authorization_with_discount_taxes_and_shipping_addtiional amount = 2518 - options = {:ip=>'127.0.0.1', - :return_url=>'http://localhost:3000/orders/1/8e06ea26f8add7608671d433f13c2193/commit_paypal?buyer_accepts_marketing=true&utm_nooverride=1', - :cancel_return_url=>'http://localhost:3000/orders/1/8e06ea26f8add7608671d433f13c2193/commit_paypal?utm_nooverride=1', - :customer=>'test6@test.com', - :email=>'test6@test.com', - :order_id=>'#1092', - :currency=>'GBP', - :subtotal=>2798, - :items=> [ - {:name=>'test4', - :description=>'test4', - :quantity=>2, - :amount=>1399, - :url=>'http://localhost:3000/products/test4'}, - ], - :discount=>280, - :no_shipping=>false} + options = { + ip: '127.0.0.1', + return_url: 'http://localhost:3000/orders/1/8e06ea26f8add7608671d433f13c2193/commit_paypal?buyer_accepts_marketing=true&utm_nooverride=1', + cancel_return_url: 'http://localhost:3000/orders/1/8e06ea26f8add7608671d433f13c2193/commit_paypal?utm_nooverride=1', + customer: 'test6@test.com', + email: 'test6@test.com', + order_id: '#1092', + currency: 'GBP', + subtotal: 2798, + items: [ + { + name: 'test4', + description: 'test4', + quantity: 2, + amount: 1399, + url: 'http://localhost:3000/products/test4' + } + ], + discount: 280, + no_shipping: false + } response = @gateway.setup_authorization(amount, options) assert response.success?, response.message end diff --git a/test/remote/gateways/remote_payflow_test.rb b/test/remote/gateways/remote_payflow_test.rb index 23b7a13f37b..ed12025cd79 100644 --- a/test/remote/gateways/remote_payflow_test.rb +++ b/test/remote/gateways/remote_payflow_test.rb @@ -8,27 +8,49 @@ def setup @credit_card = credit_card( '5105105105105100', - :brand => 'master' + brand: 'master' ) @options = { - :billing_address => address, - :email => 'cody@example.com', - :customer => 'codyexample' + billing_address: address, + email: 'cody@example.com', + customer: 'codyexample' } @extra_options = { - :order_id => '123', - :description => 'Description string', - :order_desc => 'OrderDesc string', - :comment => 'Comment string', - :comment2 => 'Comment2 string' + order_id: '123', + description: 'Description string', + order_desc: 'OrderDesc string', + comment: 'Comment string', + comment2: 'Comment2 string', + merch_descr: 'MerchDescr string' } @check = check( - :routing_number => '111111118', - :account_number => '1111111111' + routing_number: '111111118', + account_number: '1111111111' ) + + @l2_json = '{ + "Tender": { + "ACH": { + "AcctType": "C", + "AcctNum": "6355059797", + "ABA": "021000021" + } + } + }' + + @l3_json = '{ + "Invoice": { + "Date": "20190104", + "Level3Invoice": { + "CountyTax": {"Amount": "3.23"} + }, + "Items": + "11119999Widget2INQ49.999.983.008.00101.00
500 Main St.AnytownNY67890US
2003063024680
ABC01232003071454.10.15.05
22228888Gizmo5INQ9.992.503.002.5052.95
500 Main St.AnytownNY67890US
2003062813579
XYZ78902003071154.10.16.05
" + } + }' end def test_successful_purchase @@ -40,6 +62,28 @@ def test_successful_purchase assert !response.fraud_review? end + def test_successful_purchase_with_stored_credential + @options[:stored_credential] = { + initial_transaction: true, + reason_type: 'recurring', + initiator: 'cardholder', + network_transaction_id: nil + } + assert response = @gateway.purchase(100000, @credit_card, @options) + assert_equal 'Approved', response.message + assert_success response + + @options[:stored_credential] = { + initial_transaction: false, + reason_type: 'recurring', + initiator: 'merchant', + network_transaction_id: response.authorization + } + assert response = @gateway.purchase(100000, @credit_card, @options) + assert_equal 'Approved', response.message + assert_success response + end + def test_successful_purchase_with_extra_options assert response = @gateway.purchase(100000, @credit_card, @options.merge(@extra_options)) assert_equal 'Approved', response.message @@ -49,6 +93,19 @@ def test_successful_purchase_with_extra_options assert !response.fraud_review? end + def test_successful_purchase_with_application_id + ActiveMerchant::Billing::PayflowGateway.application_id = 'partner_id' + + assert response = @gateway.purchase(100000, @credit_card, @options) + assert_equal 'Approved', response.message + assert_success response + assert response.test? + assert_not_nil response.authorization + assert !response.fraud_review? + ensure + ActiveMerchant::Billing::PayflowGateway.application_id = nil + end + # In order for this remote test to pass, you must go into your Payflow test # backend and enable the correct filter. Once logged in: # "Service Settings" -> @@ -68,6 +125,50 @@ def test_successful_purchase_with_fraud_review assert response.fraud_review? end + def test_successful_purchase_with_l2_fields + options = @options.merge(level_two_fields: @l2_json) + + assert response = @gateway.purchase(100000, @credit_card, options) + assert_equal 'Approved', response.message + assert_success response + assert response.test? + assert_not_nil response.authorization + end + + def test_successful_purchase_with_l3_fields + options = @options.merge(level_three_fields: @l3_json) + + assert response = @gateway.purchase(100000, @credit_card, options) + assert_equal 'Approved', response.message + assert_success response + assert response.test? + assert_not_nil response.authorization + end + + def test_successful_purchase_with_l2_l3_fields + options = @options.merge(level_two_fields: @l2_json).merge(level_three_fields: @l3_json) + + assert response = @gateway.purchase(100000, @credit_card, options) + assert_equal 'Approved', response.message + assert_success response + assert response.test? + assert_not_nil response.authorization + end + + def test_successful_purchase_with_l3_fields_and_application_id + ActiveMerchant::Billing::PayflowGateway.application_id = 'partner_id' + + options = @options.merge(level_three_fields: @l3_json) + + assert response = @gateway.purchase(100000, @credit_card, options) + assert_equal 'Approved', response.message + assert_success response + assert response.test? + assert_not_nil response.authorization + ensure + ActiveMerchant::Billing::PayflowGateway.application_id = nil + end + def test_declined_purchase assert response = @gateway.purchase(210000, @credit_card, @options) assert_equal 'Declined', response.message @@ -125,6 +226,19 @@ def test_authorize_and_capture_with_three_d_secure_option assert_success capture end + def test_successful_authorize_with_application_id + ActiveMerchant::Billing::PayflowGateway.application_id = 'partner_id' + + assert response = @gateway.authorize(100000, @credit_card, @options) + assert_equal 'Approved', response.message + assert_success response + assert response.test? + assert_not_nil response.authorization + assert !response.fraud_review? + ensure + ActiveMerchant::Billing::PayflowGateway.application_id = nil + end + def test_authorize_and_partial_capture assert auth = @gateway.authorize(100 * 2, @credit_card, @options) assert_success auth @@ -135,6 +249,32 @@ def test_authorize_and_partial_capture assert_success capture end + def test_authorize_and_complete_capture + assert auth = @gateway.authorize(100 * 2, @credit_card, @options) + assert_success auth + assert_equal 'Approved', auth.message + assert auth.authorization + + assert capture = @gateway.capture(100, auth.authorization, capture_complete: 'Y') + assert_success capture + + assert capture = @gateway.capture(100, auth.authorization) + assert_failure capture + end + + def test_authorize_and_uncomplete_capture + assert auth = @gateway.authorize(100 * 2, @credit_card, @options) + assert_success auth + assert_equal 'Approved', auth.message + assert auth.authorization + + assert capture = @gateway.capture(100, auth.authorization, capture_complete: 'N') + assert_success capture + + assert capture = @gateway.capture(100, auth.authorization) + assert_success capture + end + def test_failed_capture assert response = @gateway.capture(100, '999') assert_failure response @@ -159,7 +299,7 @@ def test_successful_verify def test_successful_verify_amex @amex_credit_card = credit_card( '378282246310005', - :brand => 'american_express' + brand: 'american_express' ) assert response = @gateway.verify(@amex_credit_card, @options) assert_success response @@ -174,8 +314,8 @@ def test_failed_verify def test_invalid_login gateway = PayflowGateway.new( - :login => '', - :password => '' + login: '', + password: '' ) assert response = gateway.purchase(100, @credit_card, @options) assert_equal 'Invalid vendor account', response.message @@ -187,7 +327,7 @@ def test_duplicate_request_id SecureRandom.expects(:hex).times(2).returns(request_id) response1 = @gateway.purchase(100, @credit_card, @options) - assert response1.success? + assert response1.success? assert_nil response1.params['duplicate'] response2 = @gateway.purchase(100, @credit_card, @options) @@ -197,7 +337,7 @@ def test_duplicate_request_id def test_create_recurring_profile response = assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.recurring(1000, @credit_card, :periodicity => :monthly) + @gateway.recurring(1000, @credit_card, periodicity: :monthly) end assert_success response assert !response.params['profile_id'].blank? @@ -206,7 +346,7 @@ def test_create_recurring_profile def test_create_recurring_profile_with_invalid_date response = assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.recurring(1000, @credit_card, :periodicity => :monthly, :starting_at => Time.now) + @gateway.recurring(1000, @credit_card, periodicity: :monthly, starting_at: Time.now) end assert_failure response assert_equal 'Field format error: Start or next payment date must be a valid future date', response.message @@ -216,7 +356,7 @@ def test_create_recurring_profile_with_invalid_date def test_create_and_cancel_recurring_profile response = assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.recurring(1000, @credit_card, :periodicity => :monthly) + @gateway.recurring(1000, @credit_card, periodicity: :monthly) end assert_success response assert !response.params['profile_id'].blank? @@ -232,10 +372,10 @@ def test_create_and_cancel_recurring_profile def test_full_feature_set_for_recurring_profiles # Test add @options.update( - :periodicity => :weekly, - :payments => '12', - :starting_at => Time.now + 1.day, - :comment => 'Test Profile' + periodicity: :weekly, + payments: '12', + starting_at: Time.now + 1.day, + comment: 'Test Profile' ) response = assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do @gateway.recurring(100, @credit_card, @options) @@ -249,10 +389,10 @@ def test_full_feature_set_for_recurring_profiles # Test modify @options.update( - :periodicity => :monthly, - :starting_at => Time.now + 1.day, - :payments => '4', - :profile_id => @recurring_profile_id + periodicity: :monthly, + starting_at: Time.now + 1.day, + payments: '4', + profile_id: @recurring_profile_id ) response = assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do @gateway.recurring(400, @credit_card, @options) @@ -272,7 +412,7 @@ def test_full_feature_set_for_recurring_profiles # Test payment history inquiry response = assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.recurring_inquiry(@recurring_profile_id, :history => true) + @gateway.recurring_inquiry(@recurring_profile_id, history: true) end assert_equal '0', response.params['result'] assert_success response @@ -305,11 +445,13 @@ def test_reference_purchase def test_recurring_with_initial_authorization response = assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.recurring(1000, @credit_card, - :periodicity => :monthly, - :initial_transaction => { - :type => :purchase, - :amount => 500 + @gateway.recurring( + 1000, + @credit_card, + periodicity: :monthly, + initial_transaction: { + type: :purchase, + amount: 500 } ) end @@ -378,13 +520,15 @@ def test_high_verbosity def three_d_secure_option { - :three_d_secure => { - :status => 'Y', - :authentication_id => 'QvDbSAxSiaQs241899E0', - :eci => '02', - :cavv => 'jGvQIvG/5UhjAREALGYa6Vu/hto=', - :xid => 'UXZEYlNBeFNpYVFzMjQxODk5RTA=' - } + three_d_secure: { + authentication_id: 'QvDbSAxSiaQs241899E0', + authentication_response_status: 'Y', + eci: '02', + cavv: 'jGvQIvG/5UhjAREALGYa6Vu/hto=', + xid: 'UXZEYlNBeFNpYVFzMjQxODk5RTA=', + version: '2.2.0', + ds_transaction_id: '97267598-FAE6-48F2-8083-C23433990FBC' + } } end diff --git a/test/remote/gateways/remote_payflow_uk_test.rb b/test/remote/gateways/remote_payflow_uk_test.rb index ae2eafd0caa..799380eab47 100644 --- a/test/remote/gateways/remote_payflow_uk_test.rb +++ b/test/remote/gateways/remote_payflow_uk_test.rb @@ -6,51 +6,31 @@ def setup # The default partner is PayPalUk @gateway = PayflowUkGateway.new(fixtures(:payflow_uk)) - + @creditcard = CreditCard.new( - :number => '5105105105105100', - :month => 11, - :year => 2009, - :first_name => 'Cody', - :last_name => 'Fauser', - :verification_value => '000', - :brand => 'master' - ) - - @solo = CreditCard.new( - :brand => 'solo', - :number => '6334900000000005', - :month => Time.now.month, - :year => Time.now.year + 1, - :first_name => 'Test', - :last_name => 'Mensch', - :issue_number => '01' + number: '5105105105105100', + month: 11, + year: 2009, + first_name: 'Cody', + last_name: 'Fauser', + verification_value: '000', + brand: 'master' ) - - @switch = CreditCard.new( - :brand => 'switch', - :number => '5641820000000005', - :verification_value => '000', - :month => 1, - :year => 2008, - :first_name => 'Fred', - :last_name => 'Brooks' - ) - - @options = { - :billing_address => { - :name => 'Cody Fauser', - :address1 => '1234 Shady Brook Lane', - :city => 'Ottawa', - :state => 'ON', - :country => 'CA', - :zip => '90210', - :phone => '555-555-5555' + + @options = { + billing_address: { + name: 'Cody Fauser', + address1: '1234 Shady Brook Lane', + city: 'Ottawa', + state: 'ON', + country: 'CA', + zip: '90210', + phone: '555-555-5555' }, - :email => 'cody@example.com' + email: 'cody@example.com' } end - + def test_successful_purchase assert response = @gateway.purchase(100000, @creditcard, @options) assert_equal 'Approved', response.message @@ -58,31 +38,31 @@ def test_successful_purchase assert response.test? assert_not_nil response.authorization end - + def test_declined_purchase assert response = @gateway.purchase(210000, @creditcard, @options) assert_equal 'Failed merchant rule check', response.message assert_failure response assert response.test? end - + def test_successful_purchase_solo - assert response = @gateway.purchase(100000, @solo, @options) - assert_equal 'Approved', response.message - assert_success response - assert response.test? - assert_not_nil response.authorization - end - + assert response = @gateway.purchase(100000, @solo, @options) + assert_equal 'Approved', response.message + assert_success response + assert response.test? + assert_not_nil response.authorization + end + def test_no_card_issue_or_card_start_with_switch assert response = @gateway.purchase(100000, @switch, @options) assert_failure response - + assert_equal 'Field format error: CARDSTART or CARDISSUE must be present', response.message assert_failure response assert response.test? end - + def test_successful_purchase_switch_with_issue_number @switch.issue_number = '01' assert response = @gateway.purchase(100000, @switch, @options) @@ -91,7 +71,7 @@ def test_successful_purchase_switch_with_issue_number assert response.test? assert_not_nil response.authorization end - + def test_successful_purchase_switch_with_start_date @switch.start_month = 12 @switch.start_year = 1999 @@ -101,7 +81,7 @@ def test_successful_purchase_switch_with_start_date assert response.test? assert_not_nil response.authorization end - + def test_successful_purchase_switch_with_start_date_and_issue_number @switch.issue_number = '05' @switch.start_month = 12 @@ -112,7 +92,7 @@ def test_successful_purchase_switch_with_start_date_and_issue_number assert response.test? assert_not_nil response.authorization end - + def test_successful_authorization assert response = @gateway.authorize(100, @creditcard, @options) assert_equal 'Approved', response.message @@ -130,13 +110,13 @@ def test_authorize_and_capture assert capture = @gateway.capture(amount, auth.authorization) assert_success capture end - + def test_failed_capture assert response = @gateway.capture(100, '999') assert_failure response assert_equal 'Invalid tender', response.message end - + def test_authorize_and_void assert auth = @gateway.authorize(100, @creditcard, @options) assert_success auth @@ -145,26 +125,26 @@ def test_authorize_and_void assert void = @gateway.void(auth.authorization) assert_success void end - + def test_invalid_login gateway = PayflowGateway.new( - :login => '', - :password => '' + login: '', + password: '' ) assert response = gateway.purchase(100, @creditcard, @options) assert_equal 'Invalid vendor account', response.message assert_failure response end - + def test_duplicate_request_id gateway = PayflowUkGateway.new( - :login => @login, - :password => @password + login: @login, + password: @password ) - - request_id = Digest::SHA1.hexdigest(rand.to_s).slice(0,32) + + request_id = Digest::SHA1.hexdigest(rand.to_s).slice(0, 32) gateway.expects(:generate_unique_id).times(2).returns(request_id) - + response1 = gateway.purchase(100, @creditcard, @options) assert_nil response1.params['duplicate'] response2 = gateway.purchase(100, @creditcard, @options) diff --git a/test/remote/gateways/remote_payment_express_test.rb b/test/remote/gateways/remote_payment_express_test.rb index 8fb5bfc22b7..f1493090a98 100644 --- a/test/remote/gateways/remote_payment_express_test.rb +++ b/test/remote/gateways/remote_payment_express_test.rb @@ -1,17 +1,16 @@ require 'test_helper' class RemotePaymentExpressTest < Test::Unit::TestCase - def setup @gateway = PaymentExpressGateway.new(fixtures(:payment_express)) @credit_card = credit_card('4111111111111111') @options = { - :order_id => generate_unique_id, - :billing_address => address, - :email => 'cody@example.com', - :description => 'Store purchase' + order_id: generate_unique_id, + billing_address: address, + email: 'cody@example.com', + description: 'Store purchase' } @amount = 100 @@ -31,6 +30,25 @@ def test_successful_purchase_with_reference_id assert_not_nil response.authorization end + def test_successful_purchase_with_ip + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(ip: '192.168.0.1')) + assert_success response + assert_equal 'The Transaction was approved', response.message + assert_not_nil response.authorization + end + + def test_successful_purchase_with_avs_fields + options = { + enable_avs_data: 0, + avs_action: 3 + } + + assert response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'The Transaction was approved', response.message + assert_not_nil response.authorization + end + def test_declined_purchase assert response = @gateway.purchase(@amount, credit_card('5431111111111228'), @options) assert_match %r{declined}i, response.message @@ -59,26 +77,34 @@ def test_purchase_and_refund assert_success purchase assert_equal 'The Transaction was approved', purchase.message assert !purchase.authorization.blank? - assert refund = @gateway.refund(amount, purchase.authorization, :description => 'Giving a refund') + assert refund = @gateway.refund(amount, purchase.authorization, description: 'Giving a refund') assert_success refund end def test_failed_capture assert response = @gateway.capture(@amount, '999') assert_failure response - assert_equal 'DpsTxnRef Invalid', response.message + assert_equal 'The transaction has not been processed.', response.message end def test_invalid_login gateway = PaymentExpressGateway.new( - :login => '', - :password => '' + login: '', + password: '' ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_match %r{Invalid Credentials}i, response.message assert_failure response end + def test_verify + assert response = @gateway.verify(@credit_card, @options) + assert_success response + assert_equal 'The Transaction was approved', response.message + assert_not_nil token = response.authorization + assert_equal token, response.token + end + def test_store_credit_card assert response = @gateway.store(@credit_card) assert_success response @@ -88,8 +114,8 @@ def test_store_credit_card end def test_store_with_custom_token - token = Time.now.to_i.to_s #hehe - assert response = @gateway.store(@credit_card, :billing_id => token) + token = Time.now.to_i.to_s + assert response = @gateway.store(@credit_card, billing_id: token) assert_success response assert_equal 'The Transaction was approved', response.message assert_not_nil response.authorization @@ -97,7 +123,6 @@ def test_store_with_custom_token end def test_store_invalid_credit_card - original_number = @credit_card.number @credit_card.number = 2 assert response = @gateway.store(@credit_card) @@ -108,9 +133,9 @@ def test_store_and_charge assert response = @gateway.store(@credit_card) assert_success response assert_equal 'The Transaction was approved', response.message - assert (token = response.authorization) + assert(token = response.authorization) - assert purchase = @gateway.purchase( @amount, token) + assert purchase = @gateway.purchase(@amount, token) assert_equal 'The Transaction was approved', purchase.message assert_success purchase assert_not_nil purchase.authorization @@ -120,7 +145,7 @@ def test_store_and_authorize_and_capture assert response = @gateway.store(@credit_card) assert_success response assert_equal 'The Transaction was approved', response.message - assert (token = response.authorization) + assert(token = response.authorization) assert auth = @gateway.authorize(@amount, token, @options) assert_success auth @@ -140,5 +165,4 @@ def test_transcript_scrubbing assert_scrubbed(@credit_card.verification_value.to_s, clean_transcript) assert_scrubbed(@gateway.options[:password], clean_transcript) end - end diff --git a/test/remote/gateways/remote_paymentez_test.rb b/test/remote/gateways/remote_paymentez_test.rb index 6164e7e2e9b..e2a504648be 100644 --- a/test/remote/gateways/remote_paymentez_test.rb +++ b/test/remote/gateways/remote_paymentez_test.rb @@ -3,10 +3,21 @@ class RemotePaymentezTest < Test::Unit::TestCase def setup @gateway = PaymentezGateway.new(fixtures(:paymentez)) + @ecuador_gateway = PaymentezGateway.new(fixtures(:paymentez_ecuador)) @amount = 100 - @credit_card = credit_card('4111111111111111', verification_value: '555') - @declined_card = credit_card('4242424242424242', verification_value: '555') + @credit_card = credit_card('4111111111111111', verification_value: '666') + @otp_card = credit_card('36417002140808', verification_value: '666') + @elo_credit_card = credit_card( + '6362970000457013', + month: 10, + year: Time.now.year + 1, + first_name: 'John', + last_name: 'Smith', + verification_value: '737', + brand: 'elo' + ) + @declined_card = credit_card('4242424242424242', verification_value: '666') @options = { billing_address: address, description: 'Store Purchase', @@ -15,6 +26,29 @@ def setup vat: 0, dev_reference: 'Testing' } + + @cavv = 'example-cavv-value' + @xid = 'three-ds-v1-trans-id' + @eci = '01' + @three_ds_v1_version = '1.0.2' + @three_ds_v2_version = '2.1.0' + @ds_server_trans_id = 'ffffffff-9002-51a3-8000-0000000345a2' + @authentication_response_status = 'Y' + + @three_ds_v1_mpi = { + cavv: @cavv, + eci: @eci, + version: @three_ds_v1_version, + xid: @xid + } + + @three_ds_v2_mpi = { + cavv: @cavv, + eci: @eci, + version: @three_ds_v2_version, + ds_transaction_id: @ds_server_trans_id, + authentication_response_status: @authentication_response_status + } end def test_successful_purchase @@ -22,7 +56,38 @@ def test_successful_purchase assert_success response end + def test_successful_purchase_with_elo + response = @gateway.purchase(@amount, @elo_credit_card, @options) + assert_success response + end + def test_successful_purchase_with_more_options + options = { + order_id: '1', + ip: '127.0.0.1', + tax_percentage: 0.07, + phone: '333 333 3333' + } + + response = @gateway.purchase(@amount, @credit_card, @options.merge(options)) + assert_success response + end + + def test_successful_purchase_without_phone_billing_address_option + options = { + order_id: '1', + ip: '127.0.0.1', + tax_percentage: 0.07, + billing_address: { + phone: nil + } + } + + response = @gateway.purchase(@amount, @credit_card, @options.merge(options)) + assert_success response + end + + def test_successful_purchase_without_phone_option options = { order_id: '1', ip: '127.0.0.1', @@ -33,6 +98,19 @@ def test_successful_purchase_with_more_options assert_success response end + def test_successful_purchase_with_extra_params + options = { + extra_params: { + configuration1: 'value1', + configuration2: 'value2', + configuration3: 'value3' + } + } + + response = @gateway.purchase(@amount, @credit_card, @options.merge(options)) + assert_success response + end + def test_successful_purchase_with_token store_response = @gateway.store(@credit_card, @options) assert_success store_response @@ -41,24 +119,75 @@ def test_successful_purchase_with_token assert_success purchase_response end + def test_successful_purchase_with_3ds1_mpi_fields + @options[:three_d_secure] = @three_ds_v1_mpi + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + end + + def test_successful_purchase_with_3ds2_mpi_fields + @options[:three_d_secure] = @three_ds_v2_mpi + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + end + def test_failed_purchase response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response assert_equal Gateway::STANDARD_ERROR_CODE[:card_declined], response.error_code end + def test_successful_refund + auth = @gateway.purchase(@amount, @credit_card, @options) + assert_success auth + + assert refund = @gateway.refund(@amount, auth.authorization, @options) + assert_success refund + assert_equal 'Completed', refund.message + end + + def test_successful_refund_with_more_info + auth = @gateway.purchase(@amount, @credit_card, @options) + assert_success auth + + assert refund = @gateway.refund(@amount, auth.authorization, @options.merge(more_info: true)) + assert_success refund + assert_equal 'Completed', refund.message + assert_equal '00', refund.params['transaction']['carrier_code'] + assert_equal 'Reverse by mock', refund.params['transaction']['message'] + end + + def test_successful_refund_with_elo + auth = @gateway.purchase(@amount, @elo_credit_card, @options) + assert_success auth + + assert refund = @gateway.refund(@amount, auth.authorization, @options) + assert_success refund + assert_equal 'Completed', refund.message + end + def test_successful_void auth = @gateway.purchase(@amount, @credit_card, @options) assert_success auth assert void = @gateway.void(auth.authorization) assert_success void + assert_equal 'Completed', void.message + end + + def test_successful_void_with_elo + auth = @gateway.purchase(@amount, @elo_credit_card, @options) + assert_success auth + + assert void = @gateway.void(auth.authorization) + assert_success void + assert_equal 'Completed', void.message end def test_failed_void response = @gateway.void('') assert_failure response - assert_equal 'Carrier not supported', response.message + assert_equal 'ValidationError', response.message assert_equal Gateway::STANDARD_ERROR_CODE[:config_error], response.error_code end @@ -70,6 +199,22 @@ def test_successful_authorize_and_capture assert_equal 'Response by mock', capture.message end + def test_successful_authorize_and_capture_with_nil_amount + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + assert capture = @gateway.capture(nil, auth.authorization) + assert_success capture + assert_equal 'Response by mock', capture.message + end + + def test_successful_authorize_and_capture_with_elo + auth = @gateway.authorize(@amount, @elo_credit_card, @options) + assert_success auth + assert capture = @gateway.capture(@amount, auth.authorization) + assert_success capture + assert_equal 'Response by mock', capture.message + end + def test_successful_authorize_and_capture_with_token store_response = @gateway.store(@credit_card, @options) assert_success store_response @@ -84,11 +229,24 @@ def test_successful_authorize_and_capture_with_token def test_successful_authorize_and_capture_with_different_amount auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount + 100, auth.authorization) + amount = 99.0 + assert capture = @gateway.capture(amount, auth.authorization) assert_success capture assert_equal 'Response by mock', capture.message end + def test_successful_authorize_with_3ds1_mpi_fields + @options[:three_d_secure] = @three_ds_v1_mpi + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + end + + def test_successful_authorize_with_3ds2_mpi_fields + @options[:three_d_secure] = @three_ds_v2_mpi + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + end + def test_failed_authorize response = @gateway.authorize(@amount, @declined_card, @options) assert_failure response @@ -99,7 +257,8 @@ def test_partial_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth assert capture = @gateway.capture(@amount - 1, auth.authorization) - assert_failure capture # Paymentez explicitly does not support partial capture; only GREATER than auth capture + assert_success capture + assert_equal 'Response by mock', capture.message end def test_failed_capture @@ -108,11 +267,28 @@ def test_failed_capture assert_equal 'The modification of the amount is not supported by carrier', response.message end + def test_successful_capture_with_otp + @options[:vat] = 0.1 + response = @ecuador_gateway.authorize(@amount, @otp_card, @options.merge({ otp_flow: true })) + assert_success response + assert_equal 'pending', response.params['transaction']['status'] + + transaction_id = response.params['transaction']['id'] + options = @options.merge({ type: 'BY_OTP', value: '012345' }) + response = @ecuador_gateway.capture(nil, transaction_id, options) + assert_success response + end + def test_store response = @gateway.store(@credit_card, @options) assert_success response end + def test_store_with_elo + response = @gateway.store(@elo_credit_card, @options) + assert_success response + end + def test_unstore response = @gateway.store(@credit_card, @options) assert_success response @@ -121,12 +297,29 @@ def test_unstore assert_success response end + def test_unstore_with_elo + response = @gateway.store(@elo_credit_card, @options) + assert_success response + auth = response.authorization + response = @gateway.unstore(auth, @options) + 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') response = gateway.purchase(@amount, @credit_card, @options) assert_failure response - assert_equal 'BackendResponseException', response.message + assert_equal 'BackendResponseError', response.message assert_equal Gateway::STANDARD_ERROR_CODE[:config_error], response.error_code end diff --git a/test/remote/gateways/remote_paymill_test.rb b/test/remote/gateways/remote_paymill_test.rb index 880f346a47d..800605d1c99 100644 --- a/test/remote/gateways/remote_paymill_test.rb +++ b/test/remote/gateways/remote_paymill_test.rb @@ -7,7 +7,7 @@ def setup @amount = 100 @credit_card = credit_card('5500000000000004') @options = { - :email => 'Longbob.Longse@example.com' + email: 'Longbob.Longse@example.com' } @declined_card = credit_card('5105105105105100', month: 5, year: 2020) @@ -171,5 +171,4 @@ def test_verify_credentials gateway = PaymillGateway.new(public_key: 'unknown_key', private_key: 'unknown_key') assert !gateway.verify_credentials end - end diff --git a/test/remote/gateways/remote_paypal_express_test.rb b/test/remote/gateways/remote_paypal_express_test.rb index 938bd0f9c14..76857c338ea 100644 --- a/test/remote/gateways/remote_paypal_express_test.rb +++ b/test/remote/gateways/remote_paypal_express_test.rb @@ -7,27 +7,28 @@ def setup @gateway = PaypalExpressGateway.new(fixtures(:paypal_certificate)) @options = { - :order_id => '230000', - :email => 'buyer@jadedpallet.com', - :billing_address => { :name => 'Fred Brooks', - :address1 => '1234 Penny Lane', - :city => 'Jonsetown', - :state => 'NC', - :country => 'US', - :zip => '23456' - } , - :description => 'Stuff that you purchased, yo!', - :ip => '10.0.0.1', - :return_url => 'http://example.com/return', - :cancel_return_url => 'http://example.com/cancel' + order_id: '230000', + email: 'buyer@jadedpallet.com', + billing_address: { + name: 'Fred Brooks', + address1: '1234 Penny Lane', + city: 'Jonsetown', + state: 'NC', + country: 'US', + zip: '23456' + }, + description: 'Stuff that you purchased, yo!', + ip: '10.0.0.1', + return_url: 'http://example.com/return', + cancel_return_url: 'http://example.com/cancel' } end def test_set_express_authorization @options.update( - :return_url => 'http://example.com', - :cancel_return_url => 'http://example.com', - :email => 'Buyer1@paypal.com' + return_url: 'http://example.com', + cancel_return_url: 'http://example.com', + email: 'Buyer1@paypal.com' ) response = @gateway.setup_authorization(500, @options) assert response.success? @@ -37,9 +38,9 @@ def test_set_express_authorization def test_set_express_purchase @options.update( - :return_url => 'http://example.com', - :cancel_return_url => 'http://example.com', - :email => 'Buyer1@paypal.com' + return_url: 'http://example.com', + cancel_return_url: 'http://example.com', + email: 'Buyer1@paypal.com' ) response = @gateway.setup_purchase(500, @options) assert response.success? diff --git a/test/remote/gateways/remote_paypal_test.rb b/test/remote/gateways/remote_paypal_test.rb index 0e07e911d9f..90dfae72e46 100644 --- a/test/remote/gateways/remote_paypal_test.rb +++ b/test/remote/gateways/remote_paypal_test.rb @@ -8,17 +8,18 @@ def setup @declined_card = credit_card('234234234234') @params = { - :order_id => generate_unique_id, - :email => 'buyer@jadedpallet.com', - :billing_address => { :name => 'Longbob Longsen', - :address1 => '4321 Penny Lane', - :city => 'Jonsetown', - :state => 'NC', - :country => 'US', - :zip => '23456' - } , - :description => 'Stuff that you purchased, yo!', - :ip => '10.0.0.1' + order_id: generate_unique_id, + email: 'buyer@jadedpallet.com', + billing_address: { + name: 'Longbob Longsen', + address1: '4321 Penny Lane', + city: 'Jonsetown', + state: 'NC', + country: 'US', + zip: '23456' + }, + description: 'Stuff that you purchased, yo!', + ip: '10.0.0.1' } @amount = 100 @@ -27,8 +28,8 @@ def setup # each auth-id can only be reauthorized and tested once. # leave it commented if you don't want to test reauthorization. # - #@three_days_old_auth_id = "9J780651TU4465545" - #@three_days_old_auth_id2 = "62503445A3738160X" + # @three_days_old_auth_id = "9J780651TU4465545" + # @three_days_old_auth_id2 = "62503445A3738160X" end def test_transcript_scrubbing @@ -63,6 +64,32 @@ def test_successful_purchase_with_descriptors assert response.params['transaction_id'] end + def test_successful_purchase_with_order_total_elements + order_total_elements = { + subtotal: @amount / 4, + shipping: @amount / 4, + handling: @amount / 4, + tax: @amount / 4 + } + + response = @gateway.purchase(@amount, @credit_card, @params.merge(order_total_elements)) + assert_success response + assert response.params['transaction_id'] + end + + def test_successful_purchase_with_non_fractional_currency_when_any_order_total_element_is_nil + order_total_elements = { + subtotal: @amount / 4, + shipping: @amount / 4, + handling: nil, + tax: @amount / 4 + } + + response = @gateway.purchase(@amount, @credit_card, @params.merge(order_total_elements).merge(currency: 'JPY')) + assert_success response + assert response.params['transaction_id'] + end + def test_failed_purchase response = @gateway.purchase(@amount, @declined_card, @params) assert_failure response @@ -85,6 +112,7 @@ def test_failed_authorization def test_successful_reauthorization return if not @three_days_old_auth_id + auth = @gateway.reauthorize(1000, @three_days_old_auth_id) assert_success auth assert auth.authorization @@ -97,7 +125,8 @@ def test_successful_reauthorization end def test_failed_reauthorization - return if not @three_days_old_auth_id2 # was authed for $10, attempt $20 + return if not @three_days_old_auth_id2 # was authed for $10, attempt $20 + auth = @gateway.reauthorize(2000, @three_days_old_auth_id2) assert_false auth? assert !auth.authorization @@ -116,20 +145,20 @@ def test_successful_capture def test_successful_incomplete_captures auth = @gateway.authorize(100, @credit_card, @params) assert_success auth - response = @gateway.capture(60, auth.authorization, {:complete_type => 'NotComplete'}) + response = @gateway.capture(60, auth.authorization, { complete_type: 'NotComplete' }) assert_success response assert response.params['transaction_id'] assert_equal '0.60', response.params['gross_amount'] - response_2 = @gateway.capture(40, auth.authorization) - assert_success response_2 - assert response_2.params['transaction_id'] - assert_equal '0.40', response_2.params['gross_amount'] + response2 = @gateway.capture(40, auth.authorization) + assert_success response2 + assert response2.params['transaction_id'] + assert_equal '0.40', response2.params['gross_amount'] end def test_successful_capture_updating_the_invoice_id auth = @gateway.authorize(@amount, @credit_card, @params) assert_success auth - response = @gateway.capture(@amount, auth.authorization, :order_id => "NEWID#{generate_unique_id}") + response = @gateway.capture(@amount, auth.authorization, order_id: "NEWID#{generate_unique_id}") assert_success response assert response.params['transaction_id'] assert_equal '1.00', response.params['gross_amount'] @@ -143,11 +172,21 @@ def test_successful_voiding assert_success response end + def test_purchase_and_inquire + purchase_response = @gateway.purchase(@amount, @credit_card, @params) + assert_success purchase_response + assert purchase_response.params['transaction_id'] + + response = @gateway.inquire(purchase_response.authorization, {}) + assert_success response + assert_equal 'Success', response.message + end + def test_purchase_and_full_credit purchase = @gateway.purchase(@amount, @credit_card, @params) assert_success purchase - credit = @gateway.refund(@amount, purchase.authorization, :note => 'Sorry') + credit = @gateway.refund(@amount, purchase.authorization, note: 'Sorry') assert_success credit assert credit.test? assert_equal 'USD', credit.params['net_refund_amount_currency_id'] @@ -189,12 +228,12 @@ def test_successful_transfer response = @gateway.purchase(@amount, @credit_card, @params) assert_success response - response = @gateway.transfer(@amount, 'joe@example.com', :subject => 'Your money', :note => 'Thanks for taking care of that') + response = @gateway.transfer(@amount, 'joe@example.com', subject: 'Your money', note: 'Thanks for taking care of that') assert_success response end def test_failed_transfer - # paypal allows a max transfer of $10,000 + # paypal allows a max transfer of $10,000 response = @gateway.transfer(1000001, 'joe@example.com') assert_failure response end @@ -203,9 +242,11 @@ def test_successful_multiple_transfer response = @gateway.purchase(900, @credit_card, @params) assert_success response - response = @gateway.transfer([@amount, 'joe@example.com'], - [600, 'jane@example.com', {:note => 'Thanks for taking care of that'}], - :subject => 'Your money') + response = @gateway.transfer( + [@amount, 'joe@example.com'], + [600, 'jane@example.com', { note: 'Thanks for taking care of that' }], + subject: 'Your money' + ) assert_success response end @@ -214,7 +255,7 @@ def test_failed_multiple_transfer assert_success response # You can only include up to 250 recipients - recipients = (1..251).collect {|i| [100, "person#{i}@example.com"]} + recipients = (1..251).collect { |i| [100, "person#{i}@example.com"] } response = @gateway.transfer(*recipients) assert_failure response end @@ -223,7 +264,7 @@ def test_successful_email_transfer response = @gateway.purchase(@amount, @credit_card, @params) assert_success response - response = @gateway.transfer([@amount, 'joe@example.com'], :receiver_type => 'EmailAddress', :subject => 'Your money', :note => 'Thanks for taking care of that') + response = @gateway.transfer([@amount, 'joe@example.com'], receiver_type: 'EmailAddress', subject: 'Your money', note: 'Thanks for taking care of that') assert_success response end @@ -231,7 +272,7 @@ def test_successful_userid_transfer response = @gateway.purchase(@amount, @credit_card, @params) assert_success response - response = @gateway.transfer([@amount, '4ET96X3PQEN8H'], :receiver_type => 'UserID', :subject => 'Your money', :note => 'Thanks for taking care of that') + response = @gateway.transfer([@amount, '4ET96X3PQEN8H'], receiver_type: 'UserID', subject: 'Your money', note: 'Thanks for taking care of that') assert_success response end @@ -239,7 +280,7 @@ def test_failed_userid_transfer response = @gateway.purchase(@amount, @credit_card, @params) assert_success response - response = @gateway.transfer([@amount, 'joe@example.com'], :receiver_type => 'UserID', :subject => 'Your money', :note => 'Thanks for taking care of that') + response = @gateway.transfer([@amount, 'joe@example.com'], receiver_type: 'UserID', subject: 'Your money', note: 'Thanks for taking care of that') assert_failure response end @@ -254,4 +295,32 @@ def test_successful_referenced_id_purchase assert_success response2 end + def test_successful_purchase_with_3ds_version_1 + params = @params.merge!({ + three_d_secure: { + trans_status: 'Y', + eci: '05', + cavv: 'AgAAAAAAAIR8CQrXcIhbQAAAAAA', + xid: 'MDAwMDAwMDAwMDAwMDAwMzIyNzY=' + } + }) + response = @gateway.purchase(@amount, @credit_card, params) + assert_success response + assert response.params['transaction_id'] + end + + def test_successful_purchase_with_3ds_version_2 + params = @params.merge!({ + three_d_secure: { + authentication_response_status: 'Y', + eci: '05', + cavv: 'AgAAAAAAAIR8CQrXcIhbQAAAAAA', + ds_transaction_id: 'bDE9Aa1A-C5Ac-AD3a-4bBC-aC918ab1de3E', + version: '2.1.0' + } + }) + response = @gateway.purchase(@amount, @credit_card, params) + assert_success response + assert response.params['transaction_id'] + end end diff --git a/test/remote/gateways/remote_paysafe_test.rb b/test/remote/gateways/remote_paysafe_test.rb new file mode 100644 index 00000000000..20a1391e40f --- /dev/null +++ b/test/remote/gateways/remote_paysafe_test.rb @@ -0,0 +1,431 @@ +require 'test_helper' + +class RemotePaysafeTest < Test::Unit::TestCase + def setup + @gateway = PaysafeGateway.new(fixtures(:paysafe)) + + @amount = 100 + @credit_card = credit_card('4037111111000000') + @mastercard = credit_card('5200400000000009', brand: 'master') + @options = { + billing_address: address, + merchant_descriptor: { + dynamic_descriptor: 'Store Purchase', + phone: '999-8887777' + }, + email: 'profile@memail.com', + customer_id: SecureRandom.hex(16) + } + @profile_options = { + date_of_birth: { + year: 1979, + month: 1, + day: 1 + }, + email: 'profile@memail.com', + phone: '111-222-3456', + address: + } + @pm_token = @gateway.store(credit_card('4111111111111111'), @profile_options).authorization + @mc_three_d_secure_2_options = { + currency: 'EUR', + three_d_secure: { + eci: 0, + cavv: 'AAABBhkXYgAAAAACBxdiENhf7A+=', + version: '2.1.0', + ds_transaction_id: 'a3a721f3-b6fa-4cb5-84ea-c7b5c39890a2' + } + } + @visa_three_d_secure_2_options = { + currency: 'EUR', + three_d_secure: { + eci: 5, + cavv: 'AAABBhkXYgAAAAACBxdiENhf7A+=', + version: '2.1.0' + } + } + @mc_three_d_secure_1_options = { + currency: 'EUR', + three_d_secure: { + eci: 0, + cavv: 'AAABBhkXYgAAAAACBxdiENhf7A+=', + xid: 'aWg4N1ZZOE53TkFrazJuMmkyRDA=', + version: '1.0.2', + ds_transaction_id: 'a3a721f3-b6fa-4cb5-84ea-c7b5c39890a2' + } + } + @visa_three_d_secure_1_options = { + currency: 'EUR', + three_d_secure: { + eci: 5, + cavv: 'AAABBhkXYgAAAAACBxdiENhf7A+=', + xid: 'aWg4N1ZZOE53TkFrazJuMmkyRDA=', + version: '1.0.2' + } + } + @airline_details = { + airline_travel_details: { + passenger_name: 'Joe Smith', + departure_date: '2026-11-30', + origin: 'SXF', + computerized_reservation_system: 'DATS', + ticket: { + ticket_number: 9876789, + is_restricted_ticket: false + }, + customer_reference_number: 107854099, + travel_agency: { + name: 'Sally Travel', + code: 'AGENTS' + }, + trip_legs: { + leg1: { + flight: { + carrier_code: 'LH', + flight_number: '344' + }, + service_class: 'F', + is_stop_over_allowed: true, + destination: 'ISL', + fare_basis: 'VMAY', + departure_date: '2026-11-30' + }, + leg2: { + flight: { + carrier_code: 'TK', + flight_number: '999' + }, + service_class: 'F', + is_stop_over_allowed: true, + destination: 'SOF', + fare_basis: 'VMAY', + departure_date: '2026-11-30' + } + } + } + } + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'COMPLETED', response.message + assert_equal 0, response.params['availableToSettle'] + assert_not_nil response.params['authCode'] + end + + def test_successful_purchase_with_more_options + options = { + ip: '127.0.0.1', + email: 'joe@example.com' + } + + response = @gateway.purchase(@amount, @credit_card, @options.merge(options)) + assert_success response + assert_equal 'COMPLETED', response.message + assert_equal '127.0.0.1', response.params['customerIp'] + end + + # Merchant account must be setup to support split pay transactions using a specific account_id + def test_successful_purchase_with_split_payouts + @split_pay = PaysafeGateway.new(fixtures(:paysafe_split_pay)) + options = { + split_pay: [ + { + linked_account: '1002179730', + percent: 50 + } + ] + } + response = @split_pay.purchase(5000, @credit_card, @options.merge(options)) + assert_success response + assert_equal 2500, response.params['settlements'].first['splitpay'].first['amount'] + end + + def test_successful_purchase_with_airline_details + response = @gateway.purchase(@amount, @credit_card, @options.merge(@airline_details)) + assert_success response + assert_equal 'COMPLETED', response.message + assert_equal 'LH', response.params['airlineTravelDetails']['tripLegs']['leg1']['flight']['carrierCode'] + 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(150, @pm_token, @options) + assert_success response + assert_equal 'COMPLETED', response.message + assert_equal 0, response.params['availableToSettle'] + assert_not_nil response.params['authCode'] + end + + def test_successful_purchase_with_token_3ds2 + response = @gateway.purchase(155, @pm_token, @options.merge(@visa_three_d_secure_2_options)) + assert_success response + assert_equal 'COMPLETED', response.message + assert_not_nil response.params['authCode'] + end + + def test_successful_purchase_with_mastercard_3ds1 + response = @gateway.purchase(@amount, @mastercard, @options.merge(@mc_three_d_secure_1_options)) + assert_success response + assert_equal 'COMPLETED', response.message + assert_not_nil response.params['authCode'] + end + + def test_successful_purchase_with_mastercard_3ds2 + response = @gateway.purchase(@amount, @mastercard, @options.merge(@mc_three_d_secure_2_options)) + assert_success response + assert_equal 'COMPLETED', response.message + assert_not_nil response.params['authCode'] + end + + def test_successful_purchase_with_visa_3ds1 + response = @gateway.purchase(@amount, @credit_card, @options.merge(@visa_three_d_secure_1_options)) + assert_success response + assert_equal 'COMPLETED', response.message + assert_not_nil response.params['authCode'] + end + + def test_successful_purchase_with_visa_3ds2 + response = @gateway.purchase(@amount, @credit_card, @options.merge(@visa_three_d_secure_2_options)) + assert_success response + assert_equal 'COMPLETED', response.message + assert_not_nil response.params['authCode'] + end + + def test_successful_purchase_with_stored_credentials + initial_options = @options.merge( + stored_credential: { + initial_transaction: true, + reason_type: 'recurring' + } + ) + + initial_response = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success initial_response + assert_not_nil initial_response.params['storedCredential'] + network_transaction_id = initial_response.params['id'] + + stored_options = @options.merge( + stored_credential: { + initial_transaction: false, + reason_type: 'installment', + network_transaction_id: + } + ) + response = @gateway.purchase(@amount, @credit_card, stored_options) + assert_success response + assert_equal 'COMPLETED', response.message + end + + def test_successful_purchase_with_external_initial_transaction_id + initial_options = @options.merge( + external_initial_transaction_id: 'abc123', + stored_credential: { + initial_transaction: false, + reason_type: 'unscheduled' + } + ) + + initial_response = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success initial_response + assert_not_nil initial_response.params['storedCredential'] + network_transaction_id = initial_response.params['id'] + + stored_options = @options.merge( + stored_credential: { + initial_transaction: false, + reason_type: 'unscheduled', + network_transaction_id: + } + ) + response = @gateway.purchase(@amount, @credit_card, stored_options) + assert_success response + assert_equal 'COMPLETED', response.message + end + + # Merchant account must be setup to support funding transaction, and funding transaction type must be correct for the MCC + def test_successful_purchase_with_correct_funding_transaction_type + response = @gateway.purchase(@amount, @credit_card, @options.merge({ funding_transaction: 'SDW_WALLET_TRANSFER' })) + assert_success response + assert_equal 'COMPLETED', response.message + assert_equal 0, response.params['availableToSettle'] + assert_not_nil response.params['authCode'] + assert_match 'SDW_WALLET_TRANSFER', response.params['fundingTransaction']['type'] + end + + def test_failed_purchase_with_incorrect_funding_transaction_type + response = @gateway.purchase(@amount, @credit_card, @options.merge({ funding_transaction: 'PERSON_TO_PERSON' })) + assert_failure response + assert_equal 'Error(s)- code:3068, message:You submitted a funding transaction that is not correct for the merchant account.', response.message + end + + def test_failed_purchase + response = @gateway.purchase(11, @credit_card, @options) + assert_failure response + assert_equal 'Error(s)- code:3022, message:The card has been declined due to insufficient funds.', 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 'PENDING', capture.message + assert_equal @amount, capture.params['availableToRefund'] + end + + def test_successful_authorize_and_capture_with_token + auth = @gateway.authorize(@amount, @pm_token, @options) + assert_success auth + assert_equal 'COMPLETED', auth.message + assert_not_nil auth.params['authCode'] + + assert capture = @gateway.capture(@amount, auth.authorization) + assert_success capture + assert_equal 'PENDING', capture.message + assert_equal @amount, capture.params['availableToRefund'] + end + + def test_successful_authorize_with_token_3ds1 + response = @gateway.authorize(250, @pm_token, @options.merge(@visa_three_d_secure_1_options)) + assert_success response + assert_equal 'COMPLETED', response.message + assert_not_nil response.params['authCode'] + end + + def test_successful_authorize_with_token_3ds2 + response = @gateway.authorize(180, @pm_token, @options.merge(@visa_three_d_secure_2_options)) + assert_success response + assert_equal 'COMPLETED', response.message + assert_not_nil response.params['authCode'] + end + + def test_failed_authorize + response = @gateway.authorize(5, @credit_card, @options) + assert_failure response + assert_equal 'Error(s)- code:3009, message:Your request has been declined by the issuing bank.', 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, 'invalidtransactionid') + assert_failure response + assert_equal 'Error(s)- code:3201, message:The authorization ID included in this settlement request could not be found.', response.message + end + + # Can test refunds by logging into our portal and grabbing transaction IDs from settled transactions + # Refunds will return 'PENDING' status until they are batch processed at EOD + # def test_successful_refund + # auth = '' + + # assert refund = @gateway.refund(@amount, auth) + # assert_success refund + # assert_equal 'PENDING', refund.message + # end + + # def test_partial_refund + # auth = '' + + # assert refund = @gateway.refund(@amount - 1, auth) + # assert_success refund + # assert_equal 'PENDING', refund.message + # end + + def test_failed_refund + response = @gateway.refund(@amount, 'thisisnotavalidtrasactionid') + assert_failure response + assert_equal 'Error(s)- code:3407, message:The settlement referred to by the transaction response ID you provided cannot be found.', response.message + end + + def test_successful_void + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert void = @gateway.void(auth.authorization) + assert_success void + assert_equal 'COMPLETED', void.message + end + + def test_successful_void_with_token_purchase + auth = @gateway.authorize(@amount, @pm_token, @options) + assert_success auth + + assert void = @gateway.void(auth.authorization) + assert_success void + assert_equal 'COMPLETED', void.message + end + + def test_failed_void + response = @gateway.void('') + assert_failure response + assert_equal "Error(s)- code:5023, message:Request method 'POST' is not supported", response.message + end + + def test_successful_verify + response = @gateway.verify(@credit_card, @options) + assert_success response + assert_match 'COMPLETED', response.message + end + + def test_successful_verify_with_token + response = @gateway.verify(@pm_token, @options) + assert_success response + assert_match 'COMPLETED', response.message + end + + # Not including a test_failed_verify since the only way to force a failure on this + # gateway is with a specific dollar amount + + def test_successful_store_and_unstore + unstore = @gateway.unstore(@pm_token) + assert_success unstore + end + + def test_successful_credit + response = @gateway.credit(100, @credit_card, @options) + assert_success response + assert_match 'PENDING', response.message + end + + def test_invalid_login + gateway = PaysafeGateway.new(username: 'badbunny', password: 'carrotsrock', account_id: 'rejectstew') + + response = gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_equal '5279', response.error_code + assert_match 'invalid', response.params['error']['message'].downcase + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @credit_card, @options) + end + clean_transcript = @gateway.scrub(transcript) + + assert_scrubbed(@credit_card.number, clean_transcript) + assert_scrubbed(@credit_card.verification_value, clean_transcript) + end +end diff --git a/test/remote/gateways/remote_payscout_test.rb b/test/remote/gateways/remote_payscout_test.rb index d212f32845b..9aadde0d11d 100644 --- a/test/remote/gateways/remote_payscout_test.rb +++ b/test/remote/gateways/remote_payscout_test.rb @@ -9,9 +9,9 @@ def setup @declined_card = credit_card('34343') @options = { - :order_id => '1', - :description => 'Store Purchase', - :billing_address => address + order_id: '1', + description: 'Store Purchase', + billing_address: address } end @@ -21,13 +21,11 @@ def test_cvv_fail_purchase @credit_card = credit_card('4111111111111111') assert response = @gateway.purchase(@amount, @credit_card, @options) - assert_success response assert_equal 'The transaction has been approved', response.message assert_equal 'N', response.cvv_result['code'] end - def test_approved_purchase assert response = @gateway.purchase(@amount, @credit_card, @options) assert_success response @@ -150,8 +148,8 @@ def test_not_found_transaction_id_void def test_invalid_credentials gateway = PayscoutGateway.new( - :username => 'xxx', - :password => 'xxx' + username: 'xxx', + password: 'xxx' ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response diff --git a/test/remote/gateways/remote_paystation_test.rb b/test/remote/gateways/remote_paystation_test.rb index 0225ee25e4c..5144faeb8c6 100644 --- a/test/remote/gateways/remote_paystation_test.rb +++ b/test/remote/gateways/remote_paystation_test.rb @@ -1,11 +1,10 @@ require 'test_helper' class RemotePaystationTest < Test::Unit::TestCase - def setup @gateway = PaystationGateway.new(fixtures(:paystation)) - @credit_card = credit_card('5123456789012346', :month => 5, :year => 13, :verification_value => 123) + @credit_card = credit_card('5123456789012346', month: 5, year: 13, verification_value: 123) @successful_amount = 10000 @insufficient_funds_amount = 10051 @@ -14,8 +13,8 @@ def setup @bank_error_amount = 10091 @options = { - :billing_address => address, - :description => 'Store Purchase' + billing_address: address, + description: 'Store Purchase' } end @@ -27,7 +26,7 @@ def test_successful_purchase end def test_successful_purchase_in_gbp - assert response = @gateway.purchase(@successful_amount, @credit_card, @options.merge(:currency => 'GBP')) + assert response = @gateway.purchase(@successful_amount, @credit_card, @options.merge(currency: 'GBP')) assert_success response assert_equal 'Transaction successful', response.message @@ -39,18 +38,16 @@ def test_failed_purchases ['invalid_transaction', @invalid_transaction_amount, 'Transaction Type Not Supported'], ['expired_card', @expired_card_amount, 'Expired Card'], ['bank_error', @bank_error_amount, 'Error Communicating with Bank'] - ].each do |name, amount, message| - - assert response = @gateway.purchase(amount, @credit_card, @options) - assert_failure response - assert_equal message, response.message - + ].each do |_name, amount, message| + assert response = @gateway.purchase(amount, @credit_card, @options) + assert_failure response + assert_equal message, response.message end end def test_storing_token time = Time.now.to_i - assert response = @gateway.store(@credit_card, @options.merge(:token => "justatest#{time}")) + assert response = @gateway.store(@credit_card, @options.merge(token: "justatest#{time}")) assert_success response assert_equal 'Future Payment Saved Ok', response.message @@ -72,7 +69,7 @@ def test_authorize_and_capture assert_success auth assert auth.authorization - assert capture = @gateway.capture(@successful_amount, auth.authorization, @options.merge(:credit_card_verification => 123)) + assert capture = @gateway.capture(@successful_amount, auth.authorization, @options.merge(credit_card_verification: 123)) assert_success capture end diff --git a/test/remote/gateways/remote_payu_in_test.rb b/test/remote/gateways/remote_payu_in_test.rb index 182c3a1cc17..c6d8c477159 100644 --- a/test/remote/gateways/remote_payu_in_test.rb +++ b/test/remote/gateways/remote_payu_in_test.rb @@ -68,7 +68,7 @@ def test_partial_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - refund = @gateway.refund(@amount-1, purchase.authorization) + refund = @gateway.refund(@amount - 1, purchase.authorization) assert_success refund end @@ -82,30 +82,28 @@ def test_3ds_enrolled_card_fails assert_failure response assert_equal '3D-secure enrolled cards are not supported.', response.message -=begin - # This is handy for testing that 3DS is working with PayU - response = response.responses.first - - # You'll probably need a new bin from http://requestb.in - bin = "" - File.open("3ds.html", "w") do |f| - f.puts %( - - -
- - - - -
- - - ) - end - puts "Test 3D-secure via `open 3ds.html`" - puts "View results at http://requestb.in/#{bin}?inspect" - puts "Finalize with: `curl -v -d PaRes='' -d MD='' '#{response.params["form_post_vars"]["TermUrl"]}'`" -=end + # # This is handy for testing that 3DS is working with PayU + # response = response.responses.first + # + # # You'll probably need a new bin from http://requestb.in + # bin = "" + # File.open("3ds.html", "w") do |f| + # f.puts %( + # + # + #
+ # + # + # + # + #
+ # + # + # ) + # end + # puts "Test 3D-secure via `open 3ds.html`" + # puts "View results at http://requestb.in/#{bin}?inspect" + # puts "Finalize with: `curl -v -d PaRes='' -d MD='' '#{response.params["form_post_vars"]["TermUrl"]}'`" end def test_transcript_scrubbing diff --git a/test/remote/gateways/remote_payu_latam_test.rb b/test/remote/gateways/remote_payu_latam_test.rb index a33a22558dd..b84990a4422 100644 --- a/test/remote/gateways/remote_payu_latam_test.rb +++ b/test/remote/gateways/remote_payu_latam_test.rb @@ -3,14 +3,20 @@ class RemotePayuLatamTest < Test::Unit::TestCase def setup @gateway = PayuLatamGateway.new(fixtures(:payu_latam).update(payment_country: 'AR')) + @colombia_gateway = PayuLatamGateway.new(fixtures(:payu_latam).update(payment_country: 'CO', account_id: '512321')) @amount = 4000 - @credit_card = credit_card('4097440000000004', verification_value: '444', first_name: 'APPROVED', last_name: '') - @declined_card = credit_card('4097440000000004', verification_value: '444', first_name: 'REJECTED', last_name: '') - @pending_card = credit_card('4097440000000004', verification_value: '444', first_name: 'PENDING', last_name: '') + @credit_card = credit_card('4097440000000004', month: 6, year: 2035, verification_value: '777', first_name: 'APPROVED', last_name: '') + @declined_card = credit_card('4097440000000004', verification_value: '777', first_name: 'REJECTED', last_name: '') + @pending_card = credit_card('4097440000000004', verification_value: '777', first_name: 'PENDING', last_name: '') + @naranja_credit_card = credit_card('5895620000000002', verification_value: '123', first_name: 'APPROVED', last_name: '', brand: 'naranja') + @cabal_credit_card = credit_card('5896570000000004', verification_value: '123', first_name: 'APPROVED', last_name: '', brand: 'cabal') + @invalid_cabal_card = credit_card('6271700000000000', verification_value: '123', first_name: 'APPROVED', last_name: '', brand: 'cabal') + @condensa_card = credit_card('5907120000000009', month: 6, year: 2035, verification_value: '777', first_name: 'APPROVED', brand: 'condensa') @options = { dni_number: '5415668464654', + merchant_buyer_id: '1', currency: 'ARS', order_id: generate_unique_id, description: 'Active Merchant Transaction', @@ -49,6 +55,27 @@ def test_successful_purchase assert response.test? end + def test_successful_purchase_with_naranja_card + response = @gateway.purchase(@amount, @naranja_credit_card, @options) + assert_success response + assert_equal 'APPROVED', response.message + assert response.test? + end + + def test_successful_purchase_with_cabal_card + response = @gateway.purchase(@amount, @cabal_credit_card, @options) + assert_success response + assert_equal 'APPROVED', response.message + assert response.test? + end + + def test_successful_purchase_with_condensa_card + response = @colombia_gateway.purchase(@amount, @condensa_card, @options.merge(currency: 'COP')) + assert_success response + assert_equal 'APPROVED', response.message + assert response.test? + end + def test_successful_purchase_with_specified_language response = @gateway.purchase(@amount, @credit_card, @options.merge(language: 'es')) assert_success response @@ -56,8 +83,15 @@ def test_successful_purchase_with_specified_language assert response.test? end - def test_successul_purchase_with_buyer - gateway = PayuLatamGateway.new(fixtures(:payu_latam).update(:account_id => '512327', payment_country: 'BR')) + def test_successful_purchase_with_blank_billing_address_country + response = @gateway.purchase(@amount, @credit_card, @options.merge(billing_address: { address1: 'Viamonte', country: '', zip: '10001' })) + assert_success response + assert_equal 'APPROVED', response.message + assert response.test? + end + + def test_successful_purchase_with_buyer + gateway = PayuLatamGateway.new(fixtures(:payu_latam).update(account_id: '512327', payment_country: 'BR')) options_buyer = { currency: 'BRL', @@ -83,6 +117,7 @@ def test_successul_purchase_with_buyer name: 'Jorge Borges', dni_number: '5415668464123', dni_type: 'TI', + merchant_buyer_id: '2', cnpj: '32593371000110', email: 'axaxaxas@mlo.org' } @@ -95,7 +130,7 @@ def test_successul_purchase_with_buyer end def test_successful_purchase_brazil - gateway = PayuLatamGateway.new(fixtures(:payu_latam).update(:account_id => '512327', payment_country: 'BR')) + gateway = PayuLatamGateway.new(fixtures(:payu_latam).update(account_id: '512327', payment_country: 'BR')) options_brazil = { payment_country: 'BR', @@ -118,7 +153,7 @@ def test_successful_purchase_brazil zip: '01019-030', phone: '(11)756312633' ), - buyer:{ + buyer: { cnpj: '32593371000110' } } @@ -130,7 +165,7 @@ def test_successful_purchase_brazil end def test_successful_purchase_colombia - gateway = PayuLatamGateway.new(fixtures(:payu_latam).update(:account_id => '512321', payment_country: 'CO')) + gateway = PayuLatamGateway.new(fixtures(:payu_latam).update(account_id: '512321', payment_country: 'CO')) options_colombia = { payment_country: 'CO', @@ -164,7 +199,7 @@ def test_successful_purchase_colombia end def test_successful_purchase_mexico - gateway = PayuLatamGateway.new(fixtures(:payu_latam).update(:account_id => '512324', payment_country: 'MX')) + gateway = PayuLatamGateway.new(fixtures(:payu_latam).update(account_id: '512324', payment_country: 'MX')) options_mexico = { payment_country: 'MX', @@ -206,11 +241,30 @@ def test_successful_purchase_no_description end def test_failed_purchase - response = @gateway.purchase(@amount, @declined_card, @options) + response = @gateway.purchase(@amount, @declined_card) assert_failure response assert_equal 'DECLINED', response.params['transactionResponse']['state'] end + # Published API does not currently provide a way to request a CONTACT_THE_ENTITY + # def test_failed_purchase_correct_message_when_payment_network_response_error_present + # response = @gateway.purchase(@amount, @credit_card, @options) + # assert_failure response + # assert_equal 'CONTACT_THE_ENTITY | Contactar con entidad emisora', response.message + # assert_equal 'Contactar con entidad emisora', response.params['transactionResponse']['paymentNetworkResponseErrorMessage'] + + # response = @gateway.purchase(@amount, @credit_card, @options) + # assert_failure response + # assert_equal 'CONTACT_THE_ENTITY', response.message + # assert_nil response.params['transactionResponse']['paymentNetworkResponseErrorMessage'] + # end + + def test_failed_purchase_with_cabal_card + response = @gateway.purchase(@amount, @invalid_cabal_card, @options) + assert_failure response + assert_equal 'ERROR', response.params['code'] + end + def test_failed_purchase_with_no_options response = @gateway.purchase(@amount, @declined_card, {}) assert_failure response @@ -231,6 +285,20 @@ def test_successful_authorize assert_match %r(^\d+\|(\w|-)+$), response.authorization end + def test_successful_authorize_with_naranja_card + response = @gateway.authorize(@amount, @naranja_credit_card, @options) + assert_success response + assert_equal 'APPROVED', response.message + assert_match %r(^\d+\|(\w|-)+$), response.authorization + end + + def test_successful_authorize_with_cabal_card + response = @gateway.authorize(@amount, @cabal_credit_card, @options) + assert_success response + assert_equal 'APPROVED', response.message + assert_match %r(^\d+\|(\w|-)+$), response.authorization + end + def test_successful_authorize_with_specified_language response = @gateway.authorize(@amount, @credit_card, @options.merge(language: 'es')) assert_success response @@ -239,7 +307,7 @@ def test_successful_authorize_with_specified_language end def test_failed_authorize - response = @gateway.authorize(@amount, @pending_card, @options) + response = @gateway.authorize(@amount, @declined_card) assert_failure response assert_equal 'DECLINED', response.params['transactionResponse']['state'] end @@ -251,62 +319,67 @@ def test_failed_authorize_with_specified_language assert_equal 'Credenciales inválidas', response.message end - def test_well_formed_refund_fails_as_expected + def test_successful_capture + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + assert_equal 'APPROVED', response.message + assert_match %r(^\d+\|(\w|-)+$), response.authorization + + capture = @gateway.capture(@amount, response.authorization, @options) + assert_success capture + assert_equal 'APPROVED', response.message + assert response.test? + end + + def test_successful_partial_capture + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + assert_equal 'APPROVED', response.message + assert_match %r(^\d+\|(\w|-)+$), response.authorization + + capture = @gateway.capture(@amount - 1, response.authorization, @options) + assert_success capture + assert_equal 'APPROVED', response.message + assert response.test? + end + + def test_failed_capture + response = @gateway.capture(@amount, '') + assert_failure response + assert_match(/may not be null/, response.message) + end + + def test_successful_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase assert refund = @gateway.refund(@amount, purchase.authorization, @options) - assert_equal 'The payment plan id cannot be empty', refund.message + assert_success refund + assert_equal 'APPROVED', refund.message end def test_failed_refund response = @gateway.refund(@amount, '') assert_failure response - assert_match (/property: parentTransactionId, message: must not be null/), response.message + assert_match(/property: parentTransactionId, message: must not be null/, response.message) end def test_failed_refund_with_specified_language response = @gateway.refund(@amount, '', language: 'es') assert_failure response - assert_match (/property: parentTransactionId, message: No puede ser vacio/), response.message - end - - # If this test fails, support for void may have been added to the sandbox - def test_unsupported_test_void_fails_as_expected - auth = @gateway.authorize(@amount, @credit_card, @options) - assert_success auth - - assert void = @gateway.void(auth.authorization) - assert_failure void - assert_equal 'Internal payment provider error. ', void.message + assert_match(/property: parentTransactionId, message: No puede ser vacio/, response.message) end def test_failed_void response = @gateway.void('') assert_failure response - assert_match (/property: parentTransactionId, message: must not be null/), response.message + assert_match(/property: parentTransactionId, message: must not be null/, response.message) end def test_failed_void_with_specified_language response = @gateway.void('', language: 'es') assert_failure response - assert_match (/property: parentTransactionId, message: No puede ser vacio/), response.message - end - - # If this test fails, support for captures may have been added to the sandbox - def test_unsupported_test_capture_fails_as_expected - auth = @gateway.authorize(@amount, @credit_card, @options) - assert_success auth - - assert capture = @gateway.capture(@amount, auth.authorization) - assert_failure capture - assert_equal 'Internal payment provider error. ', capture.message - end - - def test_failed_capture - response = @gateway.capture(@amount, '') - assert_failure response - assert_match (/must not be null/), response.message + assert_match(/property: parentTransactionId, message: No puede ser vacio/, response.message) end def test_verify_credentials @@ -338,17 +411,17 @@ def test_successful_verify_with_specified_language end def test_failed_verify_with_specified_amount - verify = @gateway.verify(@credit_card, @options.merge(verify_amount: 1699)) + verify = @gateway.verify(@credit_card, @options.merge(verify_amount: 0)) assert_failure verify - assert_equal 'The order value is less than minimum allowed. Minimum value allowed 17 ARS', verify.message + assert_equal 'INVALID_TRANSACTION | [The given payment value [0] is inferior than minimum configured value [0.01]]', verify.message end def test_failed_verify_with_specified_language - verify = @gateway.verify(@credit_card, @options.merge(verify_amount: 1699, language: 'es')) + verify = @gateway.verify(@credit_card, @options.merge(verify_amount: 0, language: 'es')) assert_failure verify - assert_equal 'The order value is less than minimum allowed. Minimum value allowed 17 ARS', verify.message + assert_equal 'INVALID_TRANSACTION | [El valor recibido [0] es inferior al valor mínimo configurado [0,01]]', verify.message end def test_transcript_scrubbing @@ -361,4 +434,16 @@ def test_transcript_scrubbing assert_scrubbed(@credit_card.verification_value.to_s, clean_transcript) assert_scrubbed(@gateway.options[:api_key], clean_transcript) end + + def test_successful_store + store = @gateway.store(@credit_card, @options) + assert_success store + assert_equal 'SUCCESS', store.message + end + + def test_successful_purchase_with_extra_fields + response = @gateway.purchase(@amount, @credit_card, @options.merge({ extra_1: '123456', extra_2: 'abcdef', extra_3: 'testing' })) + assert_success response + assert_equal 'APPROVED', response.message + end end diff --git a/test/remote/gateways/remote_payway_dot_com_test.rb b/test/remote/gateways/remote_payway_dot_com_test.rb new file mode 100644 index 00000000000..3bb97f34bf6 --- /dev/null +++ b/test/remote/gateways/remote_payway_dot_com_test.rb @@ -0,0 +1,143 @@ +require 'test_helper' + +class RemotePaywayDotComTest < Test::Unit::TestCase + def setup + @gateway = PaywayDotComGateway.new(fixtures(:payway_dot_com)) + + @amount = 100 + @credit_card = credit_card('4000100011112224', verification_value: '737') + @declined_card = credit_card('4000300011112220') + @invalid_luhn_card = credit_card('4000300011112221') + @options = { + billing_address: address, + description: 'Store Purchase' + } + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal '5000', response.message[0, 4] + end + + def test_successful_purchase_with_more_options + options = { + order_id: '1', + ip: '127.0.0.1', + email: 'joe@example.com', + eci_type: '5', + tax: '7', + soft_descriptor: "Dan's Guitar Store" + } + + response = @gateway.purchase(101, @credit_card, options) + assert_success response + assert_equal '5000', response.message[0, 4] + end + + def test_failed_purchase + response = @gateway.purchase(102, @invalid_luhn_card, @options) + assert_failure response + assert_equal PaywayDotComGateway::STANDARD_ERROR_CODE_MAPPING['5035'], response.error_code + assert_equal '5035', response.message[0, 4] + end + + def test_successful_authorize + auth_only = @gateway.authorize(103, @credit_card, @options) + assert_success auth_only + assert_equal '5000', auth_only.message[0, 4] + end + + def test_successful_authorize_and_capture + auth = @gateway.authorize(104, @credit_card, @options) + assert_success auth + assert capture = @gateway.capture(@amount, auth.authorization, @options) + assert_success capture + assert_equal '5000', capture.message[0, 4] + end + + def test_failed_authorize + response = @gateway.authorize(105, @invalid_luhn_card, @options) + assert_failure response + assert_equal '5035', response.message[0, 4] + end + + def test_failed_capture + response = @gateway.capture(106, '') + assert_failure response + assert_equal '5025', response.message[0, 4] + end + + def test_successful_credit + credit = @gateway.credit(107, @credit_card, @options) + assert_success credit + assert_equal '5000', credit.message[0, 4] + end + + def test_failed_credit + response = @gateway.credit(108, @invalid_luhn_card, @options) + assert_failure response + assert_equal '5035', response.message[0, 4] + end + + def test_successful_void + auth = @gateway.authorize(109, @credit_card, @options) + assert_success auth + + assert void = @gateway.void(auth.authorization, @options) + assert_success void + assert_equal '5000', void.message[0, 4] + end + + def test_failed_void + response = @gateway.void('') + assert_failure response + assert_equal '5025', response.message[0, 4] + end + + def test_successful_void_of_sale + sale = @gateway.purchase(110, @credit_card, @options) + assert_success sale + + assert void = @gateway.void(sale.authorization, @options) + assert_success void + assert_equal '5000', void.message[0, 4] + end + + def test_successful_void_of_credit + credit = @gateway.credit(111, @credit_card, @options) + assert_success credit + + assert void = @gateway.void(credit.authorization, @options) + assert_success void + assert_equal '5000', void.message[0, 4] + end + + def test_invalid_login + gateway = PaywayDotComGateway.new(login: '', password: '', company_id: '', source_id: '') + + response = gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_match %r{5001}, response.message[0, 4] + 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[:password], transcript) + end + + def test_transcript_scrubbing_failed_purchase + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @invalid_luhn_card, @options) + end + transcript = @gateway.scrub(transcript) + assert_scrubbed(@invalid_luhn_card.number, transcript) + assert_scrubbed(@invalid_luhn_card.verification_value, transcript) + assert_scrubbed(@gateway.options[:password], transcript) + end +end diff --git a/test/remote/gateways/remote_payway_test.rb b/test/remote/gateways/remote_payway_test.rb index 9d900c8528b..643411f520a 100644 --- a/test/remote/gateways/remote_payway_test.rb +++ b/test/remote/gateways/remote_payway_test.rb @@ -4,46 +4,52 @@ class PaywayTest < Test::Unit::TestCase def setup @amount = 1100 - @options = {:order_id => generate_unique_id} + @options = { order_id: generate_unique_id } @gateway = ActiveMerchant::Billing::PaywayGateway.new(fixtures(:payway)) - @visa = credit_card('4564710000000004', - :month => 2, - :year => 2019, - :verification_value => '847' + @visa = credit_card( + '4564710000000004', + month: 2, + year: 2019, + verification_value: '847' ) - @mastercard = credit_card('5163200000000008', - :month => 8, - :year => 2020, - :verification_value => '070', - :brand => 'master' + @mastercard = credit_card( + '5163200000000008', + month: 8, + year: 2020, + verification_value: '070', + brand: 'master' ) - @expired = credit_card('4564710000000012', - :month => 2, - :year => 2005, - :verification_value => '963' + @expired = credit_card( + '4564710000000012', + month: 2, + year: 2005, + verification_value: '963' ) - @low = credit_card('4564710000000020', - :month => 5, - :year => 2020, - :verification_value => '234' + @low = credit_card( + '4564710000000020', + month: 5, + year: 2020, + verification_value: '234' ) - @stolen_mastercard = credit_card('5163200000000016', - :month => 12, - :year => 2019, - :verification_value => '728', - :brand => 'master' + @stolen_mastercard = credit_card( + '5163200000000016', + month: 12, + year: 2019, + verification_value: '728', + brand: 'master' ) - @invalid = credit_card('4564720000000037', - :month => 9, - :year => 2019, - :verification_value => '030' + @invalid = credit_card( + '4564720000000037', + month: 9, + year: 2019, + verification_value: '030' ) end @@ -85,10 +91,10 @@ def test_invalid_visa def test_invalid_login gateway = ActiveMerchant::Billing::PaywayGateway.new( - :username => 'bogus', - :password => 'bogus', - :merchant => 'TEST', - :pem => fixtures(:payway)['pem'] + username: 'bogus', + password: 'bogus', + merchant: 'TEST', + pem: fixtures(:payway)['pem'] ) assert response = gateway.purchase(@amount, @visa, @options) assert_failure response diff --git a/test/remote/gateways/remote_pin_test.rb b/test/remote/gateways/remote_pin_test.rb index 010830cf89b..204aac8fed8 100644 --- a/test/remote/gateways/remote_pin_test.rb +++ b/test/remote/gateways/remote_pin_test.rb @@ -5,17 +5,34 @@ def setup @gateway = PinGateway.new(fixtures(:pin)) @amount = 100 - @credit_card = credit_card('5520000000000000', :year => Time.now.year + 2) - @visa_credit_card = credit_card('4200000000000000', :year => Time.now.year + 3) + @credit_card = credit_card('5520000000000000', year: Time.now.year + 2) + @visa_credit_card = credit_card('4200000000000000', year: Time.now.year + 3) @declined_card = credit_card('4100000000000001') @options = { - :email => 'roland@pin.net.au', - :ip => '203.59.39.62', - :order_id => '1', - :billing_address => address, - :description => "Store Purchase #{DateTime.now.to_i}" + email: 'roland@pinpayments.com', + ip: '203.59.39.62', + order_id: '1', + billing_address: address, + description: "Store Purchase #{DateTime.now.to_i}" } + + @additional_options_3ds_passthrough = @options.merge( + three_d_secure: { + version: '1.0.2', + eci: '06', + cavv: 'AgAAAAAAAIR8CQrXcIhbQAAAAAA', + xid: 'MDAwMDAwMDAwMDAwMDAwMzIyNzY=' + } + ) + + @additional_options_3ds = @options.merge( + three_d_secure: { + enabled: true, + fallback_ok: true, + callback_url: 'https://yoursite.com/authentication_complete' + } + ) end def test_successful_purchase @@ -38,6 +55,25 @@ 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 + end + def test_successful_authorize_and_capture authorization = @gateway.authorize(@amount, @credit_card, @options) assert_success authorization @@ -48,6 +84,26 @@ def test_successful_authorize_and_capture assert_equal true, response.params['response']['captured'] end + def test_successful_authorize_and_capture_with_passthrough_3ds + authorization = @gateway.authorize(@amount, @credit_card, @additional_options_3ds_passthrough) + assert_success authorization + assert_equal false, authorization.params['response']['captured'] + + response = @gateway.capture(@amount, authorization.authorization, @options) + assert_success response + assert_equal true, response.params['response']['captured'] + end + + def test_successful_authorize_and_capture_with_3ds + authorization = @gateway.authorize(@amount, @credit_card, @additional_options_3ds) + assert_success authorization + assert_equal false, authorization.params['response']['captured'] + + response = @gateway.capture(@amount, authorization.authorization, @options) + assert_success response + assert_equal true, response.params['response']['captured'] + end + def test_failed_authorize response = @gateway.authorize(@amount, @declined_card, @options) assert_failure response @@ -89,16 +145,16 @@ def test_store_and_charge_with_pinjs_card_token } # Get a token equivalent to what is returned by Pin.js card_attrs = { - :number => @credit_card.number, - :expiry_month => @credit_card.month, - :expiry_year => @credit_card.year, - :cvc => @credit_card.verification_value, - :name => "#{@credit_card.first_name} #{@credit_card.last_name}", - :address_line1 => '42 Sevenoaks St', - :address_city => 'Lathlain', - :address_postcode => '6454', - :address_start => 'WA', - :address_country => 'Australia' + number: @credit_card.number, + expiry_month: @credit_card.month, + expiry_year: @credit_card.year, + cvc: @credit_card.verification_value, + name: "#{@credit_card.first_name} #{@credit_card.last_name}", + address_line1: '42 Sevenoaks St', + address_city: 'Lathlain', + address_postcode: '6454', + address_start: 'WA', + address_country: 'Australia' } url = @gateway.test_url + '/cards' @@ -136,12 +192,21 @@ def test_store_and_update assert_not_nil response.authorization assert_equal @credit_card.year, response.params['response']['card']['expiry_year'] - response = @gateway.update(response.authorization, @visa_credit_card, :address => address) + response = @gateway.update(response.authorization, @visa_credit_card, address:) assert_success response assert_not_nil response.authorization assert_equal @visa_credit_card.year, response.params['response']['card']['expiry_year'] end + def test_store_and_unstore + response = @gateway.store(@credit_card, @options) + assert_success response + assert_not_nil response.authorization + + response = @gateway.unstore(response.authorization) + assert_success response + end + def test_refund response = @gateway.purchase(@amount, @credit_card, @options) assert_success response @@ -165,8 +230,27 @@ def test_failed_refund assert_failure response end + def test_successful_void + authorization = @gateway.authorize(@amount, @credit_card, @options) + assert_success authorization + + assert void = @gateway.void(authorization.authorization, @options) + assert_success void + end + + def test_failed_void + authorization = @gateway.authorize(@amount, @credit_card, @options) + assert_success authorization + + assert void = @gateway.void(authorization.authorization, @options) + assert_success void + + assert already_voided = @gateway.void(authorization.authorization, @options) + assert_failure already_voided + end + def test_invalid_login - gateway = PinGateway.new(:api_key => '') + gateway = PinGateway.new(api_key: '') response = gateway.purchase(@amount, @credit_card, @options) assert_failure response end diff --git a/test/remote/gateways/remote_plexo_test.rb b/test/remote/gateways/remote_plexo_test.rb new file mode 100644 index 00000000000..e856d61e1e4 --- /dev/null +++ b/test/remote/gateways/remote_plexo_test.rb @@ -0,0 +1,330 @@ +require 'test_helper' + +class RemotePlexoTest < Test::Unit::TestCase + def setup + @gateway = PlexoGateway.new(fixtures(:plexo)) + + @amount = 100 + @credit_card = credit_card('5555555555554444', month: '12', year: '2024', verification_value: '111', first_name: 'Santiago', last_name: 'Navatta') + @declined_card = credit_card('5555555555554445') + @options = { + email: 'snavatta@plexo.com.uy', + ip: '127.0.0.1', + items: [ + { + name: 'prueba', + description: 'prueba desc', + quantity: '1', + price: '100', + discount: '0' + } + ], + amount_details: { + tip_amount: '5' + }, + identification_type: '1', + identification_value: '123456', + billing_address: address, + invoice_number: '12345abcde' + } + + @cancel_options = { + 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 + }) + + @decrypted_network_token = NetworkTokenizationCreditCard.new( + { + first_name: 'Joe', last_name: 'Doe', + brand: 'visa', + 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, @decrypted_network_token, @options.merge({ invoice_number: '12345abcde' })) + assert_success response + assert_equal 'You have been mocked.', response.message + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + end + + def test_successful_purchase_with_finger_print + response = @gateway.purchase(@amount, @credit_card, @options.merge({ finger_print: 'USABJHABSFASNJKN123532' })) + assert_success response + end + + def test_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 + assert_equal 'denied', response.params['status'] + assert_equal '10', response.error_code + end + + def test_successful_authorize_with_metadata + meta = { + custom_one: 'my field 1' + } + auth = @gateway.authorize(@amount, @credit_card, @options.merge({ metadata: meta })) + assert_success auth + + assert capture = @gateway.capture(@amount, auth.authorization) + assert_success capture + end + + def test_successful_authorize_and_capture + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert capture = @gateway.capture(@amount, auth.authorization) + assert_success capture + end + + def test_failed_authorize + response = @gateway.authorize(@amount, @declined_card, @options) + assert_failure response + assert_equal '10', response.error_code + assert_equal 'denied', response.params['status'] + end + + def test_partial_capture + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert capture = @gateway.capture(@amount - 1, auth.authorization) + assert_success capture + end + + def test_failed_capture + auth = @gateway.authorize(@amount, @declined_card, @options) + assert_failure auth + + response = @gateway.capture(@amount, auth.authorization) + assert_failure response + assert_equal 'The selected payment state is not valid.', response.message + end + + def test_successful_refund + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization, @cancel_options) + assert_success refund + end + + def test_partial_refund + purchase = @gateway.purchase(@amount, @credit_card, @options.merge({ refund_type: 'partial-refund' })) + assert_success purchase + + assert refund = @gateway.refund(@amount - 1, purchase.authorization, @cancel_options.merge({ type: 'partial-refund' })) + assert_success refund + end + + def test_failed_refund + auth = @gateway.authorize(@amount, @declined_card, @options) + assert_failure auth + + response = @gateway.refund(@amount, auth.authorization, @cancel_options) + assert_failure response + assert_equal 'The selected payment state is not valid.', response.message + end + + def test_successful_void + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert void = @gateway.void(auth.authorization, @cancel_options) + assert_success void + end + + def test_failed_void + auth = @gateway.authorize(@amount, @declined_card, @options) + assert_failure auth + + response = @gateway.void(auth.authorization, @cancel_options) + assert_failure response + assert_equal 'The selected payment state is not valid.', response.message + end + + # for verify tests: sometimes those fails but re-running after + # few seconds they can works + + def test_successful_verify + response = @gateway.verify(@credit_card, @options) + assert_success response + end + + def test_successful_verify_with_custom_amount + response = @gateway.verify(@credit_card, @options.merge({ verify_amount: '400' })) + assert_success response + end + + def test_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 + assert_equal 400, response.error_code + end + + def test_invalid_login + gateway = PlexoGateway.new(client_id: '', api_key: '') + + response = gateway.purchase(@amount, @credit_card, @options) + assert_failure response + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @credit_card, @options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@credit_card.number, transcript) + assert_scrubbed(@gateway.options[:api_key], transcript) + end + + def test_successful_purchase_passcard + credit_card = credit_card('6280260025383009', month: '12', year: '2024', + verification_value: '111', first_name: 'Santiago', last_name: 'Navatta') + + response = @gateway.purchase(@amount, credit_card, @options) + assert_success response + end + + def test_successful_purchase_edenred + credit_card = credit_card('6374830000000823', month: '12', year: '2024', + verification_value: '111', first_name: 'Santiago', last_name: 'Navatta') + + response = @gateway.purchase(@amount, credit_card, @options) + assert_success response + end + + def test_successful_purchase_anda + credit_card = credit_card('6031991248204901', month: '12', year: '2024', + verification_value: '111', first_name: 'Santiago', last_name: 'Navatta') + + response = @gateway.purchase(@amount, credit_card, @options) + assert_success response + end + + # This test is omitted until Plexo confirms that the transaction will indeed + # be declined as indicated in the documentation. + def test_successful_purchase_and_declined_refund_anda + omit + credit_card = credit_card('6031997614492616', month: '12', year: '2024', + verification_value: '111', first_name: 'Santiago', last_name: 'Navatta') + + purchase = @gateway.purchase(@amount, credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization, @cancel_options) + assert_failure refund + assert_equal 'An internal error occurred. Contact support.', refund.message + end + + # This test is omitted until Plexo confirms that the transaction will indeed + # be declined as indicated in the documentation. + def test_successful_purchase_and_declined_cancellation_anda + omit + credit_card = credit_card('6031998427187914', month: '12', year: '2024', + verification_value: '111', first_name: 'Santiago', last_name: 'Navatta') + + purchase = @gateway.purchase(@amount, credit_card, @options) + assert_success purchase + + assert void = @gateway.void(purchase.authorization, @cancel_options) + assert_failure void + end + + def test_successful_purchase_tarjetad + credit_card = credit_card('6018287227431046', month: '12', year: '2024', + verification_value: '111', first_name: 'Santiago', last_name: 'Navatta') + + response = @gateway.purchase(@amount, credit_card, @options) + assert_success response + end + + def test_failure_purchase_tarjetad + credit_card = credit_card('6018282227431033', month: '12', year: '2024', + verification_value: '111', first_name: 'Santiago', last_name: 'Navatta') + + response = @gateway.purchase(@amount, credit_card, @options) + assert_failure response + assert_equal 'denied', response.params['status'] + assert_equal '10', response.error_code + end + + def test_successful_purchase_sodexo + credit_card = credit_card('5058645584812145', month: '12', year: '2024', + verification_value: '111', first_name: 'Santiago', last_name: 'Navatta') + + response = @gateway.purchase(@amount, credit_card, @options) + assert_success response + end + + # This test is omitted until Plexo confirms that the transaction will indeed + # be declined as indicated in the documentation. + def test_successful_purchase_and_declined_refund_sodexo + omit + credit_card = credit_card('5058647731868699', month: '12', year: '2024', + verification_value: '111', first_name: 'Santiago', last_name: 'Navatta') + + purchase = @gateway.purchase(@amount, credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization, @cancel_options) + assert_failure refund + assert_equal 'An internal error occurred. Contact support.', refund.message + end + + def test_successful_purchase_and_declined_cancellation_sodexo + credit_card = credit_card('5058646599260130', month: '12', year: '2024', + verification_value: '111', first_name: 'Santiago', last_name: 'Navatta') + + purchase = @gateway.purchase(@amount, credit_card, @options) + assert_success purchase + + assert void = @gateway.void(purchase.authorization, @cancel_options) + assert_failure void + end +end diff --git a/test/remote/gateways/remote_plugnpay_test.rb b/test/remote/gateways/remote_plugnpay_test.rb index f05f4dc7701..2fcf76dbdd6 100644 --- a/test/remote/gateways/remote_plugnpay_test.rb +++ b/test/remote/gateways/remote_plugnpay_test.rb @@ -6,8 +6,8 @@ def setup @good_card = credit_card('4111111111111111', first_name: 'cardtest') @bad_card = credit_card('1234123412341234') @options = { - :billing_address => address, - :description => 'Store purchaes' + billing_address: address, + description: 'Store purchaes' } @amount = 100 end diff --git a/test/remote/gateways/remote_priority_test.rb b/test/remote/gateways/remote_priority_test.rb new file mode 100644 index 00000000000..d70fe153b42 --- /dev/null +++ b/test/remote/gateways/remote_priority_test.rb @@ -0,0 +1,406 @@ +require 'test_helper' + +class RemotePriorityTest < Test::Unit::TestCase + def setup + @gateway = PriorityGateway.new(fixtures(:priority)) + + @amount = 2 + @credit_amount = 2000 + @credit_card = credit_card + @invalid_credit_card = credit_card('123456') + @replay_id = rand(100...99999999) + @options = { billing_address: address } + + @additional_options = { + is_auth: false, + should_get_credit_card_level: true, + should_vault_card: false, + invoice: '123', + tax_exempt: true + } + + @additional_creditoptions = { + is_auth: true, + should_get_credit_card_level: false, + should_vault_card: false, + invoice: '123', + tax_exempt: true, + is_ticket: false, + source_zip: '30022', + auth_code: '', + ach_indicator: '', + bank_account: '', + meta: 'Harry Maguire is the best defender in premier league' + } + + @custom_pos_data = { + pos_data: { + cardholder_presence: 'NotPresent', + device_attendance: 'Unknown', + device_input_capability: 'KeyedOnly', + device_location: 'Unknown', + pan_capture_method: 'Manual', + partial_approval_support: 'Supported', + pin_capture_capability: 'Twelve' + } + } + + @purchases_data = { + 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 + } + ] + } + + @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 + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + assert_equal 'Approved or completed successfully', response.message + end + + def test_failed_authorize + response = @gateway.authorize(@amount, @invalid_credit_card, @options) + assert_failure response + assert_equal 'Invalid card number', response.message + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Approved or completed successfully', response.message + end + + def test_failed_purchase + response = @gateway.purchase(@amount, @invalid_credit_card, @options) + assert_failure response + assert_equal 'Invalid card number', response.message + end + + def test_failed_purchase_missing_card_month + card_without_month = credit_card('4242424242424242', month: '') + response = @gateway.purchase(@amount, card_without_month, @options) + + assert_failure response + assert_equal 'ValidationError', response.error_code + assert_equal 'Missing expiration month and / or year', response.message + end + + def test_failed_purchase_missing_card_verification_number + card_without_cvv = credit_card('4242424242424242', verification_value: '') + response = @gateway.purchase(@amount, card_without_cvv, @options) + + assert_failure response + assert_equal 'CVV is required based on merchant fraud settings', response.message + end + + def test_successful_authorize_and_capture + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + capture = @gateway.capture(@amount, auth.authorization, @options) + assert_success capture + assert_equal 'Approved', capture.message + end + + def test_failed_capture + capture = @gateway.capture(@amount, 'bogus_authorization', @options) + assert_failure capture + assert_equal 'Original Transaction Not Found', capture.message + end + + def test_successful_purchase_with_shipping_data + options_with_shipping = @options.merge({ ship_to_country: 'USA', ship_to_zip: 27703, ship_amount: 0.01 }) + response = @gateway.purchase(@amount, @credit_card, options_with_shipping) + + assert_success response + assert_equal 'Approved or completed successfully', response.message + end + + def test_successful_purchase_with_purchases_data + options_with_purchases = @options.merge(@purchases_data) + response = @gateway.purchase(@amount, @credit_card, options_with_purchases) + + assert_success response + assert_equal response.params['purchases'].first['name'], @purchases_data[:purchases].first[:name] + assert_equal response.params['purchases'].last['name'], @purchases_data[:purchases].last[:name] + assert_equal 'Approved or completed successfully', response.message + end + + def test_successful_purchase_with_custom_pos_data + options_with_custom_pos_data = @options.merge(@custom_pos_data) + response = @gateway.purchase(@amount, @credit_card, options_with_custom_pos_data) + + assert_success response + assert_equal 'Approved or completed successfully', response.message + end + + def test_successful_purchase_with_additional_options + options = @options.merge(@additional_options) + response = @gateway.purchase(@amount, @credit_card, options) + + assert_success response + 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) + assert_success response + assert_equal 'Approved or completed successfully', response.message + end + + def test_failed_credit + options = @options.merge(@additional_creditoptions) + response = @gateway.credit(@credit_amount, @invalid_credit_card, options) + assert_failure response + assert_equal 'Invalid card number', response.message + end + + def test_failed_credit_missing_card_month + card_without_month = credit_card('4242424242424242', month: '') + options = @options.merge(@additional_creditoptions) + response = @gateway.credit(@credit_amount, card_without_month, options) + assert_failure response + assert_equal 'ValidationError', response.error_code + assert_equal 'Missing expiration month and / or year', response.message + end + + def test_successful_void_with_batch_open + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + # Batch status is by default is set to Open when Sale transaction is created + batch_check = @gateway.get_payment_status(purchase.params['batchId']) + assert_equal 'Open', batch_check.message + + void = @gateway.void(purchase.authorization, @options) + assert_success void + assert_equal 'Success', void.message + end + + def test_successful_void_after_closing_batch + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + # Manually close open batch; resulting status should be 'Pending' + @gateway.close_batch(purchase.params['batchId']) + payment_status = @gateway.get_payment_status(purchase.params['batchId']) + assert_equal 'Pending', payment_status.message + + void = @gateway.void(purchase.authorization, @options) + assert_success void + assert_equal 'Success', void.message + end + + def test_failed_void + bogus_transaction_id = '123456' + assert void = @gateway.void(bogus_transaction_id, @options) + + assert_failure void + assert_equal 'Unauthorized', void.error_code + assert_equal 'Original Payment Not Found Or You Do Not Have Access.', void.message + end + + def test_successful_refund_with_open_batch + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + batch_check = @gateway.get_payment_status(purchase.params['batchId']) + assert_equal 'Open', batch_check.message + + refund = @gateway.refund(@amount, purchase.authorization) + assert_success refund + assert_equal 'Approved or completed successfully', refund.message + end + + def test_successful_refund_after_closing_batch + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + # Manually close open batch; resulting status should be 'Pending' + @gateway.close_batch(purchase.params['batchId']) + payment_status = @gateway.get_payment_status(purchase.params['batchId']) + assert_equal 'Pending', payment_status.message + + refund = @gateway.refund(@amount, purchase.authorization) + assert_success refund + assert_equal 'Approved or completed successfully', refund.message + end + + def test_successful_get_payment_status + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + + batch_check = @gateway.get_payment_status(response.params['batchId']) + + assert_success batch_check + assert_equal 'Open', batch_check.message + end + + def test_failed_get_payment_status + batch_check = @gateway.get_payment_status(123456) + + assert_failure batch_check + assert_equal 'Invalid JSON response', batch_check.params['message'][0..20] + end + + def test_successful_verify + response = @gateway.verify(credit_card('411111111111111')) + assert_success response + assert_match 'JPMORGAN CHASE BANK N.A.', response.params['bank']['name'] + end + + def test_failed_verify + response = @gateway.verify(@invalid_credit_card) + assert_failure response + assert_match 'No bank information found for bin number', 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(@credit_card.number, clean_transcript) + assert_scrubbed(@credit_card.verification_value.to_s, clean_transcript) + end + + def test_successful_purchase_with_duplicate_replay_id + response = @gateway.purchase(@amount, @credit_card, @options.merge(replay_id: @replay_id)) + + assert_success response + assert_equal @replay_id, response.params['replayId'] + + duplicate_response = @gateway.purchase(@amount, @credit_card, @options.merge(replay_id: response.params['replayId'])) + + assert_success duplicate_response + assert_equal response.params['id'], duplicate_response.params['id'] + end + + def test_failed_purchase_with_duplicate_replay_id + response = @gateway.purchase(@amount, @invalid_credit_card, @options.merge(replay_id: @replay_id)) + assert_failure response + + duplicate_response = @gateway.purchase(@amount, @invalid_credit_card, @options.merge(replay_id: response.params['replayId'])) + assert_failure duplicate_response + + assert_equal response.message, duplicate_response.message + assert_equal response.params['status'], duplicate_response.params['status'] + + assert_equal response.params['id'], duplicate_response.params['id'] + end + + def test_successful_purchase_with_unique_replay_id + first_purchase_response = @gateway.purchase(@amount, @credit_card, @options.merge(replay_id: @replay_id)) + + assert_success first_purchase_response + assert_equal @replay_id, first_purchase_response.params['replayId'] + + second_purchase_response = @gateway.purchase(@amount + 1, @credit_card, @options.merge(replay_id: @replay_id + 1)) + + assert_success second_purchase_response + assert_not_equal first_purchase_response.params['id'], second_purchase_response.params['id'] + end + + def test_failed_duplicate_refund + purchase_response = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase_response + + refund_response = @gateway.refund(@amount, purchase_response.authorization) + + assert_success refund_response + assert_equal 'Approved or completed successfully', refund_response.message + + duplicate_refund_response = @gateway.refund(@amount, purchase_response.authorization) + + assert_failure duplicate_refund_response + assert_equal 'Payment already refunded', duplicate_refund_response.message + end + + def test_failed_duplicate_void + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + void = @gateway.void(purchase.authorization) + assert_success void + + duplicate_void = @gateway.void(purchase.authorization) + + assert_failure duplicate_void + assert_equal 'Payment already voided.', duplicate_void.message + end +end diff --git a/test/remote/gateways/remote_pro_pay_test.rb b/test/remote/gateways/remote_pro_pay_test.rb index 5a99435ed6b..8ecea99cf19 100644 --- a/test/remote/gateways/remote_pro_pay_test.rb +++ b/test/remote/gateways/remote_pro_pay_test.rb @@ -34,7 +34,7 @@ def test_successful_purchase_with_more_options end def test_successful_recurring_purchase_without_cvv - @options.merge!({recurring_payment: 'Y'}) + @options[:recurring_payment] = 'Y' response = @gateway.purchase(@amount, @credit_card_without_cvv, @options) assert_success response assert_equal 'Success', response.message @@ -67,7 +67,7 @@ def test_partial_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization, @options) + assert capture = @gateway.capture(@amount - 1, auth.authorization, @options) assert_success capture end @@ -90,7 +90,7 @@ def test_partial_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount-1, purchase.authorization, @options) + assert refund = @gateway.refund(@amount - 1, purchase.authorization, @options) assert_success refund end diff --git a/test/remote/gateways/remote_psigate_test.rb b/test/remote/gateways/remote_psigate_test.rb index 74d643794fe..d4e50fc8539 100644 --- a/test/remote/gateways/remote_psigate_test.rb +++ b/test/remote/gateways/remote_psigate_test.rb @@ -1,7 +1,6 @@ require 'test_helper' class PsigateRemoteTest < Test::Unit::TestCase - def setup Base.mode = :test @gateway = PsigateGateway.new(fixtures(:psigate)) @@ -10,9 +9,9 @@ def setup @amount = 2400 @creditcard = credit_card('4111111111111111') @options = { - :order_id => generate_unique_id, - :billing_address => address, - :email => 'jack@example.com' + order_id: generate_unique_id, + billing_address: address, + email: 'jack@example.com' } end @@ -45,7 +44,7 @@ def test_successful_purchase_and_refund end def test_failed_purchase - assert response = @gateway.purchase(@amount, @creditcard, @options.update(:test_result => 'D')) + assert response = @gateway.purchase(@amount, @creditcard, @options.update(test_result: 'D')) assert_failure response end diff --git a/test/remote/gateways/remote_psl_card_test.rb b/test/remote/gateways/remote_psl_card_test.rb index a5ea5c9d6ed..18e911faea2 100644 --- a/test/remote/gateways/remote_psl_card_test.rb +++ b/test/remote/gateways/remote_psl_card_test.rb @@ -1,122 +1,98 @@ require 'test_helper' class RemotePslCardTest < Test::Unit::TestCase - def setup @gateway = PslCardGateway.new(fixtures(:psl_card)) - + @uk_maestro = CreditCard.new(fixtures(:psl_maestro)) @uk_maestro_address = fixtures(:psl_maestro_address) - + @solo = CreditCard.new(fixtures(:psl_solo)) @solo_address = fixtures(:psl_solo_address) - + @visa = CreditCard.new(fixtures(:psl_visa)) @visa_address = fixtures(:psl_visa_address) - + @visa_debit = CreditCard.new(fixtures(:psl_visa_debit)) @visa_address = fixtures(:psl_visa_debit_address) - + # The test results are determined by the amount of the transaction @accept_amount = 1000 @referred_amount = 6000 @declined_amount = 11000 @keep_card_amount = 15000 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 - + def test_no_login @gateway = PslCardGateway.new( - :login => '' - ) - response = @gateway.authorize(@accept_amount, @uk_maestro, - :billing_address => @uk_maestro_address + login: '' ) + 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? - + capture = @gateway.capture(@accept_amount, authorization.authorization) - + assert_success capture assert capture.test? end diff --git a/test/remote/gateways/remote_qbms_test.rb b/test/remote/gateways/remote_qbms_test.rb index 017ebd1fc95..5e9de931936 100644 --- a/test/remote/gateways/remote_qbms_test.rb +++ b/test/remote/gateways/remote_qbms_test.rb @@ -11,7 +11,7 @@ def setup @card = credit_card('4111111111111111') @options = { - :billing_address => address, + billing_address: address } end @@ -66,7 +66,7 @@ def test_successful_credit end def test_invalid_ticket - gateway = QbmsGateway.new(@gateway_options.merge(:ticket => 'test123')) + gateway = QbmsGateway.new(@gateway_options.merge(ticket: 'test123')) assert response = gateway.authorize(@amount, @card, @options) assert_instance_of Response, response @@ -102,6 +102,6 @@ def test_transcript_scrubbing private def error_card(config_id) - credit_card('4111111111111111', :first_name => "configid=#{config_id}", :last_name => '') + credit_card('4111111111111111', first_name: "configid=#{config_id}", last_name: '') end end diff --git a/test/remote/gateways/remote_quantum_test.rb b/test/remote/gateways/remote_quantum_test.rb index dfa3b5682a5..30bbf393c64 100644 --- a/test/remote/gateways/remote_quantum_test.rb +++ b/test/remote/gateways/remote_quantum_test.rb @@ -1,15 +1,13 @@ require 'test_helper' class RemoteQuantumTest < Test::Unit::TestCase - - def setup @gateway = QuantumGateway.new(fixtures(:quantum)) - + @amount = 100 @credit_card = credit_card('4000100011112224') end - + def test_successful_purchase assert response = @gateway.purchase(@amount, @credit_card) assert_success response @@ -53,9 +51,9 @@ def test_void assert response = @gateway.void(response.authorization) assert_success response end - + def test_passing_billing_address - options = {:billing_address => address} + options = { billing_address: address } assert response = @gateway.purchase(@amount, @credit_card, options) assert_success response assert_equal 'Transaction is APPROVED', response.message @@ -65,9 +63,9 @@ def test_passing_billing_address # So we check to see if the parse failed and report def test_invalid_login gateway = QuantumGateway.new( - :login => '', - :password => '' - ) + login: '', + password: '' + ) assert response = gateway.purchase(@amount, @credit_card) assert_failure response assert_equal 'ERROR: Invalid Gateway Login!!', response.message diff --git a/test/remote/gateways/remote_quickbooks_test.rb b/test/remote/gateways/remote_quickbooks_test.rb index c9e95c959c8..a319954a7ce 100644 --- a/test/remote/gateways/remote_quickbooks_test.rb +++ b/test/remote/gateways/remote_quickbooks_test.rb @@ -13,22 +13,29 @@ def setup order_id: '1', billing_address: address({ zip: 90210, country: 'US', - state: 'CA' - }), + state: 'CA' }), description: 'Store Purchase' } + + @amex = credit_card( + '378282246310005', + verification_value: '1234', + brand: 'american_express' + ) end def test_successful_purchase response = @gateway.purchase(@amount, @credit_card, @options) assert_success response assert_equal 'CAPTURED', response.message + assert_equal @gateway.options[:access_token], response.params['access_token'] + assert_equal @gateway.options[:refresh_token], response.params['refresh_token'] end def test_failed_purchase response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response - assert_equal 'cardNumber is invalid.', response.message + assert_equal 'card.number is invalid.', response.message end def test_successful_authorize_and_capture @@ -89,7 +96,7 @@ def test_successful_verify def test_failed_verify response = @gateway.verify(@declined_card, @options) assert_failure response - assert_match %r{cardNumber is invalid.}, response.message + assert_match %r{card.number is invalid.}, response.message assert_equal Gateway::STANDARD_ERROR_CODE[:processing_error], response.error_code end @@ -106,7 +113,74 @@ def test_invalid_login end end - def test_dump_transcript - # See quickbooks_test.rb for an example of a scrubbed transcript + def test_successful_void + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert void = @gateway.void(auth.authorization) + assert_success void + assert_equal 'ISSUED', void.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[:access_token], transcript) + 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) + assert_failure response + assert_equal 'AuthenticationFailed', response.params['code'] + end + + def test_successful_purchase_with_expired_token + @gateway.options[:access_token] = 'not_a_valid_token' + response = @gateway.purchase(@amount, @credit_card, @options.merge(allow_refresh: true)) + assert_success response + end + + def test_successful_purchase_without_state_in_address + options = { + order_id: '1', + billing_address: + { + zip: 90210, + # Submitting a value of an empty string for the `state` field + # results in a `region is invalid` error message from Quickbooks. + # This test ensures that an empty string is not sent from AM. + state: '', + country: '' + } + } + + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'CAPTURED', response.message + end + + def test_refresh + response = @gateway.refresh + assert_success response + assert response.params['access_token'] end end diff --git a/test/remote/gateways/remote_quickpay_test.rb b/test/remote/gateways/remote_quickpay_test.rb index c7c9c22cc1f..e547d74242d 100644 --- a/test/remote/gateways/remote_quickpay_test.rb +++ b/test/remote/gateways/remote_quickpay_test.rb @@ -8,11 +8,11 @@ def setup @amount = 100 @options = { - :order_id => generate_unique_id[0...10], - :billing_address => address + order_id: generate_unique_id[0...10], + billing_address: address } - @visa_no_cvv2 = credit_card('4000300011112220', :verification_value => nil) + @visa_no_cvv2 = credit_card('4000300011112220', verification_value: nil) @visa = credit_card('4000100011112224') @dankort = credit_card('5019717010103742') @visa_dankort = credit_card('4571100000000000') @@ -26,7 +26,7 @@ def setup @amex = credit_card('3700100000000000') # forbrugsforeningen doesn't use a verification value - @forbrugsforeningen = credit_card('6007221000000000', :verification_value => nil) + @forbrugsforeningen = credit_card('6007221000000000', verification_value: nil) end def test_successful_purchase @@ -38,7 +38,7 @@ def test_successful_purchase end def test_successful_usd_purchase - assert response = @gateway.purchase(@amount, @visa, @options.update(:currency => 'USD')) + assert response = @gateway.purchase(@amount, @visa, @options.update(currency: 'USD')) assert_equal 'OK', response.message assert_equal 'USD', response.params['currency'] assert_success response @@ -174,22 +174,22 @@ def test_successful_purchase_and_credit end def test_successful_store_and_reference_purchase - assert store = @gateway.store(@visa, @options.merge(:description => 'New subscription')) + assert store = @gateway.store(@visa, @options.merge(description: 'New subscription')) assert_success store - assert purchase = @gateway.purchase(@amount, store.authorization, @options.merge(:order_id => generate_unique_id[0...10])) + assert purchase = @gateway.purchase(@amount, store.authorization, @options.merge(order_id: generate_unique_id[0...10])) assert_success purchase end def test_failed_store - assert store = @gateway.store(credit_card('4'), @options.merge(:description => 'New subscription')) + assert store = @gateway.store(credit_card('4'), @options.merge(description: 'New subscription')) assert_failure store assert_equal 'Error in field: cardnumber', store.message end def test_invalid_login gateway = QuickpayGateway.new( - :login => '', - :password => '' + login: '', + password: '' ) assert response = gateway.purchase(@amount, @visa, @options) assert_equal 'Invalid merchant id', response.message diff --git a/test/remote/gateways/remote_quickpay_v10_test.rb b/test/remote/gateways/remote_quickpay_v10_test.rb index d10e107fa09..6cbb9e1533d 100644 --- a/test/remote/gateways/remote_quickpay_v10_test.rb +++ b/test/remote/gateways/remote_quickpay_v10_test.rb @@ -1,13 +1,12 @@ require 'test_helper' class RemoteQuickPayV10Test < Test::Unit::TestCase - def setup @gateway = QuickpayV10Gateway.new(fixtures(:quickpay_v10_api_key)) @amount = 100 @options = { - :order_id => generate_unique_id[0...10], - :billing_address => address(country: 'DNK') + order_id: generate_unique_id[0...10], + billing_address: address(country: 'DNK') } @valid_card = credit_card('1000000000000008') @@ -16,8 +15,8 @@ def setup @capture_rejected_card = credit_card('1000000000000032') @refund_rejected_card = credit_card('1000000000000040') - @valid_address = address(:phone => '4500000001') - @invalid_address = address(:phone => '4500000002') + @valid_address = address(phone: '4500000001') + @invalid_address = address(phone: '4500000002') end def card_brand(response) @@ -25,7 +24,7 @@ def card_brand(response) end def test_successful_purchase_with_short_country - options = @options.merge({billing_address: address(country: 'DK')}) + options = @options.merge({ billing_address: address(country: 'DK') }) assert response = @gateway.purchase(@amount, @valid_card, options) assert_equal 'OK', response.message @@ -35,7 +34,7 @@ def test_successful_purchase_with_short_country end def test_successful_purchase_with_order_id_format - options = @options.merge({order_id: "##{Time.new.to_f}"}) + options = @options.merge({ order_id: "##{Time.new.to_f}" }) assert response = @gateway.purchase(@amount, @valid_card, options) assert_equal 'OK', response.message @@ -60,7 +59,7 @@ def test_unsuccessful_purchase_with_invalid_card end def test_successful_usd_purchase - assert response = @gateway.purchase(@amount, @valid_card, @options.update(:currency => 'USD')) + assert response = @gateway.purchase(@amount, @valid_card, @options.update(currency: 'USD')) assert_equal 'OK', response.message assert_equal 'USD', response.params['currency'] assert_success response @@ -68,13 +67,13 @@ def test_successful_usd_purchase end def test_successful_purchase_with_acquirers - assert response = @gateway.purchase(@amount, @valid_card, @options.update(:acquirer => 'nets')) + assert response = @gateway.purchase(@amount, @valid_card, @options.update(acquirer: 'nets')) assert_equal 'OK', response.message assert_success response end def test_unsuccessful_purchase_with_invalid_acquirers - assert response = @gateway.purchase(@amount, @valid_card, @options.update(:acquirer => 'invalid')) + assert response = @gateway.purchase(@amount, @valid_card, @options.update(acquirer: 'invalid')) assert_failure response assert_equal 'Validation error', response.message end @@ -82,7 +81,7 @@ def test_unsuccessful_purchase_with_invalid_acquirers def test_unsuccessful_authorize_with_invalid_card assert response = @gateway.authorize(@amount, @invalid_card, @options) assert_failure response - assert_match /Rejected test operation/, response.message + assert_match(/Rejected test operation/, response.message) end def test_successful_authorize_and_capture @@ -95,6 +94,23 @@ def test_successful_authorize_and_capture assert_equal 'OK', capture.message end + def test_successful_authorize_and_capture_with_3ds + options = @options.merge( + three_d_secure: { + cavv: '1234', + eci: '1234', + xid: '1234' + } + ) + assert auth = @gateway.authorize(@amount, @valid_card, options) + assert_success auth + assert_equal 'OK', auth.message + assert auth.authorization + assert capture = @gateway.capture(@amount, auth.authorization) + assert_success capture + assert_equal 'OK', capture.message + end + def test_unsuccessful_authorize_and_capture assert auth = @gateway.authorize(@amount, @capture_rejected_card, @options) assert_success auth @@ -251,5 +267,4 @@ def test_transcript_scrubbing assert_scrubbed(@valid_card.verification_value.to_s, clean_transcript) assert_scrubbed(@gateway.options[:api_key], clean_transcript) end - end diff --git a/test/remote/gateways/remote_quickpay_v4_test.rb b/test/remote/gateways/remote_quickpay_v4_test.rb index 06fab13d6e8..a9517f6e09f 100644 --- a/test/remote/gateways/remote_quickpay_v4_test.rb +++ b/test/remote/gateways/remote_quickpay_v4_test.rb @@ -4,15 +4,15 @@ class RemoteQuickpayV4Test < Test::Unit::TestCase # These test assumes that you have not added your development IP in # the Quickpay Manager. def setup - @gateway = QuickpayGateway.new(fixtures(:quickpay_with_api_key).merge(:version => 4)) + @gateway = QuickpayGateway.new(fixtures(:quickpay_with_api_key).merge(version: 4)) @amount = 100 @options = { - :order_id => generate_unique_id[0...10], - :billing_address => address + order_id: generate_unique_id[0...10], + billing_address: address } - @visa_no_cvv2 = credit_card('4000300011112220', :verification_value => nil) + @visa_no_cvv2 = credit_card('4000300011112220', verification_value: nil) @visa = credit_card('4000100011112224') @dankort = credit_card('5019717010103742') @visa_dankort = credit_card('4571100000000000') @@ -51,10 +51,8 @@ def test_successful_purchase_with_all_fraud_parameters assert !response.authorization.blank? end - - def test_successful_usd_purchase - assert response = @gateway.purchase(@amount, @visa, @options.update(:currency => 'USD')) + assert response = @gateway.purchase(@amount, @visa, @options.update(currency: 'USD')) assert_equal 'OK', response.message assert_equal 'USD', response.params['currency'] assert_success response @@ -191,16 +189,16 @@ def test_successful_purchase_and_credit end def test_successful_store_and_reference_purchase - assert store = @gateway.store(@visa, @options.merge(:description => 'New subscription')) + assert store = @gateway.store(@visa, @options.merge(description: 'New subscription')) assert_success store - assert purchase = @gateway.purchase(@amount, store.authorization, @options.merge(:order_id => generate_unique_id[0...10])) + assert purchase = @gateway.purchase(@amount, store.authorization, @options.merge(order_id: generate_unique_id[0...10])) assert_success purchase end def test_invalid_login gateway = QuickpayGateway.new( - :login => '999999999', - :password => '' + login: '999999999', + password: '' ) assert response = gateway.purchase(@amount, @visa, @options) assert_equal 'Invalid merchant id', response.message diff --git a/test/remote/gateways/remote_quickpay_v5_test.rb b/test/remote/gateways/remote_quickpay_v5_test.rb index 4d458e47a66..367711d0d0d 100644 --- a/test/remote/gateways/remote_quickpay_v5_test.rb +++ b/test/remote/gateways/remote_quickpay_v5_test.rb @@ -4,15 +4,15 @@ class RemoteQuickpayV5Test < Test::Unit::TestCase # These test assumes that you have not added your development IP in # the Quickpay Manager. def setup - @gateway = QuickpayGateway.new(fixtures(:quickpay_with_api_key).merge(:version => 5)) + @gateway = QuickpayGateway.new(fixtures(:quickpay_with_api_key).merge(version: 5)) @amount = 100 @options = { - :order_id => generate_unique_id[0...10], - :billing_address => address + order_id: generate_unique_id[0...10], + billing_address: address } - @visa_no_cvv2 = credit_card('4000300011112220', :verification_value => nil) + @visa_no_cvv2 = credit_card('4000300011112220', verification_value: nil) @visa = credit_card('4000100011112224') @dankort = credit_card('5019717010103742') @visa_dankort = credit_card('4571100000000000') @@ -51,10 +51,8 @@ def test_successful_purchase_with_all_fraud_parameters assert !response.authorization.blank? end - - def test_successful_usd_purchase - assert response = @gateway.purchase(@amount, @visa, @options.update(:currency => 'USD')) + assert response = @gateway.purchase(@amount, @visa, @options.update(currency: 'USD')) assert_equal 'OK', response.message assert_equal 'USD', response.params['currency'] assert_success response @@ -191,16 +189,16 @@ def test_successful_purchase_and_credit end def test_successful_store_and_reference_purchase - assert store = @gateway.store(@visa, @options.merge(:description => 'New subscription')) + assert store = @gateway.store(@visa, @options.merge(description: 'New subscription')) assert_success store - assert purchase = @gateway.purchase(@amount, store.authorization, @options.merge(:order_id => generate_unique_id[0...10])) + assert purchase = @gateway.purchase(@amount, store.authorization, @options.merge(order_id: generate_unique_id[0...10])) assert_success purchase end def test_invalid_login gateway = QuickpayGateway.new( - :login => '999999999', - :password => '' + login: '999999999', + password: '' ) assert response = gateway.purchase(@amount, @visa, @options) assert_equal 'Invalid merchant id', response.message diff --git a/test/remote/gateways/remote_quickpay_v6_test.rb b/test/remote/gateways/remote_quickpay_v6_test.rb index 7e4d31cde22..39d66fceb2b 100644 --- a/test/remote/gateways/remote_quickpay_v6_test.rb +++ b/test/remote/gateways/remote_quickpay_v6_test.rb @@ -4,15 +4,15 @@ class RemoteQuickpayV6Test < Test::Unit::TestCase # These test assumes that you have not added your development IP in # the Quickpay Manager. def setup - @gateway = QuickpayGateway.new(fixtures(:quickpay_with_api_key).merge(:version => 6)) + @gateway = QuickpayGateway.new(fixtures(:quickpay_with_api_key).merge(version: 6)) @amount = 100 @options = { - :order_id => generate_unique_id[0...10], - :billing_address => address + order_id: generate_unique_id[0...10], + billing_address: address } - @visa_no_cvv2 = credit_card('4000300011112220', :verification_value => nil) + @visa_no_cvv2 = credit_card('4000300011112220', verification_value: nil) @visa = credit_card('4000100011112224') @dankort = credit_card('5019717010103742') @visa_dankort = credit_card('4571100000000000') @@ -51,10 +51,8 @@ def test_successful_purchase_with_all_fraud_parameters assert !response.authorization.blank? end - - def test_successful_usd_purchase - assert response = @gateway.purchase(@amount, @visa, @options.update(:currency => 'USD')) + assert response = @gateway.purchase(@amount, @visa, @options.update(currency: 'USD')) assert_equal 'OK', response.message assert_equal 'USD', response.params['currency'] assert_success response @@ -191,16 +189,16 @@ def test_successful_purchase_and_credit end def test_successful_store_and_reference_purchase - assert store = @gateway.store(@visa, @options.merge(:description => 'New subscription')) + assert store = @gateway.store(@visa, @options.merge(description: 'New subscription')) assert_success store - assert purchase = @gateway.purchase(@amount, store.authorization, @options.merge(:order_id => generate_unique_id[0...10])) + assert purchase = @gateway.purchase(@amount, store.authorization, @options.merge(order_id: generate_unique_id[0...10])) assert_success purchase end def test_invalid_login gateway = QuickpayGateway.new( - :login => '999999999', - :password => '' + login: '999999999', + password: '' ) assert response = gateway.purchase(@amount, @visa, @options) assert_equal 'Invalid merchant id', response.message diff --git a/test/remote/gateways/remote_quickpay_v7_test.rb b/test/remote/gateways/remote_quickpay_v7_test.rb index 9edfc2189b2..185f9369be5 100644 --- a/test/remote/gateways/remote_quickpay_v7_test.rb +++ b/test/remote/gateways/remote_quickpay_v7_test.rb @@ -8,11 +8,11 @@ def setup @amount = 100 @options = { - :order_id => generate_unique_id[0...10], - :billing_address => address + order_id: generate_unique_id[0...10], + billing_address: address } - @visa_no_cvv2 = credit_card('4000300011112220', :verification_value => nil) + @visa_no_cvv2 = credit_card('4000300011112220', verification_value: nil) @visa = credit_card('4000100011112224') @dankort = credit_card('5019717010103742') @visa_dankort = credit_card('4571100000000000') @@ -52,7 +52,7 @@ def test_successful_purchase_with_all_fraud_parameters end def test_successful_usd_purchase - assert response = @gateway.purchase(@amount, @visa, @options.update(:currency => 'USD')) + assert response = @gateway.purchase(@amount, @visa, @options.update(currency: 'USD')) assert_equal 'OK', response.message assert_equal 'USD', response.params['currency'] assert_success response @@ -60,13 +60,13 @@ def test_successful_usd_purchase end def test_successful_purchase_with_acquirers - assert response = @gateway.purchase(@amount, @visa, @options.update(:acquirers => 'nets')) + assert response = @gateway.purchase(@amount, @visa, @options.update(acquirers: 'nets')) assert_equal 'OK', response.message assert_success response end def test_unsuccessful_purchase_with_invalid_acquirers - assert response = @gateway.purchase(@amount, @visa, @options.update(:acquirers => 'invalid')) + assert response = @gateway.purchase(@amount, @visa, @options.update(acquirers: 'invalid')) assert_equal 'Error in field: acquirers', response.message assert_failure response end @@ -201,26 +201,26 @@ def test_successful_purchase_and_credit end def test_successful_store_and_reference_purchase - assert store = @gateway.store(@visa, @options.merge(:description => 'New subscription')) + assert store = @gateway.store(@visa, @options.merge(description: 'New subscription')) assert_success store - assert purchase = @gateway.purchase(@amount, store.authorization, @options.merge(:order_id => generate_unique_id[0...10])) + assert purchase = @gateway.purchase(@amount, store.authorization, @options.merge(order_id: generate_unique_id[0...10])) assert_success purchase end def test_successful_store_with_acquirers - assert store = @gateway.store(@visa, @options.merge(:description => 'New subscription', :acquirers => 'nets')) + assert store = @gateway.store(@visa, @options.merge(description: 'New subscription', acquirers: 'nets')) assert_success store end def test_successful_store_sans_description - assert store = @gateway.store(@visa, @options.merge(:acquirers => 'nets')) + assert store = @gateway.store(@visa, @options.merge(acquirers: 'nets')) assert_success store end def test_invalid_login gateway = QuickpayGateway.new( - :login => '999999999', - :password => '' + login: '999999999', + password: '' ) assert response = gateway.purchase(@amount, @visa, @options) assert_equal 'Invalid merchant id', response.message diff --git a/test/remote/gateways/remote_qvalent_test.rb b/test/remote/gateways/remote_qvalent_test.rb index 9c21e25b889..79360070c57 100644 --- a/test/remote/gateways/remote_qvalent_test.rb +++ b/test/remote/gateways/remote_qvalent_test.rb @@ -5,14 +5,16 @@ def setup @gateway = QvalentGateway.new(fixtures(:qvalent)) @amount = 100 - @credit_card = credit_card('4000100011112224') + @credit_card = credit_card('4242424242424242') + @mastercard = credit_card('5163200000000008', brand: 'master') @declined_card = credit_card('4000000000000000') @expired_card = credit_card('4111111113444494') @options = { order_id: generate_unique_id, billing_address: address, - description: 'Store Purchase' + description: 'Store Purchase', + customer_reference_number: generate_unique_id } end @@ -25,7 +27,7 @@ def test_invalid_login pem_password: 'bad' ) - assert_raise ActiveMerchant::ClientCertificateError do + assert_raise OpenSSL::X509::CertificateError do gateway.purchase(@amount, @credit_card, @options) end end @@ -59,9 +61,9 @@ def test_successful_purchase_with_3d_secure order_id: generate_unique_id, billing_address: address, description: 'Store Purchase', - xid: '123', - cavv: '456', - eci: '5' + xid: 'sgf7h125tr8gh24abmah', + cavv: 'MTIzNDU2Nzg5MDEyMzQ1Njc4OTA=', + eci: 'INS' } response = @gateway.purchase(@amount, @credit_card, options) @@ -189,4 +191,52 @@ def test_transcript_scrubbing assert_scrubbed(@credit_card.verification_value, clean_transcript) assert_scrubbed(@gateway.options[:password], clean_transcript) end + + def test_successful_purchase_initial + stored_credential = { + stored_credential: { + initial_transaction: true, + initiator: 'merchant', + reason_type: 'unscheduled' + } + } + + response = @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential)) + + assert_success response + assert_equal 'Succeeded', response.message + assert_not_nil response.params['response.authTraceId'] + end + + def test_successful_purchase_cardholder + stored_credential = { + stored_credential: { + initial_transaction: false, + initiator: 'cardholder', + reason_type: 'unscheduled', + network_transaction_id: 'qwerty7890' + } + } + + response = @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential)) + + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_mastercard + stored_credential = { + stored_credential: { + initial_transaction: false, + initiator: 'merchant', + reason_type: 'recurring', + network_transaction_id: 'qwerty7890' + } + } + + response = @gateway.purchase(@amount, @mastercard, @options.merge(stored_credential)) + + assert_success response + assert_equal 'Succeeded', response.message + end end diff --git a/test/remote/gateways/remote_rapyd_test.rb b/test/remote/gateways/remote_rapyd_test.rb new file mode 100644 index 00000000000..9db67dc1aac --- /dev/null +++ b/test/remote/gateways/remote_rapyd_test.rb @@ -0,0 +1,525 @@ +require 'test_helper' + +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') + @check = check + @options = { + pm_type: 'us_debit_visa_card', + currency: 'USD', + complete_payment_url: 'www.google.com', + error_payment_url: 'www.google.com', + description: 'Describe this transaction', + statement_descriptor: 'Statement Descriptor', + email: 'test@example.com', + billing_address: address(name: 'Jim Reynolds'), + order_id: '987654321' + } + @stored_credential_options = { + pm_type: 'gb_visa_card', + currency: 'GBP', + complete_payment_url: 'https://www.rapyd.net/platform/collect/online/', + error_payment_url: 'https://www.rapyd.net/platform/collect/online/', + description: 'Describe this transaction', + statement_descriptor: 'Statement Descriptor', + email: 'test@example.com', + billing_address: address(name: 'Jim Reynolds'), + order_id: '987654321' + } + @ach_options = { + pm_type: 'us_ach_bank', + currency: 'USD', + proof_of_authorization: false, + payment_purpose: 'Testing Purpose', + email: 'test@example.com', + billing_address: address(name: 'Jim Reynolds') + } + @metadata = { + array_of_objects: [ + { name: 'John Doe' }, + { type: 'customer' } + ], + array_of_strings: %w[ + color + size + ], + number: 1234567890, + object: { + string: 'person' + }, + string: 'preferred', + Boolean: true + } + @three_d_secure = { + version: '2.1.0', + cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA=', + xid: '00000000000000000501', + eci: '02' + } + + @address_object = address(line_1: '123 State Street', line_2: 'Apt. 34', zip: '12345', name: 'john doe', phone_number: '12125559999') + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + 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 + # Rapyd requires a random int between 10 and 15 digits for NTID + response = @gateway.purchase(15000, @credit_card, @stored_credential_options.merge(stored_credential: { network_transaction_id: rand.to_s[2..11], reason_type: 'recurring' })) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_purchase_with_network_transaction_id_and_initiation_type_fields + # Rapyd requires a random int between 10 and 15 digits for NTID + response = @gateway.purchase(15000, @credit_card, @stored_credential_options.merge(network_transaction_id: rand.to_s[2..11], initiation_type: 'customer_present')) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_purchase_with_network_transaction_id_and_initiation_type_fields_along_with_stored_credentials + # Rapyd requires a random int between 10 and 15 digits for NTID + response = @gateway.purchase(15000, @credit_card, @stored_credential_options.merge(stored_credential: { network_transaction_id: rand.to_s[2..11], reason_type: 'recurring' }, network_transaction_id: rand.to_s[2..11], initiation_type: 'customer_present')) + assert_success response + assert_equal 'SUCCESS', response.message + assert_equal 'customer_present', response.params['data']['initiation_type'] + end + + def test_successful_purchase_with_reccurence_type + @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 + + def test_successful_purchase_with_save_payment_method + @options[:pm_type] = 'gb_visa_mo_card' + response = @gateway.purchase(@amount, @credit_card, @options.merge(save_payment_method: true)) + assert_success response + assert_equal 'SUCCESS', response.message + assert_equal true, response.params['data']['save_payment_method'] + end + + def test_successful_purchase_with_address + billing_address = address(name: 'Henry Winkler', address1: '123 Happy Days Lane') + + response = @gateway.purchase(@amount, @credit_card, @options.merge(billing_address:)) + assert_success response + 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 + assert_equal 'SUCCESS', response.message + assert_equal 'ACT', response.params['data']['status'] + end + + def test_successful_purchase_with_options + options = @options.merge(metadata: @metadata, ewallet_id: 'ewallet_897aca846f002686e14677541f78a0f4') + response = @gateway.purchase(100000, @credit_card, options) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_failed_purchase + response = @gateway.purchase(@amount, @declined_card, @options) + assert_failure response + assert_equal 'The request attempted an operation that requires a card number, but the number was not recognized. The request was rejected. Corrective action: Use the card number of a valid card.', 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 'SUCCESS', capture.message + end + + def test_failed_authorize + response = @gateway.authorize(@amount, @declined_card, @options) + assert_failure response + assert_equal 'The request attempted an operation that requires a card number, but the number was not recognized. The request was rejected. Corrective action: Use the card number of a valid card.', 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, 'madeupauth') + assert_failure response + assert_equal 'The request tried to retrieve a payment, but the payment was not found. The request was rejected. Corrective action: Use a valid payment ID.', response.message + end + + def test_successful_refund + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization) + assert_success refund + assert_equal 'SUCCESS', refund.message + end + + def test_successful_refund_with_options + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization, @options.merge(metadata: @metadata)) + assert_success refund + assert_equal 'SUCCESS', refund.message + end + + def test_partial_refund + amount = 5000 + purchase = @gateway.purchase(amount, @credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(amount - 1050, purchase.authorization) + assert_success refund + assert_equal 'SUCCESS', refund.message + assert_equal 39.5, refund.params['data']['amount'] + end + + def test_failed_refund + response = @gateway.refund(@amount, '') + assert_failure response + assert_equal 'The request attempted an operation that requires a payment ID, but the payment was not found. The request was rejected. Corrective action: Use the ID of a valid payment.', response.message + end + + def test_failed_void_with_payment_method_error + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert void = @gateway.void(auth.authorization) + assert_failure void + 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 + end + + def test_successful_verify + 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' + @options[:currency] = 'MXN' + response = @gateway.verify(@credit_card, @options) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_failed_verify + response = @gateway.verify(@declined_card, @options) + assert_failure response + assert_equal 'The request attempted an operation that requires a card number, but the number was not recognized. The request was rejected. Corrective action: Use the card number of a valid card.', response.message + end + + def test_successful_store_and_purchase + store = @gateway.store(@credit_card, @options) + assert_success store + assert store.params.dig('data', 'id') + assert store.params.dig('data', 'default_payment_method') + + # 3DS authorization is required on storing a payment method for future transactions + # This test verifies that the card id and customer id are sent with the purchase + purchase = @gateway.purchase(100, store.authorization, @options) + assert_match(/The request tried to use a card ID, but the cardholder has not completed the 3DS verification process./, purchase.message) + end + + def test_successful_store_and_unstore + store = @gateway.store(@credit_card, @options) + assert_success store + assert customer_id = store.params.dig('data', 'id') + assert store.params.dig('data', 'default_payment_method') + + unstore = @gateway.unstore(store.authorization) + assert_success unstore + assert_equal true, unstore.params.dig('data', 'deleted') + assert_equal customer_id, unstore.params.dig('data', 'id') + end + + def test_failed_store + store = @gateway.store(@declined_card, @options) + assert_failure store + end + + def test_failed_unstore + store = @gateway.store(@credit_card, @options) + assert_success store + assert store.params.dig('data', 'id') + + unstore = @gateway.unstore('') + assert_failure unstore + assert_equal 'UNAUTHORIZED_API_CALL', unstore.message + end + + def test_invalid_login + gateway = RapydGateway.new(secret_key: '', access_key: '') + + response = gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_equal 'The request did not contain the required headers for authentication. The request was rejected. Corrective action: Add authentication headers.', 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[:secret_key], transcript) + assert_scrubbed(@gateway.options[:access_key], transcript) + end + + def test_transcript_scrubbing_with_ach + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @check, @ach_options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@check.account_number, transcript) + assert_scrubbed(@check.routing_number, transcript) + assert_scrubbed(@gateway.options[:secret_key], transcript) + assert_scrubbed(@gateway.options[:access_key], transcript) + end + + def test_successful_authorize_with_3ds_v1_options + options = @options.merge(three_d_secure: @three_d_secure) + options[:pm_type] = 'gb_visa_card' + options[:three_d_secure][:version] = '1.0.2' + + response = @gateway.authorize(105000, @credit_card, options) + assert_success response + assert_equal 'ACT', response.params['data']['status'] + assert_equal '3d_verification', response.params['data']['payment_method_data']['next_action'] + assert response.params['data']['redirect_url'] + end + + def test_successful_authorize_with_3ds_v2_options + options = @options.merge(three_d_secure: @three_d_secure) + options[:pm_type] = 'gb_visa_card' + + response = @gateway.authorize(105000, @credit_card, options) + assert_success response + assert_equal 'ACT', response.params['data']['status'] + assert_equal '3d_verification', response.params['data']['payment_method_data']['next_action'] + assert response.params['data']['redirect_url'] + end + + def test_successful_purchase_with_3ds_v2_gateway_specific + options = @options.merge(three_d_secure: { required: true }) + options[:pm_type] = 'gb_visa_card' + + response = @gateway.purchase(105000, @credit_card, options) + assert_success response + assert_equal 'ACT', response.params['data']['status'] + assert_equal '3d_verification', response.params['data']['payment_method_data']['next_action'] + assert response.params['data']['redirect_url'] + assert_match 'https://sandboxcheckout.rapyd.net/3ds-payment?token=payment_', response.params['data']['redirect_url'] + end + + def test_successful_purchase_without_3ds_v2_gateway_specific + options = @options.merge(three_d_secure: { required: false }) + options[:pm_type] = 'gb_visa_card' + response = @gateway.purchase(1000, @credit_card, options) + assert_success response + assert_equal 'CLO', response.params['data']['status'] + assert_equal 'not_applicable', response.params['data']['payment_method_data']['next_action'] + assert_equal '', response.params['data']['redirect_url'] + end + + def test_successful_authorize_with_execute_threed + ActiveSupport::JSON::Encoding.escape_html_entities_in_json = true + @options[:complete_payment_url] = 'http://www.google.com?param1=1¶m2=2' + options = @options.merge(pm_type: 'gb_visa_card', execute_threed: true) + response = @gateway.authorize(105000, @credit_card, options) + assert_success response + assert_equal 'ACT', response.params['data']['status'] + assert_equal '3d_verification', response.params['data']['payment_method_data']['next_action'] + assert response.params['data']['redirect_url'] + ensure + ActiveSupport::JSON::Encoding.escape_html_entities_in_json = false + end + + def test_successful_purchase_without_cvv + options = @options.merge({ pm_type: 'gb_visa_card', network_transaction_id: rand.to_s[2..11] }) + @credit_card.verification_value = nil + response = @gateway.purchase(100, @credit_card, options) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_recurring_transaction_without_cvv + @credit_card.verification_value = nil + response = @gateway.purchase(15000, @credit_card, @stored_credential_options.merge(stored_credential: { network_transaction_id: rand.to_s[2..11], reason_type: 'recurring' })) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_purchase_empty_network_transaction_id + response = @gateway.purchase(15000, @credit_card, @stored_credential_options.merge(network_transaction_id: '', initiation_type: 'customer_present')) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_purchase_nil_network_transaction_id + response = @gateway.purchase(15000, @credit_card, @stored_credential_options.merge(network_transaction_id: nil, initiation_type: 'customer_present')) + assert_success response + assert_equal 'SUCCESS', response.message + end + + 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 new file mode 100644 index 00000000000..9aaf8b59fda --- /dev/null +++ b/test/remote/gateways/remote_reach_test.rb @@ -0,0 +1,325 @@ +require 'test_helper' + +class RemoteReachTest < Test::Unit::TestCase + def setup + @gateway = ReachGateway.new(fixtures(:reach)) + @amount = 100 + @credit_card = credit_card('4444333322221111', { + month: 3, + year: 2030, + verification_value: 737 + }) + @not_supported_cc = credit_card('4444333322221111', { + month: 3, + year: 2030, + verification_value: 737, + brand: 'alelo' + }) + @declined_card = credit_card('4000300011112220') + @options = { + email: 'johndoe@reach.com', + order_id: '123', + description: 'Store Purchase', + currency: 'USD', + billing_address: { + address1: '1670', + address2: '1670 NW 82ND AVE', + city: 'Miami', + state: 'FL', + zip: '32191', + country: 'US' + }, + device_fingerprint: fingerprint + } + @non_valid_authorization = SecureRandom.uuid + end + + def test_successful_authorize + response = @gateway.authorize(@amount, @credit_card, @options) + + assert_success response + assert response.params['response']['Authorized'] + assert response.params['response']['OrderId'] + end + + def test_failed_authorize + @options[:currency] = 'PPP' + @options.delete(:email) + response = @gateway.authorize(@amount, @credit_card, @options) + + assert_failure response + assert_equal 'Invalid ConsumerCurrency', response.message + end + + def test_failed_authorize_with_not_supported_payment_method + response = @gateway.authorize(@amount, @not_supported_cc, @options) + + assert_failure response + assert_equal 'PaymentMethodUnsupported', response.error_code + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + assert response.params['response']['Authorized'] + assert response.params['response']['OrderId'] + end + + def test_failed_purchase + response = @gateway.purchase(@amount, @declined_card, @options) + assert_failure response + assert_equal 'NotATestCard', response.message + end + + def test_successful_purchase_with_fingerprint + @options[:device_fingerprint] = '54fd66c2-b5b5-4dbd-ab89-12a8b6177347' + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + assert response.params['response']['Authorized'] + assert response.params['response']['OrderId'] + end + + def test_successful_purchase_with_shipping_data + @options[:price] = '1.01' + @options[:taxes] = '2.01' + @options[:duty] = '1.01' + + @options[:consignee_name] = 'Jane Doe' + @options[:consignee_address] = '1670 NW 82ND STR' + @options[:consignee_city] = 'Houston' + @options[:consignee_country] = 'US' + + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + assert response.params['response']['Authorized'] + assert response.params['response']['OrderId'] + end + + def test_failed_purchase_with_incomplete_shipping_data + @options[:price] = '1.01' + @options[:taxes] = '2.01' + + @options[:consignee_name] = 'Jane Doe' + @options[:consignee_address] = '1670 NW 82ND STR' + @options[:consignee_city] = 'Houston' + @options[:consignee_country] = 'US' + + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_failure response + assert_equal 'Invalid shipping values.', response.message + end + + def test_failed_purchase_with_shipping_data_and_no_consignee_info + @options[:price] = '1.01' + @options[:taxes] = '2.01' + @options[:duty] = '1.01' + + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_failure response + assert_equal 'Invalid JSON submitted', response.message + end + + def test_successful_purchase_with_items + @options[:items] = [ + { + Sku: SecureRandom.alphanumeric, + ConsumerPrice: '10', + Quantity: 1 + }, + { + Sku: SecureRandom.alphanumeric, + ConsumerPrice: '90', + Quantity: 1 + } + ] + + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + assert response.params['response']['Authorized'] + assert response.params['response']['OrderId'] + end + + # The Complete flag in the response returns false when capture is + # in progress + def test_successful_authorize_and_capture + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + + response = @gateway.capture(@amount, response.authorization) + assert_success response + end + + def test_successful_purchase_with_store_credentials + @options[:stored_credential] = { initiator: 'cardholder', initial_transaction: true, reason_type: 'installment' } + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + + assert response.params['response']['Authorized'] + assert response.params['response']['OrderId'] + end + + def test_successful_purchase_with_store_credentials_mit + @options[:stored_credential] = { initiator: 'merchant', initial_transaction: false, reason_type: 'recurring' } + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + + assert response.params['response']['Authorized'] + assert response.params['response']['OrderId'] + end + + def test_successful_purchase_with_store_credentials_mit_and_network_transaction_id + @options[:stored_credential] = { initiator: 'cardholder', initial_transaction: true, reason_type: 'installment' } + purchase = @gateway.purchase(@amount, @credit_card, @options) + + @options[:stored_credential] = { initiator: 'merchant', initial_transaction: false, reason_type: 'unscheduled', network_transaction_id: purchase.network_transaction_id } + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + + assert response.params['response']['Authorized'] + assert response.params['response']['OrderId'] + end + + def test_failed_purchase_with_store_credentials_mit_and_network_transaction_id + @options[:stored_credential] = { initiator: 'merchant', initial_transaction: false, reason_type: 'unscheduled', network_transaction_id: 'uhh123' } + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_failure response + + assert_equal response.message, 'InvalidPreviousNetworkPaymentReference' + end + + def test_failed_purchase_payment_model_nil + @options[:stored_credential] = { initiator: 'merchant', initial_transaction: false, reason_type: 'installment', network_transaction_id: 'uhh123' } + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_failure response + assert_equal 'Invalid PaymentModel', response.message + end + + def test_failed_capture + response = @gateway.capture(@amount, "#{@gateway.options[:merchant_id]}#123") + + assert_failure response + assert_equal 'Not Found', response.message + end + + def test_successful_refund_with_reference_id + response = @gateway.refund( + @amount, + '7d689cc1-4478-4e92-8cd9-f05528cde2f4', + { reference_id: 'REFUND_TAG' } + ) + + assert_success response + assert response.params['response']['RefundId'].present? + end + + def test_successful_refund_with_order_id + response = @gateway.refund( + @amount, + '7d689cc1-4478-4e92-8cd9-f05528cde2f4', + { order_id: 'REFUND_TAG' } + ) + + assert_success response + assert response.params['response']['RefundId'].present? + end + + def test_failed_refund + purchase = @gateway.purchase(@amount, @credit_card, @options) + response = @gateway.refund(@amount, purchase.authorization, { reference_id: 'REFUND_TAG' }) + + assert_failure response + assert_equal 'OrderStateInvalid', response.error_code + assert response.params['response']['OrderId'].present? + end + + def test_successful_void + authorize = @gateway.authorize(@amount, @credit_card, @options) + response = @gateway.void(authorize.authorization, @options) + + assert_success response + assert response.params['response']['OrderId'].present? + end + + def test_failed_void + response = @gateway.void(@non_valid_authorization, @options) + assert_failure response + + assert_equal 'Not Found', response.message + assert response.params.blank? + end + + def test_successful_partial_void + authorize = @gateway.authorize(@amount / 2, @credit_card, @options) + response = @gateway.void(authorize.authorization, @options) + + assert_success response + assert response.params['response']['OrderId'].present? + end + + def test_successful_void_higher_amount + authorize = @gateway.authorize(@amount * 2, @credit_card, @options) + response = @gateway.void(authorize.authorization, @options) + + assert_success response + assert response.params['response']['OrderId'].present? + end + + def test_successful_double_void_and_idempotent + authorize = @gateway.authorize(@amount, @credit_card, @options) + response = @gateway.void(authorize.authorization, @options) + + assert_success response + assert response.params['response']['OrderId'].present? + + second_void_response = @gateway.void(authorize.authorization, @options) + + assert_success second_void_response + assert second_void_response.params['response']['OrderId'].present? + + assert_equal response.params['response']['OrderId'], second_void_response.params['response']['OrderId'] + end + + def test_successful_verify + response = @gateway.verify(@credit_card, @options) + + assert_success response + assert response.params['response']['OrderId'].present? + end + + def test_failed_verify + response = @gateway.verify(@declined_card, @options) + + assert_failure response + assert response.params['response']['OrderId'].present? + assert_equal 'PaymentAuthorizationFailed', response.error_code + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @credit_card, @options) + end + + transcript = @gateway.scrub(transcript) + assert_scrubbed(@credit_card.number, transcript) + assert_scrubbed(@credit_card.verification_value, transcript) + assert_scrubbed(@gateway.options[:merchant_id], transcript) + assert_scrubbed(@gateway.options[:secret], transcript) + end + + def fingerprint + raw_response = @gateway.ssl_get @gateway.send(:url, "fingerprint?MerchantId=#{@gateway.options[:merchant_id]}") + + 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 0e9c466fbc3..ef7561482b3 100644 --- a/test/remote/gateways/remote_realex_test.rb +++ b/test/remote/gateways/remote_realex_test.rb @@ -1,16 +1,16 @@ require 'test_helper' class RemoteRealexTest < Test::Unit::TestCase - def setup @gateway = RealexGateway.new(fixtures(:realex)) # Replace the card numbers with the test account numbers from Realex - @visa = card_fixtures(:realex_visa) - @visa_declined = card_fixtures(:realex_visa_declined) - @visa_referral_b = card_fixtures(:realex_visa_referral_b) - @visa_referral_a = card_fixtures(:realex_visa_referral_a) - @visa_coms_error = card_fixtures(:realex_visa_coms_error) + @visa = card_fixtures(:realex_visa) + @visa_declined = card_fixtures(:realex_visa_declined) + @visa_referral_b = card_fixtures(:realex_visa_referral_b) + @visa_referral_a = card_fixtures(:realex_visa_referral_a) + @visa_coms_error = card_fixtures(:realex_visa_coms_error) + @visa_3ds_enrolled = card_fixtures(:realex_visa_3ds_enrolled) @mastercard = card_fixtures(:realex_mastercard) @mastercard_declined = card_fixtures(:realex_mastercard_declined) @@ -18,14 +18,16 @@ def setup @mastercard_referral_a = card_fixtures(:realex_mastercard_referral_a) @mastercard_coms_error = card_fixtures(:realex_mastercard_coms_error) - @apple_pay = credit_card = network_tokenization_credit_card('4242424242424242', + @apple_pay = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, eci: '05', source: :apple_pay ) - @declined_apple_pay = credit_card = network_tokenization_credit_card('4000120000001154', + @declined_apple_pay = network_tokenization_credit_card( + '4000120000001154', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, eci: '05', @@ -35,18 +37,19 @@ def setup end def card_fixtures(name) - credit_card(nil, fixtures(name)) + credit_card(nil, fixtures(name).merge({ month: 1, year: Time.now.year + 1 })) end def test_realex_purchase - [ @visa, @mastercard ].each do |card| - - response = @gateway.purchase(@amount, card, - :order_id => generate_unique_id, - :description => 'Test Realex Purchase', - :billing_address => { - :zip => '90210', - :country => 'US' + [@visa, @mastercard].each do |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 @@ -59,12 +62,14 @@ def test_realex_purchase def test_realex_purchase_with_invalid_login gateway = RealexGateway.new( - :login => 'invalid', - :password => 'invalid' + login: 'invalid', + password: 'invalid' ) - response = gateway.purchase(@amount, @visa, - :order_id => generate_unique_id, - :description => 'Invalid login test' + response = gateway.purchase( + @amount, + @visa, + order_id: generate_unique_id, + description: 'Invalid login test' ) assert_not_nil response @@ -75,31 +80,34 @@ def test_realex_purchase_with_invalid_login end def test_realex_purchase_with_invalid_account - response = RealexGateway.new(fixtures(:realex_with_account)).purchase(@amount, @visa, - :order_id => generate_unique_id, - :description => 'Test Realex purchase with invalid acocunt' + 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' ) assert_not_nil response assert_failure response - assert_equal '504', response.params['result'] + assert_equal '506', response.params['result'] assert_match %r{no such}i, response.message end def test_realex_purchase_with_apple_pay - response = @gateway.purchase(1000, @apple_pay, :order_id => generate_unique_id, :description => 'Test Realex with ApplePay') + response = @gateway.purchase(1000, @apple_pay, order_id: generate_unique_id, description: 'Test Realex with ApplePay') assert_success response assert response.test? assert_equal 'Successful', response.message end def test_realex_purchase_declined - [ @visa_declined, @mastercard_declined ].each do |card| - - response = @gateway.purchase(@amount, card, - :order_id => generate_unique_id, - :description => 'Test Realex purchase declined' + [@visa_declined, @mastercard_declined].each do |card| + response = @gateway.purchase( + @amount, + card, + order_id: generate_unique_id, + description: 'Test Realex purchase declined' ) assert_not_nil response assert_failure response @@ -107,23 +115,90 @@ def test_realex_purchase_declined assert_equal '101', response.params['result'] assert_equal response.params['message'], response.message end - end def test_realex_purchase_with_apple_pay_declined - response = @gateway.purchase(1101, @declined_apple_pay, :order_id => generate_unique_id, :description => 'Test Realex with ApplePay') + response = @gateway.purchase(1101, @declined_apple_pay, order_id: generate_unique_id, description: 'Test Realex with ApplePay') assert_failure response assert response.test? assert_equal '101', response.params['result'] assert_match %r{DECLINED}i, response.message end - def test_realex_purchase_referral_b - [ @visa_referral_b, @mastercard_referral_b ].each do |card| + def test_realex_purchase_with_three_d_secure_1 + response = @gateway.purchase( + 1000, + @visa_3ds_enrolled, + three_d_secure: { + eci: '05', + cavv: 'AgAAAAAAAIR8CQrXcIhbQAAAAAA', + xid: 'MDAwMDAwMDAwMDAwMDAwMzIyNzY=', + version: '1.0.2' + }, + order_id: generate_unique_id, + description: 'Test Realex with 3DS' + ) + assert_success response + assert response.test? + assert_equal 'Successful', response.message + end + + def test_realex_purchase_with_three_d_secure_2 + response = @gateway.purchase( + 1000, + @visa_3ds_enrolled, + three_d_secure: { + eci: '05', + cavv: 'AgAAAAAAAIR8CQrXcIhbQAAAAAA', + ds_transaction_id: 'bDE9Aa1A-C5Ac-AD3a-4bBC-aC918ab1de3E', + version: '2.1.0' + }, + order_id: generate_unique_id, + description: 'Test Realex with 3DS' + ) + assert_success response + assert response.test? + assert_equal 'Successful', response.message + end + + def test_initial_purchase_with_stored_credential + options = { + stored_credential: { + initial_transaction: true, + reason_type: 'unscheduled', + initiator: 'cardholder', + network_transaction_id: nil + } + } + response = @gateway.purchase(@amount, @visa, options.merge(order_id: generate_unique_id)) + assert_success response + end + + def test_subsequent_purchase_with_stored_credential + initial_response = @gateway.purchase(@amount, @visa, order_id: generate_unique_id) + assert_success initial_response + network_id = initial_response.params['srd'] + + options = { + stored_credential: { + initial_transaction: false, + reason_type: 'recurring', + initiator: 'merchant', + network_transaction_id: network_id + } + } + + subsequent_response = @gateway.purchase(@amount, @visa, options.merge(order_id: generate_unique_id)) + assert_success subsequent_response + end - response = @gateway.purchase(@amount, card, - :order_id => generate_unique_id, - :description => 'Test Realex Referral B' + def test_realex_purchase_referral_b + [@visa_referral_b, @mastercard_referral_b].each do |card| + response = @gateway.purchase( + @amount, + card, + order_id: generate_unique_id, + description: 'Test Realex Referral B' ) assert_not_nil response assert_failure response @@ -134,43 +209,44 @@ def test_realex_purchase_referral_b end def test_realex_purchase_referral_a - [ @visa_referral_a, @mastercard_referral_a ].each do |card| - - response = @gateway.purchase(@amount, card, - :order_id => generate_unique_id, - :description => 'Test Realex Rqeferral A' + [@visa_referral_a, @mastercard_referral_a].each do |card| + response = @gateway.purchase( + @amount, + card, + order_id: generate_unique_id, + description: 'Test Realex Rqeferral A' ) assert_not_nil response assert_failure response assert_equal '103', response.params['result'] assert_equal RealexGateway::DECLINED, response.message end - end def test_realex_purchase_coms_error - [ @visa_coms_error, @mastercard_coms_error ].each do |card| - - response = @gateway.purchase(@amount, card, - :order_id => generate_unique_id, - :description => 'Test Realex coms error' + [@visa_coms_error, @mastercard_coms_error].each do |card| + response = @gateway.purchase( + @amount, + card, + order_id: generate_unique_id, + description: 'Test Realex coms error' ) - assert_not_nil response assert_failure response assert_equal '200', response.params['result'] assert_equal RealexGateway::BANK_ERROR, response.message end - end def test_realex_expiry_month_error @visa.month = 13 - response = @gateway.purchase(@amount, @visa, - :order_id => generate_unique_id, - :description => 'Test Realex expiry month error' + response = @gateway.purchase( + @amount, + @visa, + order_id: generate_unique_id, + description: 'Test Realex expiry month error' ) assert_not_nil response assert_failure response @@ -182,9 +258,11 @@ def test_realex_expiry_month_error def test_realex_expiry_year_error @visa.year = 2005 - response = @gateway.purchase(@amount, @visa, - :order_id => generate_unique_id, - :description => 'Test Realex expiry year error' + response = @gateway.purchase( + @amount, + @visa, + order_id: generate_unique_id, + description: 'Test Realex expiry year error' ) assert_not_nil response assert_failure response @@ -197,9 +275,11 @@ def test_invalid_credit_card_name @visa.first_name = '' @visa.last_name = '' - response = @gateway.purchase(@amount, @visa, - :order_id => generate_unique_id, - :description => 'test_chname_error' + response = @gateway.purchase( + @amount, + @visa, + order_id: generate_unique_id, + description: 'test_chname_error' ) assert_not_nil response assert_failure response @@ -211,9 +291,11 @@ def test_invalid_credit_card_name def test_cvn @visa_cvn = @visa.clone @visa_cvn.verification_value = '111' - response = @gateway.purchase(@amount, @visa_cvn, - :order_id => generate_unique_id, - :description => 'test_cvn' + response = @gateway.purchase( + @amount, + @visa_cvn, + order_id: generate_unique_id, + description: 'test_cvn' ) assert_not_nil response assert_success response @@ -221,10 +303,12 @@ def test_cvn end def test_customer_number - response = @gateway.purchase(@amount, @visa, - :order_id => generate_unique_id, - :description => 'test_cust_num', - :customer => 'my customer id' + response = @gateway.purchase( + @amount, + @visa, + order_id: generate_unique_id, + description: 'test_cust_num', + customer: 'my customer id' ) assert_not_nil response assert_success response @@ -232,12 +316,14 @@ def test_customer_number end def test_realex_authorize - response = @gateway.authorize(@amount, @visa, - :order_id => generate_unique_id, - :description => 'Test Realex Purchase', - :billing_address => { - :zip => '90210', - :country => 'US' + response = @gateway.authorize( + @amount, + @visa, + order_id: generate_unique_id, + description: 'Test Realex Purchase', + billing_address: { + zip: '90210', + country: 'US' } ) @@ -251,12 +337,14 @@ def test_realex_authorize def test_realex_authorize_then_capture order_id = generate_unique_id - auth_response = @gateway.authorize(@amount, @visa, - :order_id => order_id, - :description => 'Test Realex Purchase', - :billing_address => { - :zip => '90210', - :country => 'US' + auth_response = @gateway.authorize( + @amount, + @visa, + order_id:, + description: 'Test Realex Purchase', + billing_address: { + zip: '90210', + country: 'US' } ) assert auth_response.test? @@ -273,12 +361,14 @@ 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, - :order_id => order_id, - :description => 'Test Realex Purchase', - :billing_address => { - :zip => '90210', - :country => 'US' + auth_response = @gateway.authorize( + @amount * 115, + @visa, + order_id:, + description: 'Test Realex Purchase', + billing_address: { + zip: '90210', + country: 'US' } ) assert auth_response.test? @@ -295,12 +385,14 @@ 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, - :order_id => order_id, - :description => 'Test Realex Purchase', - :billing_address => { - :zip => '90210', - :country => 'US' + purchase_response = @gateway.purchase( + @amount, + @visa, + order_id:, + description: 'Test Realex Purchase', + billing_address: { + zip: '90210', + country: 'US' } ) assert purchase_response.test? @@ -316,14 +408,16 @@ def test_realex_purchase_then_void def test_realex_purchase_then_refund order_id = generate_unique_id - gateway_with_refund_password = RealexGateway.new(fixtures(:realex).merge(:rebate_secret => 'rebate')) + gateway_with_refund_password = RealexGateway.new(fixtures(:realex).merge(rebate_secret: 'rebate')) - purchase_response = gateway_with_refund_password.purchase(@amount, @visa, - :order_id => order_id, - :description => 'Test Realex Purchase', - :billing_address => { - :zip => '90210', - :country => 'US' + purchase_response = gateway_with_refund_password.purchase( + @amount, + @visa, + order_id:, + description: 'Test Realex Purchase', + billing_address: { + zip: '90210', + country: 'US' } ) assert purchase_response.test? @@ -336,15 +430,82 @@ def test_realex_purchase_then_refund assert_equal 'Successful', rebate_response.message end + def test_realex_verify + response = @gateway.verify( + @visa, + order_id: generate_unique_id, + description: 'Test Realex verify' + ) + + assert_not_nil response + assert_success response + assert response.test? + assert response.authorization.length > 0 + assert_equal 'Successful', response.message + end + + def test_realex_verify_declined + response = @gateway.verify( + @visa_declined, + order_id: generate_unique_id, + description: 'Test Realex verify declined' + ) + + assert_not_nil response + assert_failure response + assert response.test? + assert_equal '101', response.params['result'] + assert_match %r{DECLINED}i, response.message + end + + 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, + order_id: generate_unique_id, + description: 'Test Realex Credit', + billing_address: { + zip: '90210', + country: 'US' + } + ) + + assert_not_nil credit_response + assert_success credit_response + assert credit_response.authorization.length > 0 + assert_equal 'Successful', credit_response.message + end + + def test_failed_credit + 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 + assert credit_response.authorization.length > 0 + assert_equal 'Refund Hash not present.', credit_response.message + end + def test_maps_avs_and_cvv_response_codes - [ @visa, @mastercard ].each do |card| - - response = @gateway.purchase(@amount, card, - :order_id => generate_unique_id, - :description => 'Test Realex Purchase', - :billing_address => { - :zip => '90210', - :country => 'US' + [@visa, @mastercard].each do |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 @@ -356,14 +517,16 @@ def test_maps_avs_and_cvv_response_codes def test_transcript_scrubbing transcript = capture_transcript(@gateway) do - @gateway.purchase(@amount, @visa_declined, - :order_id => generate_unique_id, - :description => 'Test Realex Purchase', - :billing_address => { - :zip => '90210', - :country => 'US' - } - ) + @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..e105ea1236c --- /dev/null +++ b/test/remote/gateways/remote_redsys_rest_test.rb @@ -0,0 +1,324 @@ +require 'test_helper' + +class RemoteRedsysRestTest < Test::Unit::TestCase + def setup + @gateway = RedsysRestGateway.new(fixtures(:redsys_rest)) + @amount = 100 + @credit_card = credit_card('4548810000000011', verification_value: '123', month: '12', year: '34') + @credit_card_no_cvv = credit_card('4548812049400004', verification_value: nil) + @declined_card = credit_card + @threeds2_credit_card = credit_card('4918019199883839') + + @network_tokenized_credit_card = network_tokenization_credit_card( + '4548812049400004', + payment_cryptogram: 'AOC/WIoqDoS3AdTkVpb5AAADFA==', + eci: '05', + source: :network_token, + brand: 'visa', + month: '04', + year: '26' + ) + + @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_successful_purchase_with_network_token + response = @gateway.purchase(100, @network_tokenized_credit_card, @options.merge(terminal: '001')) + assert_success response + assert_equal 'Requires SCA authentication', 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_for_network_tokens + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @network_tokenized_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) + + # Ensure the encoded returned answer scrub the sensitive files. + + decoded_merchant_params = Base64.decode64(transcript.match(%r(Ds_MerchantParameters=(\w+)))[1]) + parsed_decoded_params = JSON.parse decoded_merchant_params + assert_equal parsed_decoded_params['Ds_Merchant_TokenData']['token'], @network_tokenized_credit_card.number + assert_equal parsed_decoded_params['Ds_Merchant_TokenData']['tokenCryptogram'], @network_tokenized_credit_card.payment_cryptogram + + decoded_clean_merchant_params = Base64.decode64(clean_transcript.match(%r(Ds_MerchantParameters=(\w+)))[1]) + + parsed_clean_params = JSON.parse decoded_clean_merchant_params + assert_equal parsed_clean_params['Ds_Merchant_TokenData']['token'], '[FILTERED]' + + assert_equal parsed_decoded_params['Ds_Merchant_TokenData']['token'], @network_tokenized_credit_card.number + assert_equal parsed_decoded_params['Ds_Merchant_TokenData']['tokenCryptogram'], @network_tokenized_credit_card.payment_cryptogram + + assert_scrubbed(@network_tokenized_credit_card.number, decoded_clean_merchant_params) + assert_scrubbed(@network_tokenized_credit_card.payment_cryptogram, decoded_clean_merchant_params) + 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_authorize_3ds_setup_with_network_token + options = @options.merge(execute_threed: true, terminal: 12) + response = @gateway.authorize(@amount, @network_tokenized_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 + order, amount, currency = response.authorization.split('|') + assert_match(/\d+/, order) + assert_equal '100', amount + assert_equal '978', currency + 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(three_ds_exemption_type: 'low_value')) + assert_success response + assert response.params['ds_emv3ds'] + assert_equal '2.2.0', 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(three_ds_exemption_type: 'low_value')) + assert_success response + assert response.params['ds_emv3ds'] + assert_equal '2.2.0', response.params['ds_emv3ds']['protocolVersion'] + assert_equal 'CardConfiguration', response.message + end + + def test_successful_purchase_using_stored_credential_recurring_cit + initial_options = stored_credential_options(:cardholder, :recurring, :initial) + assert initial_purchase = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success initial_purchase + assert network_transaction_id = initial_purchase.params['ds_merchant_cof_txnid'] + used_options = stored_credential_options(:recurring, :cardholder, id: network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + end + + def test_successful_purchase_using_stored_credential_recurring_mit + initial_options = stored_credential_options(:merchant, :recurring, :initial) + assert initial_purchase = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success initial_purchase + assert network_transaction_id = initial_purchase.params['ds_merchant_cof_txnid'] + used_options = stored_credential_options(:merchant, :recurring, id: network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + end + + def test_successful_purchase_using_stored_credential_installment_cit + initial_options = stored_credential_options(:cardholder, :installment, :initial) + assert initial_purchase = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success initial_purchase + assert network_transaction_id = initial_purchase.params['ds_merchant_cof_txnid'] + used_options = stored_credential_options(:recurring, :cardholder, id: network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + end + + def test_successful_purchase_using_stored_credential_installment_mit + initial_options = stored_credential_options(:merchant, :installment, :initial) + assert initial_purchase = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success initial_purchase + assert network_transaction_id = initial_purchase.params['ds_merchant_cof_txnid'] + used_options = stored_credential_options(:merchant, :recurring, id: network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + end + + def test_successful_purchase_using_stored_credential_unscheduled_cit + initial_options = stored_credential_options(:cardholder, :unscheduled, :initial) + assert initial_purchase = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success initial_purchase + assert network_transaction_id = initial_purchase.params['ds_merchant_cof_txnid'] + used_options = stored_credential_options(:cardholder, :unscheduled, id: network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + end + + private + + def generate_order_id + (Time.now.to_f * 100).to_i.to_s + end + + def stored_credential_options(*args, id: nil) + @options.merge(order_id: generate_unique_id, + stored_credential: stored_credential(*args, id:)) + end +end diff --git a/test/remote/gateways/remote_redsys_sha256_test.rb b/test/remote/gateways/remote_redsys_sha256_test.rb index 28e95aad071..ead7fcf27ce 100644 --- a/test/remote/gateways/remote_redsys_sha256_test.rb +++ b/test/remote/gateways/remote_redsys_sha256_test.rb @@ -3,27 +3,288 @@ class RemoteRedsysSHA256Test < Test::Unit::TestCase def setup @gateway = RedsysGateway.new(fixtures(:redsys_sha256)) + @amount = 100 @credit_card = credit_card('4548812049400004') @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, + order_id: generate_order_id } end def test_successful_purchase - response = @gateway.purchase(100, @credit_card, @options) + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Transaction Approved', response.message + end + + def test_successful_purchase_threeds1_as_mpi_has_no_effect + # 1.0.2 is NOT supported by RedSys External MPI; thus, behaviour should be same as if not present. + xid = '97267598-FAE6-48F2-8083-C23433990FBC' + cavv = '123' + eci = '01' + version = '1.0.2' + + response = @gateway.purchase( + @amount, + @credit_card, + @options.merge( + three_d_secure: { + version:, + xid:, + cavv:, + eci: + }, + description: 'description', + store: 'store', + sca_exemption: 'MOTO' + ) + ) + + assert_success response + assert_equal 'Transaction Approved', response.message + end + + def test_succesful_purchase_threeds1_as_mpi + xid = '97267598-FAE6-48F2-8083-C23433990FBC' + cavv = '123' + eci = '01' + version = '0.0.0' + + response = @gateway.purchase( + @amount, + @credit_card, + @options.merge( + three_d_secure: { + version:, + xid:, + cavv:, + eci: + }, + description: 'description', + store: 'store', + sca_exemption: 'MOTO' + ) + ) + + assert_success response + assert_equal 'Transaction Approved', response.message + end + + def test_successful_purchase_threeds2_as_mpi + three_ds_server_trans_id = '97267598-FAE6-48F2-8083-C23433990FBC' + ds_transaction_id = '97267598-FAE6-48F2-8083-C23433990FBC' + eci = '01' + version = '2.1.0' + + response = @gateway.purchase( + @amount, + @credit_card, + @options.merge( + three_d_secure: { + version:, + ds_transaction_id:, + three_ds_server_trans_id:, + eci: + }, + description: 'description', + store: 'store', + sca_exemption: 'MOTO' + ) + ) + + assert_success response + assert_equal 'Transaction Approved', response.message + end + + def test_successful_authorize_3ds + 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', JSON.parse(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, terminal: 12) + response = @gateway.purchase(@amount, @threeds2_credit_card, options) + assert_success response + assert three_ds_data = JSON.parse(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 + + # 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 + + 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 '2.2.0', JSON.parse(response.params['ds_emv3ds'])['protocolVersion'] + assert_equal 'CardConfiguration', response.message + end + + 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 '2.2.0', JSON.parse(response.params['ds_emv3ds'])['protocolVersion'] + assert_equal 'CardConfiguration', response.message + end + + def test_successful_purchase_with_stored_credentials + initial_options = @options.merge( + stored_credential: { + initial_transaction: true, + reason_type: 'recurring' + } + ) + initial_response = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success initial_response + assert_equal 'Transaction Approved', initial_response.message + assert_not_nil initial_response.params['ds_merchant_cof_txnid'] + network_transaction_id = initial_response.params['ds_merchant_cof_txnid'] + + used_options = @options.merge( + order_id: generate_order_id, + stored_credential: { + initial_transaction: false, + reason_type: 'unscheduled', + network_transaction_id: + } + ) + response = @gateway.purchase(@amount, @credit_card, used_options) assert_success response assert_equal 'Transaction Approved', response.message end + def test_successful_auth_purchase_with_stored_credentials + initial_options = @options.merge( + stored_credential: { + initial_transaction: true, + reason_type: 'recurring' + } + ) + initial_response = @gateway.authorize(@amount, @credit_card, initial_options) + assert_success initial_response + assert_equal 'Transaction Approved', initial_response.message + assert_not_nil initial_response.params['ds_merchant_cof_txnid'] + network_transaction_id = initial_response.params['ds_merchant_cof_txnid'] + + used_options = @options.merge( + order_id: generate_order_id, + stored_credential: { + initial_transaction: false, + reason_type: 'recurring', + network_transaction_id: + } + ) + response = @gateway.purchase(@amount, @credit_card, used_options) + assert_success response + assert_equal 'Transaction Approved', response.message + end + + def test_successful_3ds2_purchase_with_mit_exemption + options = @options.merge(execute_threed: true, terminal: 12) + response = @gateway.purchase(@amount, @threeds2_credit_card, options.merge(sca_exemption: 'MIT', sca_exemption_direct_payment_enabled: true)) + assert_success response + assert response.params['ds_emv3ds'] + assert response.params['ds_card_psd2'] + assert_equal '2.1.0', JSON.parse(response.params['ds_emv3ds'])['protocolVersion'] + assert_equal 'Y', response.params['ds_card_psd2'] + assert_equal 'CardConfiguration', response.message + + # ensure MIT exemption is recognized + assert response.params['ds_excep_sca'] + assert_match 'MIT', response.params['ds_excep_sca'] + end + + def test_successful_ntid_generation_with_verify + # This test validates a special use case for generating a network transaction id + # for payment methods that previously relied on the 'magic' fifteen-nines value. + # However, the transaction id will only be returned from a Redsys production + # environment, so this test simply validates that the zero-value request succeeds. + + alternate_gateway = RedsysGateway.new(fixtures(:redsys_sha256_alternate)) + options = @options.merge( + sca_exemption_behavior_override: 'endpoint_and_ntid', + sca_exemption: 'MIT', + terminal: 2, + stored_credential: { + initial_transaction: false, + reason_type: 'recurring', + network_transaction_id: '999999999999999' + } + ) + response = alternate_gateway.verify(@credit_card, options) + assert_success response + end + + def test_failed_3ds2_purchase_with_mit_exemption_but_missing_direct_payment_enabled + options = @options.merge(execute_threed: true, terminal: 12) + response = @gateway.purchase(@amount, @threeds2_credit_card, options.merge(sca_exemption: 'MIT')) + assert_success response + assert response.params['ds_emv3ds'] + assert response.params['ds_card_psd2'] + assert_equal '2.1.0', JSON.parse(response.params['ds_emv3ds'])['protocolVersion'] + assert_equal 'Y', response.params['ds_card_psd2'] + assert_equal 'CardConfiguration', response.message + + # ensure MIT exemption is recognized + assert response.params['ds_excep_sca'] + assert_match 'MIT', response.params['ds_excep_sca'] + end + + def test_successful_purchase_with_mit_exemption + initial_options = @options.merge( + stored_credential: { + initial_transaction: true, + reason_type: 'recurring' + } + ) + initial_response = @gateway.authorize(@amount, @credit_card, initial_options) + assert_success initial_response + assert_equal 'Transaction Approved', initial_response.message + assert_not_nil initial_response.params['ds_merchant_cof_txnid'] + network_transaction_id = initial_response.params['ds_merchant_cof_txnid'] + + used_options = @options.merge( + order_id: generate_order_id, + stored_credential: { + initial_transaction: false, + reason_type: 'recurring', + network_transaction_id: + }, + sca_exemption: 'MIT', + sca_exemption_direct_payment_enabled: true + ) + + response = @gateway.purchase(@amount, @credit_card, used_options) + assert_success response + assert_equal response.message, 'Transaction Approved' + end + def test_purchase_with_invalid_order_id - response = @gateway.purchase(100, @credit_card, order_id: "a%4#{generate_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_successful_purchase_using_vault_id - response = @gateway.purchase(100, @credit_card, @options.merge(store: true)) + response = @gateway.purchase(@amount, @credit_card, @options.merge(store: true)) assert_success response assert_equal 'Transaction Approved', response.message @@ -31,44 +292,44 @@ def test_successful_purchase_using_vault_id assert_not_nil credit_card_token @options[:order_id] = generate_order_id - response = @gateway.purchase(100, credit_card_token, @options) + response = @gateway.purchase(@amount, credit_card_token, @options) assert_success response assert_equal 'Transaction Approved', response.message end def test_failed_purchase - response = @gateway.purchase(100, @declined_card, @options) + response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response - assert_equal 'SIS0093 ERROR', response.message + assert_equal 'Refusal with no specific reason', response.message end def test_purchase_and_refund - purchase = @gateway.purchase(100, @credit_card, @options) + purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - refund = @gateway.refund(100, purchase.authorization) + refund = @gateway.refund(@amount, purchase.authorization) assert_success refund end # Multiple currencies are not supported in test, but should at least fail. def test_purchase_and_refund_with_currency - response = @gateway.purchase(600, @credit_card, @options.merge(:currency => 'PEN')) + response = @gateway.purchase(600, @credit_card, @options.merge(currency: 'PEN')) assert_failure response assert_equal 'SIS0027 ERROR', response.message end def test_successful_authorise_and_capture - authorize = @gateway.authorize(100, @credit_card, @options) + authorize = @gateway.authorize(@amount, @credit_card, @options) assert_success authorize assert_equal 'Transaction Approved', authorize.message assert_not_nil authorize.authorization - capture = @gateway.capture(100, authorize.authorization) + capture = @gateway.capture(@amount, authorize.authorization) assert_success capture - assert_match /Refund.*approved/, capture.message + assert_match(/Refund.*approved/, capture.message) end def test_successful_authorise_using_vault_id - authorize = @gateway.authorize(100, @credit_card, @options.merge(store: true)) + authorize = @gateway.authorize(@amount, @credit_card, @options.merge(store: true)) assert_success authorize assert_equal 'Transaction Approved', authorize.message assert_not_nil authorize.authorization @@ -77,20 +338,20 @@ def test_successful_authorise_using_vault_id assert_not_nil credit_card_token @options[:order_id] = generate_order_id - authorize = @gateway.authorize(100, credit_card_token, @options) + authorize = @gateway.authorize(@amount, credit_card_token, @options) assert_success authorize assert_equal 'Transaction Approved', authorize.message assert_not_nil authorize.authorization end def test_failed_authorize - response = @gateway.authorize(100, @declined_card, @options) + response = @gateway.authorize(@amount, @declined_card, @options) assert_failure response - assert_equal 'SIS0093 ERROR', response.message + assert_equal 'Refusal with no specific reason', response.message end def test_successful_void - authorize = @gateway.authorize(100, @credit_card, @options) + authorize = @gateway.authorize(@amount, @credit_card, @options) assert_success authorize void = @gateway.void(authorize.authorization) @@ -100,15 +361,12 @@ def test_successful_void end def test_failed_void - authorize = @gateway.authorize(100, @credit_card, @options) + 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 @@ -116,14 +374,12 @@ 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 assert response = @gateway.verify(@declined_card, @options) assert_failure response - assert_equal 'SIS0093 ERROR', response.message + assert_equal 'Refusal with no specific reason', response.message end def test_transcript_scrubbing @@ -177,6 +433,7 @@ def test_whitespace_string_cvv_transcript_scrubbing assert_equal clean_transcript.include?('[BLANK]'), true end + private def generate_order_id diff --git a/test/remote/gateways/remote_redsys_test.rb b/test/remote/gateways/remote_redsys_test.rb index 5cc9ae08273..a5cfd70aaa3 100644 --- a/test/remote/gateways/remote_redsys_test.rb +++ b/test/remote/gateways/remote_redsys_test.rb @@ -3,29 +3,99 @@ class RemoteRedsysTest < Test::Unit::TestCase def setup @gateway = RedsysGateway.new(fixtures(:redsys)) + @amount = 100 @credit_card = credit_card('4548812049400004') @declined_card = credit_card @options = { order_id: generate_order_id, description: 'Test Description' } - @amount = 100 end def test_successful_purchase - response = @gateway.purchase(100, @credit_card, @options) + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Transaction Approved', response.message + end + + def test_successful_purchase_threeds2 + three_ds_server_trans_id = '97267598-FAE6-48F2-8083-C23433990FBC' + ds_transaction_id = '97267598-FAE6-48F2-8083-C23433990FBC' + version = '2.1.0' + eci = '02' + + response = @gateway.purchase( + 100, + @credit_card, + @options.merge( + three_d_secure: { + version:, + ds_transaction_id:, + three_ds_server_trans_id:, + eci: + } + ) + ) + + assert_success response + assert_equal 'Transaction Approved', response.message + end + + def test_successful_purchase_threeds1 + xid = '97267598-FAE6-48F2-8083-C23433990FBC' + cavv = 'jJ81HADVRtXfCBATEp01CJUAAAA=' + eci = '02' + + response = @gateway.purchase( + @amount, + @credit_card, + @options.merge( + three_d_secure: { + eci:, + cavv:, + xid: + } + ) + ) + + assert_success response + assert_equal 'Transaction Approved', response.message + end + + def test_successful_purchase_with_stored_credentials + initial_options = @options.merge( + stored_credential: { + initial_transaction: true, + reason_type: 'recurring' + } + ) + initial_response = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success initial_response + assert_equal 'Transaction Approved', initial_response.message + assert_not_nil initial_response.params['ds_merchant_cof_txnid'] + network_transaction_id = initial_response.params['ds_merchant_cof_txnid'] + + used_options = @options.merge( + order_id: generate_order_id, + stored_credential: { + initial_transaction: false, + reason_type: 'unscheduled', + network_transaction_id: + } + ) + response = @gateway.purchase(@amount, @credit_card, used_options) assert_success response assert_equal 'Transaction Approved', response.message end def test_purchase_with_invalid_order_id - response = @gateway.purchase(100, @credit_card, order_id: "a%4#{generate_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_successful_purchase_using_vault_id - response = @gateway.purchase(100, @credit_card, @options.merge(store: true)) + response = @gateway.purchase(@amount, @credit_card, @options.merge(store: true)) assert_success response assert_equal 'Transaction Approved', response.message @@ -33,44 +103,44 @@ def test_successful_purchase_using_vault_id assert_not_nil credit_card_token @options[:order_id] = generate_order_id - response = @gateway.purchase(100, credit_card_token, @options) + response = @gateway.purchase(@amount, credit_card_token, @options) assert_success response assert_equal 'Transaction Approved', response.message end def test_failed_purchase - response = @gateway.purchase(100, @declined_card, @options) + response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response - assert_equal 'SIS0093 ERROR', response.message + assert_equal 'Refusal with no specific reason', response.message end def test_purchase_and_refund - purchase = @gateway.purchase(100, @credit_card, @options) + purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - refund = @gateway.refund(100, purchase.authorization) + refund = @gateway.refund(@amount, purchase.authorization) assert_success refund end # Multiple currencies are not supported in test, but should at least fail. def test_purchase_and_refund_with_currency - response = @gateway.purchase(600, @credit_card, @options.merge(:currency => 'PEN')) + response = @gateway.purchase(600, @credit_card, @options.merge(currency: 'PEN')) assert_failure response assert_equal 'SIS0027 ERROR', response.message end def test_successful_authorise_and_capture - authorize = @gateway.authorize(100, @credit_card, @options) + authorize = @gateway.authorize(@amount, @credit_card, @options) assert_success authorize assert_equal 'Transaction Approved', authorize.message assert_not_nil authorize.authorization - capture = @gateway.capture(100, authorize.authorization) + capture = @gateway.capture(@amount, authorize.authorization) assert_success capture - assert_match /Refund.*approved/, capture.message + assert_match(/Refund.*approved/, capture.message) end def test_successful_authorise_using_vault_id - authorize = @gateway.authorize(100, @credit_card, @options.merge(store: true)) + authorize = @gateway.authorize(@amount, @credit_card, @options.merge(store: true)) assert_success authorize assert_equal 'Transaction Approved', authorize.message assert_not_nil authorize.authorization @@ -79,20 +149,20 @@ def test_successful_authorise_using_vault_id assert_not_nil credit_card_token @options[:order_id] = generate_order_id - authorize = @gateway.authorize(100, credit_card_token, @options) + authorize = @gateway.authorize(@amount, credit_card_token, @options) assert_success authorize assert_equal 'Transaction Approved', authorize.message assert_not_nil authorize.authorization end def test_failed_authorize - response = @gateway.authorize(100, @declined_card, @options) + response = @gateway.authorize(@amount, @declined_card, @options) assert_failure response - assert_equal 'SIS0093 ERROR', response.message + assert_equal 'Refusal with no specific reason', response.message end def test_successful_void - authorize = @gateway.authorize(100, @credit_card, @options) + authorize = @gateway.authorize(@amount, @credit_card, @options) assert_success authorize void = @gateway.void(authorize.authorization) @@ -102,15 +172,12 @@ def test_successful_void end def test_failed_void - authorize = @gateway.authorize(100, @credit_card, @options) + 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 @@ -118,14 +185,12 @@ 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 assert response = @gateway.verify(@declined_card, @options) assert_failure response - assert_equal 'SIS0093 ERROR', response.message + assert_equal 'Refusal with no specific reason', response.message end def test_transcript_scrubbing @@ -179,6 +244,18 @@ def test_whitespace_string_cvv_transcript_scrubbing assert_equal clean_transcript.include?('[BLANK]'), true end + + def test_encrypt_handles_url_safe_character_in_secret_key_without_error + gateway = RedsysGateway.new({ + login: '091952713', + secret_key: 'yG78qf-PkHyRzRiZGSTCJdO2TvjWgFa8', + terminal: '1', + signature_algorithm: 'sha256' + }) + response = gateway.purchase(@amount, @credit_card, @options) + assert response + end + private def generate_order_id diff --git a/test/remote/gateways/remote_s5_test.rb b/test/remote/gateways/remote_s5_test.rb index 1c9f31bf0ab..b1c831ea98a 100644 --- a/test/remote/gateways/remote_s5_test.rb +++ b/test/remote/gateways/remote_s5_test.rb @@ -90,7 +90,7 @@ def test_partial_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture end @@ -111,7 +111,7 @@ def test_partial_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount-1, purchase.authorization) + assert refund = @gateway.refund(@amount - 1, purchase.authorization) assert_success refund end diff --git a/test/remote/gateways/remote_safe_charge_test.rb b/test/remote/gateways/remote_safe_charge_test.rb index fc64bacb1c1..4ab4baf29a3 100644 --- a/test/remote/gateways/remote_safe_charge_test.rb +++ b/test/remote/gateways/remote_safe_charge_test.rb @@ -16,9 +16,18 @@ def setup @three_ds_options = @options.merge(three_d_secure: true) @three_ds_gateway = SafeChargeGateway.new(fixtures(:safe_charge_three_ds)) - @three_ds_enrolled_card = credit_card('4012 0010 3749 0014') + @three_ds_enrolled_card = credit_card('4407 1064 3967 1112') @three_ds_non_enrolled_card = credit_card('5333 3062 3122 6927') @three_ds_invalid_pa_res_card = credit_card('4012 0010 3749 0006') + + @network_token_credit_card = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ + brand: 'Visa', + payment_cryptogram: 'UnVBR0RlYm42S2UzYWJKeWJBdWQ=', + number: '4012001037490014', + source: :network_token, + month: '12', + year: 2020 + }) end def test_successful_3ds_purchase @@ -43,9 +52,7 @@ def test_successful_regular_purchase_through_3ds_flow_with_non_enrolled_card def test_successful_regular_purchase_through_3ds_flow_with_invalid_pa_res response = @three_ds_gateway.purchase(@amount, @three_ds_invalid_pa_res_card, @three_ds_options) assert_success response - assert !response.params['acsurl'].blank? - assert !response.params['pareq'].blank? - assert !response.params['xid'].blank? + assert_equal 'Attempted But Card Not Enrolled', response.params['threedreason'] assert response.params['threedflow'] = 1 assert_equal 'Success', response.message end @@ -56,6 +63,122 @@ def test_successful_purchase assert_equal 'Success', response.message end + def test_successful_purchase_aud + response = @gateway.purchase(@amount, @credit_card, @options.merge(currency: 'AUD')) + assert_success response + assert_equal 'Success', response.message + assert_equal 'AUD', response.params['requestedcurrency'] + assert_equal 'AUD', response.params['processedcurrency'] + end + + def test_successful_purchase_cad + response = @gateway.purchase(@amount, @credit_card, @options.merge(currency: 'CAD')) + assert_success response + assert_equal 'Success', response.message + assert_equal 'CAD', response.params['requestedcurrency'] + assert_equal 'CAD', response.params['processedcurrency'] + end + + def test_successful_purchase_with_card_holder_verification + response = @gateway.purchase(@amount, @credit_card, @options.merge(middle_name: 'middle', card_holder_verification: 1)) + assert_success response + assert_equal 'Success', response.message + assert_equal '', response.params['cardholdernameverification'] + 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) + + assert_success response + assert_equal 'Success', response.message + assert_equal '1279', response.params['requestedamount'] + end + + def test_successful_purchase_with_mpi_options_3ds_1 + options = @options.merge({ + three_d_secure: { + xid: '00000000000000000501', + eci: '05', + cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA=' + } + }) + + response = @gateway.purchase(@amount, @three_ds_enrolled_card, options) + assert_success response + assert_equal 'Success', response.message + end + + def test_successful_purchase_with_mpi_options_3ds_2 + options = @options.merge({ + three_d_secure: { + version: '2.1.0', + ds_transaction_id: 'c5b808e7-1de1-4069-a17b-f70d3b3b1645', + eci: '05', + cavv: 'Vk83Y2t0cHRzRFZzRlZlR0JIQXo=', + challenge_preference: 'NoPreference' + } + }) + + response = @gateway.purchase(@amount, @three_ds_enrolled_card, options) + assert_success response + assert_equal 'Success', response.message + end + + def test_successful_network_tokenization_request + options = @options.merge({ + three_d_secure: { + eci: '05' + } + }) + + response = @gateway.purchase(@amount, @network_token_credit_card, options) + assert_success response + assert_equal 'Success', response.message + end + + def test_failed_purchase_with_mpi_options_3ds_2 + options = @options.merge({ + three_d_secure: { + version: '2.1.0', + ds_transaction_id: 'c5b808e7-1de1-4069-a17b-f70d3b3b1645', + eci: '05', + cavv: 'Vk83Y2t0cHRzRFZzRlZlR0JIQXo=', + challenge_preference: 'NoPreference' + } + }) + + response = @gateway.purchase(@amount, @declined_card, options) + assert_failure response + assert_equal 'Decline', response.message + end + + def test_successful_authorize_with_mpi_options_3ds_2 + options = @options.merge({ + three_d_secure: { + version: '2.1.0', + ds_transaction_id: 'c5b808e7-1de1-4069-a17b-f70d3b3b1645', + eci: '05', + cavv: 'Vk83Y2t0cHRzRFZzRlZlR0JIQXo=', + challenge_preference: 'NoPreference' + } + }) + + response = @gateway.authorize(@amount, @three_ds_enrolled_card, options) + assert_success response + assert_equal 'Success', response.message + end + def test_successful_purchase_with_more_options options = { order_id: '1', @@ -67,7 +190,8 @@ def test_successful_purchase_with_more_options merchant_descriptor: 'Test Descriptor', merchant_phone_number: '(555)555-5555', merchant_name: 'Test Merchant', - stored_credential_mode: true + stored_credential_mode: true, + product_id: 'Test Product' } response = @gateway.purchase(@amount, @credit_card, options) @@ -101,11 +225,22 @@ def test_successful_authorize_and_capture_with_more_options merchant_descriptor: 'Test Descriptor', merchant_phone_number: '(555)555-5555', merchant_name: 'Test Merchant', - stored_credential_mode: true + stored_credential_mode: true, + product_id: 'Test Product' } auth = @gateway.authorize(@amount, @credit_card, extra) assert_success auth + assert capture = @gateway.capture(@amount, auth.authorization, extra) + assert_success capture + assert_equal 'Success', capture.message + end + + def test_successful_authorize_and_capture_with_not_use_cvv + @credit_card.verification_value = nil + auth = @gateway.authorize(@amount, @credit_card, @options.merge!({ not_use_cvv: true })) + assert_success auth + assert capture = @gateway.capture(@amount, auth.authorization) assert_success capture assert_equal 'Success', capture.message @@ -121,7 +256,7 @@ def test_partial_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture end @@ -154,6 +289,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 @@ -171,7 +328,8 @@ def test_successful_credit_with_extra_options merchant_descriptor: 'Test Descriptor', merchant_phone_number: '(555)555-5555', merchant_name: 'Test Merchant', - stored_credential_mode: true + stored_credential_mode: true, + product_id: 'Test Product' } response = @gateway.credit(@amount, credit_card('4444436501403986'), extra) @@ -179,6 +337,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 @@ -230,5 +394,4 @@ def test_transcript_scrubbing assert_scrubbed(@credit_card.verification_value, transcript) assert_scrubbed(@gateway.options[:client_password], transcript) end - end diff --git a/test/remote/gateways/remote_sage_pay_test.rb b/test/remote/gateways/remote_sage_pay_test.rb index d4450ecda49..ca436bbfcbf 100644 --- a/test/remote/gateways/remote_sage_pay_test.rb +++ b/test/remote/gateways/remote_sage_pay_test.rb @@ -12,95 +12,207 @@ def setup @gateway = SagePayGateway.new(fixtures(:sage_pay)) @amex = CreditCard.new( - :number => '374200000000004', - :month => 12, - :year => next_year, - :verification_value => 4887, - :first_name => 'Tekin', - :last_name => 'Suleyman', - :brand => 'american_express' + number: '374200000000004', + month: 12, + year: next_year, + verification_value: 4887, + first_name: 'Tekin', + last_name: 'Suleyman', + brand: 'american_express' ) @maestro = CreditCard.new( - :number => '5641820000000005', - :month => 12, - :year => next_year, - :issue_number => '01', - :start_month => 12, - :start_year => next_year - 2, - :verification_value => 123, - :first_name => 'Tekin', - :last_name => 'Suleyman', - :brand => 'maestro' + number: '5641820000000005', + month: 12, + year: next_year, + start_month: 12, + start_year: next_year - 2, + verification_value: 123, + first_name: 'Tekin', + last_name: 'Suleyman', + brand: 'maestro' ) @visa = CreditCard.new( - :number => '4929000000006', - :month => 6, - :year => next_year, - :verification_value => 123, - :first_name => 'Tekin', - :last_name => 'Suleyman', - :brand => 'visa' + number: '4929000000006', + month: 6, + year: next_year, + verification_value: 123, + first_name: 'Tekin', + last_name: 'Suleyman', + brand: 'visa' ) @mastercard = CreditCard.new( - :number => '5404000000000001', - :month => 12, - :year => next_year, - :verification_value => 419, - :first_name => 'Tekin', - :last_name => 'Suleyman', - :brand => 'master' + number: '5186150660000009', + month: 12, + year: next_year, + verification_value: 419, + first_name: 'Tekin', + last_name: 'Suleyman', + 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, - :year => next_year, - :verification_value => 123, - :first_name => 'Tekin', - :last_name => 'Suleyman', - :brand => 'electron' + number: '4917300000000008', + month: 12, + year: next_year, + verification_value: 123, + first_name: 'Tekin', + last_name: 'Suleyman', + brand: 'electron' ) @declined_card = CreditCard.new( - :number => '4111111111111111', - :month => 9, - :year => next_year, - :first_name => 'Tekin', - :last_name => 'Suleyman', - :brand => 'visa' + number: '4000000000000001', + month: 9, + year: next_year, + verification_value: 123, + first_name: 'Tekin', + last_name: 'Suleyman', + brand: 'visa' ) @options = { - :billing_address => { - :name => 'Tekin Suleyman', - :address1 => 'Flat 10 Lapwing Court', - :address2 => 'West Didsbury', - :city => 'Manchester', - :county => 'Greater Manchester', - :country => 'GB', - :zip => 'M20 2PS' + 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' + } + + @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' + 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' + 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: + }, + 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 @@ -109,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 @@ -116,6 +236,19 @@ def test_unsuccessful_purchase assert response.test? end + def test_successful_purchase_via_reference + assert initial_response = @gateway.purchase(@amount, @mastercard, @options) + assert_success initial_response + + options = @options.merge(order_id: generate_unique_id) + assert first_reference_response = @gateway.purchase(@amount, initial_response.authorization, options) + assert_success first_reference_response + + options = @options.merge(order_id: generate_unique_id) + assert second_reference_response = @gateway.purchase(@amount, first_reference_response.authorization, options) + assert_success second_reference_response + end + def test_successful_authorization_and_capture assert auth = @gateway.authorize(@amount, @mastercard, @options) assert_success auth @@ -130,11 +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 @@ -157,12 +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 @@ -236,7 +360,7 @@ def test_successful_purchase_with_apply_avscv2_field @options[:apply_avscv2] = 1 response = @gateway.purchase(@amount, @visa, @options) assert_success response - assert_equal 'Y', response.cvv_result['code'] + assert_equal 'M', response.cvv_result['code'] end def test_successful_purchase_with_pay_pal_callback_url @@ -249,9 +373,9 @@ def test_successful_purchase_with_basket # Example from "Sage Pay Direct Integration and Protocol Guidelines 3.00" # Published: 27/08/2015 @options[:basket] = '4:Pioneer NSDV99 DVD-Surround Sound System:1:424.68:' \ - '74.32:499.00: 499.00:Donnie Darko Director’s Cut:3:11.91:2.08:13.99:' \ - '41.97: Finding Nemo:2:11.05:1.94:12.99:25.98: Delivery:---:---:---:---' \ - ':4.99' + '74.32:499.00: 499.00:Donnie Darko Director’s Cut:3:11.91:2.08:13.99:' \ + '41.97: Finding Nemo:2:11.05:1.94:12.99:25.98: Delivery:---:---:---:---' \ + ':4.99' response = @gateway.purchase(@amount, @visa, @options) assert_success response end @@ -262,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' @@ -331,7 +456,7 @@ def test_invalid_login message = SagePayGateway.simulate ? 'VSP Simulator cannot find your vendor name. Ensure you have have supplied a Vendor field with your VSP Vendor name assigned to it.' : '3034 : The Vendor or VendorName value is required.' gateway = SagePayGateway.new( - :login => '' + login: '' ) assert response = gateway.purchase(@amount, @mastercard, @options) assert_equal message, response.message @@ -350,7 +475,7 @@ def test_successful_store_and_repurchase_with_resupplied_verification_value assert response = @gateway.store(@visa) assert_success response assert !response.authorization.blank? - assert purchase = @gateway.purchase(@amount, response.authorization, @options.merge(customer: 1)) + assert @gateway.purchase(@amount, response.authorization, @options.merge(customer: 1)) assert purchase = @gateway.purchase(@amount, response.authorization, @options.merge(verification_value: '123', order_id: generate_unique_id)) assert_success purchase end @@ -364,13 +489,13 @@ def test_successful_store_and_authorize end def test_successful_token_creation_from_purchase - assert response = @gateway.purchase(@amount, @visa, @options.merge(:store => true)) + assert response = @gateway.purchase(@amount, @visa, @options.merge(store: true)) assert_success response assert !response.authorization.blank? end def test_successful_token_creation_from_authorize - assert response = @gateway.authorize(@amount, @visa, @options.merge(:store => true)) + assert response = @gateway.authorize(@amount, @visa, @options.merge(store: true)) assert_success response assert !response.authorization.blank? end @@ -392,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 @@ -414,56 +539,56 @@ def next_year # Based on example from http://www.sagepay.co.uk/support/basket-xml # Only kept required fields to make sense def basket_xml - <<-XML - - - DVD 1 - 2 - 24.50 - 00.50 - 25.00 - 50.00 - - + <<~XML + + + DVD 1 + 2 + 24.50 + 00.50 + 25.00 + 50.00 + + XML end # Example from http://www.sagepay.co.uk/support/customer-xml def customer_xml - <<-XML - - W - 1983-01-01 - 020 1234567 - 0799 1234567 - 0 - 10 - CUST123 - + <<~XML + + W + 1983-01-01 + 020 1234567 + 0799 1234567 + 0 + 10 + CUST123 + XML end # Example from https://www.sagepay.co.uk/support/12/36/protocol-3-00-surcharge-xml def surcharge_xml - <<-XML - - - DELTA - 2.50 - - - VISA - 2.50 - - - AMEX - 1.50 - - - MC - 1.50 - - + <<~XML + + + DELTA + 2.50 + + + VISA + 2.50 + + + AMEX + 1.50 + + + MC + 1.50 + + XML end end diff --git a/test/remote/gateways/remote_sage_test.rb b/test/remote/gateways/remote_sage_test.rb index fbab6a0417a..1bbca7cbad1 100644 --- a/test/remote/gateways/remote_sage_test.rb +++ b/test/remote/gateways/remote_sage_test.rb @@ -1,7 +1,6 @@ require 'test_helper' class RemoteSageTest < Test::Unit::TestCase - def setup @gateway = SageGateway.new(fixtures(:sage)) @@ -16,10 +15,10 @@ def setup @declined_card = credit_card('4000') @options = { - :order_id => generate_unique_id, - :billing_address => address, - :shipping_address => address, - :email => 'longbob@example.com' + order_id: generate_unique_id, + billing_address: address, + shipping_address: address, + email: 'longbob@example.com' } end @@ -157,7 +156,7 @@ def test_partial_refund purchase = @gateway.purchase(@amount, @visa, @options) assert_success purchase - assert refund = @gateway.refund(@amount-1, purchase.authorization, @options) + assert refund = @gateway.refund(@amount - 1, purchase.authorization, @options) assert_success refund assert_equal 'APPROVED', refund.message end @@ -165,8 +164,7 @@ def test_partial_refund def test_store_visa assert response = @gateway.store(@visa, @options) assert_success response - assert auth = 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 @@ -177,24 +175,22 @@ 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 def test_invalid_login gateway = SageGateway.new( - :login => '', - :password => '' - ) + login: '', + password: '' + ) assert response = gateway.purchase(@amount, @visa, @options) assert_failure response assert_equal 'SECURITY VIOLATION', response.message @@ -231,5 +227,4 @@ def test_echeck_scrubbing assert_scrubbed(@check.routing_number, transcript) assert_scrubbed(@gateway.options[:password], transcript) end - end diff --git a/test/remote/gateways/remote_sallie_mae_test.rb b/test/remote/gateways/remote_sallie_mae_test.rb index 09f14cb2337..e9f30994baf 100644 --- a/test/remote/gateways/remote_sallie_mae_test.rb +++ b/test/remote/gateways/remote_sallie_mae_test.rb @@ -3,14 +3,14 @@ class RemoteSallieMaeTest < Test::Unit::TestCase def setup @gateway = SallieMaeGateway.new(fixtures(:sallie_mae)) - + @amount = 100 @credit_card = credit_card('5454545454545454') @declined_card = credit_card('4000300011112220') - - @options = { - :billing_address => address, - :description => 'Store Purchase' + + @options = { + billing_address: address, + description: 'Store Purchase' } end @@ -43,7 +43,7 @@ def test_failed_capture end def test_invalid_login - gateway = SallieMaeGateway.new(:login => '') + gateway = SallieMaeGateway.new(login: '') assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal 'Invalid merchant', response.message diff --git a/test/remote/gateways/remote_secure_net_test.rb b/test/remote/gateways/remote_secure_net_test.rb index fc92a5276e1..4761aab6187 100644 --- a/test/remote/gateways/remote_secure_net_test.rb +++ b/test/remote/gateways/remote_secure_net_test.rb @@ -1,7 +1,6 @@ require 'test_helper' class SecureNetTest < Test::Unit::TestCase - def setup Base.mode = :test @gateway = SecureNetGateway.new(fixtures(:secure_net)) @@ -13,7 +12,7 @@ def setup n = Time.now order_id = n.to_i.to_s + n.usec.to_s @options = { - order_id: order_id, + order_id:, billing_address: address, description: 'Store Purchase' } @@ -29,9 +28,9 @@ def test_expired_credit_card def test_invalid_login gateway = SecureNetGateway.new( - :login => '9988776', - :password => 'RabbitEarsPo' - ) + login: '9988776', + password: 'RabbitEarsPo' + ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal 'SECURE KEY IS INVALID FOR SECURENET ID PROVIDED', response.message @@ -135,5 +134,4 @@ def test_transcript_scrubbing assert_scrubbed(@credit_card.verification_value, transcript) assert_scrubbed(@gateway.options[:password], transcript) end - end diff --git a/test/remote/gateways/remote_secure_pay_au_test.rb b/test/remote/gateways/remote_secure_pay_au_test.rb index 56958b29afd..401733cea3b 100644 --- a/test/remote/gateways/remote_secure_pay_au_test.rb +++ b/test/remote/gateways/remote_secure_pay_au_test.rb @@ -1,11 +1,14 @@ require 'test_helper' class RemoteSecurePayAuTest < Test::Unit::TestCase - class MyCreditCard include ActiveMerchant::Billing::CreditCardMethods attr_accessor :number, :month, :year, :first_name, :last_name, :verification_value, :brand + def initialize(params) + params.each { |k, v| instance_variable_set("@#{k}".to_sym, v) } + end + def verification_value? !@verification_value.blank? end @@ -15,12 +18,12 @@ def setup @gateway = SecurePayAuGateway.new(fixtures(:secure_pay_au)) @amount = 100 - @credit_card = credit_card('4242424242424242', {:month => 9, :year => 15}) + @credit_card = credit_card('4242424242424242', { month: 9, year: 15 }) @options = { - :order_id => '2', - :billing_address => address, - :description => 'Store Purchase' + order_id: 'order123', + billing_address: address, + description: 'Store Purchase' } end @@ -32,13 +35,13 @@ def test_successful_purchase def test_successful_purchase_with_custom_credit_card_class options = { - :number => 4242424242424242, - :month => 9, - :year => Time.now.year + 1, - :first_name => 'Longbob', - :last_name => 'Longsen', - :verification_value => '123', - :brand => 'visa' + number: 4242424242424242, + month: 9, + year: Time.now.year + 1, + first_name: 'Longbob', + last_name: 'Longsen', + verification_value: '123', + brand: 'visa' } credit_card = MyCreditCard.new(options) assert response = @gateway.purchase(@amount, credit_card, @options) @@ -73,7 +76,7 @@ def test_failed_capture assert auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount+1, auth.authorization) + assert capture = @gateway.capture(@amount + 1, auth.authorization) assert_failure capture assert_equal 'Preauth was done for smaller amount', capture.message end @@ -93,12 +96,13 @@ def test_failed_refund assert_success response authorization = response.authorization - assert response = @gateway.refund(@amount+1, authorization) + assert response = @gateway.refund(@amount + 1, authorization) assert_failure response - assert_equal 'Only $1.0 available for refund', response.message + assert_equal 'Only 1.00 AUD available for refund', response.message end def test_successful_void + omit('It appears that SecurePayAU no longer supports void') assert response = @gateway.authorize(@amount, @credit_card, @options) assert_success response @@ -111,17 +115,18 @@ def test_successful_void end def test_failed_void + omit('It appears that SecurePayAU no longer supports void') assert response = @gateway.purchase(@amount, @credit_card, @options) assert_success response authorization = response.authorization - assert response = @gateway.void(authorization+'1') + assert response = @gateway.void(authorization + '1') assert_failure response - assert_equal 'Unable to retrieve original FDR txn', response.message + assert_equal 'Transaction type not available', response.message end def test_successful_unstore - @gateway.store(@credit_card, {:billing_id => 'test1234', :amount => 15000}) rescue nil + @gateway.store(@credit_card, { billing_id: 'test1234', amount: 15000 }) rescue nil assert response = @gateway.unstore('test1234') assert_success response @@ -130,7 +135,7 @@ def test_successful_unstore end def test_repeat_unstore - @gateway.unstore('test1234') rescue nil #Ensure it is already missing + @gateway.unstore('test1234') rescue nil # Ensure it is already missing response = @gateway.unstore('test1234') @@ -140,33 +145,34 @@ def test_repeat_unstore def test_successful_store @gateway.unstore('test1234') rescue nil - assert response = @gateway.store(@credit_card, {:billing_id => 'test1234', :amount => 15000}) + assert response = @gateway.store(@credit_card, { billing_id: 'test1234', amount: 15000 }) assert_success response assert_equal 'Successful', response.message end def test_failed_store - @gateway.store(@credit_card, {:billing_id => 'test1234', :amount => 15000}) rescue nil #Ensure it already exists + @gateway.store(@credit_card, { billing_id: 'test1234', amount: 15000 }) rescue nil # Ensure it already exists - assert response = @gateway.store(@credit_card, {:billing_id => 'test1234', :amount => 15000}) + assert response = @gateway.store(@credit_card, { billing_id: 'test1234', amount: 15000 }) assert_failure response assert_equal 'Duplicate Client ID Found', response.message end def test_successful_triggered_payment - @gateway.store(@credit_card, {:billing_id => 'test1234', :amount => 15000}) rescue nil #Ensure it already exists + @gateway.store(@credit_card, { billing_id: 'test1234', amount: 15000 }) rescue nil # Ensure it already exists assert response = @gateway.purchase(12300, 'test1234', @options) assert_success response assert_equal response.params['amount'], '12300' + assert_equal response.params['ponum'], 'order123' assert_equal 'Approved', response.message end def test_failure_triggered_payment - @gateway.unstore('test1234') rescue nil #Ensure its no longer there + @gateway.unstore('test1234') rescue nil # Ensure its no longer there assert response = @gateway.purchase(12300, 'test1234', @options) assert_failure response @@ -176,9 +182,9 @@ def test_failure_triggered_payment def test_invalid_login gateway = SecurePayAuGateway.new( - :login => 'a', - :password => 'a' - ) + login: 'a', + password: 'a' + ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal 'Invalid merchant ID', response.message diff --git a/test/remote/gateways/remote_secure_pay_tech_test.rb b/test/remote/gateways/remote_secure_pay_tech_test.rb index 737d2d92280..30e46ee0e07 100644 --- a/test/remote/gateways/remote_secure_pay_tech_test.rb +++ b/test/remote/gateways/remote_secure_pay_tech_test.rb @@ -5,14 +5,14 @@ class RemoteSecurePayTechTest < Test::Unit::TestCase def setup @gateway = SecurePayTechGateway.new(fixtures(:secure_pay_tech)) - + @accepted_amount = 10000 @declined_amount = 10075 - - @credit_card = credit_card('4987654321098769', :month => '5', :year => '2013') - @options = { :billing_address => address } + + @credit_card = credit_card('4987654321098769', month: '5', year: '2013') + @options = { billing_address: address } end - + def test_successful_purchase assert response = @gateway.purchase(@accepted_amount, @credit_card, @options) assert_equal 'Transaction OK', response.message @@ -43,9 +43,9 @@ def test_unsuccessful_cvv_check def test_invalid_login gateway = SecurePayTechGateway.new( - :login => 'foo', - :password => 'bar' - ) + login: 'foo', + password: 'bar' + ) assert response = gateway.purchase(@accepted_amount, @credit_card, @options) assert_equal 'Bad or malformed request', response.message assert_failure response diff --git a/test/remote/gateways/remote_secure_pay_test.rb b/test/remote/gateways/remote_secure_pay_test.rb index 5806fd0ab72..77d41451b3b 100644 --- a/test/remote/gateways/remote_secure_pay_test.rb +++ b/test/remote/gateways/remote_secure_pay_test.rb @@ -1,23 +1,20 @@ require 'test_helper' -class RemoteSecurePayTest < Test::Unit::TestCase - +class RemoteSecurePayTest < Test::Unit::TestCase def setup @gateway = SecurePayGateway.new(fixtures(:secure_pay)) - @credit_card = credit_card('4111111111111111', - :month => '7', - :year => '2014' - ) - - @options = { :order_id => generate_unique_id, - :description => 'Store purchase', - :billing_address => address + @credit_card = credit_card('4111111111111111', month: '7', year: '2014') + + @options = { + order_id: generate_unique_id, + description: 'Store purchase', + billing_address: address } - + @amount = 100 end - + def test_successful_purchase assert response = @gateway.purchase(@amount, @credit_card, @options) assert response.success? diff --git a/test/remote/gateways/remote_securion_pay_test.rb b/test/remote/gateways/remote_securion_pay_test.rb index d6b66c7ad0b..b3e1b86ef88 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) @@ -66,10 +74,22 @@ def test_successful_purchase assert_match CHARGE_ID_REGEX, response.authorization end + def test_successful_purchase_with_three_ds_data + @options[:three_d_secure] = { + version: '1.0.2', + eci: '05', + cavv: '3q2+78r+ur7erb7vyv66vv////8=', + acs_transaction_id: '6546464645623455665165+qe-jmhabcdefg', + authentication_response_status: 'Y' + } + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + end + 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 @@ -87,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 @@ -104,9 +127,9 @@ 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 + assert_equal @amount, refund.params['refunds'].map { |r| r['amount'] }.sum assert refund.authorization end @@ -118,20 +141,21 @@ 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 assert second_refund.params['refunded'] - assert_equal @amount - 2 * @refund_amount, second_refund.params['amount'] + assert_equal @amount - (2 * @refund_amount), second_refund.params['amount'] assert_equal 2, second_refund.params['refunds'].size - assert_equal 2 * @refund_amount, second_refund.params['refunds'].map{|r| r['amount']}.sum + assert_equal 2 * @refund_amount, second_refund.params['refunds'].map { |r| r['amount'] }.sum assert second_refund.authorization end 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 @@ -154,14 +178,14 @@ def test_successful_void assert void.params['refunded'] assert_equal 0, void.params['amount'] assert_equal 1, void.params['refunds'].size - assert_equal @amount, void.params['refunds'].map{|r| r['amount']}.sum + assert_equal @amount, void.params['refunds'].map { |r| r['amount'] }.sum assert void.authorization end 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 @@ -173,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 new file mode 100644 index 00000000000..fb7ffc15530 --- /dev/null +++ b/test/remote/gateways/remote_shift4_test.rb @@ -0,0 +1,278 @@ +require 'test_helper' + +class RemoteShift4Test < Test::Unit::TestCase + def setup + @gateway = Shift4Gateway.new(fixtures(:shift4)) + access_token = @gateway.setup_access_token + + @gateway = Shift4Gateway.new(fixtures(:shift4).merge(access_token:)) + + @amount = 500 + @credit_card = credit_card('4000100011112224', verification_value: '333', first_name: 'John', last_name: 'Smith') + @declined_card = credit_card('400030001111220', first_name: 'John', last_name: 'Doe') + @unsupported_card = credit_card('4000100011112224', verification_value: '333', first_name: 'John', last_name: '成龙') + @options = {} + @extra_options = { + clerk_id: '1576', + notes: 'test notes', + tax: '2', + customer_reference: 'D019D09309F2', + destination_postal_code: '94719', + product_descriptors: %w(Hamburger Fries Soda Cookie), + order_id: '123456' + } + @customer_address = { + address1: '65 Easy St', + zip: '65144' + } + end + + def test_successful_authorize + response = @gateway.authorize(@amount, @credit_card, @options) + + assert_success response + assert_equal response.message, 'Transaction successful' + end + + def test_successful_authorize_with_extra_options + response = @gateway.authorize(@amount, @credit_card, @options.merge(@extra_options)) + assert_success response + assert_equal response.message, 'Transaction successful' + end + + def test_successful_authorize_with_store + response = @gateway.store(@credit_card, @options) + assert_success response + assert_not_empty response.authorization + assert_not_include response.authorization, '|' + + response = @gateway.authorize(@amount, response.authorization, @options) + assert_success response + assert_include 'Transaction successful', response.message + end + + def test_successful_capture + authorize_res = @gateway.authorize(@amount, @credit_card, @options) + assert_success authorize_res + response = @gateway.capture(@amount, authorize_res.authorization, @options) + + assert_success response + assert_equal response.message, 'Transaction successful' + assert response_result(response)['transaction']['invoice'].present? + assert_equal response_result(response)['transaction']['invoice'], response_result(authorize_res)['transaction']['invoice'] + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_include 'Transaction successful', response.message + end + + def test_successful_purchase_with_customer_details + response = @gateway.purchase(@amount, @credit_card, @options.merge({ billing_address: @customer_address })) + assert_success response + assert_include 'Transaction successful', response.message + end + + def test_successful_purchase_with_extra_options + response = @gateway.purchase(@amount, @credit_card, @options.merge(@extra_options)) + assert_success response + end + + def test_successful_purchase_passes_vendor_reference + response = @gateway.purchase(@amount, @credit_card, @options.merge(@extra_options)) + assert_success response + assert_equal response_result(response)['transaction']['vendorReference'], @extra_options[:order_id] + end + + def test_successful_purchase_with_stored_credential_framework + stored_credential_options = { + initial_transaction: true, + reason_type: 'recurring' + } + @options[:merchant_time_zone] = 'EST' + first_response = @gateway.purchase(@amount, @credit_card, @options.merge(@extra_options.merge({ stored_credential: stored_credential_options }))) + assert_success first_response + + ntxid = first_response.params['result'].first['transaction']['cardOnFile']['transactionId'] + stored_credential_options = { + initial_transaction: false, + reason_type: 'recurring', + network_transaction_id: ntxid + } + response = @gateway.purchase(@amount, @credit_card, @options.merge(@extra_options.merge({ stored_credential: stored_credential_options }))) + assert_success response + end + + def test_successful_purchase_with_card_on_file_fields + card_on_file_fields = { + usage_indicator: '01', + indicator: '01', + scheduled_indicator: '01' + } + first_response = @gateway.purchase(@amount, @credit_card, @options.merge(card_on_file_fields)) + assert_success first_response + + ntxid = first_response.params['result'].first['transaction']['cardOnFile']['transactionId'] + card_on_file_fields = { + usage_indicator: '02', + indicator: '01', + scheduled_indicator: '02', + transaction_id: ntxid + } + response = @gateway.purchase(@amount, @credit_card, @options.merge(@extra_options.merge(card_on_file_fields))) + assert_success response + end + + def test_successful_purchase_with_store + response = @gateway.store(@credit_card, @options) + assert_success response + assert_not_empty response.authorization + + response = @gateway.purchase(@amount, response.authorization, @options) + assert_success response + assert_include 'Transaction successful', response.message + end + + def test_successful_purchase_with_store_having_customer_details + response = @gateway.store(@credit_card, @options.merge({ billing_address: @customer_address })) + assert_success response + assert_not_empty response.authorization + + response = @gateway.purchase(@amount, response.authorization, @options) + assert_success response + assert_include 'Transaction successful', response.message + end + + def test_successful_verify + response = @gateway.verify(@credit_card, @options) + assert_success response + end + + def test_successful_verify_with_card_on_file_fields + card_on_file_fields = { + usage_indicator: '01', + indicator: '01', + scheduled_indicator: '01' + } + first_response = @gateway.purchase(@amount, @credit_card, @options.merge(card_on_file_fields)) + assert_success first_response + ntxid = first_response.params['result'].first['transaction']['cardOnFile']['transactionId'] + + card_on_file_fields = { + usage_indicator: '02', + indicator: '01', + scheduled_indicator: '02', + transaction_id: ntxid + } + response = @gateway.verify(@credit_card, @options.merge(card_on_file_fields)) + assert_success response + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.authorize(@amount, @credit_card, @options) + end + transcript = @gateway.scrub(transcript) + assert_scrubbed(@credit_card.number, transcript) + assert_scrubbed("0#{@credit_card.month}#{@credit_card.year.to_s[2..4]}", transcript) + assert_scrubbed(@credit_card.verification_value, transcript) + end + + def test_failed_purchase + response = @gateway.purchase(1500000000, @credit_card, @options) + assert_failure response + assert_include response.message, 'Transaction declined' + end + + def test_failure_on_referral_transactions + response = @gateway.purchase(99999899, @credit_card, @options) + assert_failure response + assert_include 'Transaction declined', response.message + end + + def test_failed_authorize + response = @gateway.authorize(@amount, @declined_card, @options) + assert_failure response + assert_include response.message, 'Unable to determine card type. Please check the number and re-enter.' + 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 + # 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_capture + response = @gateway.capture(@amount, '', @options) + assert_failure response + assert_include response.message, 'Unable to determine card type. Please check the number and re-enter.' + end + + def test_failed_refund + 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, 'Transaction declined' + end + + def test_successful_refund + res = @gateway.purchase(@amount, @credit_card, @options) + assert_success res + response = @gateway.refund(@amount, res.authorization, @options) + assert_success response + end + + def test_successful_refund_with_expiration_date + res = @gateway.purchase(@amount, @credit_card, @options) + assert_success res + response = @gateway.refund(@amount, res.authorization, @options.merge({ expiration_date: '1235' })) + assert_success response + end + + def test_successful_void + authorize_res = @gateway.authorize(@amount, @credit_card, @options) + assert response = @gateway.void(authorize_res.authorization, @options) + + assert_success response + assert_equal @options[:invoice], response_result(response)['transaction']['invoice'] + end + + def test_failed_void + response = @gateway.void('', @options) + assert_failure response + assert_include response.message, 'Invoice Not Found' + end + + def test_failed_access_token + gateway = Shift4Gateway.new({ client_guid: 'YOUR_CLIENT_ID', auth_token: 'YOUR_AUTH_TOKEN' }) + assert_raises(ActiveMerchant::OAuthResponseError) do + gateway.setup_access_token + end + end + + private + + def response_result(response) + response.params['result'][0] + end +end diff --git a/test/remote/gateways/remote_shift4_v2_test.rb b/test/remote/gateways/remote_shift4_v2_test.rb new file mode 100644 index 00000000000..6b38f071235 --- /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:)) + 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:)) + 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:) + 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:) + 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 new file mode 100644 index 00000000000..90f66e2f844 --- /dev/null +++ b/test/remote/gateways/remote_simetrik_test.rb @@ -0,0 +1,276 @@ +require 'test_helper' + +class RemoteSimetrikTest < Test::Unit::TestCase + def setup + @gateway = SimetrikGateway.new(fixtures(:simetrik)) + @token_acquirer = 'bc4c0f26-a357-4294-9b9e-a90e6c868c6e' + @credit_card = CreditCard.new( + first_name: 'Joe', + last_name: 'Doe', + number: '4551708161768059', + month: 7, + year: 2022, + verification_value: '111' + ) + @credit_card_invalid = CreditCard.new( + first_name: 'Joe', + last_name: 'Doe', + number: '3551708161768059', + month: 3, + year: 2026, + verification_value: '111' + ) + @amount = 100 + @sub_merchant = { + address: 'None', + extra_params: { + }, + mcc: '5816', + merchant_id: '400000008', + name: '885.519.237', + phone_number: '3434343', + postal_code: 'None', + url: 'string' + } + @psp_info = { + id: '0123', + name: 'mci', + sub_merchant: { + id: 'string', + name: 'string' + } + + } + + @authorize_options_success = { + acquire_extra_options: {}, + trace_id: SecureRandom.uuid, + user: { + id: '123', + email: 's@example.com' + }, + order_id: rand(100000000000..999999999999).to_s, + description: 'apopsicle', + order: { + datetime_local_transaction: Time.new.strftime('%Y-%m-%dT%H:%M:%S.%L%:z'), + installments: 1 + }, + vat: 19, + currency: 'USD', + authentication: { + three_ds_fields: { + version: '2.1.0', + eci: '05', + cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA', + ds_transaction_id: '97267598-FAE6-48F2-8083-C23433990FBC', + acs_transaction_id: '13c701a3-5a88-4c45-89e9-ef65e50a8bf9', + cavv_algorithm: '1', + xid: '333333333', + directory_response_status: 'Y', + authentication_response_status: 'Y', + enrolled: 'test', + three_ds_server_trans_id: '24f701e3-9a85-4d45-89e9-af67e70d8fg8' + } + }, + sub_merchant: @sub_merchant, + psp_info: @psp_info, + token_acquirer: @token_acquirer + } + 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 + assert_instance_of Response, response + assert_equal response.message, 'successful authorize' + assert_equal response.error_code, nil, 'Should expected error code equal to nil' + assert_equal response.avs_result['code'], 'G' + assert_equal response.cvv_result['code'], 'P' + assert response.test? + end + + def test_failed_authorize + options = @authorize_options_success.clone() + options.delete(:user) + + response = @gateway.authorize(@amount, @credit_card, options) + assert_failure response + assert_instance_of Response, response + assert_equal response.error_code, 'config_error' + assert_equal response.avs_result['code'], 'I' + assert_equal response.cvv_result['code'], 'P' + assert response.test? + end + + def test_failed_authorize_by_invalid_card + response = @gateway.authorize(@amount, @credit_card_invalid, @authorize_options_success) + assert_failure response + assert_instance_of Response, response + assert_equal response.error_code, 'invalid_number' + assert_equal response.avs_result['code'], 'G' + assert_equal response.cvv_result['code'], 'P' + assert response.test? + end + + def test_success_purchase + response = @gateway.purchase(@amount, @credit_card, @authorize_options_success) + assert_success response + assert_instance_of Response, response + assert_equal response.message, 'successful charge' + assert_equal response.error_code, nil, 'Should expected error code equal to nil' + assert_equal response.avs_result['code'], 'G' + assert_equal response.cvv_result['code'], 'P' + assert response.test? + end + + def test_failed_purchase + options = @authorize_options_success.clone() + options.delete(:user) + + response = @gateway.purchase(@amount, @credit_card, options) + assert_failure response + assert_instance_of Response, response + assert_equal response.error_code, 'config_error' + assert_equal response.avs_result['code'], 'I' + assert_equal response.cvv_result['code'], 'P' + assert response.test? + end + + def test_failed_purchase_by_invalid_card + response = @gateway.purchase(@amount, @credit_card_invalid, @authorize_options_success) + assert_failure response + assert_instance_of Response, response + assert_equal response.error_code, 'invalid_number' + assert_equal response.avs_result['code'], 'G' + assert_equal response.cvv_result['code'], 'P' + assert response.test? + end + + def test_successful_authorize_and_capture + auth = @gateway.authorize(@amount, @credit_card, @authorize_options_success) + assert_success auth + sleep(3) + option = { + vat: @authorize_options_success[:vat], + currency: @authorize_options_success[:currency], + + transaction_id: auth.authorization, + token_acquirer: @token_acquirer, + trace_id: @authorize_options_success[:trace_id] + } + assert capture = @gateway.capture(@amount, auth.authorization, option) + assert_success capture + assert_equal 'successful capture', capture.message + end + + def test_failed_capture + auth = @gateway.authorize(@amount, @credit_card, @authorize_options_success) + assert_success auth + + option = { + vat: @authorize_options_success[:vat], + currency: @authorize_options_success[:currency], + transaction_id: auth.authorization, + token_acquirer: @token_acquirer, + trace_id: @authorize_options_success[:trace_id] + } + sleep(3) + capture = @gateway.capture(@amount, auth.authorization, option) + assert_success capture + option = { + vat: 19, + currency: 'USD', + transaction_id: auth.authorization, + token_acquirer: @token_acquirer, + trace_id: @authorize_options_success[:trace_id] + } + + assert capture = @gateway.capture(@amount, auth.authorization, option) + assert_failure capture + assert_equal 'CAPTURE_REJECTED', capture.message + end + + def test_successful_void + auth = @gateway.authorize(@amount, @credit_card, @authorize_options_success) + assert_success auth + + option = { + token_acquirer: @token_acquirer, + trace_id: @authorize_options_success[:trace_id], + acquire_extra_options: {} + } + sleep(6) + assert void = @gateway.void(auth.authorization, option) + assert_success void + assert_equal 'successful void', void.message + end + + def test_failed_void + # First successful void + auth = @gateway.authorize(@amount, @credit_card, @authorize_options_success) + assert_success auth + + option = { + token_acquirer: @token_acquirer, + trace_id: @authorize_options_success[:trace_id], + acquire_extra_options: {} + } + sleep(3) + assert void = @gateway.void(auth.authorization, option) + assert_success void + assert_equal 'successful void', void.message + + # Second failed void + option = { + token_acquirer: @token_acquirer, + trace_id: '2717a3e0-0db2-4971-b94f-686d3b72c44b' + } + void = @gateway.void(auth.authorization, option) + assert_failure void + assert_equal 'VOID_REJECTED', void.message + end + + def test_failed_refund + response = @gateway.purchase(@amount, @credit_card, @authorize_options_success) + option = { + token_acquirer: @token_acquirer, + trace_id: '2717a3e0-0db2-4971-b94f-686d3b72c44b', + currency: 'USD', + comment: 'This is a comment', + acquire_extra_options: { + ruc: '13431131234' + } + } + assert_success response + sleep(3) + refund = @gateway.refund(@amount, response.authorization, option) + assert_failure refund + assert_equal 'REFUND_REJECTED', refund.message + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @credit_card, @authorize_options_success) + end + transcript = @gateway.scrub(transcript) + assert_scrubbed(@credit_card.number, transcript) + assert_scrubbed(@credit_card.verification_value, transcript) + assert_scrubbed(@gateway.options[:client_secret], transcript) + end +end diff --git a/test/remote/gateways/remote_skipjack_test.rb b/test/remote/gateways/remote_skipjack_test.rb index 2c8d070dcbf..1c53076c8ff 100644 --- a/test/remote/gateways/remote_skipjack_test.rb +++ b/test/remote/gateways/remote_skipjack_test.rb @@ -6,16 +6,14 @@ 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 @options = { - :order_id => generate_unique_id, - :email => 'email@foo.com', - :billing_address => address + order_id: generate_unique_id, + email: 'email@foo.com', + billing_address: address } end @@ -75,7 +73,7 @@ def test_successful_authorization_and_credit authorization = @gateway.authorize(@amount, @credit_card, @options) assert_success authorization - capture = @gateway.capture(@amount, authorization.authorization, :force_settlement => true) + capture = @gateway.capture(@amount, authorization.authorization, force_settlement: true) assert_success capture # developer login won't change transaction immediately to settled, so status will have to mismatch @@ -107,9 +105,9 @@ def test_status_unkown_order def test_invalid_login gateway = SkipJackGateway.new( - :login => '555555555555', - :password => '999999999999' - ) + login: '555555555555', + password: '999999999999' + ) response = gateway.authorize(@amount, @credit_card, @options) assert_failure response diff --git a/test/remote/gateways/remote_so_easy_pay_test.rb b/test/remote/gateways/remote_so_easy_pay_test.rb index 8803620f610..be0ed41b96f 100644 --- a/test/remote/gateways/remote_so_easy_pay_test.rb +++ b/test/remote/gateways/remote_so_easy_pay_test.rb @@ -1,22 +1,20 @@ require 'test_helper' class RemoteSoEasyPayTest < Test::Unit::TestCase - - def setup @gateway = SoEasyPayGateway.new(fixtures(:so_easy_pay)) @amount = 100 - @credit_card = credit_card('4111111111111111', {:verification_value => '000', :month => '12', :year => '2015'}) + @credit_card = credit_card('4111111111111111', { verification_value: '000', month: '12', year: '2015' }) @declined_card = credit_card('4000300011112220') @options = { - :currency => 'EUR', - :ip => '192.168.19.123', - :email => 'test@blaha.com', - :order_id => generate_unique_id, - :billing_address => address, - :description => 'Store Purchase' + currency: 'EUR', + ip: '192.168.19.123', + email: 'test@blaha.com', + order_id: generate_unique_id, + billing_address: address, + description: 'Store Purchase' } end @@ -55,12 +53,11 @@ def test_successful_void def test_invalid_login gateway = SoEasyPayGateway.new( - :login => 'one', - :password => 'wrong' - ) + login: 'one', + password: 'wrong' + ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal 'Website verification failed, wrong websiteID or password', response.message end end - diff --git a/test/remote/gateways/remote_spreedly_core_test.rb b/test/remote/gateways/remote_spreedly_core_test.rb index c9ea36a7d2c..5393149a3f7 100644 --- a/test/remote/gateways/remote_spreedly_core_test.rb +++ b/test/remote/gateways/remote_spreedly_core_test.rb @@ -1,13 +1,13 @@ require 'test_helper' class RemoteSpreedlyCoreTest < Test::Unit::TestCase - def setup @gateway = SpreedlyCoreGateway.new(fixtures(:spreedly_core)) @amount = 100 @credit_card = credit_card('5555555555554444') @declined_card = credit_card('4012888888881881') + @check = check({ routing_number: '021000021', account_number: '9876543210' }) @existing_payment_method = '3rEkRlZur2hXKbwwRBidHJAIUTO' @declined_payment_method = 'UPfh3J3JbekLeYC88BP741JWnS5' @existing_transaction = 'PJ5ICgM6h7v9pBNxDCJjRHDDxBC' @@ -60,10 +60,18 @@ def test_successful_purchase_with_credit_card assert_equal 'cached', response.params['payment_method_storage_state'] end + def test_successful_purchase_with_check + assert response = @gateway.purchase(@amount, @check) + assert_success response + assert_equal 'Succeeded!', response.message + assert_equal 'Purchase', response.params['transaction_type'] + assert_equal 'cached', response.params['payment_method_storage_state'] + end + def test_successful_purchase_with_card_and_address options = { - :email => 'joebob@example.com', - :billing_address => address, + email: 'joebob@example.com', + billing_address: address } assert response = @gateway.purchase(@amount, @credit_card, options) @@ -88,7 +96,8 @@ def test_failed_purchase_with_invalid_credit_card @credit_card.first_name = ' ' assert response = @gateway.purchase(@amount, @credit_card) assert_failure response - assert_equal "First name can't be blank", response.message + assert_equal 'The payment method is invalid.', response.message + assert_equal "First name can't be blank", response.params['payment_method_errors'].strip end def test_successful_purchase_with_store @@ -96,7 +105,7 @@ def test_successful_purchase_with_store assert_success response assert_equal 'Succeeded!', response.message assert_equal 'Purchase', response.params['transaction_type'] - assert_equal 'retained', response.params['payment_method_storage_state'] + assert %w(retained cached).include?(response.params['payment_method_storage_state']) assert !response.params['payment_method_token'].blank? end @@ -114,8 +123,8 @@ def test_successful_authorize_and_capture_with_credit_card def test_successful_authorize_with_card_and_address options = { - :email => 'joebob@example.com', - :billing_address => address, + email: 'joebob@example.com', + billing_address: address } assert response = @gateway.authorize(@amount, @credit_card, options) @@ -141,7 +150,8 @@ def test_failed_authrorize_with_invalid_credit_card @credit_card.first_name = ' ' assert response = @gateway.authorize(@amount, @credit_card) assert_failure response - assert_equal "First name can't be blank", response.message + assert_equal 'The payment method is invalid.', response.message + assert_equal "First name can't be blank", response.params['payment_method_errors'].strip end def test_successful_authorize_with_store @@ -149,7 +159,7 @@ def test_successful_authorize_with_store assert_success response assert_equal 'Succeeded!', response.message assert_equal 'Authorization', response.params['transaction_type'] - assert_equal 'retained', response.params['payment_method_storage_state'] + assert %w(retained cached).include?(response.params['payment_method_storage_state']) assert !response.params['payment_method_token'].blank? end @@ -163,28 +173,28 @@ def test_successful_store end def test_successful_store_simple_data - assert response = @gateway.store(@credit_card, { :data => 'SomeData' }) + assert response = @gateway.store(@credit_card, { data: 'SomeData' }) assert_success response assert_equal 'SomeData', response.params['payment_method_data'] end def test_successful_store_nested_data options = { - :data => { - :first_attribute => { :sub_dude => 'ExcellentSubValue' }, - :second_attribute => 'AnotherValue' + data: { + first_attribute: { sub_dude: 'ExcellentSubValue' }, + second_attribute: 'AnotherValue' } } assert response = @gateway.store(@credit_card, options) assert_success response - expected_data = { 'first_attribute' => { 'sub_dude'=>'ExcellentSubValue' }, 'second_attribute' =>'AnotherValue' } + expected_data = { 'first_attribute' => { 'sub_dude' => 'ExcellentSubValue' }, 'second_attribute' => 'AnotherValue' } assert_equal expected_data, response.params['payment_method_data'] end def test_successful_store_with_address options = { - :email => 'joebob@example.com', - :billing_address => address, + email: 'joebob@example.com', + billing_address: address } assert response = @gateway.store(@credit_card, options) @@ -198,7 +208,7 @@ def test_successful_store_with_address end def test_failed_store - assert response = @gateway.store(credit_card('5555555555554444', :last_name => ' ')) + assert response = @gateway.store(credit_card('5555555555554444', last_name: ' ')) assert_failure response assert_equal "Last name can't be blank", response.message end @@ -281,7 +291,7 @@ def test_failed_find_transaction end def test_invalid_login - gateway = SpreedlyCoreGateway.new(:login => 'Bogus', :password => 'MoreBogus', :gateway_token => 'EvenMoreBogus') + gateway = SpreedlyCoreGateway.new(login: 'Bogus', password: 'MoreBogus', gateway_token: 'EvenMoreBogus') assert response = gateway.purchase(@amount, @existing_payment_method) assert_failure response diff --git a/test/remote/gateways/remote_stripe_3ds_test.rb b/test/remote/gateways/remote_stripe_3ds_test.rb new file mode 100644 index 00000000000..58a2f84d3b4 --- /dev/null +++ b/test/remote/gateways/remote_stripe_3ds_test.rb @@ -0,0 +1,193 @@ +require 'test_helper' +require 'mechanize' + +class RemoteStripe3DSTest < Test::Unit::TestCase + CHARGE_ID_REGEX = /ch_[a-zA-Z\d]{24}/ + + def setup + @gateway = StripeGateway.new(fixtures(:stripe)) + @amount = 100 + @billing_details = address() + + @options = { + currency: 'USD', + description: 'ActiveMerchant Test Purchase', + email: 'wow@example.com', + execute_threed: true, + redirect_url: 'http://www.example.com/redirect', + callback_url: 'http://www.example.com/callback', + billing_address: @billing_details + } + @credit_card = credit_card('4000000000003063') + @non_3ds_card = credit_card('378282246310005') + + @stripe_account = fixtures(:stripe_destination)[:stripe_user_id] + end + + def test_create_3ds_card_source + assert response = @gateway.send(:create_source, @amount, @credit_card, 'card', @options) + assert_card_source(response) + end + + def test_create_non3ds_card_source + assert response = @gateway.send(:create_source, @amount, @non_3ds_card, 'card', @options) + assert_card_source(response, 'not_supported') + end + + def test_create_3ds_source + card_source = @gateway.send(:create_source, @amount, @credit_card, 'card', @options) + assert response = @gateway.send(:create_source, @amount, card_source.params['id'], 'three_d_secure', @options) + assert_success response + assert_three_ds_source(response) + end + + def test_show_3ds_source + card_source = @gateway.send(:create_source, @amount, @credit_card, 'card', @options) + assert three_d_secure_source = @gateway.send(:create_source, @amount, card_source.params['id'], 'three_d_secure', @options) + assert_success three_d_secure_source + assert_three_ds_source(three_d_secure_source) + + assert response = @gateway.send(:show_source, three_d_secure_source.params['id'], @options) + assert_three_ds_source(response) + end + + def test_create_webhook_endpoint + response = @gateway.send(:create_webhook_endpoint, @options, ['source.chargeable']) + assert_includes response.params['enabled_events'], 'source.chargeable' + assert_equal @options[:callback_url], response.params['url'] + assert_equal 'enabled', response.params['status'] + assert_nil response.params['application'] + + deleted_response = @gateway.send(:delete_webhook_endpoint, @options.merge(webhook_id: response.params['id'])) + assert_equal true, deleted_response.params['deleted'] + end + + def test_create_webhook_endpoint_on_connected_account + response = @gateway.send(:create_webhook_endpoint, @options.merge({ stripe_account: @stripe_account }), ['source.chargeable']) + assert_includes response.params['enabled_events'], 'source.chargeable' + assert_equal @options[:callback_url], response.params['url'] + assert_equal 'enabled', response.params['status'] + assert_not_nil response.params['application'] + + deleted_response = @gateway.send(:delete_webhook_endpoint, @options.merge(webhook_id: response.params['id'])) + assert_equal true, deleted_response.params['deleted'] + end + + def test_delete_webhook_endpoint + webhook = @gateway.send(:create_webhook_endpoint, @options, ['source.chargeable']) + response = @gateway.send(:delete_webhook_endpoint, @options.merge(webhook_id: webhook.params['id'])) + assert_equal response.params['id'], webhook.params['id'] + assert_equal true, response.params['deleted'] + end + + def test_delete_webhook_endpoint_on_connected_account + webhook = @gateway.send(:create_webhook_endpoint, @options.merge({ stripe_account: @stripe_account }), ['source.chargeable']) + response = @gateway.send(:delete_webhook_endpoint, @options.merge(webhook_id: webhook.params['id'])) + assert_equal response.params['id'], webhook.params['id'] + assert_equal true, response.params['deleted'] + end + + def test_show_webhook_endpoint + webhook = @gateway.send(:create_webhook_endpoint, @options, ['source.chargeable']) + response = @gateway.send(:show_webhook_endpoint, @options.merge(webhook_id: webhook.params['id'])) + assert_includes response.params['enabled_events'], 'source.chargeable' + assert_equal @options[:callback_url], response.params['url'] + assert_equal 'enabled', response.params['status'] + assert_nil response.params['application'] + + deleted_response = @gateway.send(:delete_webhook_endpoint, @options.merge(webhook_id: response.params['id'])) + assert_equal true, deleted_response.params['deleted'] + end + + def test_show_webhook_endpoint_on_connected_account + webhook = @gateway.send(:create_webhook_endpoint, @options.merge({ stripe_account: @stripe_account }), ['source.chargeable']) + response = @gateway.send(:show_webhook_endpoint, @options.merge({ webhook_id: webhook.params['id'], stripe_account: @stripe_account })) + + assert_includes response.params['enabled_events'], 'source.chargeable' + assert_equal @options[:callback_url], response.params['url'] + assert_equal 'enabled', response.params['status'] + assert_not_nil response.params['application'] + + deleted_response = @gateway.send(:delete_webhook_endpoint, @options.merge(webhook_id: response.params['id'])) + assert_equal true, deleted_response.params['deleted'] + end + + def test_list_webhook_endpoints + webhook1 = @gateway.send(:create_webhook_endpoint, @options, ['source.chargeable']) + webhook2 = @gateway.send(:create_webhook_endpoint, @options.merge({ stripe_account: @stripe_account }), ['source.chargeable']) + assert_nil webhook1.params['application'] + assert_not_nil webhook2.params['application'] + + response = @gateway.send(:list_webhook_endpoints, @options.merge({ limit: 100 })) + assert_not_nil response.params + assert_equal 'list', response.params['object'] + assert response.params['data'].size >= 2 + webhook_id_set = Set.new(response.params['data'].map { |webhook| webhook['id'] }.uniq) + assert Set[webhook1.params['id'], webhook2.params['id']].subset?(webhook_id_set) + + deleted_response1 = @gateway.send(:delete_webhook_endpoint, @options.merge(webhook_id: webhook1.params['id'])) + deleted_response2 = @gateway.send(:delete_webhook_endpoint, @options.merge(webhook_id: webhook2.params['id'])) + assert_equal true, deleted_response1.params['deleted'] + assert_equal true, deleted_response2.params['deleted'] + end + + def test_3ds_purchase + card_source_response = @gateway.send(:create_source, @amount, @credit_card, 'card', @options) + assert_card_source(card_source_response) + + assert three_ds_source_response = @gateway.send(:create_source, @amount, card_source_response.params['id'], 'three_d_secure', @options) + assert_success three_ds_source_response + assert_three_ds_source(three_ds_source_response) + + # Simulate 3DS 1.0 authentication in the test environment + authentication_url = three_ds_source_response.params['redirect']['url'] + agent = Mechanize.new + page = agent.get(authentication_url) + + form = page.forms.first + form.submit.tap do |result_page| + assert_equal '200', result_page.code + end + + # Test charging of the 3DS source + threeds_params = {} + threeds_params[:source] = three_ds_source_response.params['id'] + threeds_params[:capture] = 'true' + + @gateway.send(:add_charge_details, threeds_params, @amount, @credit_card, @options) + + assert response = @gateway.send(:commit, :post, 'charges', threeds_params, @options) + assert_equal 'charge', response.params['object'] + assert_equal 'succeeded', response.params['status'] + assert_equal true, response.params['captured'] + assert_equal 'three_d_secure', response.params.dig('source', 'type') + assert_equal true, response.params.dig('payment_method_details', 'card', 'three_d_secure', 'authenticated') + + # Check that billing details have been propagated from the card source to the charge + billing_details = response.params['billing_details'] + assert_equal @options[:email], billing_details['email'] + assert_equal @credit_card.name, billing_details['name'] + assert_equal @billing_details[:phone], billing_details['phone'] + assert_equal @billing_details[:address1], billing_details['address']['line1'] + assert_equal @billing_details[:address2], billing_details['address']['line2'] + assert_equal @billing_details[:city], billing_details['address']['city'] + assert_equal @billing_details[:state], billing_details['address']['state'] + assert_equal @billing_details[:zip], billing_details['address']['postal_code'] + assert_equal @billing_details[:country], billing_details['address']['country'] + end + + def assert_card_source(response, three_d_secure_status = 'required') + assert_success response + assert_equal 'source', response.params['object'] + assert_equal 'chargeable', response.params['status'] + assert_equal three_d_secure_status, response.params['card']['three_d_secure'] + assert_equal 'card', response.params['type'] + end + + def assert_three_ds_source(response) + assert_equal 'source', response.params['object'] + assert_equal 'pending', response.params['status'] + assert_equal 'three_d_secure', response.params['type'] + assert_equal false, response.params['three_d_secure']['authenticated'] + end +end diff --git a/test/remote/gateways/remote_stripe_android_pay_test.rb b/test/remote/gateways/remote_stripe_android_pay_test.rb deleted file mode 100644 index 10631d08d2c..00000000000 --- a/test/remote/gateways/remote_stripe_android_pay_test.rb +++ /dev/null @@ -1,48 +0,0 @@ -require 'test_helper' - -class RemoteStripeAndroidPayTest < Test::Unit::TestCase - CHARGE_ID_REGEX = /ch_[a-zA-Z\d]{24}/ - - def setup - @gateway = StripeGateway.new(fixtures(:stripe)) - @amount = 100 - - @options = { - :currency => 'USD', - :description => 'ActiveMerchant Test Purchase', - :email => 'wow@example.com' - } - end - - def test_successful_purchase_with_android_pay_raw_cryptogram - credit_card = network_tokenization_credit_card('4242424242424242', - payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', - verification_value: nil, - eci: '05', - source: :android_pay - ) - assert response = @gateway.purchase(@amount, credit_card, @options) - assert_success response - assert_equal 'charge', response.params['object'] - assert response.params['paid'] - assert_equal 'ActiveMerchant Test Purchase', response.params['description'] - assert_equal 'wow@example.com', response.params['metadata']['email'] - assert_match CHARGE_ID_REGEX, response.authorization - end - - def test_successful_auth_with_android_pay_raw_cryptogram - credit_card = network_tokenization_credit_card('4242424242424242', - payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', - verification_value: nil, - eci: '05', - source: :android_pay - ) - assert response = @gateway.authorize(@amount, credit_card, @options) - assert_success response - assert_equal 'charge', response.params['object'] - assert response.params['paid'] - assert_equal 'ActiveMerchant Test Purchase', response.params['description'] - assert_equal 'wow@example.com', response.params['metadata']['email'] - assert_match CHARGE_ID_REGEX, response.authorization - end -end \ No newline at end of file diff --git a/test/remote/gateways/remote_stripe_apple_pay_test.rb b/test/remote/gateways/remote_stripe_apple_pay_test.rb index d7d966ddb45..1387880aff2 100644 --- a/test/remote/gateways/remote_stripe_apple_pay_test.rb +++ b/test/remote/gateways/remote_stripe_apple_pay_test.rb @@ -8,100 +8,15 @@ def setup @amount = 100 @options = { - :currency => 'USD', - :description => 'ActiveMerchant Test Purchase', - :email => 'wow@example.com' + currency: 'USD', + description: 'ActiveMerchant Test Purchase', + email: 'wow@example.com' } - @apple_pay_payment_token = apple_pay_payment_token - end - - def test_successful_purchase_with_apple_pay_payment_token - assert response = @gateway.purchase(@amount, @apple_pay_payment_token, @options) - assert_success response - assert_equal 'charge', response.params['object'] - assert response.params['paid'] - assert_equal 'ActiveMerchant Test Purchase', response.params['description'] - assert_equal 'wow@example.com', response.params['metadata']['email'] - assert_match CHARGE_ID_REGEX, response.authorization - end - - def test_authorization_and_capture_with_apple_pay_payment_token - assert authorization = @gateway.authorize(@amount, @apple_pay_payment_token, @options) - assert_success authorization - refute authorization.params['captured'] - assert_equal 'ActiveMerchant Test Purchase', authorization.params['description'] - assert_equal 'wow@example.com', authorization.params['metadata']['email'] - - assert capture = @gateway.capture(@amount, authorization.authorization) - assert_success capture - end - - def test_authorization_and_void_with_apple_pay_payment_token - assert authorization = @gateway.authorize(@amount, @apple_pay_payment_token, @options) - assert_success authorization - refute authorization.params['captured'] - - assert void = @gateway.void(authorization.authorization) - assert_success void - end - - def test_successful_void_with_apple_pay_payment_token - assert response = @gateway.purchase(@amount, @apple_pay_payment_token, @options) - assert_success response - assert response.authorization - assert void = @gateway.void(response.authorization) - assert_success void - end - - def test_successful_store_with_apple_pay_payment_token - assert response = @gateway.store(@apple_pay_payment_token, {:description => 'Active Merchant Test Customer', :email => 'email@example.com'}) - assert_success response - assert_equal 'customer', response.params['object'] - assert_equal 'Active Merchant Test Customer', response.params['description'] - assert_equal 'email@example.com', response.params['email'] - first_card = response.params['cards']['data'].first - assert_equal response.params['default_card'], first_card['id'] - assert_equal '4242', first_card['dynamic_last4'] # when stripe is in test mode, token exchanged will return a test card with dynamic_last4 4242 - assert_equal '0000', first_card['last4'] # last4 is 0000 when using an apple pay token - end - - def test_successful_store_with_existing_customer_and_apple_pay_payment_token - assert response = @gateway.store(@credit_card, {:description => 'Active Merchant Test Customer'}) - assert_success response - - assert response = @gateway.store(@apple_pay_payment_token, {:customer => response.params['id'], :description => 'Active Merchant Test Customer', :email => 'email@example.com'}) - assert_success response - assert_equal 2, response.responses.size - - card_response = response.responses[0] - assert_equal 'card', card_response.params['object'] - assert_equal '4242', card_response.params['dynamic_last4'] # when stripe is in test mode, token exchanged will return a test card with dynamic_last4 4242 - assert_equal '0000', card_response.params['last4'] # last4 is 0000 when using an apple pay token - - customer_response = response.responses[1] - assert_equal 'customer', customer_response.params['object'] - assert_equal 'Active Merchant Test Customer', customer_response.params['description'] - assert_equal 'email@example.com', customer_response.params['email'] - assert_equal 2, customer_response.params['cards']['count'] - end - - def test_successful_recurring_with_apple_pay_payment_token - assert response = @gateway.store(@apple_pay_payment_token, {:description => 'Active Merchant Test Customer', :email => 'email@example.com'}) - assert_success response - assert recharge_options = @options.merge(:customer => response.params['id']) - assert response = @gateway.purchase(@amount, nil, recharge_options) - assert_success response - assert_equal 'charge', response.params['object'] - assert response.params['paid'] - end - - def test_purchase_with_unsuccessful_apple_pay_token_exchange - assert response = @gateway.purchase(@amount, ApplePayPaymentToken.new('garbage'), @options) - assert_failure response 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', @@ -117,7 +32,8 @@ 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 @@ -132,7 +48,8 @@ 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', @@ -148,7 +65,8 @@ 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 @@ -161,6 +79,4 @@ def test_successful_auth_with_apple_pay_raw_cryptogram_without_eci assert_equal 'wow@example.com', response.params['metadata']['email'] assert_match CHARGE_ID_REGEX, response.authorization end - end - diff --git a/test/remote/gateways/remote_stripe_connect_test.rb b/test/remote/gateways/remote_stripe_connect_test.rb index a8cd4664f36..f206eece901 100644 --- a/test/remote/gateways/remote_stripe_connect_test.rb +++ b/test/remote/gateways/remote_stripe_connect_test.rb @@ -10,21 +10,21 @@ def setup @new_credit_card = credit_card('5105105105105100') @options = { - :currency => 'USD', - :description => 'ActiveMerchant Test Purchase', - :email => 'wow@example.com', - :stripe_account => fixtures(:stripe_destination)[:stripe_user_id] + currency: 'USD', + description: 'ActiveMerchant Test Purchase', + email: 'wow@example.com', + stripe_account: fixtures(:stripe_destination)[:stripe_user_id] } end def test_application_fee_for_stripe_connect - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(:application_fee => 12 )) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(application_fee: 12)) assert_success response end def test_successful_refund_with_application_fee - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(:application_fee => 12)) - assert refund = @gateway.refund(@amount, response.authorization, @options.merge(:refund_application_fee => true)) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(application_fee: 12)) + assert refund = @gateway.refund(@amount, response.authorization, @options.merge(refund_application_fee: true)) assert_success refund # Verify the application fee is refunded @@ -35,8 +35,8 @@ def test_successful_refund_with_application_fee end def test_refund_partial_application_fee - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(:application_fee => 12)) - assert refund = @gateway.refund(@amount-20, response.authorization, @options.merge(:refund_fee_amount => '10')) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(application_fee: 12)) + assert refund = @gateway.refund(@amount - 20, response.authorization, @options.merge(refund_fee_amount: '10')) assert_success refund # Verify the application fee is partially refunded @@ -47,8 +47,8 @@ def test_refund_partial_application_fee end def test_refund_application_fee_amount_zero - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(:application_fee => 12)) - assert refund = @gateway.refund(@amount-20, response.authorization, @options.merge(:refund_fee_amount => '0')) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(application_fee: 12)) + assert refund = @gateway.refund(@amount - 20, response.authorization, @options.merge(refund_fee_amount: '0')) assert_success refund # Verify the application fee is not refunded diff --git a/test/remote/gateways/remote_stripe_emv_test.rb b/test/remote/gateways/remote_stripe_emv_test.rb index 929d05d2cc2..2e9baee7d8e 100644 --- a/test/remote/gateways/remote_stripe_emv_test.rb +++ b/test/remote/gateways/remote_stripe_emv_test.rb @@ -14,15 +14,15 @@ def setup } @options = { - :currency => 'USD', - :description => 'ActiveMerchant Test Purchase', - :email => 'wow@example.com' + currency: 'USD', + description: 'ActiveMerchant Test Purchase', + email: 'wow@example.com' } # This capture hex says that the payload is a transaction cryptogram (TC) but does not # provide the actual cryptogram. This will only work in test mode and would cause real # cards to be declined. - @capture_options = { icc_data: '9F270140' } + @capture_options = { icc_data: '9F270140' } end # for EMV contact transactions, it's advised to do a separate auth + capture @@ -140,7 +140,7 @@ def test_purchase_and_void_with_emv_contactless_credit_card end def test_authorization_emv_credit_card_in_us_with_metadata - assert authorization = @gateway.authorize(@amount, @emv_credit_cards[:us], @options.merge({:metadata => {:this_is_a_random_key_name => 'with a random value', :i_made_up_this_key_too => 'canyoutell'}, :order_id => '42', :email => 'foo@wonderfullyfakedomain.com'})) + assert authorization = @gateway.authorize(@amount, @emv_credit_cards[:us], @options.merge({ metadata: { this_is_a_random_key_name: 'with a random value', i_made_up_this_key_too: 'canyoutell' }, order_id: '42', email: 'foo@wonderfullyfakedomain.com' })) assert_success authorization end end diff --git a/test/remote/gateways/remote_stripe_payment_intents_test.rb b/test/remote/gateways/remote_stripe_payment_intents_test.rb new file mode 100644 index 00000000000..fdc9f3ccc8e --- /dev/null +++ b/test/remote/gateways/remote_stripe_payment_intents_test.rb @@ -0,0 +1,1717 @@ +require 'test_helper' + +class RemoteStripeIntentsTest < Test::Unit::TestCase + def setup + @gateway = StripePaymentIntentsGateway.new(fixtures(:stripe)) + @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', + verification_value: '737', + month: 10, + 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_not_required_card = credit_card( + '4000000000003055', + verification_value: '737', + month: 10, + year: 2028 + ) + + @three_ds_external_data_card = credit_card( + '4000002760003184', + verification_value: '737', + month: 10, + year: 2031 + ) + + @visa_card = credit_card( + '4242424242424242', + verification_value: '737', + month: 10, + year: 2028 + ) + + @visa_card_brand_choice = credit_card( + '4000002500001001', + verification_value: '737', + month: 10, + year: 2028 + ) + + @google_pay = network_tokenization_credit_card( + '4242424242424242', + payment_cryptogram: 'AgAAAAAABk4DWZ4C28yUQAAAAAA=', + source: :google_pay, + brand: 'visa', + eci: '05', + month: '09', + year: '2030', + first_name: 'Longbob', + last_name: 'Longsen' + ) + + @apple_pay = network_tokenization_credit_card( + '4242424242424242', + payment_cryptogram: 'AMwBRjPWDnAgAA7Rls7mAoABFA==', + source: :apple_pay, + brand: 'visa', + eci: '05', + month: '09', + year: '2030', + first_name: 'Longbob', + 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 + + def test_authorization_and_void + options = { + currency: 'GBP', + customer: @customer + } + assert authorization = @gateway.authorize(@amount, @visa_payment_method, options) + + assert_equal 'requires_capture', authorization.params['status'] + refute authorization.params.dig('charges', 'data')[0]['captured'] + + assert void = @gateway.void(authorization.authorization) + assert_success void + end + + def test_successful_purchase + options = { + currency: 'GBP', + 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_google_pay_fpan + options = { + currency: 'GBP', + customer: @customer + } + assert purchase = @gateway.purchase(@amount, @visa_payment_method, options.merge(wallet_type: :non_tokenized_google_pay)) + assert_equal 'succeeded', purchase.params['status'] + 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 + options = { + currency: 'GBP', + customer: @customer, + shipping_address: { + name: 'John Adam', + phone_number: '+0018313818368', + city: 'San Diego', + country: 'USA', + address1: 'block C', + address2: 'street 48', + zip: '22400', + state: 'California', + email: 'test@email.com' + } + } + + assert response = @gateway.purchase(@amount, @visa_payment_method, options) + assert_success response + assert_equal 'succeeded', response.params['status'] + assert_nil response.params['shipping']['email'] + end + + def test_successful_purchase_with_level3_data + options = { + currency: 'USD', + customer: @customer, + merchant_reference: 123, + customer_reference: 456, + shipping_address_zip: 71601, + shipping_from_zip: 71601, + shipping_amount: 10, + line_items: [ + { + 'product_code' => 1234, + 'product_description' => 'An item', + 'unit_cost' => 15, + 'quantity' => 2, + 'tax_amount' => 0 + }, + { + 'product_code' => 999, + 'product_description' => 'A totes different item', + 'tax_amount' => 10, + 'unit_cost' => 50, + 'quantity' => 1 + } + ] + } + + assert response = @gateway.purchase(100, @visa_card, options) + assert_success response + assert_equal 'succeeded', response.params['status'] + assert response.params.dig('charges', 'data')[0]['captured'] + end + + def test_unsuccessful_purchase_google_pay_with_invalid_card_number + options = { + currency: 'GBP', + new_ap_gp_route: true + } + + @google_pay.number = '378282246310000' + purchase = @gateway.purchase(@amount, @google_pay, options) + 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', + new_ap_gp_route: true + } + @google_pay.payment_cryptogram = '' + purchase = @gateway.purchase(@amount, @google_pay, options) + 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', + new_ap_gp_route: true + } + @google_pay.month = '' + purchase = @gateway.purchase(@amount, @google_pay, options) + 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', + new_ap_gp_route: true + } + @google_pay.eci = '5' + assert_match('5', @google_pay.eci) + + auth = @gateway.authorize(@amount, @google_pay, options) + assert auth.success? + assert_match('google_pay', auth.params.dig('charges', 'data')[0].dig('payment_method_details', 'card', 'wallet', 'type')) + end + + def test_successful_purchase_with_google_pay + options = { + currency: 'GBP', + new_ap_gp_route: true + } + + purchase = @gateway.purchase(@amount, @google_pay, options) + assert purchase.success? + assert_match('google_pay', purchase.params.dig('charges', 'data')[0]['payment_method_details']['card']['wallet']['type']) + end + + def test_successful_purchase_with_tokenized_visa + options = { + currency: 'USD', + last_4: '4242' + } + + purchase = @gateway.purchase(@amount, @network_token_credit_card, options) + assert_equal(nil, purchase.responses.first.params.dig('token', 'card', 'tokenization_method')) + assert purchase.success? + assert_not_nil(purchase.params.dig('charges', 'data')[0]['payment_method_details']['card']['network_token']) + end + + def test_successful_purchase_with_network_token_cc + options = { + currency: 'USD' + } + + purchase = @gateway.purchase(@amount, @network_token_credit_card, options) + assert_equal(nil, purchase.responses.first.params.dig('token', 'card', 'tokenization_method')) + assert purchase.success? + assert_not_nil(purchase.params.dig('charges', 'data')[0]['payment_method_details']['card']['network_token']) + end + + def test_successful_purchase_with_google_pay_when_sending_the_billing_address + options = { + currency: 'GBP', + billing_address: address, + new_ap_gp_route: true + } + + 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_successful_purchase_with_apple_pay_and_cit + options = { + currency: 'GBP', + new_ap_gp_route: true, + stored_credential_transaction_type: true, + stored_credential: { + initiator: 'cardholder', + reason_type: 'unscheduled', + initial_transaction: 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_succeeds_apple_pay_ntid_and_passes_it_to_mit + options = { + currency: 'GBP', + new_ap_gp_route: true, + stored_credential_transaction_type: true, + stored_credential: { + initiator: 'cardholder', + reason_type: 'unscheduled', + initial_transaction: true + } + } + + cit_purchase = @gateway.purchase(@amount, @apple_pay, options) + assert cit_purchase.success? + + assert purchase = @gateway.purchase(@amount, @apple_pay, { + 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: cit_purchase.params.dig('charges', 'data', 0, 'payment_method_details', 'card', 'network_transaction_id'), + off_session: '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_purchases_with_same_idempotency_key + options = { + currency: 'GBP', + customer: @customer, + idempotency_key: SecureRandom.hex + } + assert purchase1 = @gateway.purchase(@amount, @visa_payment_method, options) + assert_equal 'succeeded', purchase1.params['status'] + assert purchase1.params.dig('charges', 'data')[0]['captured'] + + assert purchase2 = @gateway.purchase(@amount, @visa_payment_method, options) + assert purchase2.success? + assert_equal purchase1.authorization, purchase2.authorization + assert_equal purchase1.params['charges']['data'][0]['id'], purchase2.params['charges']['data'][0]['id'] + end + + def test_credit_card_purchases_with_same_idempotency_key + options = { + currency: 'GBP', + customer: @customer, + idempotency_key: SecureRandom.hex + } + assert purchase1 = @gateway.purchase(@amount, @visa_card, options) + assert_equal 'succeeded', purchase1.params['status'] + assert purchase1.params.dig('charges', 'data')[0]['captured'] + + assert purchase2 = @gateway.purchase(@amount, @visa_card, options) + assert purchase2.success? + assert_equal purchase1.authorization, purchase2.authorization + assert_equal purchase1.params['charges']['data'][0]['id'], purchase2.params['charges']['data'][0]['id'] + end + + def test_purchases_with_same_idempotency_key_different_options + options = { + currency: 'GBP', + customer: @customer, + idempotency_key: SecureRandom.hex + } + assert purchase = @gateway.purchase(@amount, @visa_payment_method, options) + assert_equal 'succeeded', purchase.params['status'] + assert purchase.params.dig('charges', 'data')[0]['captured'] + + options[:currency] = 'USD' + assert purchase = @gateway.purchase(@amount, @visa_payment_method, options) + refute purchase.success? + assert_match(/^Keys for idempotent requests can only be used with the same parameters they were first used with/, purchase.message) + end + + def test_credit_card_purchases_with_same_idempotency_key_different_options + options = { + currency: 'GBP', + customer: @customer, + idempotency_key: SecureRandom.hex + } + assert purchase = @gateway.purchase(@amount, @visa_card, options) + assert_equal 'succeeded', purchase.params['status'] + assert purchase.params.dig('charges', 'data')[0]['captured'] + + options[:currency] = 'USD' + assert purchase = @gateway.purchase(@amount, @visa_card, options) + refute purchase.success? + assert_match(/^Keys for idempotent requests can only be used with the same parameters they were first used with/, purchase.message) + end + + def test_unsuccessful_purchase + options = { + currency: 'GBP', + customer: @customer + } + assert purchase = @gateway.purchase(@amount, @declined_payment_method, options) + + assert_equal 'Your card was declined.', purchase.message + refute purchase.params.dig('error', 'payment_intent', 'charges', 'data')[0]['captured'] + end + + def test_unsuccessful_purchase_returns_header_response + options = { + currency: 'GBP', + customer: @customer + } + assert purchase = @gateway.purchase(@amount, @declined_payment_method, options) + + assert_equal 'Your card was declined.', purchase.message + assert_not_nil purchase.params['response_headers']['stripe_should_retry'] + end + + def test_successful_purchase_with_external_auth_data_3ds_1 + options = { + currency: 'GBP', + three_d_secure: { + eci: '05', + cavv: '4BQwsg4yuKt0S1LI1nDZTcO9vUM=', + xid: 'd+NEBKSpEMauwleRhdrDY06qj4A=' + } + } + + assert purchase = @gateway.purchase(@amount, @three_ds_external_data_card, options) + + assert_equal 'succeeded', purchase.params['status'] + assert purchase.params.dig('charges', 'data')[0]['captured'] + end + + def test_successful_purchase_with_external_auth_data_3ds_2 + options = { + currency: 'GBP', + three_d_secure: { + version: '2.1.0', + eci: '02', + cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA=', + ds_transaction_id: 'f879ea1c-aa2c-4441-806d-e30406466d79' + } + } + + assert purchase = @gateway.purchase(@amount, @three_ds_external_data_card, options) + + assert_equal 'succeeded', purchase.params['status'] + assert purchase.params.dig('charges', 'data')[0]['captured'] + end + + def test_successful_purchase_with_customer_token_and_external_auth_data_3ds_2 + options = { + currency: 'GBP', + customer: @customer, + three_d_secure: { + version: '2.1.0', + eci: '02', + cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA=', + ds_transaction_id: 'f879ea1c-aa2c-4441-806d-e30406466d79' + } + } + + assert purchase = @gateway.purchase(@amount, @three_ds_authentication_required, options) + + assert_equal 'succeeded', purchase.params['status'] + assert purchase.params.dig('charges', 'data')[0]['captured'] + end + + def test_successful_purchase_with_radar_session + options = { + radar_session_id: 'rse_1JXSfZAWOtgoysogUpPJa4sm' + } + assert purchase = @gateway.purchase(@amount, @visa_card, options) + + assert_equal 'succeeded', purchase.params['status'] + assert purchase.params.dig('charges', 'data')[0]['captured'] + end + + def test_successful_purchase_with_skip_radar_rules + options = { skip_radar_rules: true } + assert purchase = @gateway.purchase(@amount, @visa_card, options) + + assert_equal 'succeeded', purchase.params['status'] + assert_equal ['all'], purchase.params['charges']['data'][0]['radar_options']['skip_rules'] + end + + def test_successful_authorization_with_external_auth_data_3ds_2 + options = { + currency: 'GBP', + three_d_secure: { + version: '2.1.0', + eci: '02', + cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA=', + ds_transaction_id: 'f879ea1c-aa2c-4441-806d-e30406466d79' + } + } + + assert authorization = @gateway.authorize(@amount, @three_ds_external_data_card, options) + + assert_equal 'requires_capture', authorization.params['status'] + refute authorization.params.dig('charges', 'data')[0]['captured'] + end + + def test_successful_authorization_with_radar_session + options = { + radar_session_id: 'rse_1JXSfZAWOtgoysogUpPJa4sm' + } + assert authorization = @gateway.authorize(@amount, @visa_card, options) + + assert_equal 'requires_capture', authorization.params['status'] + refute authorization.params.dig('charges', 'data')[0]['captured'] + end + + def test_create_payment_intent_manual_capture_method + options = { + currency: 'USD', + capture_method: 'manual' + } + + assert response = @gateway.create_intent(@amount, nil, options) + + assert_success response + assert_equal 'payment_intent', response.params['object'] + assert_equal 'manual', response.params['capture_method'] + end + + def test_create_payment_intent_manual_confimation_method + options = { + currency: 'USD', + description: 'ActiveMerchant Test Purchase', + confirmation_method: 'manual' + } + + assert response = @gateway.create_intent(@amount, nil, options) + + assert_success response + assert_equal 'payment_intent', response.params['object'] + assert_equal 'manual', response.params['confirmation_method'] + end + + def test_create_payment_intent_with_customer + options = { + currency: 'USD', + customer: @customer || 'set customer in fixtures' + } + + assert response = @gateway.create_intent(@amount, nil, options) + + assert_success response + assert_equal 'payment_intent', response.params['object'] + assert_equal @customer, response.params['customer'] + end + + def test_create_payment_intent_with_credit_card + options = { + currency: 'USD', + customer: @customer + } + + assert response = @gateway.create_intent(@amount, @three_ds_credit_card, options) + + assert_success response + assert_equal 'payment_intent', response.params['object'] + end + + def test_create_payment_intent_with_return_url + options = { + currency: 'USD', + customer: @customer, + confirm: true, + return_url: 'https://www.example.com', + execute_threed: true + } + + assert response = @gateway.create_intent(@amount, @three_ds_credit_card, options) + + assert_success response + assert_equal 'https://www.example.com', response.params['next_action']['redirect_to_url']['return_url'] + end + + def test_create_payment_intent_with_metadata + suffix = 'SUFFIX' + + options = { + currency: 'USD', + customer: @customer, + description: 'ActiveMerchant Test Purchase', + receipt_email: 'test@example.com', + statement_descriptor: 'Statement Descriptor', + statement_descriptor_suffix: suffix, + metadata: { key_1: 'value_1', key_2: 'value_2' }, + event_type: 'concert' + } + + assert response = @gateway.create_intent(@amount, nil, options) + + assert_success response + assert_equal 'value_1', response.params['metadata']['key_1'] + assert_equal 'concert', response.params['metadata']['event_type'] + assert_equal 'ActiveMerchant Test Purchase', response.params['description'] + assert_equal 'test@example.com', response.params['receipt_email'] + assert_equal 'Statement Descriptor', response.params['statement_descriptor'] + assert_equal suffix, response.params['statement_descriptor_suffix'] + end + + def test_create_payment_intent_that_saves_payment_method + options = { + currency: 'USD', + customer: @customer, + save_payment_method: true + } + + assert response = @gateway.create_intent(@amount, @three_ds_credit_card, options) + assert_success response + + assert response = @gateway.create_intent(@amount, nil, options) + assert_failure response + assert_equal 'A payment method must be provided or already '\ + 'attached to the PaymentIntent when `save_payment_method=true`.', response.message + + options.delete(:customer) + assert response = @gateway.create_intent(@amount, @three_ds_credit_card, options) + assert_failure response + assert_equal 'A valid `customer` must be provided when `save_payment_method=true`.', response.message + end + + def test_create_payment_intent_with_setup_future_usage + options = { + currency: 'USD', + customer: @customer, + setup_future_usage: 'on_session' + } + + assert response = @gateway.create_intent(@amount, @three_ds_credit_card, options) + assert_success response + assert_equal 'on_session', response.params['setup_future_usage'] + end + + def test_3ds_unauthenticated_authorize_with_off_session + options = { + currency: 'USD', + customer: @customer, + off_session: true + } + + assert response = @gateway.authorize(@amount, @three_ds_credit_card, options) + assert_failure response + end + + def test_create_setup_intent_with_setup_future_usage + [@three_ds_credit_card, @three_ds_authentication_required_setup_for_off_session].each do |card_to_use| + assert authorize_response = @gateway.create_setup_intent(card_to_use, { + address: { + email: 'test@example.com', + name: 'John Doe', + line1: '1 Test Ln', + city: 'Durham', + tracking_number: '123456789' + }, + currency: 'USD', + confirm: true, + execute_threed: true, + return_url: 'https://example.com' + }) + + assert_equal 'requires_action', authorize_response.params['status'] + assert_match 'https://hooks.stripe.com', authorize_response.params.dig('next_action', 'redirect_to_url', 'url') + + # since we cannot "click" the stripe hooks URL to confirm the authorization + # we will at least confirm we can retrieve the created setup_intent and it contains the structure we expect + setup_intent_id = authorize_response.params['id'] + + assert si_reponse = @gateway.retrieve_setup_intent(setup_intent_id) + assert_equal 'requires_action', si_reponse.params['status'] + + assert_not_empty si_reponse.params.dig('latest_attempt', 'payment_method_details', 'card') + assert_nil si_reponse.params.dig('latest_attempt', 'payment_method_details', 'card', 'network_transaction_id') + 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_setup_future_usage_and_moto_exemption + 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', + confirm: true, + moto: true, + return_url: 'https://example.com' + }) + + assert_equal 'succeeded', response.params['status'] + # 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, { + address: { + email: 'test@example.com', + name: 'John Doe', + line1: '1 Test Ln', + city: 'Durham', + tracking_number: '123456789' + }, + currency: 'USD', + confirm: true, + execute_threed: true, + return_url: 'https://example.com', + stripe_account: @destination_account + }) + + assert_equal 'requires_action', authorize_response.params['status'] + assert_match 'https://hooks.stripe.com', authorize_response.params.dig('next_action', 'redirect_to_url', 'url') + + # since we cannot "click" the stripe hooks URL to confirm the authorization + # we will at least confirm we can retrieve the created setup_intent and it contains the structure we expect + setup_intent_id = authorize_response.params['id'] + + # If we did not pass the stripe_account header it would return an error + assert si_response = @gateway.retrieve_setup_intent(setup_intent_id, { + stripe_account: @destination_account + }) + assert_equal 'requires_action', si_response.params['status'] + + assert_not_empty si_response.params.dig('latest_attempt', 'payment_method_details', 'card') + assert_nil si_response.params.dig('latest_attempt', 'payment_method_details', 'card', 'network_transaction_id') + end + end + + def test_create_setup_intent_with_request_three_d_secure + [@three_ds_credit_card, @three_ds_authentication_required_setup_for_off_session].each do |card_to_use| + assert authorize_response = @gateway.create_setup_intent(card_to_use, { + address: { + email: 'test@example.com', + name: 'John Doe', + line1: '1 Test Ln', + city: 'Durham', + tracking_number: '123456789' + }, + currency: 'USD', + confirm: true, + execute_threed: true, + return_url: 'https://example.com', + request_three_d_secure: 'any' + + }) + + assert_equal 'requires_action', authorize_response.params['status'] + assert_match 'https://hooks.stripe.com', authorize_response.params.dig('next_action', 'redirect_to_url', 'url') + + assert_equal 'any', authorize_response.params.dig('payment_method_options', 'card', 'request_three_d_secure') + + # since we cannot "click" the stripe hooks URL to confirm the authorization + # we will at least confirm we can retrieve the created setup_intent and it contains the structure we expect + setup_intent_id = authorize_response.params['id'] + + assert si_reponse = @gateway.retrieve_setup_intent(setup_intent_id) + assert_equal 'requires_action', si_reponse.params['status'] + + assert_not_empty si_reponse.params.dig('latest_attempt', 'payment_method_details', 'card') + assert_nil si_reponse.params.dig('latest_attempt', 'payment_method_details', 'card', 'network_transaction_id') + end + end + + def test_retrieving_error_for_non_existant_setup_intent + assert si_reponse = @gateway.retrieve_setup_intent('seti_does_not_exist') + assert_nil si_reponse.params['status'] + assert_nil si_reponse.params.dig('latest_attempt', 'payment_method_details', 'card', 'network_transaction_id') + + assert_match 'resource_missing', si_reponse.params.dig('error', 'code') + assert_match "No such setupintent: 'seti_does_not_exist'", si_reponse.params.dig('error', 'message') + end + + def test_3ds_unauthenticated_authorize_with_off_session_requires_capture + [@three_ds_off_session_credit_card, @three_ds_authentication_required_setup_for_off_session].each do |card_to_use| + assert authorize_response = @gateway.authorize(@amount, card_to_use, { + address: { + email: 'test@example.com', + name: 'John Doe', + line1: '1 Test Ln', + city: 'Durham', + tracking_number: '123456789' + }, + currency: 'USD', + confirm: true, + setup_future_usage: 'off_session', + execute_threed: true, + three_d_secure: { + version: '2.2.0', + eci: '02', + cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA=', + ds_transaction_id: 'f879ea1c-aa2c-4441-806d-e30406466d79' + } + }) + + assert_success authorize_response + assert_equal 'requires_capture', authorize_response.params['status'] + assert_not_empty authorize_response.params.dig('charges', 'data')[0]['payment_method_details']['card']['network_transaction_id'] + end + end + + def test_purchase_sends_network_transaction_id_separate_from_stored_creds + [@visa_card, @three_ds_authentication_required_setup_for_off_session].each do |card_to_use| + assert purchase = @gateway.purchase(@amount, card_to_use, { + currency: 'USD', + execute_threed: true, + confirm: true, + off_session: true, + network_transaction_id: '1234567891011' + }) + 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 + end + + def test_purchase_works_with_stored_credentials + [@three_ds_off_session_credit_card, @three_ds_authentication_required_setup_for_off_session].each do |card_to_use| + assert purchase = @gateway.purchase(@amount, card_to_use, { + currency: 'USD', + execute_threed: true, + confirm: true, + off_session: true, + setup_future_usage: true, + stored_credential: { + network_transaction_id: '1098510912210968', # TEST env seems happy with any value :/ + ds_transaction_id: 'null' # this is not req + } + }) + + assert_success purchase + assert_equal 'succeeded', purchase.params['status'] + assert purchase.params.dig('charges', 'data')[0]['captured'] + end + end + + def test_purchase_works_with_stored_credentials_without_optional_ds_transaction_id + [@three_ds_off_session_credit_card, @three_ds_authentication_required_setup_for_off_session].each do |card_to_use| + assert purchase = @gateway.purchase(@amount, card_to_use, { + currency: 'USD', + execute_threed: true, + confirm: true, + off_session: true, + stored_credential: { + network_transaction_id: '1098510912210968' # TEST env seems happy with any value :/ + } + }) + + assert_success purchase + assert_equal 'succeeded', purchase.params['status'] + assert purchase.params.dig('charges', 'data')[0]['captured'] + end + end + + def test_succeeds_with_ntid_in_stored_credentials_and_separately + [@visa_card, @three_ds_authentication_required_setup_for_off_session].each do |card_to_use| + assert purchase = @gateway.purchase(@amount, card_to_use, { + currency: 'USD', + execute_threed: true, + confirm: true, + off_session: true, + network_transaction_id: '1078784111114777', + stored_credential: { + network_transaction_id: '1098510912210968', + ds_transaction_id: 'null' + } + }) + 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 + 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_subsequent_cit_3ds_required + 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: '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 + 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, { + currency: 'USD', + execute_thread: true, + confirm: true, + off_session: true, + claim_without_transaction_id: true + }) + assert_success response + assert_equal 'succeeded', response.params['status'] + assert response.params.dig('charges', 'data')[0]['captured'] + end + end + + def test_successful_off_session_purchase_with_authentication_when_claim_without_transaction_id_is_false + assert response = @gateway.purchase(@amount, @three_ds_authentication_required_setup_for_off_session, { + currency: 'USD', + execute_thread: true, + confirm: true, + off_session: true, + claim_without_transaction_id: false + }) + # Purchase should succeed since other credentials are passed + assert_success response + assert_equal 'succeeded', response.params['status'] + assert response.params.dig('charges', 'data')[0]['captured'] + end + + def test_failed_off_session_purchase_with_card_when_claim_without_transaction_id_is_false + assert response = @gateway.purchase(@amount, @three_ds_off_session_credit_card, { + currency: 'USD', + execute_thread: true, + confirm: true, + off_session: true, + claim_without_transaction_id: false + }) + # Purchase should fail since no other credentials are passed, + # and Stripe will not manage the transaction without a transaction id + assert_failure response + assert_equal 'failed', response.params.dig('error', 'payment_intent', 'charges', 'data')[0]['status'] + assert !response.params.dig('error', 'payment_intent', 'charges', 'data')[0]['captured'] + end + + def test_purchase_fails_on_unexpected_3ds_initiation + options = { + currency: 'USD', + customer: @customer, + confirm: true, + return_url: 'https://www.example.com' + } + + 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 + options = { + currency: 'USD', + customer: @customer, + shipping_address: { + address1: '1 Test Ln', + city: 'Durham', + name: 'John Doe' + } + } + + assert response = @gateway.create_intent(@amount, nil, options) + assert_success response + assert response.params['shipping']['address'] + assert_equal 'John Doe', response.params['shipping']['name'] + end + + def test_create_payment_intent_with_billing_address + options = { + currency: 'USD', + customer: @customer, + billing_address: address, + email: 'jim@widgets.inc', + confirm: true + } + assert response = @gateway.create_intent(@amount, @visa_card, options) + assert_success response + assert billing_details = response.params.dig('charges', 'data')[0].dig('billing_details') + assert_equal 'Ottawa', billing_details['address']['city'] + assert_equal 'jim@widgets.inc', billing_details['email'] + end + + def test_create_payment_intent_with_name_if_billing_address_absent + options = { + currency: 'USD', + customer: @customer, + confirm: true + } + name_on_card = [@visa_card.first_name, @visa_card.last_name].join(' ') + + assert response = @gateway.create_intent(@amount, @visa_card, options) + assert_success response + assert_equal name_on_card, response.params.dig('charges', 'data')[0].dig('billing_details', 'name') + end + + def test_create_payment_intent_with_connected_account + transfer_group = 'XFERGROUP' + application_fee = 100 + + # You may not provide the application_fee_amount parameter and the transfer_data[amount] parameter + # simultaneously. They are mutually exclusive. + options = { + currency: 'USD', + customer: @customer, + application_fee:, + transfer_destination: @destination_account, + on_behalf_of: @destination_account, + transfer_group: + } + + assert response = @gateway.create_intent(@amount, nil, options) + assert_success response + assert_equal application_fee, response.params['application_fee_amount'] + assert_equal transfer_group, response.params['transfer_group'] + assert_equal @destination_account, response.params['on_behalf_of'] + assert_equal @destination_account, response.params.dig('transfer_data', 'destination') + end + + def test_create_payment_intent_with_fulfillment_date + options = { + currency: 'USD', + customer: @customer, + fulfillment_date: 1636756194 + } + assert response = @gateway.authorize(@amount, @visa_payment_method, options) + assert_success response + end + + def test_create_a_payment_intent_and_confirm + options = { + currency: 'GBP', + customer: @customer, + return_url: 'https://www.example.com', + confirmation_method: 'manual', + capture_method: 'manual' + } + assert create_response = @gateway.create_intent(@amount, @three_ds_payment_method, options) + assert_equal 'requires_confirmation', create_response.params['status'] + intent_id = create_response.params['id'] + + assert get_response = @gateway.show_intent(intent_id, options) + assert_equal 'requires_confirmation', get_response.params['status'] + + assert confirm_response = @gateway.confirm_intent(intent_id, nil, return_url: 'https://example.com/return-to-me') + assert_equal 'redirect_to_url', confirm_response.params.dig('next_action', 'type') + end + + def test_create_a_payment_intent_and_manually_capture + options = { + currency: 'GBP', + customer: @customer, + confirmation_method: 'manual', + capture_method: 'manual', + confirm: true + } + assert create_response = @gateway.create_intent(@amount, @visa_payment_method, 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_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', + customer: @customer, + confirm: true, + error_on_requires_action: true + } + assert create_response = @gateway.create_intent(@amount, @three_ds_credit_card, options) + assert create_response.message.include?('This payment required an authentication action to complete, but `error_on_requires_action` was set.') + end + + def test_successful_create_a_payment_intent_with_set_error_on_requires_action + options = { + currency: 'GBP', + customer: @customer, + confirm: true, + error_on_requires_action: true + } + assert create_response = @gateway.create_intent(@amount, @visa_payment_method, options) + assert_equal 'succeeded', create_response.params['status'] + end + + def test_amount_localization + amount = 200000 + options = { + currency: 'XPF', + customer: @customer, + confirmation_method: 'manual', + capture_method: 'manual', + confirm: true + } + assert create_response = @gateway.create_intent(amount, @visa_payment_method, 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 2000, capture_response.params['amount'] + end + + def test_auth_and_capture_with_destination_account_and_fee + options = { + currency: 'GBP', + customer: @customer, + confirmation_method: 'manual', + capture_method: 'manual', + transfer_destination: @destination_account, + confirm: true + } + assert create_response = @gateway.create_intent(@amount, @visa_payment_method, options) + intent_id = create_response.params['id'] + assert_equal 'requires_capture', create_response.params['status'] + assert_equal @destination_account, create_response.params['transfer_data']['destination'] + assert_nil create_response.params['application_fee_amount'] + + assert capture_response = @gateway.capture(@amount, intent_id, { application_fee: 100 }) + assert_equal 'succeeded', capture_response.params['status'] + assert_equal @destination_account, capture_response.params['transfer_data']['destination'] + assert_equal 100, capture_response.params['application_fee_amount'] + assert_equal 'Payment complete.', capture_response.params.dig('charges', 'data')[0].dig('outcome', 'seller_message') + end + + def test_create_a_payment_intent_and_automatically_capture + options = { + currency: 'GBP', + customer: @customer, + confirmation_method: 'manual', + confirm: true + } + assert create_response = @gateway.create_intent(@amount, @visa_payment_method, options) + assert_nil create_response.params['next_action'] + assert_equal 'succeeded', create_response.params['status'] + assert_equal 'Payment complete.', create_response.params.dig('charges', 'data')[0].dig('outcome', 'seller_message') + end + + def test_failed_capture_after_creation + options = { + currency: 'GBP', + customer: @customer, + confirmation_method: 'manual', + confirm: true + } + assert create_response = @gateway.create_intent(@amount, 'pm_card_chargeDeclined', options) + assert_equal 'requires_payment_method', create_response.params.dig('error', 'payment_intent', 'status') + assert_equal false, create_response.params.dig('error', 'payment_intent', 'charges', 'data')[0].dig('captured') + end + + def test_create_a_payment_intent_and_update + amount = 200000 + update_amount = 250000 + options = { + currency: 'XPF', + customer: @customer, + confirmation_method: 'manual', + capture_method: 'manual' + } + assert create_response = @gateway.create_intent(amount, @visa_payment_method, options) + intent_id = create_response.params['id'] + assert_equal 2000, create_response.params['amount'] + + assert update_response = @gateway.update_intent(update_amount, intent_id, nil, options.merge(payment_method_types: 'card')) + assert_equal 2500, update_response.params['amount'] + assert_equal 'requires_confirmation', update_response.params['status'] + end + + def test_create_a_payment_intent_and_confirm_with_different_payment_method + options = { + currency: 'USD', + payment_method_types: %w[afterpay_clearpay], + metadata: { key_1: 'value_1', key_2: 'value_2' } + } + assert create_response = @gateway.setup_purchase(@amount, options) + assert_equal 'requires_payment_method', create_response.params['status'] + intent_id = create_response.params['id'] + assert_equal 2000, create_response.params['amount'] + assert_equal 'afterpay_clearpay', create_response.params['payment_method_types'][0] + + assert confirm_response = @gateway.confirm_intent(intent_id, @visa_payment_method, payment_method_types: 'card') + assert_equal 'card', confirm_response.params['payment_method_types'][0] + end + + def test_create_a_payment_intent_and_void + options = { + currency: 'GBP', + customer: @customer, + confirmation_method: 'manual', + capture_method: 'manual', + confirm: true + } + + void_options = { + cancellation_reason: 'requested_by_customer', + order_id: '123445abcde' + } + assert create_response = @gateway.create_intent(@amount, @visa_payment_method, options) + intent_id = create_response.params['id'] + + assert cancel_response = @gateway.void(intent_id, void_options) + assert_equal @amount, cancel_response.params.dig('charges', 'data')[0].dig('amount_refunded') + assert_equal 'canceled', cancel_response.params['status'] + assert_equal 'requested_by_customer', cancel_response.params['cancellation_reason'] + end + + def test_create_a_payment_intent_and_void_requires_unique_idempotency_key + idempotency_key = SecureRandom.hex + options = { + currency: 'GBP', + customer: @customer, + return_url: 'https://www.example.com', + confirmation_method: 'manual', + capture_method: 'manual', + idempotency_key: + } + assert create_response = @gateway.create_intent(@amount, @three_ds_payment_method, options) + assert_equal 'requires_confirmation', create_response.params['status'] + intent_id = create_response.params['id'] + + assert get_response = @gateway.show_intent(intent_id, options) + assert_equal 'requires_confirmation', get_response.params['status'] + + assert_failure cancel_response = @gateway.void(intent_id, cancellation_reason: 'requested_by_customer', idempotency_key:) + assert_match(/^Keys for idempotent requests can only be used for the same endpoint they were first used for/, cancel_response.message) + + assert cancel_response = @gateway.void(intent_id, cancellation_reason: 'requested_by_customer', idempotency_key: "#{idempotency_key}-auto-void") + assert_equal 'canceled', cancel_response.params['status'] + assert_equal 'requested_by_customer', cancel_response.params['cancellation_reason'] + end + + def test_failed_void_after_capture + options = { + currency: 'GBP', + customer: @customer, + confirmation_method: 'manual', + confirm: true + } + assert create_response = @gateway.create_intent(@amount, @visa_payment_method, options) + assert_equal 'succeeded', create_response.params['status'] + intent_id = create_response.params['id'] + + assert cancel_response = @gateway.void(intent_id, cancellation_reason: 'requested_by_customer') + assert_equal 'You cannot cancel this PaymentIntent because ' \ + 'it has a status of succeeded. Only a PaymentIntent with ' \ + 'one of the following statuses may be canceled: ' \ + 'requires_payment_method, requires_capture, requires_confirmation, requires_action, processing.', cancel_response.message + end + + def test_refund_a_payment_intent + options = { + currency: 'GBP', + customer: @customer, + confirmation_method: 'manual', + capture_method: 'manual', + confirm: true + } + + refund_options = { + order_id: '123445abcde' + } + + assert create_response = @gateway.create_intent(@amount, @visa_payment_method, options) + intent_id = create_response.params['id'] + + assert @gateway.capture(@amount, intent_id, options) + + assert refund = @gateway.refund(@amount - 20, intent_id, refund_options) + assert_equal @amount - 20, refund.params['charge']['amount_refunded'] + assert_equal true, refund.params['charge']['captured'] + assert_equal '123445abcde', refund.params['metadata']['order_id'] + refund_id = refund.params['id'] + assert_equal refund.authorization, refund_id + end + + def test_refund_when_payment_intent_not_captured + options = { + currency: 'GBP', + customer: @customer, + confirmation_method: 'manual', + capture_method: 'manual', + confirm: true + } + assert create_response = @gateway.create_intent(@amount, @visa_payment_method, options) + intent_id = create_response.params['id'] + + refund = @gateway.refund(@amount - 20, intent_id) + assert_failure refund + assert refund.params['error'] + end + + def test_refund_when_payment_intent_requires_action + options = { + currency: 'GBP', + customer: @customer, + confirmation_method: 'manual', + capture_method: 'manual', + confirm: true + } + assert create_response = @gateway.create_intent(@amount, @three_ds_authentication_required, options) + assert_equal 'requires_action', create_response.params['status'] + intent_id = create_response.params['id'] + + refund = @gateway.refund(@amount - 20, intent_id) + assert_failure refund + assert_match(/has a status of requires_action/, refund.message) + end + + def test_successful_store_purchase_and_unstore + options = { + currency: 'GBP' + } + assert store = @gateway.store(@visa_card, options) + assert store.params['customer'].start_with?('cus_') + + assert purchase = @gateway.purchase(@amount, store.authorization, options) + assert 'succeeded', purchase.params['status'] + + assert unstore = @gateway.unstore(store.authorization) + assert_nil unstore.params['customer'] + end + + def test_successful_store_with_idempotency_key + idempotency_key = SecureRandom.hex + + options = { + currency: 'GBP', + idempotency_key: + } + + assert store1 = @gateway.store(@visa_card, options) + assert store1.success? + assert store1.params['customer'].start_with?('cus_') + + assert store2 = @gateway.store(@visa_card, options) + assert store2.success? + assert_equal store1.authorization, store2.authorization + 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', + validate: false + } + assert store = @gateway.store(@visa_card, options) + assert store.params['customer'].start_with?('cus_') + assert_equal 'unchecked', store.params['card']['checks']['cvc_check'] + end + + def test_successful_store_with_true_validate_option + options = { + currency: 'GBP', + validate: true + } + assert store = @gateway.store(@visa_card, options) + assert store.params['customer'].start_with?('cus_') + assert_equal 'pass', store.params['card']['checks']['cvc_check'] + end + + def test_successful_verify + options = { + customer: @customer, + billing_address: address + } + 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_successful_verify_returns_card_three_3d_supported + options = { + customer: @customer, + billing_address: address + } + assert verify = @gateway.verify(@visa_card, options) + assert_equal true, verify.params.dig('three_d_secure_usage_supported') + end + + def test_failed_verify + options = { + customer: @customer + } + 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 + options = { + currency: 'GBP', + confirm: true + } + assert purchase = @gateway.purchase(@amount, @three_ds_moto_enabled, options) + + assert_equal 'requires_action', purchase.params['status'] + end + + def test_moto_enabled_card_succeeds_when_marked + options = { + currency: 'GBP', + confirm: true, + moto: true + } + assert purchase = @gateway.purchase(@amount, @three_ds_moto_enabled, options) + + assert_equal 'succeeded', purchase.params['status'] + assert purchase.params.dig('charges', 'data')[0]['captured'] + end + + def test_certain_cards_require_action_even_when_marked_as_moto + options = { + currency: 'GBP', + confirm: true, + moto: true + } + assert purchase = @gateway.purchase(@amount, @three_ds_authentication_required, options) + + assert_failure purchase + assert_equal 'Your card was declined. This transaction requires authentication.', purchase.message + end + + def test_request_three_d_secure + options = { + currency: 'GBP', + request_three_d_secure: 'any' + } + assert purchase = @gateway.purchase(@amount, @three_ds_not_required_card, options) + assert_equal 'requires_action', purchase.params['status'] + + options = { + currency: 'GBP', + request_three_d_secure: 'challenge' + } + assert purchase = @gateway.purchase(@amount, @three_ds_not_required_card, options) + assert_equal 'requires_action', purchase.params['status'] + + options = { + currency: 'GBP' + } + assert purchase = @gateway.purchase(@amount, @three_ds_not_required_card, options) + assert_equal 'succeeded', purchase.params['status'] + end + + def test_setup_purchase + options = { + currency: 'USD', + payment_method_types: %w[afterpay_clearpay card], + metadata: { key_1: 'value_1', key_2: 'value_2' } + } + + assert response = @gateway.setup_purchase(@amount, options) + assert_equal 'requires_payment_method', response.params['status'] + assert_equal 'value_1', response.params['metadata']['key_1'] + assert_equal 'value_2', response.params['metadata']['key_2'] + assert response.params['client_secret'].start_with?('pi') + end + + def test_failed_setup_purchase + options = { + currency: 'GBP', + payment_method_types: %w[afterpay_clearpay card] + } + + assert response = @gateway.setup_purchase(@amount, options) + assert_failure response + assert_match 'The currency provided (gbp) is invalid for one or more payment method types on this PaymentIntent.', response.message + end + + def test_transcript_scrubbing + options = { + currency: 'GBP', + customer: @customer, + confirmation_method: 'manual', + return_url: 'https://www.example.com/return', + confirm: true + } + transcript = capture_transcript(@gateway) do + @gateway.create_intent(@amount, @three_ds_credit_card, options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@three_ds_credit_card.number, transcript) + 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_stripe_test.rb b/test/remote/gateways/remote_stripe_test.rb index 3d34ae2c75a..9b4176b083b 100644 --- a/test/remote/gateways/remote_stripe_test.rb +++ b/test/remote/gateways/remote_stripe_test.rb @@ -13,28 +13,37 @@ def setup @check = check({ bank_name: 'STRIPE TEST BANK', account_number: '000123456789', - routing_number: '110000000', + routing_number: '110000000' }) @verified_bank_account = fixtures(:stripe_verified_bank_account) @options = { - :currency => 'USD', - :description => 'ActiveMerchant Test Purchase', - :email => 'wow@example.com' + currency: 'USD', + description: 'ActiveMerchant Test Purchase', + email: 'wow@example.com' } end def test_transcript_scrubbing + credit_card = credit_card('4242424242424242', verification_value: '745') transcript = capture_transcript(@gateway) do - @gateway.purchase(@amount, @credit_card, @options) + @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(credit_card.number, transcript) + assert_scrubbed(credit_card.verification_value, transcript) assert_scrubbed(@gateway.options[:login], transcript) end + def test_check_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.store(@check) + end + transcript = @gateway.scrub(transcript) + assert_scrubbed(@check.account_number, transcript) + end + def test_successful_purchase assert response = @gateway.purchase(@amount, @credit_card, @options) assert_success response @@ -46,7 +55,7 @@ def test_successful_purchase end def test_successful_purchase_with_blank_referer - options = @options.merge({referrer: ''}) + options = @options.merge({ referrer: '' }) assert response = @gateway.purchase(@amount, @credit_card, options) assert_success response assert_equal 'charge', response.params['object'] @@ -57,7 +66,7 @@ def test_successful_purchase_with_blank_referer end def test_successful_purchase_with_recurring_flag - custom_options = @options.merge(:eci => 'recurring') + custom_options = @options.merge(eci: 'recurring') assert response = @gateway.purchase(@amount, @credit_card, custom_options) assert_success response assert_equal 'charge', response.params['object'] @@ -68,7 +77,7 @@ def test_successful_purchase_with_recurring_flag def test_successful_purchase_with_destination destination = fixtures(:stripe_destination)[:stripe_user_id] - custom_options = @options.merge(:destination => destination) + custom_options = @options.merge(destination:) assert response = @gateway.purchase(@amount, @credit_card, custom_options) assert_success response assert_equal 'charge', response.params['object'] @@ -80,7 +89,7 @@ def test_successful_purchase_with_destination def test_successful_purchase_with_destination_and_amount destination = fixtures(:stripe_destination)[:stripe_user_id] - custom_options = @options.merge(:destination => destination, :destination_amount => @amount - 20) + custom_options = @options.merge(destination:, destination_amount: @amount - 20) assert response = @gateway.purchase(@amount, @credit_card, custom_options) assert_success response assert_equal 'charge', response.params['object'] @@ -90,17 +99,125 @@ def test_successful_purchase_with_destination_and_amount assert_equal 'wow@example.com', response.params['metadata']['email'] end + def test_successful_purchase_with_level3_data + @options[:merchant_reference] = 123 + @options[:customer_reference] = 456 + @options[:shipping_address_zip] = 98765 + @options[:shipping_from_zip] = 54321 + @options[:shipping_amount] = 10 + @options[:line_items] = [ + { + 'product_code' => 1234, + 'product_description' => 'An item', + 'unit_cost' => 15, + 'quantity' => 2, + 'tax_amount' => 0 + }, + { + 'product_code' => 999, + 'product_description' => 'A totes different item', + 'tax_amount' => 10, + 'unit_cost' => 50, + 'quantity' => 1 + } + ] + + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'charge', response.params['object'] + assert_equal response.authorization, response.params['id'] + assert response.params['paid'] + assert_equal 'ActiveMerchant Test Purchase', response.params['description'] + assert_equal 'wow@example.com', response.params['metadata']['email'] + end + + def test_successful_purchase_with_shipping_address + @options[:shipping_address] = {} + @options[:shipping_address][:name] = 'Jim Doe' + @options[:shipping_address][:phone_number] = '9194041014' + @options[:shipping_address][:address1] = '100 W Main St' + @options[:shipping_address][:address2] = 'Apt 2' + @options[:shipping_address][:city] = 'Baltimore' + @options[:shipping_address][:state] = 'MD' + @options[:shipping_address][:zip] = '21201' + @options[:shipping_address][:country] = 'US' + + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'charge', response.params['object'] + assert_equal response.authorization, response.params['id'] + assert response.params['paid'] + assert_equal 'ActiveMerchant Test Purchase', response.params['description'] + assert_equal 'Jim Doe', response.params['shipping']['name'] + assert_equal '9194041014', response.params['shipping']['phone'] + assert_equal '100 W Main St', response.params['shipping']['address']['line1'] + assert_equal 'Apt 2', response.params['shipping']['address']['line2'] + assert_equal 'Baltimore', response.params['shipping']['address']['city'] + assert_equal 'MD', response.params['shipping']['address']['state'] + assert_equal '21201', response.params['shipping']['address']['postal_code'] + assert_equal 'US', response.params['shipping']['address']['country'] + end + + def test_purchase_with_connected_account + destination = fixtures(:stripe_destination)[:stripe_user_id] + transfer_group = 'XFERGROUP' + application_fee_amount = 100 + + # You may not provide the application_fee_amount parameter and the transfer_data[amount] parameter + # simultaneously. They are mutually exclusive. + options = @options.merge({ + customer: @customer, + application_fee_amount:, + transfer_destination: destination, + on_behalf_of: destination, + transfer_group: + }) + + assert response = @gateway.purchase(@amount, @credit_card, options) + + assert_success response + assert_equal application_fee_amount, response.params['application_fee_amount'] + assert_equal transfer_group, response.params['transfer_group'] + assert_equal destination, response.params['on_behalf_of'] + assert_equal destination, response.params.dig('transfer_data', 'destination') + end + + def test_successful_purchase_with_radar_session + options = @options.merge(radar_session_id: 'rse_1JXSfZAWOtgoysogUpPJa4sm') + assert response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'charge', response.params['object'] + assert response.params['paid'] + assert_equal 'ActiveMerchant Test Purchase', response.params['description'] + assert_equal 'wow@example.com', response.params['metadata']['email'] + end + + def test_successful_purchase_with_skip_radar_rules + options = @options.merge(skip_radar_rules: true) + assert purchase = @gateway.purchase(@amount, @credit_card, options) + assert_success purchase + assert_equal ['all'], purchase.params['radar_options']['skip_rules'] + end + def test_unsuccessful_purchase assert response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response assert_match %r{Your card was declined}, response.message assert_match Gateway::STANDARD_ERROR_CODE[:card_declined], response.error_code - assert_match /ch_[a-zA-Z\d]+/, response.authorization + assert_match(/ch_[a-zA-Z\d]+/, response.authorization) + end + + def test_unsuccessful_purchase_returns_response_headers + assert response = @gateway.purchase(@amount, @declined_card, @options) + assert_failure response + assert_match %r{Your card was declined}, response.message + assert_match Gateway::STANDARD_ERROR_CODE[:card_declined], response.error_code + assert_not_nil response.params['response_headers']['stripe_should_retry'] end def test_unsuccessful_purchase_with_destination_and_amount destination = fixtures(:stripe_destination)[:stripe_user_id] - custom_options = @options.merge(:destination => destination, :destination_amount => @amount + 20) + custom_options = @options.merge(destination:, destination_amount: @amount + 20) assert response = @gateway.purchase(@amount, @credit_card, custom_options) assert_failure response assert_match %r{must be less than or equal to the charge amount}, response.message @@ -124,6 +241,23 @@ def test_unsuccessful_direct_bank_account_purchase assert_equal 'Direct bank account transactions are not supported. Bank accounts must be stored and verified before use.', response.message end + def test_unsuccessful_echeck_auth_with_verified_account + customer_id = @verified_bank_account[:customer_id] + bank_account_id = @verified_bank_account[:bank_account_id] + + payment = [customer_id, bank_account_id].join('|') + + response = @gateway.authorize(@amount, payment, @options) + assert_failure response + assert_equal 'You cannot pass capture=false for this payment type.', response.message + end + + def test_unsuccessful_direct_bank_account_auth + response = @gateway.authorize(@amount, @check, @options) + assert_failure response + assert_equal 'Direct bank account transactions are not supported for authorize.', response.message + end + def test_authorization_and_capture assert authorization = @gateway.authorize(@amount, @credit_card, @options) assert_success authorization @@ -137,7 +271,7 @@ def test_authorization_and_capture def test_authorization_and_capture_with_destination destination = fixtures(:stripe_destination)[:stripe_user_id] - custom_options = @options.merge(:destination => destination) + custom_options = @options.merge(destination:) assert authorization = @gateway.authorize(@amount, @credit_card, custom_options) assert_success authorization @@ -152,7 +286,7 @@ def test_authorization_and_capture_with_destination def test_authorization_and_capture_with_destination_and_amount destination = fixtures(:stripe_destination)[:stripe_user_id] - custom_options = @options.merge(:destination => destination, :destination_amount => @amount - 20) + custom_options = @options.merge(destination:, destination_amount: @amount - 20) assert authorization = @gateway.authorize(@amount, @credit_card, custom_options) assert_success authorization @@ -165,6 +299,18 @@ def test_authorization_and_capture_with_destination_and_amount assert_success capture end + def test_successful_authorization_and_capture_with_radar_session + options = @options.merge(radar_session_id: 'rse_1JXSfZAWOtgoysogUpPJa4sm') + assert authorization = @gateway.authorize(@amount, @credit_card, options) + assert_success authorization + refute authorization.params['captured'] + assert_equal 'ActiveMerchant Test Purchase', authorization.params['description'] + assert_equal 'wow@example.com', authorization.params['metadata']['email'] + + assert capture = @gateway.capture(@amount, authorization.authorization) + assert_success capture + end + def test_authorization_and_void assert authorization = @gateway.authorize(@amount, @credit_card, @options) assert_success authorization @@ -188,10 +334,42 @@ def test_successful_void_with_metadata assert_success response assert response.authorization - assert void = @gateway.void(response.authorization, metadata: { test_metadata: 123 }) + void_options = { + metadata: { + test_metadata: 123 + }, + order_id: '123445abcde' + } + + assert void = @gateway.void(response.authorization, void_options) assert void.test? assert_success void assert_equal '123', void.params['metadata']['test_metadata'] + assert_equal '123445abcde', void.params['metadata']['order_id'] + end + + def test_successful_void_with_reason + assert response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + assert response.authorization + + assert void = @gateway.void(response.authorization, reason: 'fraudulent') + assert void.test? + assert_success void + assert_equal 'fraudulent', void.params['reason'] + end + + def test_successful_void_with_reverse_transfer + destination = fixtures(:stripe_destination)[:stripe_user_id] + assert response = @gateway.authorize(@amount, @credit_card, @options.merge(destination:)) + assert_success response + + @gateway.capture(@amount, response.authorization) + + assert void = @gateway.void(response.authorization, reverse_transfer: true) + assert_match %r{trr_}, void.params['transfer_reversal'] + assert_success void + assert_equal 'Transaction approved', void.message end def test_unsuccessful_void @@ -211,6 +389,40 @@ def test_successful_refund assert_success refund end + def test_successful_refund_with_reason + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert response.authorization + + assert refund = @gateway.refund(@amount - 20, response.authorization, reason: 'fraudulent') + assert refund.test? + refund_id = refund.params['id'] + assert_equal refund.authorization, refund_id + assert_success refund + assert_equal 'fraudulent', refund.params['reason'] + end + + def test_successful_refund_with_metada_and_order_id + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert response.authorization + + refund_options = { + metadata: { + test_metadata: 123 + }, + order_id: '123445abcde' + } + + assert refund = @gateway.refund(@amount - 20, response.authorization, refund_options) + assert refund.test? + refund_id = refund.params['id'] + assert_equal refund.authorization, refund_id + assert_success refund + assert_equal '123445abcde', refund.params['metadata']['order_id'] + assert_equal '123', refund.params['metadata']['test_metadata'] + end + def test_successful_refund_on_verified_bank_account customer_id = @verified_bank_account[:customer_id] bank_account_id = @verified_bank_account[:bank_account_id] @@ -228,7 +440,7 @@ def test_successful_refund_on_verified_bank_account def test_refund_with_reverse_transfer destination = fixtures(:stripe_destination)[:stripe_user_id] - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(destination: destination)) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(destination:)) assert_success response assert refund = @gateway.refund(@amount - 20, response.authorization, reverse_transfer: true) @@ -381,9 +593,10 @@ def test_successful_store_with_existing_customer def test_successful_store_with_existing_account account = fixtures(:stripe_destination)[:stripe_user_id] - - assert response = @gateway.store(@debit_card, account: account) + assert response = @gateway.store(@debit_card, account:) assert_success response + # Delete the stored external account to prevent hitting the limit + @gateway.delete_latest_test_external_account(account) assert_equal 'card', response.params['object'] end @@ -411,20 +624,18 @@ def test_successful_purchase_using_stored_card_on_existing_customer assert_equal '5100', response.params['source']['last4'] end - def test_successful_purchase_using_stored_card_and_deprecated_api + def test_successful_purchase_using_stored_card_with_customer_id assert store = @gateway.store(@credit_card) assert_success store - recharge_options = @options.merge(:customer => store.params['id']) - assert_deprecation_warning do - response = @gateway.purchase(@amount, nil, recharge_options) - assert_success response - assert_equal '4242', response.params['source']['last4'] - end + recharge_options = @options.merge(customer: store.params['id']) + response = @gateway.purchase(@amount, nil, recharge_options) + assert_success response + assert_equal '4242', response.params['source']['last4'] end def test_successful_unstore - creation = @gateway.store(@credit_card, {:description => 'Active Merchant Unstore Customer'}) + creation = @gateway.store(@credit_card, { description: 'Active Merchant Unstore Customer' }) card_id = creation.params['sources']['data'].first['id'] assert response = @gateway.unstore(creation.authorization) @@ -435,7 +646,7 @@ def test_successful_unstore end def test_successful_unstore_using_deprecated_api - creation = @gateway.store(@credit_card, {:description => 'Active Merchant Unstore Customer'}) + creation = @gateway.store(@credit_card, { description: 'Active Merchant Unstore Customer' }) card_id = creation.params['sources']['data'].first['id'] customer_id = creation.params['id'] @@ -450,8 +661,8 @@ def test_successful_store_of_bank_account response = @gateway.store(@check, @options) assert_success response customer_id, bank_account_id = response.authorization.split('|') - assert_match /^cus_/, customer_id - assert_match /^ba_/, bank_account_id + assert_match(/^cus_/, customer_id) + assert_match(/^ba_/, bank_account_id) end def test_unsuccessful_purchase_from_stored_but_unverified_bank_account @@ -479,7 +690,7 @@ def test_successful_purchase_from_stored_and_verified_bank_account end def test_invalid_login - gateway = StripeGateway.new(:login => 'active_merchant_test') + gateway = StripeGateway.new(login: 'active_merchant_test') assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_match 'Invalid API Key provided', response.message @@ -488,7 +699,7 @@ def test_invalid_login # These "track data present" tests fail with invalid expiration dates. The # test track data probably needs to be updated. def test_card_present_purchase - @credit_card.track_data = '%B378282246310005^LONGSON/LONGBOB^2205101130504392?' + @credit_card.track_data = '%B378282246310005^LONGSON/LONGBOB^2705101130504392?' assert response = @gateway.purchase(@amount, @credit_card, @options) assert_success response assert_equal 'charge', response.params['object'] @@ -496,7 +707,7 @@ def test_card_present_purchase end def test_card_present_authorize_and_capture - @credit_card.track_data = '%B378282246310005^LONGSON/LONGBOB^2205101130504392?' + @credit_card.track_data = '%B378282246310005^LONGSON/LONGBOB^2705101130504392?' assert authorization = @gateway.authorize(@amount, @credit_card, @options) assert_success authorization refute authorization.params['captured'] @@ -506,25 +717,25 @@ def test_card_present_authorize_and_capture end def test_creditcard_purchase_with_customer - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(:customer => '1234')) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(customer: '1234')) assert_success response assert_equal 'charge', response.params['object'] assert response.params['paid'] end def test_expanding_objects - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(:expand => 'balance_transaction')) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(expand: 'balance_transaction')) assert_success response assert response.params['balance_transaction'].is_a?(Hash) assert_equal 'balance_transaction', response.params['balance_transaction']['object'] end def test_successful_update - creation = @gateway.store(@credit_card, {:description => 'Active Merchant Update Credit Card'}) + creation = @gateway.store(@credit_card, { description: 'Active Merchant Update Credit Card' }) customer_id = creation.params['id'] card_id = creation.params['sources']['data'].first['id'] - assert response = @gateway.update(customer_id, card_id, { :name => 'John Doe', :address_line1 => '123 Main Street', :address_city => 'Pleasantville', :address_state => 'NY', :address_zip => '12345', :exp_year => Time.now.year + 2, :exp_month => 6 }) + assert response = @gateway.update(customer_id, card_id, { name: 'John Doe', address_line1: '123 Main Street', address_city: 'Pleasantville', address_state: 'NY', address_zip: '12345', exp_year: Time.now.year + 2, exp_month: 6 }) assert_success response assert_equal 'John Doe', response.params['name'] assert_equal '123 Main Street', response.params['address_line1'] @@ -603,11 +814,18 @@ def test_stripe_account_header assert_success response end + def test_statement_descriptor_suffix + suffix = 'SUFFIX' + + assert response = @gateway.purchase(@amount, @credit_card, statement_descriptor_suffix: suffix) + assert_success response + assert_equal suffix, response.params['statement_descriptor_suffix'] + end + def test_verify_credentials assert @gateway.verify_credentials gateway = StripeGateway.new(login: 'an_unknown_api_key') assert !gateway.verify_credentials 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..8269dcb6213 --- /dev/null +++ b/test/remote/gateways/remote_sum_up_test.rb @@ -0,0 +1,137 @@ +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_successful_purchase_with_partner_id + options = { + partner_id: 'PartnerId', + order_id: SecureRandom.uuid + } + + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal "#{options[:partner_id]}-#{options[:order_id]}", response.params['checkout_reference'] + 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_swipe_checkout_test.rb b/test/remote/gateways/remote_swipe_checkout_test.rb index 6299a506fd2..db3c144ff3e 100644 --- a/test/remote/gateways/remote_swipe_checkout_test.rb +++ b/test/remote/gateways/remote_swipe_checkout_test.rb @@ -8,7 +8,7 @@ def setup @accepted_card = credit_card('1234123412341234') @declined_card = credit_card('1111111111111111') @invalid_card = credit_card('1000000000000000') - @empty_card = credit_card('') + @empty_card = credit_card('') @options = { order_id: '1', @@ -24,7 +24,7 @@ def test_successful_purchase end def test_region_switching - assert response = @gateway.purchase(@amount, @accepted_card, @options.merge(:region => 'CA')) + assert response = @gateway.purchase(@amount, @accepted_card, @options.merge(region: 'CA')) assert_success response assert_equal 'Transaction approved', response.message end diff --git a/test/remote/gateways/remote_tns_test.rb b/test/remote/gateways/remote_tns_test.rb index cc1aa80efa1..f515c9854ff 100644 --- a/test/remote/gateways/remote_tns_test.rb +++ b/test/remote/gateways/remote_tns_test.rb @@ -1,15 +1,14 @@ require 'test_helper' class RemoteTnsTest < Test::Unit::TestCase - def setup TnsGateway.ssl_strict = false # Sandbox has an improperly installed cert @gateway = TnsGateway.new(fixtures(:tns)) @amount = 100 - @credit_card = credit_card('5123456789012346') - @ap_credit_card = credit_card('5424180279791732', month: 05, year: 2017, verification_value: 222) - @declined_card = credit_card('4000300011112220') + @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 = { order_id: generate_unique_id, @@ -37,7 +36,7 @@ def test_successful_purchase_sans_options def test_successful_purchase_with_more_options more_options = @options.merge({ ip: '127.0.0.1', - email: 'joe@example.com', + email: 'joe@example.com' }) assert response = @gateway.purchase(@amount, @credit_card, @options.merge(more_options)) @@ -45,6 +44,18 @@ def test_successful_purchase_with_more_options assert_equal 'Succeeded', response.message end + # This requires a test account flagged for pay/purchase mode. + # The primary test account (TESTSPREEDLY01) is not flagged for this mode. + # This was initially tested with a private account. + def test_successful_purchase_in_pay_mode + gateway = TnsGateway.new(fixtures(:tns_pay_mode).merge(region: 'europe')) + + assert response = gateway.purchase(@amount, @credit_card, @options.merge(currency: 'GBP', pay_mode: true)) + assert_success response + assert_equal 'Succeeded', response.message + assert_equal 'CAPTURED', response.params['order']['status'] + end + def test_successful_purchase_with_region @gateway = TnsGateway.new(fixtures(:tns_ap).merge(region: 'asia_pacific')) @@ -56,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 @@ -73,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 @@ -104,9 +115,9 @@ def test_successful_verify def test_invalid_login gateway = TnsGateway.new( - :userid => 'nosuch', - :password => 'thing' - ) + userid: 'nosuch', + password: 'thing' + ) response = gateway.authorize(@amount, @credit_card, @options) assert_failure response assert_equal 'ERROR - INVALID_REQUEST - Invalid credentials.', response.message @@ -123,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_trans_first_test.rb b/test/remote/gateways/remote_trans_first_test.rb index 3b19be25327..2202d87b4e8 100644 --- a/test/remote/gateways/remote_trans_first_test.rb +++ b/test/remote/gateways/remote_trans_first_test.rb @@ -1,7 +1,6 @@ require 'test_helper' class RemoteTransFirstTest < Test::Unit::TestCase - def setup @gateway = TransFirstGateway.new(fixtures(:trans_first)) @@ -9,9 +8,9 @@ def setup @check = check @amount = 1201 @options = { - :order_id => generate_unique_id, - :invoice => 'ActiveMerchant Sale', - :billing_address => address + order_id: generate_unique_id, + invoice: 'ActiveMerchant Sale', + billing_address: address } end @@ -78,7 +77,7 @@ def test_successful_void assert_success void end - # Refunds can only be successfully run on settled transactions which take 24 hours + # Refunds can only be successfully run on settled transactions which take 24 hours # def test_successful_refund # assert purchase = @gateway.purchase(@amount, @credit_card, @options) # assert_success purchase @@ -107,8 +106,8 @@ def test_successful_refund_with_echeck def test_invalid_login gateway = TransFirstGateway.new( - :login => '', - :password => '' + login: '', + password: '' ) assert response = gateway.purchase(1100, @credit_card, @options) assert_failure response diff --git a/test/remote/gateways/remote_trans_first_transaction_express_test.rb b/test/remote/gateways/remote_trans_first_transaction_express_test.rb index ed32f7e9f1d..b2b8eed63d6 100644 --- a/test/remote/gateways/remote_trans_first_transaction_express_test.rb +++ b/test/remote/gateways/remote_trans_first_transaction_express_test.rb @@ -1,13 +1,12 @@ require 'test_helper' class RemoteTransFirstTransactionExpressTest < Test::Unit::TestCase - def setup @gateway = TransFirstTransactionExpressGateway.new(fixtures(:trans_first_transaction_express)) @amount = 100 @declined_amount = 21 - @credit_card = credit_card('4485896261017708') + @credit_card = credit_card('4485896261017708', verification_value: 999) @check = check billing_address = address({ @@ -16,14 +15,14 @@ def setup city: 'Broomfield', state: 'CO', zip: '85284', - phone: '(333) 444-5555', + phone: '(333) 444-5555' }) @options = { order_id: generate_unique_id, company_name: 'Acme', title: 'QA Manager', - billing_address: billing_address, + billing_address:, shipping_address: billing_address, email: 'example@example.com', description: 'Store Purchase' @@ -45,7 +44,7 @@ def test_successful_purchase assert_equal 'Street address does not match, but 5-digit postal code matches.', response.avs_result['message'] assert_equal 'CVV matches', response.cvv_result['message'] end - + def test_successful_purchase_no_avs options = @options.dup options[:shipping_address] = nil @@ -59,12 +58,12 @@ def test_successful_purchase_with_only_required options = @options.dup options[:shipping_address] = { address1: '450 Main', - zip: '85284', + zip: '85284' } options[:billing_address] = { address1: '450 Main', - zip: '85284', + zip: '85284' } response = @gateway.purchase(@amount, @credit_card, options) @@ -76,15 +75,34 @@ def test_successful_purchase_with_only_required assert_equal 'CVV matches', response.cvv_result['message'] end + def test_successful_purchase_without_address2 + # Test that empty string in `address2` doesn't cause transaction failure + options = @options.dup + options[:shipping_address] = { + address1: '450 Main', + address2: '', + zip: '85284' + } + + options[:billing_address] = { + address1: '450 Main', + address2: '', + zip: '85284' + } + + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Succeeded', response.message + end def test_successful_purchase_without_cvv credit_card_opts = { - :number => 4485896261017708, - :month => Date.new((Time.now.year + 1), 9, 30).month, - :year => Date.new((Time.now.year + 1), 9, 30).year, - :first_name => 'Longbob', - :last_name => 'Longsen', - :brand => 'visa' + number: 4485896261017708, + month: Date.new((Time.now.year + 1), 9, 30).month, + year: Date.new((Time.now.year + 1), 9, 30).year, + first_name: 'Longbob', + last_name: 'Longsen', + brand: 'visa' } credit_card = CreditCard.new(credit_card_opts) @@ -95,13 +113,39 @@ def test_successful_purchase_without_cvv def test_successful_purchase_with_empty_string_cvv credit_card_opts = { - :number => 4485896261017708, - :month => Date.new((Time.now.year + 1), 9, 30).month, - :year => Date.new((Time.now.year + 1), 9, 30).year, - :first_name => 'Longbob', - :last_name => 'Longsen', - :verification_value => '', - :brand => 'visa' + number: 4485896261017708, + month: Date.new((Time.now.year + 1), 9, 30).month, + year: Date.new((Time.now.year + 1), 9, 30).year, + first_name: 'Longbob', + last_name: 'Longsen', + verification_value: '', + brand: 'visa' + } + + credit_card = CreditCard.new(credit_card_opts) + response = @gateway.purchase(@amount, credit_card, @options) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_without_name + credit_card_opts = { + number: 4485896261017708, + month: Date.new((Time.now.year + 1), 9, 30).month, + year: Date.new((Time.now.year + 1), 9, 30).year, + first_name: '', + last_name: '' + } + + credit_card = CreditCard.new(credit_card_opts) + response = @gateway.purchase(@amount, credit_card, @options) + assert_success response + assert_equal 'Succeeded', response.message + + credit_card_opts = { + number: 4485896261017708, + month: Date.new((Time.now.year + 1), 9, 30).month, + year: Date.new((Time.now.year + 1), 9, 30).year } credit_card = CreditCard.new(credit_card_opts) @@ -233,7 +277,7 @@ def test_failed_refund def test_successful_refund_with_echeck purchase = @gateway.purchase(@amount, @check, @options) assert_success purchase - assert_match /purchase_echeck/, purchase.authorization + assert_match(/purchase_echeck/, purchase.authorization) refund = @gateway.refund(@amount, purchase.authorization) assert_success refund @@ -349,4 +393,12 @@ def test_transcript_scrubbing assert_scrubbed(@gateway.options[:gateway_id], clean_transcript) assert_scrubbed(@gateway.options[:reg_key], clean_transcript) end + + def test_transcript_scrubbing_account_number + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @check, @options) + end + clean_transcript = @gateway.scrub(transcript) + assert_scrubbed(@check.account_number, clean_transcript) + end end diff --git a/test/remote/gateways/remote_transact_pro_test.rb b/test/remote/gateways/remote_transact_pro_test.rb index 5e5c428ae29..504a5b65d29 100644 --- a/test/remote/gateways/remote_transact_pro_test.rb +++ b/test/remote/gateways/remote_transact_pro_test.rb @@ -57,7 +57,7 @@ def test_partial_capture assert_success auth assert_raise(ArgumentError) do - @gateway.capture(@amount-1, auth.authorization) + @gateway.capture(@amount - 1, auth.authorization) end end @@ -80,7 +80,7 @@ def test_partial_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - refund = @gateway.refund(@amount-1, purchase.authorization) + refund = @gateway.refund(@amount - 1, purchase.authorization) assert_success refund assert_equal 'Refund Success', refund.message end @@ -89,7 +89,7 @@ def test_failed_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - refund = @gateway.refund(@amount+1, purchase.authorization) + refund = @gateway.refund(@amount + 1, purchase.authorization) assert_failure refund end diff --git a/test/remote/gateways/remote_transax_test.rb b/test/remote/gateways/remote_transax_test.rb index 1e7bbc00399..7685274b61d 100644 --- a/test/remote/gateways/remote_transax_test.rb +++ b/test/remote/gateways/remote_transax_test.rb @@ -5,15 +5,15 @@ def setup @gateway = TransaxGateway.new(fixtures(:transax)) @amount = 100 - @credit_card = credit_card('4111111111111111', :year => 10, :month => 10) + @credit_card = credit_card('4111111111111111', year: 10, month: 10) @declined_card = credit_card(0xDEADBEEF_0000.to_s) @check = check() @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase' + order_id: '1', + billing_address: address, + description: 'Store Purchase' } end @@ -76,13 +76,13 @@ def test_purchase_and_update assert_success response assert_equal 'This transaction has been approved', response.message assert response.authorization - assert update = @gateway.amend(response.authorization, :shipping_carrier => 'usps') + assert update = @gateway.amend(response.authorization, shipping_carrier: 'usps') assert_equal 'This transaction has been approved', update.message assert_success update end def test_successful_purchase_with_sku - @options['product_sku_#']='123456' + @options['product_sku_#'] = '123456' assert response = @gateway.purchase(@amount, @credit_card, @options) assert_success response assert_equal 'This transaction has been approved', response.message @@ -115,9 +115,9 @@ def test_failed_verify def test_invalid_login gateway = TransaxGateway.new( - :login => '', - :password => '' - ) + login: '', + password: '' + ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal 'Invalid Username', response.message diff --git a/test/remote/gateways/remote_trexle_test.rb b/test/remote/gateways/remote_trexle_test.rb index cd8f5360315..a307c63f31d 100644 --- a/test/remote/gateways/remote_trexle_test.rb +++ b/test/remote/gateways/remote_trexle_test.rb @@ -123,7 +123,7 @@ def test_store_and_update assert_not_nil response.authorization assert_equal @credit_card.year, response.params['response']['card']['expiry_year'] - response = @gateway.update(response.authorization, @credit_card, address: address) + response = @gateway.update(response.authorization, @credit_card, address:) assert_success response assert_not_nil response.authorization assert_equal @credit_card.year, response.params['response']['card']['expiry_year'] @@ -163,7 +163,7 @@ def test_transcript_scrubbing @gateway.purchase(@amount, @credit_card, @options) end clean_transcript = @gateway.scrub(transcript) - + assert_scrubbed(@credit_card.number, clean_transcript) assert_scrubbed(@credit_card.verification_value.to_s, clean_transcript) end diff --git a/test/remote/gateways/remote_trust_commerce_test.rb b/test/remote/gateways/remote_trust_commerce_test.rb index 8fd3215f4f1..62b3b08588c 100644 --- a/test/remote/gateways/remote_trust_commerce_test.rb +++ b/test/remote/gateways/remote_trust_commerce_test.rb @@ -5,6 +5,8 @@ def setup @gateway = TrustCommerceGateway.new(fixtures(:trust_commerce)) @credit_card = credit_card('4111111111111111') + @declined_credit_card = credit_card('4111111111111112') + @check = check({ account_number: 55544433221, routing_number: 789456124 }) @amount = 100 @@ -12,27 +14,36 @@ def setup @invalid_verification_value = '1234' @valid_address = { - :address1 => '123 Test St.', - :address2 => nil, - :city => 'Somewhere', - :state => 'CA', - :zip => '90001' + address1: '123 Test St.', + address2: nil, + city: 'Somewhere', + state: 'CA', + zip: '90001' } @invalid_address = { - :address1 => '187 Apple Tree Lane.', - :address2 => nil, - :city => 'Woodside', - :state => 'CA', - :zip => '94062' + address1: '187 Apple Tree Lane.', + address2: nil, + city: 'Woodside', + state: 'CA', + zip: '94062' + } + + # The Trust Commerce API does not return anything different when custom fields are present. + # To confirm that the field values are being stored with the transactions, add a custom + # field in your account in the Vault UI, then examine the transactions after running the + # test suite. + custom_fields = { + 'customfield1' => 'test1' } @options = { - :ip => '10.10.10.10', - :order_id => '#1000.1', - :email => 'cody@example.com', - :billing_address => @valid_address, - :shipping_address => @valid_address + ip: '10.10.10.10', + order_id: '#1000.1', + email: 'cody@example.com', + billing_address: @valid_address, + shipping_address: @valid_address, + custom_fields: } end @@ -41,9 +52,9 @@ def test_bad_login assert response = @gateway.purchase(@amount, @credit_card, @options) assert_equal Response, response.class - assert_equal ['error', - 'offenders', - 'status'], response.params.keys.sort + assert_equal %w[error + offenders + status], response.params.keys.sort assert_match %r{A field was improperly formatted, such as non-digit characters in a number field}, response.message @@ -59,6 +70,14 @@ def test_successful_purchase_with_avs assert !response.authorization.blank? end + def test_successful_purchase_with_check + assert response = @gateway.purchase(@amount, @check, @options) + assert_match %r{The transaction was successful}, response.message + + assert_success response + assert !response.authorization.blank? + end + def test_unsuccessful_purchase_with_invalid_cvv @credit_card.verification_value = @invalid_verification_value assert response = @gateway.purchase(@amount, @credit_card, @options) @@ -69,14 +88,26 @@ def test_unsuccessful_purchase_with_invalid_cvv end def test_purchase_with_avs_for_invalid_address - assert response = @gateway.purchase(@amount, @credit_card, @options.update(:billing_address => @invalid_address)) + assert response = @gateway.purchase(@amount, @credit_card, @options.update(billing_address: @invalid_address)) assert_equal 'N', response.params['avs'] assert_match %r{The transaction was successful}, response.message assert_success response end + # Requires enabling the setting: 'Allow voids to process or settle on processing node' in the Trust Commerce vault UI + def test_purchase_and_void + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + void = @gateway.void(purchase.authorization) + assert_success void + assert_equal 'The transaction was successful', void.message + assert_equal 'accepted', void.params['status'] + assert void.params['transid'] + end + def test_successful_authorize_with_avs - assert response = @gateway.authorize(@amount, @credit_card, :billing_address => @valid_address) + assert response = @gateway.authorize(@amount, @credit_card, billing_address: @valid_address) assert_equal 'Y', response.avs_result['code'] assert_match %r{The transaction was successful}, response.message @@ -93,7 +124,7 @@ def test_unsuccessful_authorize_with_invalid_cvv end def test_authorization_with_avs_for_invalid_address - assert response = @gateway.authorize(@amount, @credit_card, @options.update(:billing_address => @invalid_address)) + assert response = @gateway.authorize(@amount, @credit_card, @options.update(billing_address: @invalid_address)) assert_equal 'N', response.params['avs'] assert_match %r{The transaction was successful}, response.message assert_success response @@ -128,36 +159,103 @@ def test_successful_credit assert_success response end - def test_store_failure + def test_successful_check_refund + purchase = @gateway.purchase(@amount, @check, @options) + + assert response = @gateway.refund(@amount, purchase.authorization) + + assert_match %r{The transaction was successful}, response.message + assert_success response + end + + def test_successful_store assert response = @gateway.store(@credit_card) assert_equal Response, response.class - assert_match %r{The merchant can't accept data passed in this field}, response.message - assert_failure response + assert_equal 'approved', response.params['status'] + assert_match %r{The transaction was successful}, response.message + end + + def test_failed_store + assert response = @gateway.store(@declined_credit_card) + + 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('testme') + assert response = @gateway.unstore('does-not-exist') - assert_match %r{The merchant can't accept data passed in this field}, response.message + assert_match %r{A field was longer or shorter than the server allows}, response.message assert_failure response end - def test_recurring_failure - assert response = @gateway.recurring(@amount, @credit_card, :periodicity => :weekly) + 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 - assert_match %r{The merchant can't accept data passed in this field}, response.message + 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) + + assert_match %r{The transaction was successful}, response.message + assert_success response + end + + def test_failed_recurring + assert response = @gateway.recurring(@amount, @declined_credit_card, periodicity: :weekly) + + assert_bad_data_response(response) + end + def test_transcript_scrubbing @credit_card.verification_value = @invalid_verification_value transcript = capture_transcript(@gateway) do - @gateway.purchase(@amount, @credit_card, @options) + @gateway.purchase(@amount, @credit_card, @options) end clean_transcript = @gateway.scrub(transcript) - + assert_scrubbed(@credit_card.number, clean_transcript) assert_scrubbed(@credit_card.verification_value.to_s, clean_transcript) end + + def test_transcript_scrubbing_echeck + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @check, @options) + end + clean_transcript = @gateway.scrub(transcript) + + assert_scrubbed(@check.account_number, clean_transcript) + end + + private + + def assert_bad_data_response(response) + assert_equal Response, response.class + assert_equal 'A field was improperly formatted, such as non-digit characters in a number field', response.message + assert_equal 'baddata', response.params['status'] + end end diff --git a/test/remote/gateways/remote_usa_epay_advanced_test.rb b/test/remote/gateways/remote_usa_epay_advanced_test.rb index 2f7bc1fc264..edcb26491d9 100644 --- a/test/remote/gateways/remote_usa_epay_advanced_test.rb +++ b/test/remote/gateways/remote_usa_epay_advanced_test.rb @@ -8,102 +8,90 @@ def setup @amount = 2111 @credit_card = ActiveMerchant::Billing::CreditCard.new( - :number => '4000100011112224', - :month => 9, - :year => Time.now.year + 1, - :brand => 'visa', - :verification_value => '123', - :first_name => 'Fred', - :last_name => 'Flintstone' + number: '4000100011112224', + month: 9, + year: Time.now.year + 1, + brand: 'visa', + verification_value: '123', + first_name: 'Fred', + last_name: 'Flintstone' ) @bad_credit_card = ActiveMerchant::Billing::CreditCard.new( - :number => '4000300011112220', - :month => 9, - :year => 14, - :brand => 'visa', - :verification_value => '999', - :first_name => 'Fred', - :last_name => 'Flintstone' + number: '4000300011112220', + month: 9, + year: 14, + brand: 'visa', + verification_value: '999', + first_name: 'Fred', + last_name: 'Flintstone' ) @check = ActiveMerchant::Billing::Check.new( - :account_number => '123456789', - :routing_number => '120450780', - :account_type => 'checking', - :first_name => 'Fred', - :last_name => 'Flintstone' + account_number: '123456789', + routing_number: '120450780', + account_type: 'checking', + first_name: 'Fred', + last_name: 'Flintstone' ) cc_method = [ - {:name => 'My CC', :sort => 5, :method => @credit_card}, - {:name => 'Other CC', :sort => 12, :method => @credit_card} + { name: 'My CC', sort: 5, method: @credit_card }, + { name: 'Other CC', sort: 12, method: @credit_card } ] @options = { - :client_ip => '127.0.0.1', - :billing_address => address, + client_ip: '127.0.0.1', + billing_address: address } @transaction_options = { - :order_id => '1', - :description => 'Store Purchase' + order_id: '1', + description: 'Store Purchase' } @customer_options = { - :id => 123, - :notes => 'Customer note.', - :data => 'complex data', - :url => 'somesite.com', - :payment_methods => cc_method + id: 123, + notes: 'Customer note.', + data: 'complex data', + url: 'somesite.com', + payment_methods: cc_method } @update_customer_options = { - :notes => 'NEW NOTE!' + notes: 'NEW NOTE!' } @add_payment_options = { - :make_default => true, - :payment_method => { - :name => 'My new card.', - :sort => 10, - :method => @credit_card + make_default: true, + payment_method: { + name: 'My new card.', + sort: 10, + method: @credit_card } } @run_transaction_options = { - :payment_method => @credit_card, - :command => 'sale', - :amount => 10000 + payment_method: @credit_card, + command: 'sale', + amount: 10000 } @run_transaction_check_options = { - :payment_method => @check, - :command => 'check', - :amount => 10000 + payment_method: @check, + command: 'check', + amount: 10000 } @run_sale_options = { - :payment_method => @credit_card, - :amount => 5000 + payment_method: @credit_card, + amount: 5000 } @run_check_sale_options = { - :payment_method => @check, - :amount => 2500 + payment_method: @check, + amount: 2500 } - - payment_methods = [ - { - :name => 'My Visa', # optional - :sort => 2, # optional - :method => @credit_card - }, - { - :name => 'My Checking', - :method => @check - } - ] end # Standard Gateway ================================================== @@ -150,10 +138,10 @@ def test_refund def test_invalid_login gateway = UsaEpayAdvancedGateway.new( - :login => '', - :password => '', - :software_id => '' - ) + login: '', + password: '', + software_id: '' + ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal 'Invalid software ID', response.message @@ -170,7 +158,7 @@ def test_update_customer response = @gateway.add_customer(@options.merge(@customer_options)) customer_number = response.params['add_customer_return'] - @options.merge!(@update_customer_options.merge!(:customer_number => customer_number)) + @options.merge!(@update_customer_options.merge!(customer_number:)) response = @gateway.update_customer(@options) assert response.params['update_customer_return'] end @@ -179,7 +167,7 @@ def test_quick_update_customer response = @gateway.add_customer(@options.merge(@customer_options)) customer_number = response.params['add_customer_return'] - response = @gateway.quick_update_customer({customer_number: customer_number, update_data: @update_customer_options}) + response = @gateway.quick_update_customer({ customer_number:, update_data: @update_customer_options }) assert response.params['quick_update_customer_return'] end @@ -187,10 +175,10 @@ def test_enable_disable_customer response = @gateway.add_customer(@options.merge(@customer_options)) customer_number = response.params['add_customer_return'] - response = @gateway.enable_customer(:customer_number => customer_number) + response = @gateway.enable_customer(customer_number:) assert response.params['enable_customer_return'] - response = @gateway.disable_customer(:customer_number => customer_number) + response = @gateway.disable_customer(customer_number:) assert response.params['disable_customer_return'] end @@ -198,7 +186,7 @@ def test_add_customer_payment_method response = @gateway.add_customer(@options.merge(@customer_options)) customer_number = response.params['add_customer_return'] - @options.merge!(:customer_number => customer_number).merge!(@add_payment_options) + @options.merge!(customer_number:).merge!(@add_payment_options) response = @gateway.add_customer_payment_method(@options) assert response.params['add_customer_payment_method_return'] end @@ -208,7 +196,7 @@ def test_add_customer_payment_method_verify customer_number = response.params['add_customer_return'] @add_payment_options[:payment_method][:method] = @bad_credit_card - @options.merge!(:customer_number => customer_number, :verify => true).merge!(@add_payment_options) + @options.merge!(customer_number:, verify: true).merge!(@add_payment_options) response = @gateway.add_customer_payment_method(@options) assert response.params['faultstring'] end @@ -217,7 +205,7 @@ def test_get_customer_payment_methods response = @gateway.add_customer(@options.merge(@customer_options)) customer_number = response.params['add_customer_return'] - response = @gateway.get_customer_payment_methods(:customer_number => customer_number) + response = @gateway.get_customer_payment_methods(customer_number:) assert response.params['get_customer_payment_methods_return']['item'] end @@ -225,10 +213,10 @@ def test_get_customer_payment_method response = @gateway.add_customer(@options.merge(@customer_options)) customer_number = response.params['add_customer_return'] - response = @gateway.get_customer_payment_methods(:customer_number => customer_number) + response = @gateway.get_customer_payment_methods(customer_number:) id = response.params['get_customer_payment_methods_return']['item'][0]['method_id'] - response = @gateway.get_customer_payment_method(:customer_number => customer_number, :method_id => id) + response = @gateway.get_customer_payment_method(customer_number:, method_id: id) assert response.params['get_customer_payment_method_return'] end @@ -236,12 +224,12 @@ def test_update_customer_payment_method response = @gateway.add_customer(@options.merge(@customer_options)) customer_number = response.params['add_customer_return'] - @options.merge!(:customer_number => customer_number).merge!(@add_payment_options) + @options.merge!(customer_number:).merge!(@add_payment_options) response = @gateway.add_customer_payment_method(@options) payment_method_id = response.params['add_customer_payment_method_return'] - update_payment_options = @add_payment_options[:payment_method].merge(:method_id => payment_method_id, - :name => 'Updated Card.') + update_payment_options = @add_payment_options[:payment_method].merge(method_id: payment_method_id, + name: 'Updated Card.') response = @gateway.update_customer_payment_method(update_payment_options) assert response.params['update_customer_payment_method_return'] @@ -251,11 +239,11 @@ def test_delete_customer_payment_method response = @gateway.add_customer(@options.merge(@customer_options)) customer_number = response.params['add_customer_return'] - @options.merge!(:customer_number => customer_number).merge!(@add_payment_options) + @options.merge!(customer_number:).merge!(@add_payment_options) response = @gateway.add_customer_payment_method(@options) id = response.params['add_customer_payment_method_return'] - response = @gateway.delete_customer_payment_method(:customer_number => customer_number, :method_id => id) + response = @gateway.delete_customer_payment_method(customer_number:, method_id: id) assert response.params['delete_customer_payment_method_return'] end @@ -263,7 +251,7 @@ def test_delete_customer response = @gateway.add_customer(@options.merge(@customer_options)) customer_number = response.params['add_customer_return'] - response = @gateway.delete_customer(:customer_number => customer_number) + response = @gateway.delete_customer(customer_number:) assert response.params['delete_customer_return'] end @@ -271,8 +259,8 @@ def test_run_customer_transaction response = @gateway.add_customer(@options.merge(@customer_options)) customer_number = response.params['add_customer_return'] - response = @gateway.run_customer_transaction(:customer_number => customer_number,# :method_id => 0, # optional - :command => 'Sale', :amount => 3000) + response = @gateway.run_customer_transaction(customer_number:, # :method_id => 0, # optional + command: 'Sale', amount: 3000) assert response.params['run_customer_transaction_return'] end @@ -324,7 +312,7 @@ def test_run_check_credit # TODO get offline auth_code? def test_post_auth - @options.merge!(:authorization_code => 123456) + @options[:authorization_code] = 123456 response = @gateway.post_auth(@options) assert response.params['post_auth_return'] end @@ -334,7 +322,7 @@ def test_capture_transaction response = @gateway.run_auth_only(options) reference_number = response.params['run_auth_only_return']['ref_num'] - options = @options.merge(:reference_number => reference_number) + options = @options.merge(reference_number:) response = @gateway.capture_transaction(options) assert response.params['capture_transaction_return'] end @@ -344,7 +332,7 @@ def test_void_transaction response = @gateway.run_sale(options) reference_number = response.params['run_sale_return']['ref_num'] - options = @options.merge(:reference_number => reference_number) + options = @options.merge(reference_number:) response = @gateway.void_transaction(options) assert response.params['void_transaction_return'] end @@ -354,7 +342,7 @@ def test_refund_transaction response = @gateway.run_sale(options) reference_number = response.params['run_sale_return']['ref_num'] - options = @options.merge(:reference_number => reference_number, :amount => 0) + options = @options.merge(reference_number:, amount: 0) response = @gateway.refund_transaction(options) assert response.params['refund_transaction_return'] end @@ -365,7 +353,7 @@ def test_override_transaction response = @gateway.run_check_sale(options) reference_number = response.params['run_check_sale_return']['ref_num'] - response = @gateway.override_transaction(:reference_number => reference_number, :reason => 'Because I said so') + response = @gateway.override_transaction(reference_number:, reason: 'Because I said so') assert response.params['faultstring'] end @@ -374,7 +362,7 @@ def test_run_quick_sale response = @gateway.run_sale(@options) reference_number = response.params['run_sale_return']['ref_num'] - response = @gateway.run_quick_sale(:reference_number => reference_number, :amount => 9900) + response = @gateway.run_quick_sale(reference_number:, amount: 9900) assert response.params['run_quick_sale_return'] end @@ -383,7 +371,7 @@ def test_run_quick_sale_check response = @gateway.run_check_sale(@options) reference_number = response.params['run_check_sale_return']['ref_num'] - response = @gateway.run_quick_sale(:reference_number => reference_number, :amount => 9900) + response = @gateway.run_quick_sale(reference_number:, amount: 9900) assert response.params['run_quick_sale_return'] end @@ -392,7 +380,7 @@ def test_run_quick_credit response = @gateway.run_sale(@options) reference_number = response.params['run_sale_return']['ref_num'] - response = @gateway.run_quick_credit(:reference_number => reference_number, :amount => 0) + response = @gateway.run_quick_credit(reference_number:, amount: 0) assert response.params['run_quick_credit_return'] end @@ -401,7 +389,7 @@ def test_run_quick_credit_check response = @gateway.run_check_sale(@options) reference_number = response.params['run_check_sale_return']['ref_num'] - response = @gateway.run_quick_credit(:reference_number => reference_number, :amount => 1234) + response = @gateway.run_quick_credit(reference_number:, amount: 1234) assert response.params['run_quick_credit_return'] end @@ -411,7 +399,7 @@ def test_get_transaction response = @gateway.run_sale(@options.merge(@run_sale_options)) reference_number = response.params['run_sale_return']['ref_num'] - response = @gateway.get_transaction(:reference_number => reference_number) + response = @gateway.get_transaction(reference_number:) assert response.params['get_transaction_return'] end @@ -419,7 +407,7 @@ def test_get_transaction_status response = @gateway.run_sale(@options.merge(@run_sale_options)) reference_number = response.params['run_sale_return']['ref_num'] - response = @gateway.get_transaction_status(:reference_number => reference_number) + response = @gateway.get_transaction_status(reference_number:) assert response.params['get_transaction_status_return'] end @@ -427,11 +415,11 @@ def test_get_transaction_custom response = @gateway.run_sale(@options.merge(@run_sale_options)) reference_number = response.params['run_sale_return']['ref_num'] - response = @gateway.get_transaction_custom(:reference_number => reference_number, - :fields => ['Response.StatusCode', 'Response.Status']) + response = @gateway.get_transaction_custom(reference_number:, + fields: ['Response.StatusCode', 'Response.Status']) assert response.params['get_transaction_custom_return'] - response = @gateway.get_transaction_custom(:reference_number => reference_number, - :fields => ['Response.StatusCode']) + response = @gateway.get_transaction_custom(reference_number:, + fields: ['Response.StatusCode']) assert response.params['get_transaction_custom_return'] end @@ -439,7 +427,7 @@ def test_get_check_trace response = @gateway.run_check_sale(@options.merge(@run_check_sale_options)) reference_number = response.params['run_check_sale_return']['ref_num'] - response = @gateway.get_check_trace(:reference_number => reference_number) + response = @gateway.get_check_trace(reference_number:) assert response.params['get_check_trace_return'] end diff --git a/test/remote/gateways/remote_usa_epay_transaction_test.rb b/test/remote/gateways/remote_usa_epay_transaction_test.rb index ac553b144de..6426f7f85fc 100644 --- a/test/remote/gateways/remote_usa_epay_transaction_test.rb +++ b/test/remote/gateways/remote_usa_epay_transaction_test.rb @@ -3,11 +3,13 @@ class RemoteUsaEpayTransactionTest < Test::Unit::TestCase def setup @gateway = UsaEpayTransactionGateway.new(fixtures(:usa_epay)) + @gateway_with_pin = UsaEpayTransactionGateway.new(fixtures(:usa_epay_with_pin)) @credit_card = credit_card('4000100011112224') @declined_card = credit_card('4000300011112220') @credit_card_with_track_data = credit_card_with_track_data('4000100011112224') + @invalid_transaction_card = credit_card('4000300511112225') @check = check - @options = { :billing_address => address(:zip => '27614', :state => 'NC'), :shipping_address => address } + @options = { billing_address: address(zip: '27614', state: 'NC'), shipping_address: address } @amount = 100 end @@ -17,6 +19,26 @@ def test_successful_purchase assert_success response end + def test_successful_purchase_with_store + response = @gateway.store(@credit_card, @options) + assert_success response + + payment_token = response.authorization + assert response = @gateway.purchase(@amount, payment_token, @options) + assert_equal 'Success', response.message + assert_success response + end + + def test_successful_authorize_with_store + response = @gateway.store(@credit_card, @options) + assert_success response + + payment_token = response.authorization + assert response = @gateway.authorize(@amount, payment_token, @options) + assert_equal 'Success', response.message + assert_success response + end + def test_successful_purchase_with_track_data assert response = @gateway.purchase(@amount, @credit_card_with_track_data, @options) assert_equal 'Success', response.message @@ -29,6 +51,13 @@ def test_successful_purchase_with_echeck assert_success response end + def test_successful_purchase_with_echeck_and_extra_options + extra_options = @options.merge(check_format: 'ARC', account_type: 'savings') + assert response = @gateway.purchase(@amount, @check, extra_options) + assert_equal 'Success', response.message + assert_success response + end + def test_successful_authorization_with_manual_entry @credit_card.manual_entry = true assert response = @gateway.authorize(@amount, @credit_card, @options) @@ -36,36 +65,90 @@ def test_successful_authorization_with_manual_entry assert_success response end - def test_successful_purchase_with_manual_entry + def test_successful_purchase_with_manual_entry @credit_card.manual_entry = true assert response = @gateway.purchase(@amount, @credit_card, @options) assert_equal 'Success', response.message assert_success response - end + end def test_successful_purchase_with_extra_details - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(:order_id => generate_unique_id, :description => 'socool')) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(order_id: generate_unique_id, description: 'socool')) assert_equal 'Success', response.message assert_success response end def test_successful_purchase_with_extra_test_mode - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(:test_mode => true)) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(test_mode: true)) assert_equal 'Success', response.message assert_success response end def test_successful_purchase_with_email_receipt - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(:email => 'hank@hill.com',:cust_receipt => 'Yes')) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(email: 'hank@hill.com', cust_receipt: 'Yes')) + assert_equal 'Success', response.message + assert_success response + end + + def test_successful_purchase_with_recurring_fields + recurring_fields = [ + add_customer: true, + schedule: 'quarterly', + bill_source_key: 'bill source key', + bill_amount: 123, + num_left: 5, + start: '20501212', + recurring_receipt: true + ] + + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(recurring_fields:)) assert_equal 'Success', response.message assert_success response end + def test_successful_purchase_with_custom_fields + custom_fields = { + 1 => 'multi', + 2 => 'pass', + 3 => 'korben', + 4 => 'dallas' + } + + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(custom_fields:)) + assert_equal 'Success', response.message + assert_success response + end + + def test_successful_purchase_with_line_items + line_items = [ + { sku: 'abc123', cost: 119, quantity: 1 }, + { sku: 'def456', cost: 200, quantity: 2, name: 'an item' } + ] + + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(line_items:)) + assert_equal 'Success', response.message + assert_success response + end + + def test_successful_purchase_with_pin + assert response = @gateway_with_pin.purchase(@amount, @credit_card, @options) + assert_equal 'Success', response.message + assert_success response + end + + def test_unsuccessful_purchase_with_bad_pin + gateway = UsaEpayTransactionGateway.new(fixtures(:usa_epay_with_pin).merge({ pin: 'bad_pin' })) + + assert response = gateway.purchase(@amount, @credit_card, @options) + assert_equal 'Transaction authentication failed', response.message + assert_failure response + end + def test_unsuccessful_purchase # For some reason this will fail with "You have tried this card too # many times, please contact merchant" unless a unique order id is # passed. - assert response = @gateway.purchase(@amount, @declined_card, @options.merge(:order_id => generate_unique_id)) + assert response = @gateway.purchase(@amount, @declined_card, @options.merge(order_id: generate_unique_id)) assert_failure response assert_match(/declined/i, response.message) assert Gateway::STANDARD_ERROR_CODE[:card_declined], response.error_code @@ -161,7 +244,7 @@ def test_unsuccessful_void_release end def test_invalid_key - gateway = UsaEpayTransactionGateway.new(:login => '') + gateway = UsaEpayTransactionGateway.new(login: '') assert response = gateway.purchase(@amount, @credit_card, @options) assert_equal 'Specified source key not found.', response.message assert_failure response @@ -206,4 +289,10 @@ def test_transcript_scrubbing assert_scrubbed(@check.account_number, transcript) assert_scrubbed(@gateway.options[:login], transcript) end + + def test_processing_error + assert response = @gateway.purchase(@amount, @invalid_transaction_card, @options) + assert_equal 'processing_error', response.error_code + assert_failure response + end end diff --git a/test/remote/gateways/remote_vanco_test.rb b/test/remote/gateways/remote_vanco_test.rb index 077b9b1a248..c35cdf02ad9 100644 --- a/test/remote/gateways/remote_vanco_test.rb +++ b/test/remote/gateways/remote_vanco_test.rb @@ -1,6 +1,8 @@ require 'test_helper' class RemoteVancoTest < Test::Unit::TestCase + SECONDS_PER_DAY = 3600 * 24 + def setup @gateway = VancoGateway.new(fixtures(:vanco)) @@ -34,6 +36,42 @@ def test_successful_purchase_with_ip_address assert_equal 'Success', response.message end + def test_successful_purchase_with_existing_session_id + previous_login_response = @gateway.send(:login) + + response = @gateway.purchase( + @amount, + @credit_card, + @options.merge( + session_id: + { + id: previous_login_response.params['response_sessionid'], + created_at: Time.parse(previous_login_response.params['auth_requesttime']) + } + ) + ) + assert_success response + assert_equal 'Success', response.message + end + + def test_successful_purchase_with_expired_session_id + two_days_from_now = Time.now - (2 * SECONDS_PER_DAY) + previous_login_response = @gateway.send(:login) + + response = @gateway.purchase( + @amount, + @credit_card, + @options.merge( + session_id: { + id: previous_login_response.params['response_sessionid'], + created_at: two_days_from_now + } + ) + ) + assert_success response + assert_equal 'Success', response.message + end + def test_successful_purchase_sans_minimal_options response = @gateway.purchase(@amount, @credit_card) assert_success response @@ -73,7 +111,7 @@ def test_partial_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - refund = @gateway.refund(@amount-1, purchase.authorization) + refund = @gateway.refund(@amount - 1, purchase.authorization) assert_success refund end @@ -81,7 +119,7 @@ def test_failed_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - refund = @gateway.refund(@amount+500, purchase.authorization) + refund = @gateway.refund(@amount + 500, purchase.authorization) assert_failure refund assert_match(/Amount Cannot Be Greater Than/, refund.message) end @@ -97,6 +135,15 @@ def test_transcript_scrubbing assert_scrubbed(@gateway.options[:password], transcript) end + def test_account_number_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @check, @options) + end + clean_transcript = @gateway.scrub(transcript) + + assert_scrubbed(@check.account_number, clean_transcript) + end + def test_invalid_login gateway = VancoGateway.new( user_id: 'unknown_id', 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_verifi_test.rb b/test/remote/gateways/remote_verifi_test.rb index d9579c15752..8cb815df077 100644 --- a/test/remote/gateways/remote_verifi_test.rb +++ b/test/remote/gateways/remote_verifi_test.rb @@ -10,9 +10,9 @@ def setup # Replace with your login and password for the Verifi test environment @options = { - :order_id => '37', - :email => 'test@example.com', - :billing_address => address + order_id: '37', + email: 'test@example.com', + billing_address: address } @amount = 100 @@ -58,15 +58,6 @@ def test_authorization_and_capture assert_equal 'Transaction was Approved', capture.message end - def test_authorization_and_void - assert authorization = @gateway.authorize(@amount, @credit_card, @options) - assert_success authorization - assert authorization - assert void = @gateway.void(authorization.authorization, @options) - assert_success void - assert_equal 'Transaction was Approved', void.message - end - # Credits are not enabled on test accounts, so this should always fail def test_credit assert response = @gateway.credit(@amount, @credit_card, @options) @@ -94,8 +85,8 @@ def test_purchase_and_credit def test_bad_login gateway = VerifiGateway.new( - :login => 'X', - :password => 'Y' + login: 'X', + password: 'Y' ) assert response = gateway.purchase(@amount, @credit_card, @options) diff --git a/test/remote/gateways/remote_versa_pay_test.rb b/test/remote/gateways/remote_versa_pay_test.rb new file mode 100644 index 00000000000..f9ebe68ad30 --- /dev/null +++ b/test/remote/gateways/remote_versa_pay_test.rb @@ -0,0 +1,317 @@ +require 'test_helper' + +class RemoteVersaPayTest < Test::Unit::TestCase + def setup + @gateway = VersaPayGateway.new(fixtures(:versa_pay)) + @bad_gateway = VersaPayGateway.new(api_token: 'bad_token', api_key: 'bad_key') + + @amount = 500 + @credit_card = credit_card('4895281000000006', verification_value: '123', month: 12, year: Time.now.year + 1) + @credit_card_match_cvv = credit_card('4895281000000006', verification_value: '234', month: 12, year: Time.now.year + 1) + @credit_card_not_match_cvv = credit_card('4895281000000006', verification_value: '345', month: 12, year: Time.now.year + 1) + @decline_credit_card = credit_card('4264280001234500') + @no_valid_date_credit_card = credit_card('4895281000000006', month: 9, year: Time.now.year - 1) + + @options = { + order_id: 'ABCDF', + description: 'An authorize', + email: 'john.smith@test.com', + order_number: SecureRandom.uuid, + billing_address: address # billing address is required for all transactions + } + + @options_with_shipping = @options.dup.merge({ shipping_address: address }) + end + + def test_successful_authorize + response = @gateway.authorize(@amount, @credit_card, @options) + + assert_success response + assert_equal response.message, 'Succeeded' + assert_equal @options[:order_id], response.params['order'] + assert_equal response.authorization, response.params['transaction'] + assert_equal response.params['transactions'][0]['action'], 'authorize' + assert_nil response.error_code + end + + def test_successful_authorize_with_shipping_address + response = @gateway.authorize(@amount, @credit_card, @options_with_shipping) + + assert_success response + assert_equal response.message, 'Succeeded' + assert_equal @options[:order_id], response.params['order'] + assert_equal response.authorization, response.params['transaction'] + assert_equal response.params['transactions'][0]['action'], 'authorize' + end + + def test_failed_authorize_declined_credit_card + response = @gateway.authorize(@amount, @decline_credit_card, @options) + + assert_failure response + assert_equal response.message, 'gateway_error_message: DECLINED | gateway_response_errors: [gateway - DECLINED]' + assert_equal @options[:order_id], response.params['order'] + assert_equal response.authorization, response.params['transaction'] + assert_equal response.params['transactions'][0]['action'], 'verify' + + assert_equal response.error_code, '567.005' + end + + def test_failed_authorize_declined_amount + response = @gateway.authorize(501, @decline_credit_card, @options) + assert_failure response + assert_equal response.message, 'gateway_error_message: DECLINED | gateway_response_errors: [gateway - DECLINED]' + assert_equal @options[:order_id], response.params['order'] + assert_equal response.authorization, response.params['transaction'] + assert_equal response.params['transactions'][0]['action'], 'verify' + + assert_equal response.error_code, '567.005' + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Succeeded', response.message + assert_equal @options[:order_id], response.params['order'] + assert_equal response.authorization, response.params['transaction'] + assert_equal response.params['transactions'][0]['action'], 'sale' + end + + def test_failed_purchase_declined_credit_card + response = @gateway.purchase(@amount, @decline_credit_card, @options) + + assert_failure response + assert_equal response.message, 'gateway_error_message: DECLINED | gateway_response_errors: [gateway - DECLINED]' + assert_equal response.params['transactions'][0]['action'], 'verify' + assert_equal response.error_code, '567.005' + end + + def test_failed_purchase_declined_amount + response = @gateway.purchase(501, @decline_credit_card, @options) + assert_failure response + assert_equal response.message, 'gateway_error_message: DECLINED | gateway_response_errors: [gateway - DECLINED]' + assert_equal response.params['transactions'][0]['action'], 'verify' + assert_equal response.error_code, '567.005' + end + + def test_failed_purchase_no_billing_address + options_no_address = @options.dup + options_no_address.delete(:billing_address).delete(:shipping_address) + response = @gateway.purchase(@amount, @credit_card, options_no_address) + assert_failure response + + assert_equal response.message, 'errors: fund_address_unspecified' + + assert_equal response.error_code, nil + end + + def test_failed_purchase_no_found_credit_card + response = @gateway.purchase(@amount, @no_valid_date_credit_card, @options) + assert_failure response + assert_equal response.message, 'errors: Validation failed: Credit card gateway token not found' + assert_equal response.error_code, nil + end + + def test_successful_capture + authorize = @gateway.authorize(@amount, @credit_card, @options) + + assert_success authorize + response = @gateway.capture(@amount, authorize.authorization, @options) + assert_success response + assert_equal 'Succeeded', response.message + assert_equal authorize.params['order'], response.params['order'] + assert_equal @options[:order_id], response.params['order'] + assert_equal response.authorization, response.params['transaction'] + assert_equal response.params['transactions'][0]['action'], 'capture' + end + + def test_successful_partial_capture + authorize = @gateway.authorize(@amount, @credit_card, @options) + + assert_success authorize + response = @gateway.capture(@amount - 100, authorize.authorization, @options) + assert_success response + + assert_equal 'Succeeded', response.message + assert_equal authorize.params['order'], response.params['order'] + assert_equal @options[:order_id], response.params['order'] + assert_equal response.authorization, response.params['transaction'] + assert_equal response.params['transactions'][0]['action'], 'capture' + end + + def test_successful_verify + response = @gateway.verify(@credit_card, @options) + + assert_success response + assert_equal response.message, 'Succeeded' + assert_equal @options[:order_id], response.params['order'] + assert_equal response.authorization, response.params['transaction'] + assert_equal response.params['transactions'][0]['action'], 'verify' + end + + # verify return both avs_response and cvv_response + + def test_avs_match_cvv_not_proccessed + response = @gateway.verify(@credit_card, @options) + + assert_success response + assert_equal response.message, 'Succeeded' + assert_equal response.avs_result, { 'code' => 'D', 'message' => 'Street address and postal code match.', 'postal_match' => 'Y', 'street_match' => 'Y' } + assert_equal response.cvv_result, { 'code' => 'P', 'message' => 'CVV not processed' } + end + + def test_avs_match_cvv_match + response = @gateway.verify(@credit_card_match_cvv, @options) + + assert_success response + assert_equal response.message, 'Succeeded' + + # verify return both avs_response and cvv_response + assert_equal response.avs_result, { 'code' => 'D', 'message' => 'Street address and postal code match.', 'postal_match' => 'Y', 'street_match' => 'Y' } + assert_equal response.cvv_result, { 'code' => 'M', 'message' => 'CVV matches' } + end + + def test_avs_no_match_cvv_not_match + options = @options.dup + options[:billing_address][:address1] = '234 Elm Street' + options[:billing_address][:zip] = 80803 + + response = @gateway.verify(@credit_card_match_cvv, options) + + assert_failure response + assert_equal response.message, 'gateway_response_errors: [gateway - Failed AVS Check]' + + # verify return both avs_response and cvv_response + assert_equal response.avs_result, { 'code' => 'N', 'message' => "Street address and postal code do not match. For American Express: Card member's name, street address and postal code do not match.", 'postal_match' => 'N', 'street_match' => 'N' } + assert_equal response.cvv_result, { 'code' => 'M', 'message' => 'CVV matches' } + end + + def test_successful_void + authorize = @gateway.authorize(@amount, @credit_card, @options) + + assert_success authorize + response = @gateway.void(authorize.authorization, @options) + assert_success response + assert_equal 'Succeeded', response.message + assert_equal authorize.params['order'], response.params['order'] + assert_equal @options[:order_id], response.params['order'] + assert_equal response.authorization, response.params['transaction'] + assert_equal response.params['transactions'][0]['action'], 'void' + end + + def test_failed_void + response = @gateway.void('123456', @options) + assert_failure response + assert_equal response.message, 'errors: order_not_found' # come from a 500 HTTP error + assert_equal response.error_code, nil + end + + def test_successful_refund + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + response = @gateway.refund(@amount, purchase.authorization, @options) + assert_success response + assert_equal 'Succeeded', response.message + assert_equal purchase.params['order'], response.params['order'] + assert_equal @options[:order_id], response.params['order'] + assert_equal response.authorization, response.params['transaction'] + assert_equal response.params['transactions'][0]['action'], 'refund' + end + + def test_failed_refund + response = @gateway.refund(@amount, '123456', @options) + assert_failure response + assert_equal response.message, 'errors: order_not_found' # come from a 500 HTTP error + assert_equal response.error_code, nil + end + + def test_successful_credit + response = @gateway.credit(@amount, @credit_card, @options) + assert_success response + assert_equal 'Succeeded', response.message + assert_equal @options[:order_id], response.params['order'] + assert_equal response.authorization, response.params['transaction'] + assert_equal response.params['transactions'][0]['action'], 'credit' + end + + def test_failed_credit + response = @gateway.credit(@amount, @no_valid_date_credit_card, @options) + assert_failure response + assert_equal response.message, 'errors: Validation failed: Credit card gateway token not found' + assert_equal response.error_code, nil + end + + def test_successful_store + response = @gateway.store(@credit_card, @options) + assert_success response + assert_equal 'Succeeded', response.message + + wallet_token = response.params['wallet_token'] + fund_token = response.params['fund_token'] + assert_equal response.authorization, "#{wallet_token}|#{fund_token}" + assert_include response.params, 'wallet_token' + assert_include response.params, 'fund_token' + assert_include response.params, 'wallets' + assert_include response.params['wallets'][0], 'token' + assert_include response.params['wallets'][0], 'credit_cards' + assert_include response.params['wallets'][0]['credit_cards'][0], 'token' + assert_match response.params['wallets'][0]['token'], response.authorization + assert_match response.params['wallets'][0]['credit_cards'][0]['token'], response.authorization + end + + def test_failed_account_loggin + response = @bad_gateway.purchase(@credit_card, @options) + assert_failure response + assert_equal response.message, 'error: Please log in or create an account to continue.' + assert_equal response.error_code, nil + end + + def test_failed_stored_with_invalid_cvv + credit_card = @credit_card.dup + credit_card.verification_value = nil + response = @gateway.store(credit_card, @options) + assert_failure response + assert_equal response.message, "error: Validation failed: CVV can't be blank, CVV should be a number, CVV too short (minimum is 3 characters), CVV should be 3 digits" + assert_equal response.error_code, nil + end + + def test_failed_purchase_with_invalid_cvv + credit_card = @credit_card.dup + credit_card.verification_value = nil + response = @gateway.purchase(@amount, credit_card, @options) + assert_failure response + assert_equal response.message, "errors: CVV can't be blank, CVV should be a number, CVV too short (minimum is 3 characters" + assert_equal response.error_code, nil + end + + def test_successful_purchase_after_store + store = @gateway.store(@credit_card, @options) + assert_success store + response = @gateway.purchase(@amount, store.authorization, @options) + assert_success response + assert_equal 'Succeeded', response.message + assert_equal @options[:order_id], response.params['order'] + assert_equal response.authorization, response.params['transaction'] + assert_equal response.params['transactions'][0]['action'], 'sale' + end + + def test_successful_unstore + store = @gateway.store(@credit_card, @options) + assert_success store + fund_token = store.params['fund_token'] + response = @gateway.unstore(store.authorization, @options) + assert_success response + assert_equal 'Succeeded', response.message + assert_equal response.authorization, fund_token + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @credit_card, @options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@gateway.send(:basic_auth), transcript) + assert_scrubbed(@credit_card.number, transcript) + assert_scrubbed(@credit_card.verification_value, transcript) + end +end diff --git a/test/remote/gateways/remote_viaklix_test.rb b/test/remote/gateways/remote_viaklix_test.rb index 02b2bf06df8..c4d4c63094a 100644 --- a/test/remote/gateways/remote_viaklix_test.rb +++ b/test/remote/gateways/remote_viaklix_test.rb @@ -3,41 +3,41 @@ class RemoteViaklixTest < Test::Unit::TestCase def setup @gateway = ViaklixGateway.new(fixtures(:viaklix)) - - @credit_card = credit_card + + @credit_card = credit_card @bad_credit_card = credit_card('invalid') - + @options = { - :order_id => '#1000.1', - :email => 'paul@domain.com', - :description => 'Test Transaction', - :billing_address => address + order_id: '#1000.1', + email: 'paul@domain.com', + description: 'Test Transaction', + billing_address: address } @amount = 100 end - + def test_successful_purchase assert response = @gateway.purchase(@amount, @credit_card, @options) - + assert_success response assert response.test? assert_equal 'APPROVED', response.message assert response.authorization end - + def test_failed_purchase assert response = @gateway.purchase(@amount, @bad_credit_card, @options) - + assert_failure response assert response.test? assert_equal 'The Credit Card Number supplied in the authorization request appears invalid.', response.message end - + def test_credit assert purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - + assert credit = @gateway.credit(@amount, @credit_card) assert_success credit end -end \ No newline at end of file +end diff --git a/test/remote/gateways/remote_visanet_peru_test.rb b/test/remote/gateways/remote_visanet_peru_test.rb index ff1b0160c76..9309d63a622 100644 --- a/test/remote/gateways/remote_visanet_peru_test.rb +++ b/test/remote/gateways/remote_visanet_peru_test.rb @@ -58,7 +58,7 @@ def test_successful_authorize_and_capture assert_equal @options[:order_id], response.params['externalTransactionId'] assert_equal '1.00', response.params['data']['IMP_AUTORIZADO'] - capture = @gateway.capture(response.authorization, @options) + capture = @gateway.capture(@amount, response.authorization, @options) assert_success capture assert_equal 'OK', capture.message assert capture.authorization @@ -73,23 +73,27 @@ def test_successful_authorize_fractional_amount assert_equal '1.99', response.params['data']['IMP_AUTORIZADO'] end - def test_failed_authorize + def test_failed_authorize_declined_card response = @gateway.authorize(@amount, @declined_card, @options) assert_failure response assert_equal 400, response.error_code assert_equal 'Operacion Denegada.', response.message + end + def test_failed_authorize_bad_email @options[:email] = 'cybersource@reject.com' response = @gateway.authorize(@amount, @credit_card, @options) assert_failure response assert_equal 400, response.error_code - assert_equal 'REJECT', response.message + + # this also exercises message joining for errorMessage and DSC_COD_ACCION when both are present + assert_equal 'REJECT | Operacion denegada', response.message end def test_failed_capture - response = @gateway.capture('900000044') + response = @gateway.capture(@amount, '900000044') assert_failure response - assert_match /NUMORDEN 900000044 no se encuentra registrado/, response.message + assert_match(/NUMORDEN 900000044 no se encuentra registrado/, response.message) assert_equal 400, response.error_code end @@ -107,16 +111,16 @@ def test_successful_refund_unsettled assert_success response new_auth = "_|#{response.authorization.split('|')[1]}" - refund = @gateway.refund(@amount, new_auth, @options.merge(force_full_refund_if_unsettled: true, ruc: '20341198217')) + @gateway.refund(@amount, new_auth, @options.merge(force_full_refund_if_unsettled: true, ruc: '20341198217')) # this test will fail currently because there is no E2E test working for visanet # assert_success refund # assert_equal "OK", refund.message end def test_failed_refund - response = @gateway.refund(@amount, '900000044' ) + response = @gateway.refund(@amount, '900000044') assert_failure response - assert_match /NUMORDEN 900000044 no se encuentra registrado/, response.message + assert_match(/NUMORDEN 900000044 no se encuentra registrado/, response.message) assert_equal 400, response.error_code end @@ -132,7 +136,7 @@ def test_successful_void def test_failed_void response = @gateway.void('900000044') assert_failure response - assert_match /NUMORDEN no se encuentra registrado/, response.message + assert_match(/NUMORDEN no se encuentra registrado/, response.message) assert_equal 400, response.error_code end diff --git a/test/remote/gateways/remote_vpos_test.rb b/test/remote/gateways/remote_vpos_test.rb new file mode 100644 index 00000000000..18d41400a04 --- /dev/null +++ b/test/remote/gateways/remote_vpos_test.rb @@ -0,0 +1,115 @@ +require 'test_helper' + +class RemoteVposTest < Test::Unit::TestCase + def setup + @gateway = VposGateway.new(fixtures(:vpos)) + + @amount = 100000 + @credit_card = credit_card('5418630110000014', month: 8, year: 2026, verification_value: '277') + @declined_card = credit_card('4000300011112220') + @options = { + billing_address: address, + description: 'Store Purchase' + } + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Transaccion aprobada', response.message + end + + def test_failed_purchase + response = @gateway.purchase(@amount, @declined_card, @options) + assert_failure response + assert_equal 'IMPORTE DE LA TRN INFERIOR AL M¿NIMO PERMITIDO', response.message + end + + def test_successful_refund_using_auth + shop_process_id = SecureRandom.random_number(10**15) + + assert purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + authorization = purchase.authorization + + assert refund = @gateway.refund(@amount, authorization, @options.merge(shop_process_id:)) + assert_success refund + assert_equal 'Transaccion aprobada', refund.message + end + + def test_successful_refund_using_shop_process_id + shop_process_id = SecureRandom.random_number(10**15) + + assert purchase = @gateway.purchase(@amount, @credit_card, @options.merge(shop_process_id:)) + assert_success purchase + + assert refund = @gateway.refund(@amount, nil, original_shop_process_id: shop_process_id) # 315300749110268, 21611732218038 + assert_success refund + assert_equal 'Transaccion aprobada', refund.message + end + + def test_successful_credit + assert credit = @gateway.credit(@amount, @credit_card) + assert_success credit + assert_equal 'Transaccion aprobada', credit.message + end + + def test_failed_credit + response = @gateway.credit(@amount, @declined_card) + assert_failure response + assert_equal 'RefundsServiceError:TIPO DE TRANSACCION NO PERMITIDA PARA TARJETAS EXTRANJERAS', response.message + end + + def test_successful_void + shop_process_id = SecureRandom.random_number(10**15) + options = @options.merge({ shop_process_id: }) + + purchase = @gateway.purchase(@amount, @credit_card, options) + assert_success purchase + + assert void = @gateway.void(purchase.authorization, options) + assert_success void + assert_equal 'RollbackSuccessful:Transacción Aprobada', void.message + end + + def test_duplicate_void_fails + shop_process_id = SecureRandom.random_number(10**15) + options = @options.merge({ shop_process_id: }) + + purchase = @gateway.purchase(@amount, @credit_card, options) + assert_success purchase + + assert void = @gateway.void(purchase.authorization, options) + assert_success void + assert_equal 'RollbackSuccessful:Transacción Aprobada', void.message + + assert duplicate_void = @gateway.void(purchase.authorization, options) + assert_failure duplicate_void + assert_equal 'AlreadyRollbackedError:The payment has already been rollbacked.', duplicate_void.message + end + + def test_failed_void + response = @gateway.void('abc#123') + assert_failure response + assert_equal 'BuyNotFoundError:Business Error', response.message + end + + def test_invalid_login + gateway = VposGateway.new(private_key: '', public_key: '', encryption_key: OpenSSL::PKey::RSA.new(512), commerce: 123, commerce_branch: 45) + + response = gateway.void('') + assert_failure response + assert_match %r{InvalidPublicKeyError}, response.message + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @credit_card, @options) + end + transcript = @gateway.scrub(transcript) + + # does not contain anything other than '[FILTERED]' + assert_no_match(/token\\":\\"[^\[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 new file mode 100644 index 00000000000..879f7370326 --- /dev/null +++ b/test/remote/gateways/remote_vpos_without_key_test.rb @@ -0,0 +1,125 @@ +require 'test_helper' + +class RemoteVposWithoutKeyTest < Test::Unit::TestCase + def setup + vpos_fixtures = fixtures(:vpos) + vpos_fixtures.delete(:encryption_key) + @gateway = VposGateway.new(vpos_fixtures) + + @amount = 100000 + @credit_card = credit_card('5418630110000014', month: 8, year: 2026, verification_value: '277') + @declined_card = credit_card('4000300011112220') + @options = { + billing_address: address, + description: 'Store Purchase' + } + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Transaccion aprobada', response.message + end + + def test_failed_purchase + response = @gateway.purchase(@amount, @declined_card, @options) + assert_failure response + assert_equal 'IMPORTE DE LA TRN INFERIOR AL M¿NIMO PERMITIDO', response.message + end + + def test_successful_refund_using_auth + shop_process_id = SecureRandom.random_number(10**15) + + assert purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + authorization = purchase.authorization + + assert refund = @gateway.refund(@amount, authorization, @options.merge(shop_process_id:)) + assert_success refund + assert_equal 'Transaccion aprobada', refund.message + end + + def test_successful_refund_using_shop_process_id + shop_process_id = SecureRandom.random_number(10**15) + + assert purchase = @gateway.purchase(@amount, @credit_card, @options.merge(shop_process_id:)) + assert_success purchase + + assert refund = @gateway.refund(@amount, nil, original_shop_process_id: shop_process_id) # 315300749110268, 21611732218038 + assert_success refund + assert_equal 'Transaccion aprobada', refund.message + end + + def test_successful_credit + assert credit = @gateway.credit(@amount, @credit_card) + assert_success credit + assert_equal 'Transaccion aprobada', credit.message + end + + def test_failed_credit + response = @gateway.credit(@amount, @declined_card) + assert_failure response + assert_equal 'RefundsServiceError:TIPO DE TRANSACCION NO PERMITIDA PARA TARJETAS EXTRANJERAS', response.message + end + + def test_successful_void + shop_process_id = SecureRandom.random_number(10**15) + options = @options.merge({ shop_process_id: }) + + purchase = @gateway.purchase(@amount, @credit_card, options) + assert_success purchase + + assert void = @gateway.void(purchase.authorization, options) + assert_success void + assert_equal 'RollbackSuccessful:Transacción Aprobada', void.message + end + + def test_duplicate_void_fails + shop_process_id = SecureRandom.random_number(10**15) + options = @options.merge({ shop_process_id: }) + + purchase = @gateway.purchase(@amount, @credit_card, options) + assert_success purchase + + assert void = @gateway.void(purchase.authorization, options) + assert_success void + assert_equal 'RollbackSuccessful:Transacción Aprobada', void.message + + assert duplicate_void = @gateway.void(purchase.authorization, options) + assert_failure duplicate_void + assert_equal 'AlreadyRollbackedError:The payment has already been rollbacked.', duplicate_void.message + end + + def test_failed_void + response = @gateway.void('abc#123') + assert_failure response + assert_equal 'BuyNotFoundError:Business Error', response.message + end + + def test_invalid_login + gateway = VposGateway.new(private_key: '', public_key: '', encryption_key: OpenSSL::PKey::RSA.new(512), commerce: 123, commerce_branch: 45) + + response = gateway.void('') + assert_failure response + assert_match %r{InvalidPublicKeyError}, response.message + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @credit_card, @options) + end + transcript = @gateway.scrub(transcript) + + # does not contain anything other than '[FILTERED]' + assert_no_match(/token\\":\\"[^\[FILTERD\]]/, transcript) + assert_no_match(/card_encrypted_data\\":\\"[^\[FILTERD\]]/, transcript) + end + + def test_regenerate_encryption_key + puts 'Regenerating encryption key.' + puts 'Before running the standard vpos remote test suite, run this test individually:' + puts '$ ruby -Ilib:test test/remote/gateways/remote_vpos_without_key_test.rb -n test_regenerate_encryption_key' + puts 'Then copy this key into your fixtures file.' + p @gateway.one_time_public_key + end +end diff --git a/test/remote/gateways/remote_webpay_test.rb b/test/remote/gateways/remote_webpay_test.rb index 26e084f49e0..42662e25e8e 100644 --- a/test/remote/gateways/remote_webpay_test.rb +++ b/test/remote/gateways/remote_webpay_test.rb @@ -1,8 +1,8 @@ # coding: utf-8 + require 'test_helper' class RemoteWebpayTest < Test::Unit::TestCase - def setup @gateway = WebpayGateway.new(fixtures(:webpay)) @@ -13,8 +13,8 @@ def setup @new_credit_card = credit_card('5105105105105100') @options = { - :description => 'ActiveMerchant Test Purchase', - :email => 'wow@example.com' + description: 'ActiveMerchant Test Purchase', + email: 'wow@example.com' } end @@ -32,13 +32,13 @@ def test_appropriate_purchase_amount end def test_purchase_description - assert response = @gateway.purchase(@amount, @credit_card, { :description => 'TheDescription', :email => 'email@example.com' }) + assert response = @gateway.purchase(@amount, @credit_card, { description: 'TheDescription', email: 'email@example.com' }) assert_equal 'TheDescription', response.params['description'], "Use the description if it's specified." - assert response = @gateway.purchase(@amount, @credit_card, { :email => 'email@example.com' }) + assert response = @gateway.purchase(@amount, @credit_card, { email: 'email@example.com' }) assert_equal 'email@example.com', response.params['description'], 'Use the email if no description is specified.' - assert response = @gateway.purchase(@amount, @credit_card, { }) + assert response = @gateway.purchase(@amount, @credit_card, {}) assert_nil response.params['description'], 'No description or email specified.' end @@ -104,7 +104,7 @@ def test_unsuccessful_refund end def test_successful_store - assert response = @gateway.store(@credit_card, {:description => 'Active Merchant Test Customer', :email => 'email@example.com'}) + assert response = @gateway.store(@credit_card, { description: 'Active Merchant Test Customer', email: 'email@example.com' }) assert_success response assert_equal 'customer', response.params['object'] assert_equal 'Active Merchant Test Customer', response.params['description'] @@ -113,7 +113,7 @@ def test_successful_store end def test_successful_update - creation = @gateway.store(@credit_card, {:description => 'Active Merchant Update Customer'}) + creation = @gateway.store(@credit_card, { description: 'Active Merchant Update Customer' }) assert response = @gateway.update(creation.params['id'], @new_credit_card) assert_success response assert_equal 'Active Merchant Update Customer', response.params['description'] @@ -121,17 +121,16 @@ def test_successful_update end def test_successful_unstore - creation = @gateway.store(@credit_card, {:description => 'Active Merchant Unstore Customer'}) + creation = @gateway.store(@credit_card, { description: 'Active Merchant Unstore Customer' }) assert response = @gateway.unstore(creation.params['id']) assert_success response assert_equal true, response.params['deleted'] end def test_invalid_login - gateway = WebpayGateway.new(:login => 'active_merchant_test') + gateway = WebpayGateway.new(login: 'active_merchant_test') assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal 'Invalid API key provided. Check your API key is correct.', response.message end - end diff --git a/test/remote/gateways/remote_wepay_test.rb b/test/remote/gateways/remote_wepay_test.rb index 4a1b01f061d..a227956ddec 100644 --- a/test/remote/gateways/remote_wepay_test.rb +++ b/test/remote/gateways/remote_wepay_test.rb @@ -128,7 +128,7 @@ def test_unsuccessful_store_via_create_with_cvv # end def test_successful_store_with_defaulted_email - response = @gateway.store(@credit_card, {billing_address: address}) + response = @gateway.store(@credit_card, { billing_address: address }) assert_success response end @@ -169,7 +169,7 @@ def test_authorize_and_capture authorize = @gateway.authorize(@amount, @credit_card, @options) assert_success authorize - sleep 30 # Wait for authorization to clear. Doesn't always work. + sleep 30 # Wait for authorization to clear. Doesn't always work. assert capture = @gateway.capture(nil, authorize.authorization) assert_success capture end diff --git a/test/remote/gateways/remote_wirecard_test.rb b/test/remote/gateways/remote_wirecard_test.rb index f47eff22762..213d037d231 100644 --- a/test/remote/gateways/remote_wirecard_test.rb +++ b/test/remote/gateways/remote_wirecard_test.rb @@ -1,4 +1,5 @@ # encoding: UTF-8 + require 'test_helper' class RemoteWirecardTest < Test::Unit::TestCase @@ -57,7 +58,7 @@ def test_successful_authorize_and_partial_capture assert_match %r{THIS IS A DEMO}, auth.message assert auth.authorization - #Capture some of the authorized amount + # Capture some of the authorized amount assert capture = @gateway.capture(@amount - 10, auth.authorization, @options) assert_success capture end @@ -118,14 +119,14 @@ def test_successful_purchase_with_german_address_german_state_and_german_phone end def test_successful_purchase_with_german_address_no_state_and_invalid_phone - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(billing_address: @german_address.merge({state: nil, phone: '1234'}))) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(billing_address: @german_address.merge({ state: nil, phone: '1234' }))) assert_success response assert response.message[/THIS IS A DEMO/] end def test_successful_purchase_with_german_address_and_valid_phone - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(billing_address: @german_address.merge({phone: '+049-261-1234-123'}))) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(billing_address: @german_address.merge({ phone: '+049-261-1234-123' }))) assert_success response assert response.message[/THIS IS A DEMO/] @@ -189,13 +190,13 @@ def test_successful_store_then_purchase_by_reference end def test_successful_authorization_as_recurring_transaction_type_initial - assert response = @gateway.authorize(@amount, @credit_card, @options.merge(:recurring => 'Initial')) + assert response = @gateway.authorize(@amount, @credit_card, @options.merge(recurring: 'Initial')) assert_success response assert response.authorization end def test_successful_purchase_as_recurring_transaction_type_initial - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(:recurring => 'Initial')) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(recurring: 'Initial')) assert_success response assert response.authorization end @@ -213,7 +214,7 @@ def test_wrong_creditcard_purchase assert response = @gateway.purchase(@amount, @declined_card, @options) assert response.test? assert_failure response - assert response.message[ /Credit card number not allowed in demo mode/ ], 'Got wrong response message' + assert response.message[/Credit card number not allowed in demo mode/], 'Got wrong response message' assert_equal '24997', response.params['ErrorCode'] end @@ -221,7 +222,7 @@ def test_wrong_creditcard_store assert response = @gateway.store(@declined_card, @options) assert response.test? assert_failure response - assert response.message[ /Credit card number not allowed in demo mode/ ], 'Got wrong response message' + assert response.message[/Credit card number not allowed in demo mode/], 'Got wrong response message' end def test_unauthorized_capture @@ -257,7 +258,7 @@ def test_invalid_login def test_transcript_scrubbing transcript = capture_transcript(@gateway) do - @gateway.purchase(@amount, @credit_card, @options) + @gateway.purchase(@amount, @credit_card, @options) end clean_transcript = @gateway.scrub(transcript) diff --git a/test/remote/gateways/remote_wompi_test.rb b/test/remote/gateways/remote_wompi_test.rb new file mode 100644 index 00000000000..30034b46d8b --- /dev/null +++ b/test/remote/gateways/remote_wompi_test.rb @@ -0,0 +1,130 @@ +require 'test_helper' + +class RemoteWompiTest < Test::Unit::TestCase + def setup + @gateway = WompiGateway.new(fixtures(:wompi)) + + @amount = 150000 + @credit_card = credit_card('4242424242424242') + @credit_card_without_cvv = credit_card('4242424242424242', verification_value: nil) + @declined_card = credit_card('4111111111111111') + @options = { + billing_address: address, + description: 'Store Purchase', + currency: 'COP' + } + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + end + + def test_successful_purchase_with_more_options + reference = SecureRandom.alphanumeric(12) + response = @gateway.purchase(@amount, @credit_card, @options.merge(reference:, installments: 3)) + assert_success response + response_data = response.params['data'] + assert_equal response_data.dig('reference'), reference + assert_equal response_data.dig('payment_method', 'installments'), 3 + end + + def test_successful_purchase_without_cvv + response = @gateway.purchase(@amount, @credit_card_without_cvv, @options) + 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 + assert_equal 'La transacción fue rechazada (Sandbox)', response.message + end + + def test_successful_authorize_and_capture + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + + assert capture = @gateway.capture(@amount, response.authorization) + assert_success capture + end + + def test_successful_auth_capture_void + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + + assert capture = @gateway.capture(@amount, response.authorization) + assert_success capture + assert void = @gateway.void(capture.authorization) + assert_success void + end + + def test_failed_capture + response = @gateway.authorize(@amount, @declined_card, @options) + assert_success response + + assert capture = @gateway.capture(@amount, response.authorization) + assert_failure capture + end + + def test_successful_refund + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization) + assert_success refund + end + + def test_partial_refund + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount - 50000, purchase.authorization) + assert_success refund + end + + # def test_failed_refund + # response = @gateway.refund(@amount, '') + # assert_failure response + # message = JSON.parse(response.message) + # assert_equal 'transaction_id Debe ser completado', message['transaction_id'].first + # end + + def test_successful_void + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert void = @gateway.void(purchase.authorization) + assert_success void + end + + def test_failed_void + response = @gateway.void('bad_auth') + assert_failure response + assert_equal 'La entidad solicitada no existe', response.message + end + + def test_invalid_login + gateway = WompiGateway.new(test_public_key: 'weet', test_private_key: 'woo') + + response = gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_match %r{La llave proporcionada no corresponde a este ambiente}, 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[:test_private_key], transcript) + end +end diff --git a/test/remote/gateways/remote_world_net_test.rb b/test/remote/gateways/remote_world_net_test.rb index ece5f935926..d7db06354fd 100644 --- a/test/remote/gateways/remote_world_net_test.rb +++ b/test/remote/gateways/remote_world_net_test.rb @@ -8,7 +8,7 @@ def setup @declined_amount = 101 @credit_card = credit_card('3779810000000005') @options = { - order_id: generate_order_id, + order_id: generate_order_id } @refund_options = { operator: 'mr.nobody', @@ -28,7 +28,7 @@ def test_successful_purchase_with_more_options email: 'joe@example.com', billing_address: address, description: 'Store Purchase', - ip: '127.0.0.1', + ip: '127.0.0.1' } response = @gateway.purchase(@amount, @credit_card, options) @@ -72,7 +72,7 @@ def test_partial_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture end @@ -95,7 +95,7 @@ def test_partial_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount-1, purchase.authorization, @refund_options) + assert refund = @gateway.refund(@amount - 1, purchase.authorization, @refund_options) assert_success refund end @@ -109,10 +109,10 @@ def test_successful_void auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert void = @gateway.void(auth.authorization) - # UNSUPPORTED - # assert_success void - # assert_equal 'REPLACE WITH SUCCESSFUL VOID MESSAGE', response.message + assert @gateway.void(auth.authorization) + # UNSUPPORTED + # assert_success void + # assert_equal 'REPLACE WITH SUCCESSFUL VOID MESSAGE', response.message end def test_failed_void @@ -156,7 +156,6 @@ def test_unsuccessful_unstore response = @gateway.store(@credit_card, @options) assert_success response assert_equal nil, response.message - card_reference = response.authorization assert response = @gateway.unstore('123456789', @options) assert_failure response diff --git a/test/remote/gateways/remote_worldpay_online_payments_test.rb b/test/remote/gateways/remote_worldpay_online_payments_test.rb index 4f0b2ff05c2..bcb99f603fa 100644 --- a/test/remote/gateways/remote_worldpay_online_payments_test.rb +++ b/test/remote/gateways/remote_worldpay_online_payments_test.rb @@ -67,7 +67,7 @@ def test_partial_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth - assert capture = @gateway.capture(@amount-1, auth.authorization) + assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture end @@ -104,7 +104,7 @@ def test_failed_partial_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - refund = @gateway.refund(@amount+1, purchase.authorization) + refund = @gateway.refund(@amount + 1, purchase.authorization) assert_failure refund end diff --git a/test/remote/gateways/remote_worldpay_test.rb b/test/remote/gateways/remote_worldpay_test.rb index e1cb7f3cb64..fb0d7e10e3f 100644 --- a/test/remote/gateways/remote_worldpay_test.rb +++ b/test/remote/gateways/remote_worldpay_test.rb @@ -1,17 +1,238 @@ require 'test_helper' class RemoteWorldpayTest < Test::Unit::TestCase - def setup @gateway = WorldpayGateway.new(fixtures(:world_pay_gateway)) @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') - @declined_card = credit_card('4111111111111111', :first_name => nil, :last_name => 'REFUSED') - @threeDS_card = credit_card('4111111111111111', :first_name => nil, :last_name => '3D') + @amex_card = credit_card('3714 496353 98431') + @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', + month: 10, + year: @year + ) + @cabal_card = credit_card('6035220000000006') + @naranja_card = credit_card('5895620000000002') + @sodexo_voucher = credit_card('6060704495764400', brand: 'sodexo') + @declined_card = credit_card('4111111111111111', first_name: nil, last_name: 'REFUSED') + @threeDS_card = credit_card('4111111111111111', first_name: nil, last_name: 'doot') + @threeDS2_card = credit_card('4111111111111111', first_name: nil, last_name: '3DS_V2_FRICTIONLESS_IDENTIFIED') + @threeDS2_challenge_card = credit_card('4000000000001091', first_name: nil, last_name: 'challenge-me-plz') + @threeDS_card_external_MPI = credit_card('4444333322221111', first_name: 'AA', last_name: 'BD') + @nt_credit_card = network_tokenization_credit_card( + '4895370015293175', + brand: 'visa', + eci: '07', + source: :network_token, + 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=' + ) + + @options = { + order_id: generate_unique_id, + email: 'wow@example.com', + ip: '127.0.0.1' + } + + @level_two_data = { + level_2_data: { + invoice_reference_number: 'INV12233565', + customer_reference: 'CUST00000101', + card_acceptor_tax_id: 'VAT1999292', + tax_amount: '20', + ship_from_postal_code: '43245' + } + } + + @level_three_data = { + level_3_data: { + customer_reference: 'CUST00000102', + card_acceptor_tax_id: 'VAT1999285', + 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: '1500', + unit_of_measure: 'each', + 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' + }] + } + } + + @store_options = { + customer: generate_unique_id, + email: 'wow@example.com' + } + + @sub_merchant_options = { + sub_merchant_data: { + pf_id: '12345678901', + sub_name: 'Example Shop', + sub_id: '1234567', + sub_street: '123 Street', + sub_city: 'San Francisco', + sub_state: 'CA', + sub_country_code: '840', + sub_postal_code: '94101', + sub_tax_id: '987-65-4321' + } + } + + @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: 'abc1234567890', + eci: '07', + transaction_id: 'abc123', + 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' + ) - @options = {order_id: generate_unique_id, email: 'wow@example.com'} + @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' + ) + + @aft_options = { + account_funding_transaction: true, + aft_type: 'A', + aft_payment_purpose: '01', + aft_sender_account_type: '02', + aft_sender_account_reference: '4111111111111112', + aft_sender_full_name: { + first: 'First', + middle: 'Middle', + last: 'Sender' + }, + aft_sender_funding_address: { + address1: '123 Sender St', + address2: 'Apt 1', + postal_code: '12345', + city: 'Senderville', + state: 'NC', + country_code: 'US' + }, + aft_recipient_account_type: '03', + aft_recipient_account_reference: '4111111111111111', + aft_recipient_full_name: { + first: 'First', + middle: 'Middle', + last: 'Recipient' + }, + aft_recipient_funding_address: { + address1: '123 Recipient St', + address2: 'Apt 1', + postal_code: '12345', + city: 'Recipientville', + state: 'NC', + country_code: 'US' + }, + aft_recipient_funding_data: { + telephone_number: '123456789', + birth_date: { + day_of_month: '01', + month: '01', + year: '1980' + } + } + } + + @aft_less_options = { + account_funding_transaction: true, + aft_type: 'A', + aft_payment_purpose: '01', + aft_sender_account_type: '02', + aft_sender_account_reference: '4111111111111112', + aft_sender_full_name: { + first: 'First', + last: 'Sender' + }, + aft_sender_funding_address: { + address1: '123 Sender St', + postal_code: '12345', + city: 'Senderville', + state: 'NC', + country_code: 'US' + }, + aft_recipient_account_type: '03', + aft_recipient_account_reference: '4111111111111111', + aft_recipient_full_name: { + first: 'First', + last: 'Recipient' + }, + aft_recipient_funding_address: { + address1: '123 Recipient St', + postal_code: '12345', + city: 'Recipientville', + state: 'NC', + country_code: 'US' + }, + aft_recipient_funding_data: { + telephone_number: '123456789', + birth_date: { + day_of_month: '01', + month: '01', + year: '1980' + } + } + } end def test_successful_purchase @@ -20,12 +241,273 @@ def test_successful_purchase assert_equal 'SUCCESS', response.message end + def test_successful_purchase_with_network_token + assert response = @gateway.purchase(@amount, @nt_credit_card, @options) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_purchase_with_network_token_with_shopper_ip_address + assert response = @gateway.purchase(@amount, @nt_credit_card, @options.merge(ip: '127.0.0.1')) + assert_success response + assert_equal 'SUCCESS', response.message + end + + 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 + + def test_successful_authorize_with_card_holder_name_apple_pay + response = @gateway.authorize(@amount, @apple_pay_network_token, @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_card_holder_name_google_pay + response = @gateway.authorize(@amount, @google_pay_network_token, @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_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 = '' + + response = @gateway.authorize(@amount, @apple_pay_network_token, @options) + + assert_success response + assert_equal 'authorize', response.params['action'] + 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_unsucessfull_authorize_without_token_number_apple_pay + @apple_pay_network_token.number = nil + response = @gateway.authorize(@amount, @apple_pay_network_token, @options) + + assert_failure response + assert_equal response.error_code, '2' + assert_match "Missing required elements 'tokenNumber'", response.message + end + + def test_unsucessfull_authorize_with_token_number_as_empty_string_apple_pay + @apple_pay_network_token.number = '' + response = @gateway.authorize(@amount, @apple_pay_network_token, @options) + + assert_failure response + assert_equal response.error_code, '2' + assert_match "Missing required elements 'tokenNumber'", response.message + end + + def test_unsucessfull_authorize_with_invalid_token_number_apple_pay + @apple_pay_network_token.first_name = 'REFUSED' # Magic value for testing purposes + @apple_pay_network_token.last_name = '' + + response = @gateway.authorize(@amount, @apple_pay_network_token, @options) + assert_failure response + assert_equal 'REFUSED', response.message + end + + def test_unsuccessful_authorize_with_overdue_expire_date_apple_pay + @apple_pay_network_token.month = 10 + @apple_pay_network_token.year = 2019 + + response = @gateway.authorize(@amount, @apple_pay_network_token, @options) + assert_failure response + assert_equal 'Invalid payment details : Expiry date = 10/2019', response.message + end + + def test_unsuccessful_authorize_without_expire_date_apple_pay + @apple_pay_network_token.month = nil + @apple_pay_network_token.year = nil + + response = @gateway.authorize(@amount, @apple_pay_network_token, @options) + assert_failure response + assert_match(/of type NMTOKEN must be a name token/, response.message) + end + + def test_purchase_with_apple_pay_card_apple_pay + assert auth = @gateway.purchase(@amount, @apple_pay_network_token, @options) + assert_success auth + assert_equal 'SUCCESS', auth.message + assert auth.authorization + end + + def test_successful_authorize_with_void_apple_pay + assert auth = @gateway.authorize(@amount, @apple_pay_network_token, @options) + assert_success auth + assert_equal 'authorize', auth.params['action'] + assert_equal @amount, auth.params['amount_value'].to_i + assert_equal 'GBP', auth.params['amount_currency_code'] + 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_purchase_with_refund_apple_pay + assert auth = @gateway.purchase(@amount, @apple_pay_network_token, @options) + assert_success auth + assert_equal 'capture', auth.params['action'] + assert_equal @amount, auth.params['amount_value'].to_i + assert_equal 'GBP', auth.params['amount_currency_code'] + assert auth.authorization + assert refund = @gateway.refund(@amount, auth.authorization, @options.merge(authorization_validated: true)) + assert_success refund + end + + def test_successful_store_apple_pay + assert response = @gateway.store(@apple_pay_network_token, @store_options) + assert_success response + assert_equal 'SUCCESS', response.message + assert_match response.params['payment_token_id'], response.authorization + assert_match 'shopper', response.authorization + assert_match @store_options[:customer], response.authorization + end + + def test_successful_purchase_with_elo + assert response = @gateway.purchase(@amount, @elo_credit_card, @options.merge(currency: 'BRL')) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_purchase_with_two_digits_expiration_year + assert response = @gateway.purchase(@amount, @credit_card_with_two_digits_year, @options) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_purchase_with_cabal + response = @gateway.purchase(@amount, @cabal_card, @options.merge(currency: 'ARS')) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_purchase_with_naranja + response = @gateway.purchase(@amount, @naranja_card, @options.merge(currency: 'ARS')) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_purchase_skipping_capture + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(skip_capture: true)) + assert_success response + assert response.responses.length == 1 + assert_equal 'SUCCESS', response.message + end + + def test_successful_authorize_avs_and_cvv + card = credit_card('4111111111111111', verification_value: 555) + assert response = @gateway.authorize(@amount, card, @options.merge(billing_address: address.update(zip: 'CCCC'))) + assert_success response + assert_equal 'SUCCESS', response.message + assert_match %r{Street address does not match, but 5-digit postal code matches}, response.avs_result['message'] + assert_match %r{CVV matches}, response.cvv_result['message'] + end + + def test_successful_authorize_with_sub_merchant_data + options = @options.merge(@sub_merchant_options) + assert response = @gateway.authorize(@amount, @credit_card, options) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_3ds2_authorize + options = @options.merge({ execute_threed: true, three_ds_version: '2.0' }) + assert response = @gateway.authorize(@amount, @threeDS2_card, options) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_3ds2_authorize_with_browser_size + options = @options.merge({ execute_threed: true, three_ds_version: '2.0', browser_size: '390x400' }) + assert response = @gateway.authorize(@amount, @threeDS2_card, options) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_authorize_with_risk_data + options = @options.merge({ execute_threed: true, three_ds_version: '2.0', risk_data: }) + assert response = @gateway.authorize(@amount, @threeDS2_card, options) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_purchase_with_sub_merchant_data + options = @options.merge(@sub_merchant_options) + assert response = @gateway.authorize(@amount, @credit_card, options) + assert_success response + assert_equal 'SUCCESS', response.message + end + def test_successful_purchase_with_hcg_additional_data - @options.merge!(hcg_additional_data: { + @options[:hcg_additional_data] = { key1: 'value1', key2: 'value2', key3: 'value3' - }) + } assert response = @gateway.purchase(@amount, @credit_card, @options) assert_success response @@ -44,8 +526,8 @@ def test_authorize_and_capture assert_success auth assert_equal 'SUCCESS', auth.message assert auth.authorization - sleep(40) - assert capture = @gateway.capture(@amount, auth.authorization) + + assert capture = @gateway.capture(@amount, auth.authorization, authorization_validated: true) assert_success capture end @@ -53,15 +535,14 @@ def test_authorize_and_capture_by_reference assert auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth assert_equal 'SUCCESS', auth.message - sleep(40) - assert capture = @gateway.capture(@amount, auth.authorization) - assert_success capture + assert capture = @gateway.capture(@amount, auth.authorization, authorization_validated: true) + assert_success capture assert reference = auth.authorization @options[:order_id] = generate_unique_id + assert auth = @gateway.authorize(@amount, reference, @options) - sleep(40) - assert capture = @gateway.capture(@amount, auth.authorization) + assert capture = @gateway.capture(@amount, auth.authorization, authorization_validated: true) assert_success capture end @@ -69,52 +550,324 @@ def test_authorize_and_purchase_by_reference assert auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth assert_equal 'SUCCESS', auth.message - sleep(40) - assert capture = @gateway.capture(@amount, auth.authorization) - assert_success capture + assert capture = @gateway.capture(@amount, auth.authorization, authorization_validated: true) + assert_success capture assert reference = auth.authorization + @options[:order_id] = generate_unique_id assert auth = @gateway.authorize(@amount, reference, @options) + @options[:order_id] = generate_unique_id - sleep(40) assert capture = @gateway.purchase(@amount, auth.authorization, @options) assert_success capture end + def test_authorize_and_purchase_with_instalments + assert auth = @gateway.authorize(@amount, @credit_card, @options.merge(instalment: 3)) + assert_success auth + assert_equal 'SUCCESS', auth.message + assert auth.authorization + + assert capture = @gateway.capture(@amount, auth.authorization, authorization_validated: true) + assert_success capture + end + def test_successful_authorize_with_3ds session_id = generate_unique_id - order_id = @options[:order_id] options = @options.merge( - { - execute_threed: true, - accept_header: 'text/html', - user_agent: 'Mozilla/5.0', - session_id: session_id, - ip: '127.0.0.1', - cookie: 'machine=32423423' - }) + { + execute_threed: true, + accept_header: 'text/html', + user_agent: 'Mozilla/5.0', + session_id:, + ip: '127.0.0.1', + cookie: 'machine=32423423' + } + ) + 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['cookie'].blank? + refute first_message.params['session_id'].blank? + end + + # Ensure the account is configured to use this feature to proceed successfully + def test_marking_3ds_purchase_as_moto + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(metadata: { manual_entry: true })) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_authorize_with_3ds2_challenge + session_id = generate_unique_id + options = @options.merge( + { + execute_threed: true, + accept_header: 'text/html', + user_agent: 'Mozilla/5.0', + session_id:, + ip: '127.0.0.1' + } + ) + assert response = @gateway.authorize(@amount, @threeDS2_challenge_card, options) + assert response.test? + refute response.authorization.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 = stored_credential(:initial, :unscheduled, :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, :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 + 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_gateway_specific_stored_credentials + assert auth = @gateway.authorize(@amount, @credit_card, @options.merge(stored_credential_usage: 'FIRST')) + 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: 'UNSCHEDULED', + 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_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_recurring_purchase_with_apple_pay_credentials + stored_credential_params = stored_credential(:initial, :recurring, :merchant) + assert auth = @gateway.authorize(@amount, @apple_pay_network_token, @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, @apple_pay_network_token, @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 = stored_credential(:initial, :unscheduled, :merchant) + options = @options.merge( + { + execute_threed: true, + accept_header: 'text/html', + user_agent: 'Mozilla/5.0', + session_id:, + ip: '127.0.0.1', + cookie: 'machine=32423423', + stored_credential: stored_credential_params + } + ) + assert first_message = @gateway.authorize(@amount, @threeDS_card, options) + assert first_message.test? + refute first_message.authorization.blank? + assert first_message.success? + refute first_message.params['cookie'].blank? + refute first_message.params['session_id'].blank? + end + + def test_successful_authorize_with_3ds_with_gateway_specific_stored_credentials + session_id = generate_unique_id + options = @options.merge( + { + execute_threed: true, + accept_header: 'text/html', + user_agent: 'Mozilla/5.0', + session_id:, + ip: '127.0.0.1', + cookie: 'machine=32423423', + stored_credential_usage: 'FIRST' + } + ) assert first_message = @gateway.authorize(@amount, @threeDS_card, options) - assert_equal "A transaction status of 'AUTHORISED' is required.", first_message.message 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 + def test_successful_purchase_with_level_two_fields + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(@level_two_data)) + assert_success response + assert_equal true, response.params['ok'] + assert_equal 'SUCCESS', response.message + end + + def test_successful_purchase_with_level_two_fields_and_sales_tax_zero + @level_two_data[:level_2_data][:tax_amount] = 0 + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(@level_two_data)) + assert_success response + assert_equal true, response.params['ok'] + assert_equal 'SUCCESS', response.message + end + + def test_successful_purchase_with_level_three_fields + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(@level_three_data)) + assert_success response + assert_equal true, response.params['ok'] + assert_equal 'SUCCESS', response.message + end + + def test_unsuccessful_purchase_level_three_data_without_item_mastercard + @level_three_data[:level_3_data][: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 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 + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(@level_two_data, @level_three_data)) + assert_success response + assert_equal true, response.params['ok'] + assert_equal 'SUCCESS', response.message + end + + def test_successful_purchase_with_custom_string_fields + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(custom_string_fields: { custom_string_field_1: 'testvalue1', custom_string_field_2: 'testvalue2' })) + assert_success response + assert_equal true, response.params['ok'] + assert_equal 'SUCCESS', response.message + end + + def test_failed_purchase_with_blank_custom_string_field + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(custom_string_fields: { custom_string_field_1: '' })) + assert_failure response + + assert_equal "The tag 'customStringField1' cannot be empty", response.message + end + + # Fails currently because the sandbox doesn't actually validate the stored_credential options + # def test_failed_authorize_with_bad_stored_cred_options + # assert auth = @gateway.authorize(@amount, @credit_card, @options.merge(stored_credential_usage: 'FIRST')) + # 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: 'MEH', + # stored_credential_initiated_reason: 'BLAH', + # stored_credential_transaction_id: 'nah' + # ) + # assert next_auth = @gateway.authorize(@amount, @credit_card, options) + # assert_failure next_auth + # end + def test_failed_authorize_with_3ds session_id = generate_unique_id - order_id = @options[:order_id] options = @options.merge( - { - execute_threed: true, - accept_header: 'text/html', - session_id: session_id, - ip: '127.0.0.1', - cookie: 'machine=32423423' - }) + { + execute_threed: true, + accept_header: 'text/html', + session_id:, + ip: '127.0.0.1', + cookie: 'machine=32423423' + } + ) assert first_message = @gateway.authorize(@amount, @threeDS_card, options) assert_match %r{missing info for 3D-secure transaction}i, first_message.message assert first_message.test? @@ -122,6 +875,60 @@ def test_failed_authorize_with_3ds assert first_message.params['pa_request'].blank? end + def test_3ds_version_1_parameters_pass_thru + options = @options.merge( + { + three_d_secure: { + version: '1.0.2', + xid: 'z9UKb06xLziZMOXBEmWSVA1kwG0=', + cavv: 'MAAAAAAAAAAAAAAAAAAAAAAAAAA=', + eci: '05' + } + } + ) + + assert response = @gateway.authorize(@amount, @threeDS_card_external_MPI, @options.merge(options)) + assert response.test? + assert response.success? + assert response.params['last_event'] || response.params['ok'] + end + + def test_3ds_version_2_parameters_pass_thru + options = @options.merge( + { + three_d_secure: { + version: '2.1.0', + ds_transaction_id: 'c5b808e7-1de1-4069-a17b-f70d3b3b1645', + cavv: 'MAAAAAAAAAAAAAAAAAAAAAAAAAA=', + eci: '05' + } + } + ) + + assert response = @gateway.authorize(@amount, @threeDS_card_external_MPI, @options.merge(options)) + assert response.test? + assert response.success? + assert response.params['last_event'] || response.params['ok'] + end + + def test_3ds_version_2_parameters_for_nt + options = @options.merge( + { + three_d_secure: { + version: '2.1.0', + ds_transaction_id: 'c5b808e7-1de1-4069-a17b-f70d3b3b1645', + cavv: 'MAAAAAAAAAAAAAAAAAAAAAAAAAA=', + eci: '05' + } + } + ) + + assert response = @gateway.authorize(@amount, @nt_credit_card, @options.merge(options)) + assert response.test? + assert response.success? + assert response.params['last_event'] || response.params['ok'] + end + def test_failed_capture assert response = @gateway.capture(@amount, 'bogus') assert_failure response @@ -129,7 +936,7 @@ def test_failed_capture end def test_billing_address - assert_success @gateway.authorize(@amount, @credit_card, @options.merge(:billing_address => address)) + assert_success @gateway.authorize(@amount, @credit_card, @options.merge(billing_address: address)) end def test_partial_address @@ -137,7 +944,13 @@ def test_partial_address billing_address.delete(:address1) billing_address.delete(:zip) billing_address.delete(:country) - assert_success @gateway.authorize(@amount, @credit_card, @options.merge(:billing_address => billing_address)) + assert_success @gateway.authorize(@amount, @credit_card, @options.merge(billing_address:)) + end + + def test_state_omitted + billing_address = address + billing_address.delete(:state) + assert_success @gateway.authorize(@amount, @credit_card, @options.merge(billing_address:)) end def test_ip_address @@ -145,9 +958,15 @@ def test_ip_address end def test_void - assert_success(response = @gateway.authorize(@amount, @credit_card, @options)) - sleep(40) - assert_success (void = @gateway.void(response.authorization)) + assert_success response = @gateway.authorize(@amount, @credit_card, @options) + assert_success void = @gateway.void(response.authorization, authorization_validated: true) + assert_equal 'SUCCESS', void.message + assert void.params['cancel_received_order_code'] + end + + def test_void_with_elo + assert_success response = @gateway.authorize(@amount, @elo_credit_card, @options.merge(currency: 'BRL')) + assert_success void = @gateway.void(response.authorization, authorization_validated: true) assert_equal 'SUCCESS', void.message assert void.params['cancel_received_order_code'] end @@ -158,21 +977,21 @@ def test_void_nonexistent_transaction end def test_authorize_fractional_currency - assert_success(result = @gateway.authorize(1234, @credit_card, @options.merge(:currency => 'USD'))) + assert_success(result = @gateway.authorize(1234, @credit_card, @options.merge(currency: 'USD'))) assert_equal 'USD', result.params['amount_currency_code'] assert_equal '1234', result.params['amount_value'] assert_equal '2', result.params['amount_exponent'] end def test_authorize_nonfractional_currency - assert_success(result = @gateway.authorize(1234, @credit_card, @options.merge(:currency => 'IDR'))) + assert_success(result = @gateway.authorize(1234, @credit_card, @options.merge(currency: 'IDR'))) assert_equal 'IDR', result.params['amount_currency_code'] assert_equal '12', result.params['amount_value'] assert_equal '0', result.params['amount_exponent'] end def test_authorize_three_decimal_currency - assert_success(result = @gateway.authorize(1234, @credit_card, @options.merge(:currency => 'OMR'))) + assert_success(result = @gateway.authorize(1234, @credit_card, @options.merge(currency: 'OMR'))) assert_equal 'OMR', result.params['amount_currency_code'] assert_equal '1234', result.params['amount_value'] assert_equal '3', result.params['amount_exponent'] @@ -180,11 +999,11 @@ def test_authorize_three_decimal_currency def test_reference_transaction assert_success(original = @gateway.authorize(100, @credit_card, @options)) - assert_success(@gateway.authorize(200, original.authorization, :order_id => generate_unique_id)) + assert_success(@gateway.authorize(200, original.authorization, order_id: generate_unique_id)) end def test_invalid_login - gateway = WorldpayGateway.new(:login => '', :password => '') + gateway = WorldpayGateway.new(login: '', password: '') assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal 'Invalid credentials', response.message @@ -196,7 +1015,7 @@ def test_refund_fails_unless_status_is_captured assert refund = @gateway.refund(30, response.authorization) assert_failure refund - assert_equal "A transaction status of 'CAPTURED' or 'SETTLED' or 'SETTLED_BY_MERCHANT' is required.", refund.message + assert_equal 'Order not ready', refund.message end def test_refund_nonexistent_transaction @@ -210,21 +1029,142 @@ def test_successful_verify assert_match %r{SUCCESS}, response.message end + def test_successful_verify_with_0_auth + options = @options.merge(zero_dollar_auth: true) + response = @gateway.verify(@credit_card, options) + assert_success response + assert_match %r{SUCCESS}, response.message + end + + def test_successful_verify_with_0_auth_and_ineligible_card + options = @options.merge(zero_dollar_auth: true) + response = @gateway.verify(@amex_card, options) + assert_success response + assert_match %r{SUCCESS}, response.message + end + + def test_successful_verify_with_elo + response = @gateway.verify(@elo_credit_card, @options.merge(currency: 'BRL')) + assert_success response + assert_match %r{SUCCESS}, response.message + end + def test_failed_verify response = @gateway.verify(@declined_card, @options) assert_failure response assert_match %r{REFUSED}, response.message end - def test_successful_credit_on_cft_gateway + def test_successful_visa_credit_on_cft_gateway credit = @cftgateway.credit(@amount, @credit_card, @options) assert_success credit assert_equal 'SUCCESS', credit.message end + def test_successful_mastercard_credit_on_cft_gateway + cc = credit_card('5555555555554444') + credit = @cftgateway.credit(@amount, cc, @options) + assert_success credit + assert_equal 'SUCCESS', credit.message + end + + def test_successful_visa_account_funding_transfer + credit = @gateway.credit(@amount, @credit_card, @options.merge(@aft_options)) + assert_success credit + assert_equal 'SUCCESS', credit.message + end + + def test_successful_visa_account_funding_transfer_via_token + assert store = @gateway.store(@credit_card, @store_options) + assert_success store + + credit = @gateway.credit(@amount, store.authorization, @options.merge(@aft_options)) + assert_success credit + assert_equal 'SUCCESS', credit.message + end + + def test_failed_visa_account_funding_transfer + credit = @gateway.credit(@amount, credit_card('4111111111111111', name: 'REFUSED'), @options.merge(@aft_options)) + assert_failure credit + assert_equal 'REFUSED', credit.message + end + + def test_failed_visa_account_funding_transfer_acquirer_error + credit = @gateway.credit(@amount, credit_card('4111111111111111', name: 'ACQERROR'), @options.merge(@aft_options)) + assert_failure credit + assert_equal 'ACQUIRER ERROR', credit.message + assert_equal '20', credit.error_code + end + + def test_successful_authorize_visa_account_funding_transfer + auth = @gateway.authorize(@amount, @credit_card, @options.merge(@aft_options)) + assert_success auth + assert_equal 'funding_transfer_transaction', auth.params['action'] + assert_equal 'SUCCESS', auth.message + end + + def test_successful_authorize_visa_account_funding_transfer_via_token + assert store = @gateway.store(@credit_card, @store_options) + assert_success store + + auth = @gateway.authorize(@amount, store.authorization, @options.merge(@aft_options)) + assert_success auth + assert_equal 'funding_transfer_transaction', auth.params['action'] + assert_equal 'SUCCESS', auth.message + end + + def test_failed_authorize_visa_account_funding_transfer + auth = @gateway.authorize(@amount, credit_card('4111111111111111', name: 'REFUSED'), @options.merge(@aft_options)) + assert_failure auth + assert_equal 'funding_transfer_transaction', auth.params['action'] + assert_equal 'REFUSED', auth.message + end + + def test_failed_authorize_visa_account_funding_transfer_acquirer_error + auth = @gateway.authorize(@amount, credit_card('4111111111111111', name: 'ACQERROR'), @options.merge(@aft_options)) + assert_failure auth + assert_equal 'ACQUIRER ERROR', auth.message + assert_equal 'funding_transfer_transaction', auth.params['action'] + assert_equal '20', auth.error_code + end + + def test_successful_authorize_visa_account_funding_transfer_with_no_middle_name_address2 + auth = @gateway.authorize(@amount, @credit_card, @options.merge(@aft_less_options)) + assert_success auth + assert_equal 'funding_transfer_transaction', auth.params['action'] + assert_equal 'SUCCESS', auth.message + end + + def test_successful_fast_fund_credit_on_cft_gateway + options = @options.merge({ fast_fund_credit: true }) + + credit = @cftgateway.credit(@amount, @credit_card, options) + assert_success credit + assert_equal 'SUCCESS', credit.message + end + + def test_successful_fast_fund_credit_with_token_on_cft_gateway + assert store = @gateway.store(@credit_card, @store_options) + assert_success store + + options = @options.merge({ fast_fund_credit: true }) + assert credit = @cftgateway.credit(@amount, store.authorization, options) + assert_success credit + 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 - @gateway.purchase(@amount, @credit_card, @options) + @gateway.purchase(@amount, @credit_card, @options) end clean_transcript = @gateway.scrub(transcript) @@ -232,26 +1172,390 @@ def test_transcript_scrubbing assert_scrubbed(@credit_card.verification_value.to_s, clean_transcript) end + def test_failed_authorize_with_unknown_card + assert auth = @gateway.authorize(@amount, @sodexo_voucher, @options) + assert_failure auth + assert_equal '5', auth.error_code + assert_match %r{XML failed validation: Invalid payment details : Card number not recognised:}, auth.message + end + + def test_failed_purchase_with_unknown_card + assert response = @gateway.purchase(@amount, @sodexo_voucher, @options) + assert_failure response + assert_equal '5', response.error_code + assert_match %r{XML failed validation: Invalid payment details : Card number not recognised:}, response.message + end + + def test_failed_verify_with_unknown_card + response = @gateway.verify(@sodexo_voucher, @options) + assert_failure response + assert_equal '5', response.error_code + assert_match %r{XML failed validation: Invalid payment details : Card number not recognised:}, response.message + end # Worldpay has a delay between asking for a transaction to be captured and actually marking it as captured # These 2 tests work if you get authorizations from a purchase, wait some time and then perform the refund/void operation. - - # def get_authorization - # assert_success(response = @gateway.purchase(@amount, @credit_card, @options)) + # + # def test_get_authorization + # response = @gateway.purchase(@amount, @credit_card, @options) # assert response.authorization - # puts "auth: " + response.authorization + # puts 'auth: ' + response.authorization # end + # + def test_refund + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'SUCCESS', response.message + assert response.authorization - # def test_refund - # refund = @gateway.refund(@amount, 'replace_with_authorization') - # assert_success refund - # assert_equal "SUCCESS", refund.message - # end + refund = @gateway.refund(@amount, response.authorization, authorization_validated: true) + assert_success refund + assert_equal 'SUCCESS', refund.message + end + + def test_cancel_or_refund_non_captured_purchase + response = @gateway.purchase(@amount, @credit_card, @options.merge(skip_capture: true)) + assert_success response + assert_equal 'SUCCESS', response.message + assert response.authorization + + refund = @gateway.refund(@amount, response.authorization, authorization_validated: true, cancel_or_refund: true) + assert_success refund + assert_equal 'SUCCESS', refund.message + end + + def test_cancel_or_refund_captured_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'SUCCESS', response.message + assert response.authorization + + refund = @gateway.refund(@amount, response.authorization, authorization_validated: true, cancel_or_refund: true) + assert_success refund + assert_equal 'SUCCESS', refund.message + end + + def test_cancel_or_refund_non_captured_purchase_with_void + response = @gateway.purchase(@amount, @credit_card, @options.merge(skip_capture: true)) + assert_success response + assert_equal 'SUCCESS', response.message + assert response.authorization + + refund = @gateway.void(response.authorization, authorization_validated: true, cancel_or_refund: true) + assert_success refund + assert_equal 'SUCCESS', refund.message + end + + def test_cancel_or_refund_captured_purchase_with_void + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'SUCCESS', response.message + assert response.authorization + + refund = @gateway.void(response.authorization, authorization_validated: true, cancel_or_refund: true) + assert_success refund + assert_equal 'SUCCESS', refund.message + end + + def test_multiple_refunds + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + assert_equal 'SUCCESS', purchase.message + + partial_amount = @amount - 1 + assert_success refund1 = @gateway.refund(partial_amount, purchase.authorization, authorization_validated: true) + assert_equal 'SUCCESS', refund1.message + + assert_success refund2 = @gateway.refund(@amount - partial_amount, purchase.authorization, authorization_validated: true) + assert_equal 'SUCCESS', refund2.message + end # def test_void_fails_unless_status_is_authorised # response = @gateway.void('replace_with_authorization') # existing transaction in CAPTURED state # assert_failure response - # assert_equal "A transaction status of 'AUTHORISED' is required.", response.message + # assert_equal 'A transaction status of 'AUTHORISED' is required.', response.message + # end + + def test_successful_store + assert response = @gateway.store(@credit_card, @store_options) + assert_success response + assert_equal 'SUCCESS', response.message + assert_match response.params['payment_token_id'], response.authorization + assert_match 'shopper', response.authorization + assert_match @store_options[:customer], response.authorization + end + + def test_successful_store_with_transaction_identifier_using_gateway_specific_field + transaction_identifier = 'ABC123' + options_with_transaction_id = @store_options.merge(stored_credential_transaction_id: transaction_identifier) + assert response = @gateway.store(@credit_card, options_with_transaction_id) + + assert_success response + assert_equal 'SUCCESS', response.message + assert_match transaction_identifier, response.params['transaction_identifier'] + end + + def test_successful_store_with_transaction_identifier_using_normalized_fields + transaction_identifier = 'CDE456' + options_with_transaction_id = @store_options.merge(stored_credential: { network_transaction_id: transaction_identifier }) + assert response = @gateway.store(@credit_card, options_with_transaction_id) + + assert_success response + assert_equal 'SUCCESS', response.message + assert_match transaction_identifier, response.params['transaction_identifier'] + end + + def test_successful_purchase_with_statement_narrative + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(statement_narrative: 'Merchant Statement Narrative')) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_authorize_using_token + assert store = @gateway.store(@credit_card, @store_options) + assert_success store + + assert response = @gateway.authorize(@amount, store.authorization, @options) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_authorize_using_token_and_minimum_options + assert store = @gateway.store(@credit_card, @store_options) + assert_success store + + assert response = @gateway.authorize(@amount, store.authorization, order_id: generate_unique_id) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_purchase_using_token + assert store = @gateway.store(@credit_card, @store_options) + assert_success store + + assert response = @gateway.authorize(@amount, store.authorization, @options) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_verify_using_token + assert store = @gateway.store(@credit_card, @store_options) + assert_success store + + response = @gateway.verify(store.authorization, @options) + assert_success response + assert_match %r{SUCCESS}, response.message + end + + def test_successful_credit_using_token + assert store = @cftgateway.store(@credit_card, @store_options) + assert_success store + + credit = @cftgateway.credit(@amount, store.authorization, @options) + assert_success credit + assert_equal 'SUCCESS', credit.message + end + + def test_failed_store + assert response = @gateway.store(@credit_card, @store_options.merge(customer: '_invalidId')) + assert_failure response + assert_equal '2', response.error_code + assert_equal 'authenticatedShopperID cannot start with an underscore', response.message + end + + def test_failed_authorize_using_token + assert store = @gateway.store(@declined_card, @store_options) + assert_success store + + assert response = @gateway.authorize(@amount, store.authorization, @options) + assert_failure response + assert_equal '5', response.error_code + assert_equal 'REFUSED', response.message + end + + def test_failed_authorize_using_bogus_token + assert response = @gateway.authorize(@amount, '|this|is|bogus', @options) + assert_failure response + assert_equal '2', response.error_code + assert_match 'tokenScope', response.message + end + + def test_failed_verify_using_token + assert store = @gateway.store(@declined_card, @store_options) + assert_success store + + response = @gateway.verify(store.authorization, @options) + assert_failure response + assert_equal '5', response.error_code + assert_match %r{REFUSED}, response.message + end + + def test_authorize_and_capture_synchronous_response + card = credit_card('4111111111111111', verification_value: 555) + assert auth = @cftgateway.authorize(@amount, card, @options) + assert_success auth + + assert capture = @cftgateway.capture(@amount, auth.authorization, @options.merge(authorization_validated: true)) + assert_success capture + + assert duplicate_capture = @cftgateway.capture(@amount, auth.authorization, @options.merge(authorization_validated: true)) + assert_failure duplicate_capture + end + + def test_capture_wrong_amount_synchronous_response + card = credit_card('4111111111111111', verification_value: 555) + assert auth = @cftgateway.authorize(@amount, card, @options) + assert_success auth + + assert capture = @cftgateway.capture(@amount + 1, auth.authorization, @options.merge(authorization_validated: true)) + assert_failure capture + assert_equal '5', capture.error_code + assert_equal 'Requested capture amount (GBP 1.01) exceeds the authorised balance for this payment (GBP 1.00)', capture.message + end + + def test_successful_refund_synchronous_response + response = @cftgateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'SUCCESS', response.message + assert response.authorization + + assert @cftgateway.refund(@amount, response.authorization, authorization_validated: true) + end + + def test_failed_refund_synchronous_response + auth = @cftgateway.authorize(@amount, @credit_card, @options) + assert_success auth + assert_equal 'SUCCESS', auth.message + assert auth.authorization + + refund = @cftgateway.refund(@amount, auth.authorization, authorization_validated: true) + assert_failure refund + assert_equal 'This order is not refundable', refund.message + + assert capture = @cftgateway.capture(@amount, auth.authorization, @options.merge(authorization_validated: true)) + assert_success capture + + refund = @cftgateway.refund(@amount * 2, auth.authorization, authorization_validated: true) + assert_failure refund + assert_equal 'Invalid amount: The refund amount should be equal to the captured value', refund.message + end + + def test_successful_purchase_with_options_synchronous_response + options = @options + 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)) + assert_success purchase + end + + # There is a delay of up to 5 minutes for a transaction to be recorded by Worldpay. Inquiring + # too soon will result in an error "Order not ready". Leaving commented out due to included sleeps. + # def test_successful_inquire_with_order_id + # order_id = @options[:order_id] + # assert auth = @gateway.authorize(@amount, @credit_card, @options) + # assert_success auth + # assert auth.authorization + # sleep 60 + + # assert inquire = @gateway.inquire(nil, { order_id: order_id }) + # assert_success inquire + # assert auth.authorization == inquire.authorization # end + # def test_successful_inquire_with_authorization + # assert auth = @gateway.authorize(@amount, @credit_card, @options) + # assert_success auth + # assert auth.authorization + # sleep 60 + + # assert inquire = @gateway.inquire(auth.authorization, {}) + # assert_success inquire + # assert auth.authorization == inquire.authorization + # end + + private + + def risk_data + return @risk_data if @risk_data + + authentication_time = Time.now + shopper_account_creation_date = Date.today + shopper_account_modification_date = Date.today - 1.day + shopper_account_password_change_date = Date.today - 2.days + shopper_account_shipping_address_first_use_date = Date.today - 3.day + shopper_account_payment_account_first_use_date = Date.today - 4.day + transaction_risk_data_pre_order_date = Date.today + 1.day + + @risk_data = { + authentication_risk_data: { + authentication_method: 'localAccount', + authentication_date: { + day_of_month: authentication_time.strftime('%d'), + month: authentication_time.strftime('%m'), + year: authentication_time.strftime('%Y'), + hour: authentication_time.strftime('%H'), + minute: authentication_time.strftime('%M'), + second: authentication_time.strftime('%S') + } + }, + shopper_account_risk_data: { + transactions_attempted_last_day: '1', + transactions_attempted_last_year: '2', + purchases_completed_last_six_months: '3', + add_card_attempts_last_day: '4', + previous_suspicious_activity: 'false', # Boolean (true or false) + shipping_name_matches_account_name: 'true', # Boolean (true or false) + shopper_account_age_indicator: 'lessThanThirtyDays', # Possible Values: noAccount, createdDuringTransaction, lessThanThirtyDays, thirtyToSixtyDays, moreThanSixtyDays + shopper_account_change_indicator: 'thirtyToSixtyDays', # Possible values: changedDuringTransaction, lessThanThirtyDays, thirtyToSixtyDays, moreThanSixtyDays + shopper_account_password_change_indicator: 'noChange', # Possible Values: noChange, changedDuringTransaction, lessThanThirtyDays, thirtyToSixtyDays, moreThanSixtyDays + shopper_account_shipping_address_usage_indicator: 'moreThanSixtyDays', # Possible Values: thisTransaction, lessThanThirtyDays, thirtyToSixtyDays, moreThanSixtyDays + shopper_account_payment_account_indicator: 'thirtyToSixtyDays', # Possible Values: noAccount, duringTransaction, lessThanThirtyDays, thirtyToSixtyDays, moreThanSixtyDays + shopper_account_creation_date: { + day_of_month: shopper_account_creation_date.strftime('%d'), + month: shopper_account_creation_date.strftime('%m'), + year: shopper_account_creation_date.strftime('%Y') + }, + shopper_account_modification_date: { + day_of_month: shopper_account_modification_date.strftime('%d'), + month: shopper_account_modification_date.strftime('%m'), + year: shopper_account_modification_date.strftime('%Y') + }, + shopper_account_password_change_date: { + day_of_month: shopper_account_password_change_date.strftime('%d'), + month: shopper_account_password_change_date.strftime('%m'), + year: shopper_account_password_change_date.strftime('%Y') + }, + shopper_account_shipping_address_first_use_date: { + day_of_month: shopper_account_shipping_address_first_use_date.strftime('%d'), + month: shopper_account_shipping_address_first_use_date.strftime('%m'), + year: shopper_account_shipping_address_first_use_date.strftime('%Y') + }, + shopper_account_payment_account_first_use_date: { + day_of_month: shopper_account_payment_account_first_use_date.strftime('%d'), + month: shopper_account_payment_account_first_use_date.strftime('%m'), + year: shopper_account_payment_account_first_use_date.strftime('%Y') + } + }, + transaction_risk_data: { + shipping_method: 'digital', + delivery_timeframe: 'electronicDelivery', + delivery_email_address: 'abe@lincoln.gov', + reordering_previous_purchases: 'false', + pre_order_purchase: 'false', + gift_card_count: '0', + transaction_risk_data_gift_card_amount: { + value: '123', + currency: 'EUR', + exponent: '2', + debit_credit_indicator: 'credit' + }, + transaction_risk_data_pre_order_date: { + day_of_month: transaction_risk_data_pre_order_date.strftime('%d'), + month: transaction_risk_data_pre_order_date.strftime('%m'), + year: transaction_risk_data_pre_order_date.strftime('%Y') + } + } + } + end end diff --git a/test/remote/gateways/remote_worldpay_us_test.rb b/test/remote/gateways/remote_worldpay_us_test.rb index a0e50945fa6..29cce46d447 100644 --- a/test/remote/gateways/remote_worldpay_us_test.rb +++ b/test/remote/gateways/remote_worldpay_us_test.rb @@ -5,9 +5,9 @@ def setup @gateway = WorldpayUsGateway.new(fixtures(:worldpay_us)) @amount = 100 - @credit_card = credit_card('4446661234567892', :verification_value => '987') + @credit_card = credit_card('4446661234567892', verification_value: '987') @declined_card = credit_card('4000300011112220') - @check = check(:number => '12345654321') + @check = check(number: '12345654321') @options = { order_id: generate_unique_id, @@ -23,7 +23,7 @@ def test_successful_purchase end def test_successful_purchase_on_backup_url - gateway = WorldpayUsGateway.new(fixtures(:worldpay_us).merge({ use_backup_url: true})) + gateway = WorldpayUsGateway.new(fixtures(:worldpay_us).merge({ use_backup_url: true })) response = gateway.purchase(@amount, @credit_card, @options) assert_success response assert_equal 'Succeeded', response.message @@ -101,16 +101,16 @@ def test_failed_verify end def test_passing_billing_address - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(:billing_address => address)) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(billing_address: address)) assert_success response end def test_invalid_login gateway = WorldpayUsGateway.new( - :acctid => '', - :subid => '', - :merchantpin => '' - ) + acctid: '', + subid: '', + merchantpin: '' + ) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response assert response.message =~ /DECLINED/ 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/cyber_source/CyberSourceTransaction_1.153.xsd b/test/schema/cyber_source/CyberSourceTransaction_1.153.xsd new file mode 100644 index 00000000000..1bd9f4a1b04 --- /dev/null +++ b/test/schema/cyber_source/CyberSourceTransaction_1.153.xsd @@ -0,0 +1,4770 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/schema/cyber_source/CyberSourceTransaction_1.155.xsd b/test/schema/cyber_source/CyberSourceTransaction_1.155.xsd new file mode 100644 index 00000000000..577ae9fc8a6 --- /dev/null +++ b/test/schema/cyber_source/CyberSourceTransaction_1.155.xsd @@ -0,0 +1,4857 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/schema/cyber_source/CyberSourceTransaction_1.156.xsd b/test/schema/cyber_source/CyberSourceTransaction_1.156.xsd new file mode 100644 index 00000000000..74dc2e7b7a5 --- /dev/null +++ b/test/schema/cyber_source/CyberSourceTransaction_1.156.xsd @@ -0,0 +1,4894 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/schema/cyber_source/CyberSourceTransaction_1.164.xsd b/test/schema/cyber_source/CyberSourceTransaction_1.164.xsd new file mode 100644 index 00000000000..8333412b1a0 --- /dev/null +++ b/test/schema/cyber_source/CyberSourceTransaction_1.164.xsd @@ -0,0 +1,5111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/schema/cyber_source/CyberSourceTransaction_1.181.xsd b/test/schema/cyber_source/CyberSourceTransaction_1.181.xsd new file mode 100644 index 00000000000..3e0c1658642 --- /dev/null +++ b/test/schema/cyber_source/CyberSourceTransaction_1.181.xsd @@ -0,0 +1,5290 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/schema/cyber_source/CyberSourceTransaction_1.198.xsd b/test/schema/cyber_source/CyberSourceTransaction_1.198.xsd new file mode 100644 index 00000000000..aed30c01e3c --- /dev/null +++ b/test/schema/cyber_source/CyberSourceTransaction_1.198.xsd @@ -0,0 +1,5349 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/schema/cyber_source/CyberSourceTransaction_1.201.xsd b/test/schema/cyber_source/CyberSourceTransaction_1.201.xsd new file mode 100644 index 00000000000..ec16a9d7dcd --- /dev/null +++ b/test/schema/cyber_source/CyberSourceTransaction_1.201.xsd @@ -0,0 +1,5106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/schema/firstdata_e4/v27.xsd b/test/schema/firstdata_e4/v27.xsd new file mode 100644 index 00000000000..775018838c6 --- /dev/null +++ b/test/schema/firstdata_e4/v27.xsd @@ -0,0 +1,223 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/schema/orbital/Request_PTI54.xsd b/test/schema/orbital/Request_PTI77.xsd old mode 100644 new mode 100755 similarity index 81% rename from test/schema/orbital/Request_PTI54.xsd rename to test/schema/orbital/Request_PTI77.xsd index 0a75e3a164d..bcae535f6f5 --- a/test/schema/orbital/Request_PTI54.xsd +++ b/test/schema/orbital/Request_PTI77.xsd @@ -1,951 +1,1093 @@ - - - - - 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 - - - - - - - - - - - - - - - - - - - Credit Card - - - - - Swith/Solo - - - - - Electronic Check - - - - - PINLess Debit - - - - - European Direct Debit - - - - - International Maestro - - - - - - - - - - - - - - - - - - - United States - - - - - Canada - - - - - Germany - - - - - Great Britain - - - - - - - - - - - - - - - - - Yes - - - - - Yes - - - - - No - - - - - No - - - - - - - - - - - - - - - - - - - - - - - - - - - Authenticated - - - - - Attempted - - - - - - - - - - - - - - - - - - - First Recurring Transaction - - - - - Subsequent Recurring Transactions - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + 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 + + + + + + + + + + + + + + + + + + + Credit Card + + + + + Swith/Solo + + + + + Electronic Check + + + + + PINLess Debit + + + + + European Direct Debit + + + + + International Maestro + + + + + International Maestro + + + + + International Maestro + + + + + International Maestro + + + + + + + + + + + + + + + + + + + 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/schema/orbital/Request_PTI83.xsd b/test/schema/orbital/Request_PTI83.xsd new file mode 100644 index 00000000000..e2d7e825180 --- /dev/null +++ b/test/schema/orbital/Request_PTI83.xsd @@ -0,0 +1,1142 @@ + + + + + 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 + + + + + + + + + + + + + + + + + + + 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/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/support/mercury_helper.rb b/test/support/mercury_helper.rb index a8afafa25e5..6641b95633a 100644 --- a/test/support/mercury_helper.rb +++ b/test/support/mercury_helper.rb @@ -46,7 +46,7 @@ def hashify_xml!(xml, response) private - def close_batch(gateway=@gateway) + def close_batch(gateway = @gateway) gateway = ActiveMerchant::Billing::MercuryGateway.new(gateway.options) gateway.extend(BatchClosing) gateway.close_batch diff --git a/test/test_helper.rb b/test/test_helper.rb index e26efecf87d..c29ac059954 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,5 +1,4 @@ -#!/usr/bin/env ruby -$:.unshift File.expand_path('../../lib', __FILE__) +$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) require 'bundler/setup' @@ -32,7 +31,7 @@ class SubclassGateway < SimpleTestGateway module ActiveMerchant module Assertions - AssertionClass = defined?(Minitest) ? MiniTest::Assertion : Test::Unit::AssertionFailedError + ASSERTION_CLASS = defined?(Minitest) ? MiniTest::Assertion : Test::Unit::AssertionFailedError def assert_field(field, value) clean_backtrace do @@ -69,20 +68,20 @@ def assert_false(boolean, message = nil) # # A message will automatically show the inspection of the response # object if things go afoul. - def assert_success(response, message=nil) + def assert_success(response, message = nil) clean_backtrace do assert response.success?, build_message(nil, "#{message + "\n" if message}Response expected to succeed: ", response) end end # The negative of +assert_success+ - def assert_failure(response, message=nil) + def assert_failure(response, message = nil) clean_backtrace do assert !response.success?, build_message(nil, "#{message + "\n" if message}Response expected to fail: ", response) end end - def assert_valid(model, message=nil) + def assert_valid(model, message = nil) errors = model.validate clean_backtrace do @@ -102,7 +101,7 @@ def assert_not_valid(model) errors end - def assert_deprecation_warning(message=nil) + def assert_deprecation_warning(message = nil) ActiveMerchant.expects(:deprecated).with(message || anything) yield end @@ -127,20 +126,22 @@ def assert_scrubbed(unexpected_value, transcript) end private - def clean_backtrace(&block) + + def clean_backtrace(&) yield - rescue AssertionClass => e + rescue ASSERTION_CLASS => e path = File.expand_path(__FILE__) - raise AssertionClass, e.message, e.backtrace.reject { |line| File.expand_path(line) =~ /#{path}/ } + raise ASSERTION_CLASS, e.message, (e.backtrace.reject { |line| File.expand_path(line).match?(/#{path}/) }) end end module Fixtures - HOME_DIR = RUBY_PLATFORM =~ /mswin32/ ? ENV['HOMEPATH'] : ENV['HOME'] unless defined?(HOME_DIR) + HOME_DIR = RUBY_PLATFORM.match?('mswin32') ? ENV['HOMEPATH'] : ENV['HOME'] unless defined?(HOME_DIR) LOCAL_CREDENTIALS = File.join(HOME_DIR.to_s, '.active_merchant/fixtures.yml') unless defined?(LOCAL_CREDENTIALS) DEFAULT_CREDENTIALS = File.join(File.dirname(__FILE__), 'fixtures.yml') unless defined?(DEFAULT_CREDENTIALS) private + def default_expiration_date @default_expiration_date ||= Date.new((Time.now.year + 1), 9, 30) end @@ -150,14 +151,15 @@ 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, - :year => default_expiration_date.year, - :first_name => 'Longbob', - :last_name => 'Longsen', - :verification_value => options[:verification_value] || '123', - :brand => 'visa' + number:, + month: default_expiration_date.month, + year: default_expiration_date.year, + first_name: 'Longbob', + last_name: 'Longsen', + verification_value: options[:verification_value] || '123', + brand: 'visa' }.update(options) Billing::CreditCard.new(defaults) @@ -167,7 +169,7 @@ def credit_card_with_track_data(number = '4242424242424242', options = {}) exp_date = default_expiration_date.strftime('%y%m') defaults = { - :track_data => "%B#{number}^LONGSEN/L. ^#{exp_date}1200000000000000**123******?", + track_data: "%B#{number}^LONGSEN/L. ^#{exp_date}1200000000000000**123******?" }.update(options) Billing::CreditCard.new(defaults) @@ -175,13 +177,13 @@ def credit_card_with_track_data(number = '4242424242424242', options = {}) def network_tokenization_credit_card(number = '4242424242424242', options = {}) defaults = { - :number => number, - :month => default_expiration_date.month, - :year => default_expiration_date.year, - :first_name => 'Longbob', - :last_name => 'Longsen', - :verification_value => '123', - :brand => 'visa' + number:, + month: default_expiration_date.month, + year: default_expiration_date.year, + first_name: 'Longbob', + last_name: 'Longsen', + verification_value: '123', + brand: 'visa' }.update(options) Billing::NetworkTokenizationCreditCard.new(defaults) @@ -189,13 +191,13 @@ def network_tokenization_credit_card(number = '4242424242424242', options = {}) def check(options = {}) defaults = { - :name => 'Jim Smith', - :bank_name => 'Bank of Elbonia', - :routing_number => '244183602', - :account_number => '15378535', - :account_holder_type => 'personal', - :account_type => 'checking', - :number => '1' + name: 'Jim Smith', + bank_name: 'Bank of Elbonia', + routing_number: '244183602', + account_number: '15378535', + account_holder_type: 'personal', + account_type: 'checking', + number: '1' }.update(options) Billing::Check.new(defaults) @@ -212,7 +214,8 @@ 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] @@ -234,6 +237,19 @@ def address(options = {}) }.update(options) end + def shipping_address(options = {}) + { + name: 'Jon Smith', + address1: '123 Your Street', + address2: 'Apt 2', + city: 'Toronto', + state: 'ON', + zip: 'K2C3N7', + country: 'CA', + phone_number: '(123)456-7890' + }.update(options) + end + def statement_address(options = {}) { address1: '456 My Street', @@ -244,6 +260,27 @@ def statement_address(options = {}) }.update(options) end + def stored_credential(*args, **options) + id = options.delete(:id) || options.delete(:network_transaction_id) + + stored_credential = { + network_transaction_id: id, + initial_transaction: false + } + + stored_credential[:initial_transaction] = true if args.include?(:initial) + + 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) + + stored_credential + end + def generate_unique_id SecureRandom.hex(16) end @@ -261,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.load(File.read(file_name)) + yaml_data = YAML.safe_load(File.read(file_name), aliases: true) credentials.merge!(symbolize_keys(yaml_data)) end credentials @@ -272,7 +309,7 @@ def symbolize_keys(hash) return unless hash.is_a?(Hash) hash.symbolize_keys! - hash.each{|k,v| symbolize_keys(v)} + hash.each { |_k, v| symbolize_keys(v) } end end end @@ -296,54 +333,24 @@ def dump_transcript_and_fail(gateway, amount, credit_card, params) gateway.purchase(amount, credit_card, params) end - File.open('transcript.log', 'w') { |f| f.write(transcript) } + File.write('transcript.log', transcript) assert false, 'A purchase transcript has been written to transcript.log for you to test scrubbing with.' end end -module ActionViewHelperTestHelper - def self.included(base) - base.send(:include, ActiveMerchant::Billing::Integrations::ActionViewHelper) - base.send(:include, ActionView::Helpers::FormHelper) - base.send(:include, ActionView::Helpers::FormTagHelper) - base.send(:include, ActionView::Helpers::UrlHelper) - base.send(:include, ActionView::Helpers::TagHelper) - base.send(:include, ActionView::Helpers::CaptureHelper) - base.send(:include, ActionView::Helpers::TextHelper) - base.send(:attr_accessor, :output_buffer) - end - - def setup - @controller = Class.new do - attr_reader :url_for_options - def url_for(options, *parameters_for_method_reference) - @url_for_options = options - end - end - @controller = @controller.new - @output_buffer = '' - end - - protected - def protect_against_forgery? - false - end -end - - class MockResponse attr_reader :code, :body, :message attr_accessor :headers - def self.succeeded(body, message='') + def self.succeeded(body, message = '') MockResponse.new(200, body, message) end - def self.failed(body, http_status_code=422, message='') + def self.failed(body, http_status_code = 422, message = '') MockResponse.new(http_status_code, body, message) end - def initialize(code, body, message='', headers={}) + def initialize(code, body, message = '', headers = {}) @code, @body, @message, @headers = code, body, message, headers end diff --git a/test/unit/avs_result_test.rb b/test/unit/avs_result_test.rb index f3b960647b7..7e069c6f1ac 100644 --- a/test/unit/avs_result_test.rb +++ b/test/unit/avs_result_test.rb @@ -6,7 +6,7 @@ def test_nil end def test_no_match - result = AVSResult.new(:code => 'N') + result = AVSResult.new(code: 'N') assert_equal 'N', result.code assert_equal 'N', result.street_match assert_equal 'N', result.postal_match @@ -14,7 +14,7 @@ def test_no_match end def test_only_street_match - result = AVSResult.new(:code => 'A') + result = AVSResult.new(code: 'A') assert_equal 'A', result.code assert_equal 'Y', result.street_match assert_equal 'N', result.postal_match @@ -22,7 +22,7 @@ def test_only_street_match end def test_only_postal_match - result = AVSResult.new(:code => 'W') + result = AVSResult.new(code: 'W') assert_equal 'W', result.code assert_equal 'N', result.street_match assert_equal 'Y', result.postal_match @@ -30,30 +30,30 @@ def test_only_postal_match end def test_nil_data - result = AVSResult.new(:code => nil) + result = AVSResult.new(code: nil) assert_nil result.code assert_nil result.message end def test_empty_data - result = AVSResult.new(:code => '') + result = AVSResult.new(code: '') assert_nil result.code assert_nil result.message end def test_to_hash - avs_data = AVSResult.new(:code => 'X').to_hash + avs_data = AVSResult.new(code: 'X').to_hash assert_equal 'X', avs_data['code'] assert_equal AVSResult.messages['X'], avs_data['message'] end def test_street_match - avs_data = AVSResult.new(:street_match => 'Y') + avs_data = AVSResult.new(street_match: 'Y') assert_equal 'Y', avs_data.street_match end def test_postal_match - avs_data = AVSResult.new(:postal_match => 'Y') + avs_data = AVSResult.new(postal_match: 'Y') assert_equal 'Y', avs_data.postal_match end end diff --git a/test/unit/base_test.rb b/test/unit/base_test.rb index 6e5ac461ab7..82e13503378 100644 --- a/test/unit/base_test.rb +++ b/test/unit/base_test.rb @@ -12,7 +12,6 @@ def teardown def test_should_return_a_new_gateway_specified_by_symbol_name assert_equal BogusGateway, Base.gateway(:bogus) assert_equal MonerisGateway, Base.gateway(:moneris) - assert_equal MonerisUsGateway, Base.gateway(:moneris_us) assert_equal AuthorizeNetGateway, Base.gateway(:authorize_net) assert_equal UsaEpayGateway, Base.gateway(:usa_epay) assert_equal LinkpointGateway, Base.gateway(:linkpoint) diff --git a/test/unit/check_test.rb b/test/unit/check_test.rb index e5908553ab9..bdd03346d1d 100644 --- a/test/unit/check_test.rb +++ b/test/unit/check_test.rb @@ -4,35 +4,55 @@ class CheckTest < Test::Unit::TestCase VALID_ABA = '111000025' INVALID_ABA = '999999999' MALFORMED_ABA = 'I like fish' + VALID_ELECTRONIC_CBA = '000194611' + VALID_MICR_CBA = '94611001' + INVALID_NINE_DIGIT_CBA = '012345678' + INVALID_SEVEN_DIGIT_ROUTING_NUMBER = '0123456' ACCOUNT_NUMBER = '123456789012' + CHECK_US = Check.new( + name: 'Fred Bloggs', + routing_number: VALID_ABA, + account_number: ACCOUNT_NUMBER, + account_holder_type: 'personal', + account_type: 'checking' + ) + CHECK_CAN_E_FORMAT = Check.new( + name: 'Tim Horton', + routing_number: VALID_ELECTRONIC_CBA, + account_number: ACCOUNT_NUMBER, + account_holder_type: 'personal', + account_type: 'checking' + ) + CHECK_CAN_MICR_FORMAT = Check.new( + name: 'Tim Horton', + routing_number: VALID_MICR_CBA, + account_number: ACCOUNT_NUMBER, + account_holder_type: 'personal', + account_type: 'checking' + ) + def test_validation assert_not_valid Check.new end def test_first_name_last_name - check = Check.new(:name => 'Fred Bloggs') + check = Check.new(name: 'Fred Bloggs') assert_equal 'Fred', check.first_name assert_equal 'Bloggs', check.last_name assert_equal 'Fred Bloggs', check.name end def test_nil_name - check = Check.new(:name => nil) + check = Check.new(name: nil) assert_nil check.first_name assert_nil check.last_name assert_equal '', check.name end def test_valid - assert_valid Check.new( - :name => 'Fred Bloggs', - :routing_number => VALID_ABA, - :account_number => ACCOUNT_NUMBER, - :account_holder_type => 'personal', - :account_type => 'checking' - ) + assert_valid CHECK_US end def test_credit_card? @@ -40,12 +60,12 @@ def test_credit_card? end def test_invalid_routing_number - errors = assert_not_valid Check.new(:routing_number => INVALID_ABA) + errors = assert_not_valid Check.new(routing_number: INVALID_ABA) assert_equal ['is invalid'], errors[:routing_number] end def test_malformed_routing_number - errors = assert_not_valid Check.new(:routing_number => MALFORMED_ABA) + errors = assert_not_valid Check.new(routing_number: MALFORMED_ABA) assert_equal ['is invalid'], errors[:routing_number] end @@ -78,4 +98,46 @@ def test_account_type c.account_type = nil assert !c.validate[:account_type] end + + def test_valid_canada_routing_number_electronic_format + assert_valid CHECK_CAN_E_FORMAT + end + + def test_valid_canada_routing_number_micr_format + assert_valid CHECK_CAN_MICR_FORMAT + end + + def test_invalid_canada_routing_number + errors = assert_not_valid Check.new( + name: 'Tim Horton', + routing_number: INVALID_NINE_DIGIT_CBA, + account_number: ACCOUNT_NUMBER, + account_holder_type: 'personal', + account_type: 'checking' + ) + + assert_equal ['is invalid'], errors[:routing_number] + end + + def test_invalid_routing_number_length + errors = assert_not_valid Check.new( + name: 'Routing Shortlength', + routing_number: INVALID_SEVEN_DIGIT_ROUTING_NUMBER, + account_number: ACCOUNT_NUMBER, + account_holder_type: 'personal', + account_type: 'checking' + ) + + assert_equal ['is invalid'], errors[:routing_number] + end + + def test_format_routing_number + assert CHECK_CAN_E_FORMAT.micr_format_routing_number == '94611001' + assert CHECK_CAN_MICR_FORMAT.micr_format_routing_number == '94611001' + assert CHECK_US.micr_format_routing_number == '111000025' + + assert CHECK_CAN_E_FORMAT.electronic_format_routing_number == '000194611' + assert CHECK_CAN_MICR_FORMAT.electronic_format_routing_number == '000194611' + assert CHECK_US.electronic_format_routing_number == '111000025' + end end diff --git a/test/unit/connection_test.rb b/test/unit/connection_test.rb index 03261fcbf0c..c6bdc1d72e4 100644 --- a/test/unit/connection_test.rb +++ b/test/unit/connection_test.rb @@ -1,13 +1,12 @@ require 'test_helper' class ConnectionTest < Test::Unit::TestCase - def setup - @ok = stub(:code => 200, :message => 'OK', :body => 'success') + @ok = stub(code: 200, message: 'OK', body: 'success') @endpoint = 'https://example.com/tx.php' @connection = ActiveMerchant::Connection.new(@endpoint) - @connection.logger = stub(:info => nil, :debug => nil, :error => nil) + @connection.logger = stub(info: nil, debug: nil, error: nil) end def test_connection_endpoint_parses_string_to_uri @@ -28,52 +27,56 @@ def test_connection_endpoint_raises_uri_error def test_connection_passes_env_proxy_by_default spy = Net::HTTP.new('example.com', 443) - Net::HTTP.expects(:new).with('example.com', 443, :ENV, nil).returns(spy) + Net::HTTP.expects(:new).with('example.com', 443, :ENV, nil, nil, nil).returns(spy) spy.expects(:start).returns(true) - spy.expects(:get).with('/tx.php', {'connection' => 'close'}).returns(@ok) + spy.expects(:get).with('/tx.php', { 'connection' => 'close' }).returns(@ok) @connection.request(:get, nil, {}) end def test_connection_does_pass_requested_proxy @connection.proxy_address = 'proxy.example.com' @connection.proxy_port = 8080 + @connection.proxy_user = 'user' + @connection.proxy_password = 'password' spy = Net::HTTP.new('example.com', 443) - Net::HTTP.expects(:new).with('example.com', 443, 'proxy.example.com', 8080).returns(spy) + Net::HTTP.expects(:new).with('example.com', 443, 'proxy.example.com', 8080, 'user', 'password').returns(spy) spy.expects(:start).returns(true) - spy.expects(:get).with('/tx.php', {'connection' => 'close'}).returns(@ok) + spy.expects(:get).with('/tx.php', { 'connection' => 'close' }).returns(@ok) @connection.request(:get, nil, {}) end def test_connection_does_not_mutate_headers_argument headers = { 'Content-Type' => 'text/xml' }.freeze + Net::HTTP.any_instance.expects(:get).with('/tx.php', headers.merge({ 'connection' => 'close' })).returns(@ok) + Net::HTTP.any_instance.expects(:start).returns(true) @connection.request(:get, nil, headers) assert_equal({ 'Content-Type' => 'text/xml' }, headers) end def test_successful_get_request @connection.logger.expects(:info).twice - Net::HTTP.any_instance.expects(:get).with('/tx.php', {'connection' => 'close'}).returns(@ok) + Net::HTTP.any_instance.expects(:get).with('/tx.php', { 'connection' => 'close' }).returns(@ok) Net::HTTP.any_instance.expects(:start).returns(true) response = @connection.request(:get, nil, {}) assert_equal 'success', response.body end def test_successful_post_request - Net::HTTP.any_instance.expects(:post).with('/tx.php', 'data', ActiveMerchant::Connection::RUBY_184_POST_HEADERS.merge({'connection' => 'close'})).returns(@ok) + Net::HTTP.any_instance.expects(:post).with('/tx.php', 'data', ActiveMerchant::Connection::RUBY_184_POST_HEADERS.merge({ 'connection' => 'close' })).returns(@ok) Net::HTTP.any_instance.expects(:start).returns(true) response = @connection.request(:post, 'data', {}) assert_equal 'success', response.body end def test_successful_put_request - Net::HTTP.any_instance.expects(:put).with('/tx.php', 'data', {'connection' => 'close'}).returns(@ok) + Net::HTTP.any_instance.expects(:put).with('/tx.php', 'data', { 'connection' => 'close' }).returns(@ok) Net::HTTP.any_instance.expects(:start).returns(true) response = @connection.request(:put, 'data', {}) assert_equal 'success', response.body end def test_successful_delete_request - Net::HTTP.any_instance.expects(:delete).with('/tx.php', {'connection' => 'close'}).returns(@ok) + Net::HTTP.any_instance.expects(:delete).with('/tx.php', { 'connection' => 'close' }).returns(@ok) Net::HTTP.any_instance.expects(:start).returns(true) response = @connection.request(:delete, nil, {}) assert_equal 'success', response.body @@ -86,13 +89,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) @@ -213,9 +209,12 @@ def test_failure_then_success_with_retry_safe_enabled end def test_mixture_of_failures_with_retry_safe_enabled - Net::HTTP.any_instance.expects(:start).times(3).raises(Errno::ECONNRESET). - raises(Errno::ECONNREFUSED). - raises(EOFError) + Net::HTTP.any_instance. + expects(:start). + times(3). + raises(Errno::ECONNRESET). + raises(Errno::ECONNREFUSED). + raises(EOFError) @connection.retry_safe = true @@ -239,5 +238,4 @@ def test_wiredump_service_raises_on_frozen_object @connection.wiredump_device = transcript end end - end diff --git a/test/unit/country_code_test.rb b/test/unit/country_code_test.rb index b455d05f3d7..12ee3ae4ab4 100644 --- a/test/unit/country_code_test.rb +++ b/test/unit/country_code_test.rb @@ -26,6 +26,6 @@ def test_numeric_code end def test_invalid_code_format - assert_raises(ActiveMerchant::CountryCodeFormatError){ ActiveMerchant::CountryCode.new('Canada') } + assert_raises(ActiveMerchant::CountryCodeFormatError) { ActiveMerchant::CountryCode.new('Canada') } end end diff --git a/test/unit/country_test.rb b/test/unit/country_test.rb index a48e73fa36f..711118396cf 100644 --- a/test/unit/country_test.rb +++ b/test/unit/country_test.rb @@ -2,7 +2,7 @@ class CountryTest < Test::Unit::TestCase def test_country_from_hash - country = ActiveMerchant::Country.new(:name => 'Canada', :alpha2 => 'CA', :alpha3 => 'CAN', :numeric => '124') + country = ActiveMerchant::Country.new(name: 'Canada', alpha2: 'CA', alpha3: 'CAN', numeric: '124') assert_equal 'CA', country.code(:alpha2).value assert_equal 'CAN', country.code(:alpha3).value assert_equal '124', country.code(:numeric).value @@ -59,6 +59,30 @@ def test_find_united_kingdom assert_equal 'GB', country.code(:alpha2).value end + def test_find_romania + country = ActiveMerchant::Country.find('ROM') + assert_equal 'RO', country.code(:alpha2).value + + country = ActiveMerchant::Country.find('ROU') + assert_equal 'RO', country.code(:alpha2).value + + country = ActiveMerchant::Country.find('Romania') + assert_equal 'ROU', country.code(:alpha3).value + + country = ActiveMerchant::Country.find('Romania') + assert_not_equal 'ROM', country.code(:alpha3).value + end + + def test_find_vietnam_with_either_spelling + country = ActiveMerchant::Country.find('Viet Nam') + assert_equal 'VN', country.code(:alpha2).value + assert_equal 'VNM', country.code(:alpha3).value + + country = ActiveMerchant::Country.find('Vietnam') + assert_equal 'VN', country.code(:alpha2).value + assert_equal 'VNM', country.code(:alpha3).value + end + def test_raise_on_nil_name assert_raises(ActiveMerchant::InvalidCountryCodeError) do ActiveMerchant::Country.find(nil) @@ -66,7 +90,7 @@ def test_raise_on_nil_name end def test_country_names_are_alphabetized - country_names = ActiveMerchant::Country::COUNTRIES.map { | each | each[:name] } + country_names = ActiveMerchant::Country::COUNTRIES.map { |each| each[:name] } assert_equal(country_names.sort, country_names) end diff --git a/test/unit/credit_card_formatting_test.rb b/test/unit/credit_card_formatting_test.rb index 73e70c9e151..90915f241b3 100644 --- a/test/unit/credit_card_formatting_test.rb +++ b/test/unit/credit_card_formatting_test.rb @@ -6,6 +6,11 @@ class CreditCardFormattingTest < Test::Unit::TestCase def test_should_format_number_by_rule assert_equal 2005, format(2005, :steven_colbert) + assert_equal '2022', format(22, :four_digits_year) + assert_equal '2022', format(2022, :four_digits_year) + assert_equal '2022', format('22', :four_digits_year) + assert_equal '2022', format('2022', :four_digits_year) + assert_equal '0005', format(05, :four_digits) assert_equal '2005', format(2005, :four_digits) diff --git a/test/unit/credit_card_methods_test.rb b/test/unit/credit_card_methods_test.rb index 0854a059065..40ce730f693 100644 --- a/test/unit/credit_card_methods_test.rb +++ b/test/unit/credit_card_methods_test.rb @@ -9,19 +9,44 @@ class CreditCard def maestro_card_numbers %w[ - 5000000000000000 5099999999999999 5600000000000000 - 5899999999999999 6000000000000000 6999999999999999 - 6761999999999999 6763000000000000 5038999999999999 + 5612590000000000 5817500000000000 5818000000000000 + 6390000000000000 6390700000000000 6390990000000000 + 6761999999999999 6763000000000000 6799999999999999 + 5000330000000000 5811499999999999 5010410000000000 + 5010630000000000 5892440000000000 5016230000000000 ] end def non_maestro_card_numbers %w[ 4999999999999999 5100000000000000 5599999999999999 - 5900000000000000 5999999999999999 7000000000000000 + 5612709999999999 5817520000000000 5818019999999999 + 5912600000000000 6000009999999999 7000000000000000 ] end + 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 501623 + 501800 501089 501091 501092 501095 501104 501105 501107 501108 501109 501500 501879 + 502000 502113 502301 503175 503645 503800 + 503670 504310 504338 504363 504533 504587 504620 504738 504781 504910 + 505616 + 507001 507002 507004 507082 507090 560014 560565 561033 572402 572610 572626 576904 578614 + 585274 585697 586509 588729 588792 589244 589300 589407 589471 589605 589633 589647 589671 + 590043 590206 590263 590265 + 590278 590361 590362 590379 590393 590590 591235 591420 591481 591620 591770 591948 591994 592024 + 592161 592184 592186 592201 592384 592393 592528 592566 592704 592735 592879 592884 593074 593264 + 593272 593355 593496 593556 593589 593666 593709 593825 593963 593994 594184 594409 594468 594475 + 594581 594665 594691 594710 594874 594968 595355 595364 595532 595547 595561 595568 595743 595929 + 596245 596289 596399 596405 596590 596608 596645 596646 596791 596808 596815 596846 597077 597094 + 597143 597370 597410 597765 597855 597862 598053 598054 598395 598585 598793 598794 598815 598835 + 598838 598880 598889 599000 599069 599089 599148 599191 599310 599741 599742 599867 + 601070 604983 601638 606126 + 630400 636380 636422 636502 636639 637046 637756 639130 639229 690032] + end + def test_should_be_able_to_identify_valid_expiry_months assert_false valid_month?(-1) assert_false valid_month?(13) @@ -99,67 +124,289 @@ def test_should_detect_electron_dk_as_visa def test_should_detect_diners_club assert_equal 'diners_club', CreditCard.brand?('36148010000000') + assert_equal 'diners_club', CreditCard.brand?('3000000000000004') end def test_should_detect_diners_club_dk assert_equal 'diners_club', CreditCard.brand?('30401000000000') end + def test_should_detect_jcb_cards + assert_equal 'jcb', CreditCard.brand?('3528000000000000') + assert_equal 'jcb', CreditCard.brand?('3580000000000000') + assert_equal 'jcb', CreditCard.brand?('3088000000000017') + assert_equal 'jcb', CreditCard.brand?('3094000000000017') + assert_equal 'jcb', CreditCard.brand?('3096000000000000') + assert_equal 'jcb', CreditCard.brand?('3102000000000017') + assert_equal 'jcb', CreditCard.brand?('3112000000000000') + assert_equal 'jcb', CreditCard.brand?('3120000000000017') + assert_equal 'jcb', CreditCard.brand?('3158000000000000') + assert_equal 'jcb', CreditCard.brand?('3159000000000017') + assert_equal 'jcb', CreditCard.brand?('3337000000000000') + assert_equal 'jcb', CreditCard.brand?('3349000000000017') + end + def test_should_detect_maestro_dk_as_maestro assert_equal 'maestro', CreditCard.brand?('6769271000000000') end def test_should_detect_maestro_cards - assert_equal 'maestro', CreditCard.brand?('5020100000000000') + assert_equal 'maestro', CreditCard.brand?('675675000000000') maestro_card_numbers.each { |number| assert_equal 'maestro', CreditCard.brand?(number) } + maestro_bins.each { |bin| assert_equal 'maestro', CreditCard.brand?("#{bin}0000000000") } non_maestro_card_numbers.each { |number| assert_not_equal 'maestro', CreditCard.brand?(number) } end def test_should_detect_mastercard - assert_equal 'master', CreditCard.brand?('6771890000000000') + assert_equal 'master', CreditCard.brand?('2720890000000000') assert_equal 'master', CreditCard.brand?('5413031000000000') + assert_equal 'master', CreditCard.brand?('6052721000000000') + assert_equal 'master', CreditCard.brand?('6062821000000000') + assert_equal 'master', CreditCard.brand?('6370951000000000') + assert_equal 'master', CreditCard.brand?('6375681000000000') + assert_equal 'master', CreditCard.brand?('6375991000000000') + assert_equal 'master', CreditCard.brand?('6376091000000000') end def test_should_detect_forbrugsforeningen assert_equal 'forbrugsforeningen', CreditCard.brand?('6007221000000000') end - def test_should_detect_laser_card - # 16 digits - assert_equal 'laser', CreditCard.brand?('6304985028090561') + 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 + assert_equal 'alia', CreditCard.brand?('5049970000000000') + assert_equal 'alia', CreditCard.brand?('5058780000000000') + assert_equal 'alia', CreditCard.brand?('6010300000000000') + assert_equal 'alia', CreditCard.brand?('6010730000000000') + assert_equal 'alia', CreditCard.brand?('5058740000000000') + end + + def test_should_detect_mada_card + assert_equal 'mada', CreditCard.brand?('5043000000000000') + assert_equal 'mada', CreditCard.brand?('5852650000000000') + assert_equal 'mada', CreditCard.brand?('5888500000000000') + assert_equal 'mada', CreditCard.brand?('6361200000000000') + assert_equal 'mada', CreditCard.brand?('9682040000000000') + end + + def test_alia_number_not_validated + 10.times do + number = rand(5058740000000001..5058749999999999).to_s + assert_equal 'alia', CreditCard.brand?(number) + assert CreditCard.valid_number?(number) + end + end + + def test_should_detect_confiable_card + assert_equal 'confiable', CreditCard.brand?('5607180000000000') + end + + def test_should_detect_bp_plus_card + assert_equal 'bp_plus', CreditCard.brand?('70501 501021600 378') + assert_equal 'bp_plus', CreditCard.brand?('70502 111111111 111') + assert_equal 'bp_plus', CreditCard.brand?('7050 15605297 00114') + assert_equal 'bp_plus', CreditCard.brand?('7050 15546992 00062') + end + + def test_should_validate_bp_plus_card + assert_true CreditCard.valid_number?('70501 501021600 378') + assert_true CreditCard.valid_number?('7050 15605297 00114') + assert_true CreditCard.valid_number?('7050 15546992 00062') + assert_true CreditCard.valid_number?('7050 16150146 00110') + assert_true CreditCard.valid_number?('7050 16364764 00070') + + # numbers with invalid formats + assert_false CreditCard.valid_number?('7050_15546992_00062') + assert_false CreditCard.valid_number?('70501 55469920 0062') + assert_false CreditCard.valid_number?('70 501554699 200062') + + # numbers that are luhn-invalid + assert_false CreditCard.valid_number?('70502 111111111 111') + assert_false CreditCard.valid_number?('7050 16364764 00071') + assert_false CreditCard.valid_number?('7050 16364764 00072') + end + + def test_confiable_number_not_validated + 10.times do + number = rand(5607180000000001..5607189999999999).to_s + assert_equal 'confiable', CreditCard.brand?(number) + assert CreditCard.valid_number?(number) + end + end + + def test_should_detect_maestro_no_luhn_card + assert_equal 'maestro_no_luhn', CreditCard.brand?('5010800000000000') + assert_equal 'maestro_no_luhn', CreditCard.brand?('5010810000000000') + assert_equal 'maestro_no_luhn', CreditCard.brand?('5010820000000000') + assert_equal 'maestro_no_luhn', CreditCard.brand?('501082000000') + assert_equal 'maestro_no_luhn', CreditCard.brand?('5010820000000000000') + end - # 18 digits - assert_equal 'laser', CreditCard.brand?('630498502809056151') + def test_maestro_no_luhn_number_not_validated + 10.times do + number = rand(5010800000000001..5010829999999999).to_s + assert_equal 'maestro_no_luhn', CreditCard.brand?(number) + assert CreditCard.valid_number?(number) + end + end - # 19 digits - assert_equal 'laser', CreditCard.brand?('6304985028090561515') + def test_should_detect_olimpica_card + assert_equal 'olimpica', CreditCard.brand?('6368530000000000') + end - # 17 digits - assert_not_equal 'laser', CreditCard.brand?('63049850280905615') + def test_should_detect_sodexo_no_luhn_card + number1 = '5058645584812145' + number2 = '5058655584812145' + assert_equal 'sodexo', CreditCard.brand?(number1) + assert CreditCard.valid_number?(number1) + assert_equal 'sodexo', CreditCard.brand?(number2) + assert CreditCard.valid_number?(number2) + end - # 15 digits - assert_not_equal 'laser', CreditCard.brand?('630498502809056') + def test_should_validate_sodexo_no_luhn_card + assert_true CreditCard.valid_number?('5058645584812145') + assert_false CreditCard.valid_number?('5058665584812110') + end - # Alternate format - assert_equal 'laser', CreditCard.brand?('6706950000000000000') + def test_should_detect_passcard_card + assert_equal 'passcard', CreditCard.brand?('6280260025383009') + assert_equal 'passcard', CreditCard.brand?('6280260025383280') + assert_equal 'passcard', CreditCard.brand?('6280260025383298') + assert_equal 'passcard', CreditCard.brand?('6280260025383306') + assert_equal 'passcard', CreditCard.brand?('6280260025383314') + end + + def test_should_validate_passcard_card + assert_true CreditCard.valid_number?('6280260025383009') + # numbers with invalid formats + assert_false CreditCard.valid_number?('6280_26002538_0005') + # numbers that are luhn-invalid + assert_false CreditCard.valid_number?('6280260025380991') + end - # Alternate format (16 digits) - assert_equal 'laser', CreditCard.brand?('6706123456789012') + def test_should_detect_edenred_card + assert_equal 'edenred', CreditCard.brand?('6374830000000823') + assert_equal 'edenred', CreditCard.brand?('6374830000000799') + assert_equal 'edenred', CreditCard.brand?('6374830000000807') + assert_equal 'edenred', CreditCard.brand?('6374830000000815') + assert_equal 'edenred', CreditCard.brand?('6374830000000823') + end - # New format (16 digits) - assert_equal 'laser', CreditCard.brand?('6709123456789012') + def test_should_validate_edenred_card + assert_true CreditCard.valid_number?('6374830000000369') + # numbers with invalid formats + assert_false CreditCard.valid_number?('6374 8300000 00369') + # numbers that are luhn-invalid + assert_false CreditCard.valid_number?('6374830000000111') + end - # Ulster bank (Ireland) with 12 digits - assert_equal 'laser', CreditCard.brand?('677117111234') + def test_should_detect_anda_card + assert_equal 'anda', CreditCard.brand?('6031998427187914') end - def test_should_detect_sodexo_card - assert_equal 'sodexo', CreditCard.brand?('60606944957644') + # Creditos directos a.k.a tarjeta d + def test_should_detect_tarjetad_card + assert_equal 'tarjeta-d', CreditCard.brand?('6018282227431033') + end + + def test_should_detect_creditel_card + assert_equal 'creditel', CreditCard.brand?('6019330047539016') end def test_should_detect_vr_card - assert_equal 'vr', CreditCard.brand?('63703644957644') + assert_equal 'vr', CreditCard.brand?('6370364495764400') + assert_equal 'vr', CreditCard.brand?('6274160000000001') + end + + def test_should_detect_elo_card + assert_equal 'elo', CreditCard.brand?('5090510000000000') + assert_equal 'elo', CreditCard.brand?('5067530000000000') + assert_equal 'elo', CreditCard.brand?('6277800000000000') + assert_equal 'elo', CreditCard.brand?('6509550000000000') + assert_equal 'elo', CreditCard.brand?('5090890000000000') + assert_equal 'elo', CreditCard.brand?('5092570000000000') + assert_equal 'elo', CreditCard.brand?('5094100000000000') + end + + def test_should_detect_alelo_card + assert_equal 'alelo', CreditCard.brand?('5067490000000010') + assert_equal 'alelo', CreditCard.brand?('5067700000000028') + assert_equal 'alelo', CreditCard.brand?('5067600000000036') + assert_equal 'alelo', CreditCard.brand?('5067600000000044') + assert_equal 'alelo', CreditCard.brand?('5099920000000000') + assert_equal 'alelo', CreditCard.brand?('5067630000000000') + assert_equal 'alelo', CreditCard.brand?('5098870000000000') + end + + def test_should_detect_naranja_card + assert_equal 'naranja', CreditCard.brand?('5895627823453005') + assert_equal 'naranja', CreditCard.brand?('5895620000000002') + assert_equal 'naranja', CreditCard.brand?('5895626746595650') + assert_equal 'naranja', CreditCard.brand?('5895628637412581') + assert_equal 'naranja', CreditCard.brand?('5895627087232438') + end + + # Alelo BINs beginning with the digit 4 overlap with Visa's range of valid card numbers. + # We intentionally misidentify these cards as Visa, which works because transactions with + # such cards will run on Visa rails. + def test_should_detect_alelo_number_beginning_with_4_as_visa + assert_equal 'visa', CreditCard.brand?('4025880000000010') + assert_equal 'visa', CreditCard.brand?('4025880000000028') + assert_equal 'visa', CreditCard.brand?('4025880000000036') + assert_equal 'visa', CreditCard.brand?('4025880000000044') + end + + def test_should_detect_cabal_card + assert_equal 'cabal', CreditCard.brand?('6044009000000000') + assert_equal 'cabal', CreditCard.brand?('5896575500000000') + assert_equal 'cabal', CreditCard.brand?('6035224400000000') + assert_equal 'cabal', CreditCard.brand?('6502723300000000') + assert_equal 'cabal', CreditCard.brand?('6500870000000000') + assert_equal 'cabal', CreditCard.brand?('6509000000000000') + end + + def test_should_detect_unionpay_card + assert_equal 'unionpay', CreditCard.brand?('6221260000000000') + assert_equal 'unionpay', CreditCard.brand?('6250941006528599') + assert_equal 'unionpay', CreditCard.brand?('6282000000000000') + assert_equal 'unionpay', CreditCard.brand?('8100000000000000') + assert_equal 'unionpay', CreditCard.brand?('814400000000000000') + 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 + assert_equal 'synchrony', CreditCard.brand?('7006000000000000') + end + + def test_should_detect_routex_card + number = '7006760000000000000' + 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 @@ -168,14 +415,14 @@ def test_should_detect_when_an_argument_brand_does_not_match_calculated_brand end def test_detecting_full_range_of_maestro_card_numbers - maestro = '50000000000' + maestro = '63900000000' assert_equal 11, maestro.length assert_not_equal 'maestro', CreditCard.brand?(maestro) while maestro.length < 19 maestro << '0' - assert_equal 'maestro', CreditCard.brand?(maestro) + assert_equal 'maestro', CreditCard.brand?(maestro), "Failed for bin #{maestro}" end assert_equal 19, maestro.length @@ -187,7 +434,6 @@ def test_detecting_full_range_of_maestro_card_numbers def test_matching_discover_card assert_equal 'discover', CreditCard.brand?('6011000000000000') assert_equal 'discover', CreditCard.brand?('6500000000000000') - assert_equal 'discover', CreditCard.brand?('6221260000000000') assert_equal 'discover', CreditCard.brand?('6450000000000000') assert_not_equal 'discover', CreditCard.brand?('6010000000000000') @@ -197,24 +443,59 @@ def test_matching_discover_card def test_matching_invalid_card assert_nil CreditCard.brand?('XXXXXXXXXXXX0000') assert_false CreditCard.valid_number?('XXXXXXXXXXXX0000') + assert_false CreditCard.valid_number?(nil) + end + + def test_matching_valid_naranja + %w[5895627823453005 5895627087232438 5895628637412581].each do |number| + assert_equal 'naranja', CreditCard.brand?(number) + assert CreditCard.valid_number?(number) + end + end + + def test_matching_valid_creditel + number = '6019330047539016' + assert_equal 'creditel', CreditCard.brand?(number) + assert CreditCard.valid_number?(number) end def test_16_digit_maestro_uk number = '6759000000000000' assert_equal 16, number.length - assert_equal 'switch', CreditCard.brand?(number) + assert_equal 'maestro', CreditCard.brand?(number) end def test_18_digit_maestro_uk number = '675900000000000000' assert_equal 18, number.length - assert_equal 'switch', CreditCard.brand?(number) + assert_equal 'maestro', CreditCard.brand?(number) end def test_19_digit_maestro_uk number = '6759000000000000000' assert_equal 19, number.length - assert_equal 'switch', CreditCard.brand?(number) + assert_equal 'maestro', CreditCard.brand?(number) + end + + def test_carnet_cards + numbers = %w[ + 5062280000000000 + 6046220312312312 + 6393889871239871 + 5022751231231231 + 6275350000000001 + ] + numbers.each do |num| + assert_equal 16, num.length + assert_equal 'carnet', CreditCard.brand?(num) + end + end + + def test_should_detect_cartes_bancaires_cards + assert_equal 'cartes_bancaires', CreditCard.brand?('5855010000000000') + assert_equal 'cartes_bancaires', CreditCard.brand?('5075935000000000') + assert_equal 'cartes_bancaires', CreditCard.brand?('5075901100000000') + assert_equal 'cartes_bancaires', CreditCard.brand?('5075890130000000') end def test_electron_cards @@ -230,6 +511,9 @@ def test_electron_cards end end + # nil check + assert_false electron_test.call(nil) + # Visa range assert_false electron_test.call('4245180000000000') assert_false electron_test.call('4918810000000000') @@ -241,6 +525,98 @@ 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_should_detect_patagonia_365_cards + assert_equal 'patagonia_365', CreditCard.brand?('5046562602769006') + end + + def test_should_validate_patagonia_365_card + assert_true CreditCard.valid_number?('5046562602769006') + end + + def test_should_detect_invalid_patagonia_365_card + assert_false CreditCard.valid_number?('5046562602769005') + end + + def test_should_detect_sol_cards + assert_equal 'tarjeta_sol', CreditCard.brand?('5046391746825544') + end + + def test_should_validate_sol_card + assert_true CreditCard.valid_number?('5046391746825544') + end + + def test_should_detect_invalid_sol_card + assert_false CreditCard.valid_number?('5046390000000001') + 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 b39600376c9..595b3698bfa 100644 --- a/test/unit/credit_card_test.rb +++ b/test/unit/credit_card_test.rb @@ -3,8 +3,9 @@ class CreditCardTest < Test::Unit::TestCase def setup CreditCard.require_verification_value = false - @visa = credit_card('4779139500118580', :brand => 'visa') - @solo = credit_card('676700000000000000', :brand => 'solo', :issue_number => '01') + @visa = credit_card('4779139500118580', brand: 'visa') + @maestro = credit_card('676700000000000000', brand: 'maestro', verification_value: '') + @bp_plus = credit_card('70501 501021600 378', brand: 'bp_plus') end def teardown @@ -32,8 +33,13 @@ def test_should_be_a_valid_visa_card assert_valid @visa end - def test_should_be_a_valid_solo_card - assert_valid @solo + def test_should_be_a_valid_maestro_card + assert_valid @maestro + end + + def test_should_be_a_valid_maestro_card_when_require_cvv_is_true + CreditCard.require_verification_value = true + assert_valid @maestro end def test_cards_with_empty_names_should_not_be_valid @@ -44,7 +50,7 @@ def test_cards_with_empty_names_should_not_be_valid end def test_should_be_able_to_liberate_a_bogus_card - c = credit_card('', :brand => 'bogus') + c = credit_card('', brand: 'bogus') assert_valid c c.brand = 'visa' @@ -145,11 +151,11 @@ def test_should_be_invalid_with_empty_year end def test_should_not_be_valid_for_edge_year_cases - @visa.year = Time.now.year - 1 + @visa.year = Time.now.year - 1 errors = assert_not_valid @visa assert errors[:year] - @visa.year = Time.now.year + 21 + @visa.year = Time.now.year + 21 errors = assert_not_valid @visa assert errors[:year] end @@ -167,34 +173,28 @@ def test_expired_card_should_have_one_error_on_year assert_match(/expired/, errors[:year].first) end - def test_should_be_valid_with_start_month_and_year_as_string - @solo.start_month = '2' - @solo.start_year = '2007' - assert_valid @solo - end - def test_should_identify_wrong_card_brand - c = credit_card(:brand => 'master') + c = credit_card('4779139500118580', brand: 'master') assert_not_valid c end def test_should_display_number - assert_equal 'XXXX-XXXX-XXXX-1234', CreditCard.new(:number => '1111222233331234').display_number - assert_equal 'XXXX-XXXX-XXXX-1234', CreditCard.new(:number => '111222233331234').display_number - assert_equal 'XXXX-XXXX-XXXX-1234', CreditCard.new(:number => '1112223331234').display_number + assert_equal 'XXXX-XXXX-XXXX-1234', CreditCard.new(number: '1111222233331234').display_number + assert_equal 'XXXX-XXXX-XXXX-1234', CreditCard.new(number: '111222233331234').display_number + assert_equal 'XXXX-XXXX-XXXX-1234', CreditCard.new(number: '1112223331234').display_number - assert_equal 'XXXX-XXXX-XXXX-', CreditCard.new(:number => nil).display_number - assert_equal 'XXXX-XXXX-XXXX-', CreditCard.new(:number => '').display_number - assert_equal 'XXXX-XXXX-XXXX-123', CreditCard.new(:number => '123').display_number - assert_equal 'XXXX-XXXX-XXXX-1234', CreditCard.new(:number => '1234').display_number - assert_equal 'XXXX-XXXX-XXXX-1234', CreditCard.new(:number => '01234').display_number + assert_equal 'XXXX-XXXX-XXXX-', CreditCard.new(number: nil).display_number + assert_equal 'XXXX-XXXX-XXXX-', CreditCard.new(number: '').display_number + assert_equal 'XXXX-XXXX-XXXX-123', CreditCard.new(number: '123').display_number + assert_equal 'XXXX-XXXX-XXXX-1234', CreditCard.new(number: '1234').display_number + assert_equal 'XXXX-XXXX-XXXX-1234', CreditCard.new(number: '01234').display_number end def test_should_correctly_identify_card_brand assert_equal 'visa', CreditCard.brand?('4242424242424242') assert_equal 'american_express', CreditCard.brand?('341111111111111') assert_equal 'master', CreditCard.brand?('5105105105105100') - (222100..272099).each {|bin| assert_equal 'master', CreditCard.brand?(bin.to_s + '1111111111'), "Failed with BIN #{bin}"} + (222100..272099).each { |bin| assert_equal 'master', CreditCard.brand?(bin.to_s + '1111111111'), "Failed with BIN #{bin}" } assert_nil CreditCard.brand?('') end @@ -205,7 +205,7 @@ def test_should_be_able_to_require_a_verification_value def test_should_not_be_valid_when_requiring_a_verification_value CreditCard.require_verification_value = true - card = credit_card('4242424242424242', :verification_value => nil) + card = credit_card('4242424242424242', verification_value: nil) assert_not_valid card card.verification_value = '1234' @@ -215,7 +215,7 @@ def test_should_not_be_valid_when_requiring_a_verification_value card.verification_value = '123' assert_valid card - card = credit_card('341111111111111', :verification_value => '123', :brand => 'american_express') + card = credit_card('341111111111111', verification_value: '123', brand: 'american_express') errors = assert_not_valid card assert_equal errors[:verification_value], ['should be 4 digits'] @@ -225,7 +225,7 @@ def test_should_not_be_valid_when_requiring_a_verification_value def test_should_be_valid_when_not_requiring_a_verification_value CreditCard.require_verification_value = true - card = credit_card('4242424242424242', :verification_value => nil, :require_verification_value => false) + card = credit_card('4242424242424242', verification_value: nil, require_verification_value: false) assert_valid card card.verification_value = '1234' @@ -242,61 +242,33 @@ def test_bogus_cards_are_not_valid_without_verification_value assert_not_valid card end - def test_should_require_valid_start_date_for_solo_or_switch - @solo.start_month = nil - @solo.start_year = nil - @solo.issue_number = nil - - errors = assert_not_valid @solo - assert errors[:start_month] - assert errors[:start_year] - assert errors[:issue_number] - - @solo.start_month = 2 - @solo.start_year = 2007 - assert_valid @solo - end - - def test_should_require_a_valid_issue_number_for_solo_or_switch - @solo.start_month = nil - @solo.start_year = 2005 - @solo.issue_number = nil - - errors = assert_not_valid @solo - assert errors[:start_month] - assert_equal ['cannot be empty'], errors[:issue_number] - - @solo.issue_number = 3 - assert_valid @solo - end - - def test_should_require_a_validate_non_empty_issue_number_for_solo_or_switch - @solo.issue_number = 'invalid' - - errors = assert_not_valid @solo - assert_equal ['is invalid'], errors[:issue_number] - - @solo.issue_number = 3 - assert_valid @solo - end - def test_should_return_last_four_digits_of_card_number - ccn = CreditCard.new(:number => '4779139500118580') + ccn = CreditCard.new(number: '4779139500118580') assert_equal '8580', ccn.last_digits end def test_bogus_last_digits - ccn = CreditCard.new(:number => '1') + ccn = CreditCard.new(number: '1') assert_equal '1', ccn.last_digits end + def test_should_return_empty_string_for_first_digits_of_nil_card_number + ccn = CreditCard.new + assert_equal '', ccn.first_digits + end + + def test_should_return_empty_string_for_last_digits_of_nil_card_number + ccn = CreditCard.new + assert_equal '', ccn.last_digits + end + def test_should_return_first_four_digits_of_card_number - ccn = CreditCard.new(:number => '4779139500118580') + ccn = CreditCard.new(number: '4779139500118580') assert_equal '477913', ccn.first_digits end def test_should_return_first_bogus_digit_of_card_number - ccn = CreditCard.new(:number => '1') + ccn = CreditCard.new(number: '1') assert_equal '1', ccn.first_digits end @@ -304,7 +276,7 @@ def test_should_be_true_when_credit_card_has_a_first_name c = CreditCard.new assert_false c.first_name? - c = CreditCard.new(:first_name => 'James') + c = CreditCard.new(first_name: 'James') assert c.first_name? end @@ -312,7 +284,7 @@ def test_should_be_true_when_credit_card_has_a_last_name c = CreditCard.new assert_false c.last_name? - c = CreditCard.new(:last_name => 'Herdman') + c = CreditCard.new(last_name: 'Herdman') assert c.last_name? end @@ -320,48 +292,48 @@ def test_should_test_for_a_full_name c = CreditCard.new assert_false c.name? - c = CreditCard.new(:first_name => 'James', :last_name => 'Herdman') + c = CreditCard.new(first_name: 'James', last_name: 'Herdman') assert c.name? end def test_should_handle_full_name_when_first_or_last_is_missing - c = CreditCard.new(:first_name => 'James') + c = CreditCard.new(first_name: 'James') assert c.name? assert_equal 'James', c.name - c = CreditCard.new(:last_name => 'Herdman') + c = CreditCard.new(last_name: 'Herdman') assert c.name? assert_equal 'Herdman', c.name end def test_should_assign_a_full_name - c = CreditCard.new :name => 'James Herdman' + c = CreditCard.new name: 'James Herdman' assert_equal 'James', c.first_name assert_equal 'Herdman', c.last_name - c = CreditCard.new :name => 'Rocket J. Squirrel' + c = CreditCard.new name: 'Rocket J. Squirrel' assert_equal 'Rocket J.', c.first_name assert_equal 'Squirrel', c.last_name - c = CreditCard.new :name => 'Twiggy' + c = CreditCard.new name: 'Twiggy' assert_equal '', c.first_name assert_equal 'Twiggy', c.last_name assert_equal 'Twiggy', c.name end def test_should_remove_trailing_whitespace_on_name - c = CreditCard.new(:last_name => 'Herdman') + c = CreditCard.new(last_name: 'Herdman') assert_equal 'Herdman', c.name - c = CreditCard.new(:last_name => 'Herdman', first_name: '') + c = CreditCard.new(last_name: 'Herdman', first_name: '') assert_equal 'Herdman', c.name end def test_should_remove_leading_whitespace_on_name - c = CreditCard.new(:first_name => 'James') + c = CreditCard.new(first_name: 'James') assert_equal 'James', c.name - c = CreditCard.new(:last_name => '', first_name: 'James') + c = CreditCard.new(last_name: '', first_name: 'James') assert_equal 'James', c.name end @@ -378,29 +350,29 @@ def test_validate_new_card # The following is a regression for a bug where the keys of the # credit card card_companies hash were not duped when detecting the brand def test_create_and_validate_credit_card_from_brand - credit_card = CreditCard.new(:brand => CreditCard.brand?('4242424242424242')) + credit_card = CreditCard.new(brand: CreditCard.brand?('4242424242424242')) assert_nothing_raised do credit_card.validate end end def test_autodetection_of_credit_card_brand - credit_card = CreditCard.new(:number => '4242424242424242') + credit_card = CreditCard.new(number: '4242424242424242') assert_equal 'visa', credit_card.brand end def test_card_brand_should_not_be_autodetected_when_provided - credit_card = CreditCard.new(:number => '4242424242424242', :brand => 'master') + credit_card = CreditCard.new(number: '4242424242424242', brand: 'master') assert_equal 'master', credit_card.brand end def test_detecting_bogus_card - credit_card = CreditCard.new(:number => '1') + credit_card = CreditCard.new(number: '1') assert_equal 'bogus', credit_card.brand end def test_validating_bogus_card - credit_card = credit_card('1', :brand => nil) + credit_card = credit_card('1', brand: nil) assert_valid credit_card end @@ -437,7 +409,7 @@ def test_brand_is_aliased_as_type assert_equal @visa.type, @visa.brand end assert_deprecation_warning('CreditCard#type is deprecated and will be removed from a future release of ActiveMerchant. Please use CreditCard#brand instead.') do - assert_equal @solo.type, @solo.brand + assert_equal @maestro.type, @maestro.brand end end @@ -458,11 +430,6 @@ def test_month_and_year_are_immediately_converted_to_integers assert_nil card.month card.year = nil assert_nil card.year - - card.start_month = '1' - assert_equal 1, card.start_month - card.start_year = '1' - assert_equal 1, card.start_year end def test_should_report_as_emv_if_icc_data_present @@ -472,4 +439,11 @@ def test_should_report_as_emv_if_icc_data_present def test_should_not_report_as_emv_if_icc_data_not_present refute CreditCard.new.emv? end + + def test_bp_plus_number_validation + assert_valid @bp_plus + assert_include @bp_plus.number, ' ' + assert_equal @bp_plus.brand, 'bp_plus' + assert @bp_plus.allow_spaces_in_card? + end end diff --git a/test/unit/cvv_result_test.rb b/test/unit/cvv_result_test.rb index 12ed48d1e35..282fc97c8a6 100644 --- a/test/unit/cvv_result_test.rb +++ b/test/unit/cvv_result_test.rb @@ -6,28 +6,28 @@ def test_nil_data assert_nil result.code assert_nil result.message end - + def test_blank_data result = CVVResult.new('') assert_nil result.code assert_nil result.message end - + def test_successful_match result = CVVResult.new('M') assert_equal 'M', result.code assert_equal CVVResult.messages['M'], result.message end - + def test_failed_match result = CVVResult.new('N') assert_equal 'N', result.code assert_equal CVVResult.messages['N'], result.message end - + def test_to_hash result = CVVResult.new('M').to_hash assert_equal 'M', result['code'] assert_equal CVVResult.messages['M'], result['message'] end -end \ No newline at end of file +end diff --git a/test/unit/expiry_date_test.rb b/test/unit/expiry_date_test.rb index 0306b339447..07a44b26c15 100644 --- a/test/unit/expiry_date_test.rb +++ b/test/unit/expiry_date_test.rb @@ -6,27 +6,27 @@ def test_should_be_expired date = CreditCard::ExpiryDate.new(last_month.month, last_month.year) assert date.expired? end - + def test_today_should_not_be_expired today = Time.now.utc date = CreditCard::ExpiryDate.new(today.month, today.year) assert_false date.expired? end - + def test_dates_in_the_future_should_not_be_expired next_month = 1.month.from_now date = CreditCard::ExpiryDate.new(next_month.month, next_month.year) assert_false date.expired? end - + def test_invalid_date expiry = CreditCard::ExpiryDate.new(13, 2009) assert_equal Time.at(0).utc, expiry.expiration end - + def test_month_and_year_coerced_to_integer expiry = CreditCard::ExpiryDate.new('13', '2009') assert_equal 13, expiry.month assert_equal 2009, expiry.year end -end \ No newline at end of file +end diff --git a/test/unit/fixtures_test.rb b/test/unit/fixtures_test.rb index 5f5baa93eb0..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.load(File.read(ActiveMerchant::Fixtures::DEFAULT_CREDENTIALS)).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 c0e0f69b3d5..97e82fafa0c 100644 --- a/test/unit/gateways/adyen_test.rb +++ b/test/unit/gateways/adyen_test.rb @@ -10,31 +10,120 @@ def setup merchant_account: 'merchantAccount' ) - @credit_card = credit_card('4111111111111111', - :month => 8, - :year => 2018, - :first_name => 'Test', - :last_name => 'Card', - :verification_value => '737', - :brand => 'visa' + @bank_account = check() + + @credit_card = credit_card( + '4111111111111111', + month: 8, + year: 2018, + first_name: 'Test', + last_name: 'Card', + verification_value: '737', + brand: 'visa' + ) + + @elo_credit_card = credit_card( + '5066 9911 1111 1118', + month: 10, + year: 2020, + first_name: 'John', + last_name: 'Smith', + verification_value: '737', + brand: 'elo' + ) + + @cabal_credit_card = credit_card( + '6035 2277 1642 7021', + month: 10, + year: 2020, + first_name: 'John', + last_name: 'Smith', + verification_value: '737', + brand: 'cabal' + ) + + @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' + ) + + @three_ds_enrolled_card = credit_card('4212345678901237', brand: :visa) + + @apple_pay_card = network_tokenization_credit_card( + '4111111111111111', + payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', + month: '08', + year: '2018', + source: :apple_pay, + verification_value: nil + ) + + @google_pay_card = network_tokenization_credit_card( + '4761209980011439', + payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', + month: '11', + year: '2022', + source: :google_pay, + verification_value: nil ) - @apple_pay_card = network_tokenization_credit_card('4111111111111111', - :payment_cryptogram => 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', - :month => '08', - :year => '2018', - :source => :apple_pay, - :verification_value => nil + @nt_credit_card = network_tokenization_credit_card( + '4895370015293175', + brand: 'visa', + eci: '07', + source: :network_token, + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' ) @amount = 100 @options = { billing_address: address(), + shipping_address: address(), shopper_reference: 'John Smith', order_id: '345123', - installments: 2 + installments: 2, + stored_credential: { reason_type: 'unscheduled' }, + email: 'john.smith@test.com', + ip: '77.110.174.153' + } + + @options_shopper_data = { + email: 'john.smith@test.com', + ip: '77.110.174.153', + shopper_email: 'john2.smith@test.com', + shopper_ip: '192.168.100.100' + } + + @normalized_3ds_2_options = { + reference: '345123', + email: 'john.smith@test.com', + ip: '77.110.174.153', + shopper_reference: 'John Smith', + billing_address: address(), + order_id: '123', + stored_credential: { reason_type: 'unscheduled' }, + three_ds_2: { + channel: 'browser', + browser_info: { + accept_header: 'unknown', + depth: 100, + java: false, + language: 'US', + height: 1000, + width: 500, + timezone: '-120', + user_agent: 'unknown' + } + } } + + @long_order_id = 'asdfjkl;asdfjkl;asdfj;aiwyutinvpoaieryutnmv;203987528752098375j3q-p489756ijmfpvbijpq348nmdf;vbjp3845' end # Subdomains are only valid for production gateways, so the test_url check must be manually bypassed for this test to pass. @@ -55,6 +144,16 @@ def setup # assert response # assert_success response # end + def test_endpoint + assert_equal 'Recurring/v68/disable', @gateway.send(:endpoint, 'disable') + assert_equal 'Recurring/v68/storeToken', @gateway.send(:endpoint, 'storeToken') + assert_equal 'Payout/v68/payout', @gateway.send(:endpoint, 'payout') + assert_equal 'Payment/v68/authorise', @gateway.send(:endpoint, 'authorise') + end + + def test_supported_card_types + assert_equal AdyenGateway.supported_cardtypes, %i[visa master american_express diners_club jcb dankort maestro discover elo naranja cabal unionpay patagonia_365 tarjeta_sol] + end def test_successful_authorize @gateway.expects(:ssl_post).returns(successful_authorize_response) @@ -63,7 +162,140 @@ def test_successful_authorize assert_success response assert_equal '#7914775043909934#', response.authorization + assert_equal 'R', response.avs_result['code'] + assert_equal 'M', response.cvv_result['code'] + assert response.test? + end + + def test_successful_authorize_bank_account + @gateway.expects(:ssl_post).returns(successful_authorize_response) + + response = @gateway.authorize(@amount, @bank_account, @options) + assert_success response + + assert_equal '#7914775043909934#', response.authorization + assert_equal 'R', response.avs_result['code'] + assert_equal 'M', response.cvv_result['code'] + assert response.test? + end + + def test_successful_authorize_with_3ds + @gateway.expects(:ssl_post).returns(successful_authorize_with_3ds_response) + + response = @gateway.authorize(@amount, @three_ds_enrolled_card, @options.merge(execute_threed: true)) assert response.test? + refute response.authorization.blank? + assert_equal '#8835440446784145#', response.authorization + assert_equal response.params['resultCode'], 'RedirectShopper' + refute response.params['issuerUrl'].blank? + refute response.params['md'].blank? + refute response.params['paRequest'].blank? + end + + def test_failed_authorize_with_unexpected_3ds + @gateway.expects(:ssl_post).returns(successful_authorize_with_3ds_response) + response = @gateway.authorize(@amount, @three_ds_enrolled_card, @options) + assert_failure response + assert_match 'Received unexpected 3DS authentication response, 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 + + def test_successful_authorize_with_shopper_interaction_ecommerce + stub_comms do + @gateway.authorize(100, @credit_card, { order_id: '345123' }) + end.check_request do |_endpoint, data, _headers| + assert_equal 'Ecommerce', JSON.parse(data)['shopperInteraction'] + end.respond_with(successful_authorize_response) + end + + def test_successful_authorize_with_recurring_detail_reference + stub_comms do + @gateway.authorize(100, @credit_card, @options.merge(recurring_detail_reference: '12345')) + end.check_request do |_endpoint, data, _headers| + assert_equal 'john.smith@test.com', JSON.parse(data)['shopperEmail'] + assert_equal '12345', JSON.parse(data)['selectedRecurringDetailReference'] + end.respond_with(successful_authorize_response) + end + + def test_successful_authorize_with_localized_shopper_statement + stub_comms do + @gateway.authorize(100, @credit_card, @options.merge(localized_shopper_statement: { 'ja-Kana' => 'ADYEN - セラーA' })) + end.check_request do |_endpoint, data, _headers| + assert_equal 'ADYEN - セラーA', JSON.parse(data)['localizedShopperStatement']['ja-Kana'] + end.respond_with(successful_authorize_response) + end + + def test_adds_3ds1_standalone_fields + eci = '05' + cavv = '3q2+78r+ur7erb7vyv66vv\/\/\/\/8=' + cavv_algorithm = '1' + xid = 'ODUzNTYzOTcwODU5NzY3Qw==' + enrolled = 'Y' + authentication_response_status = 'Y' + options_with_3ds1_standalone = @options.merge( + three_d_secure: { + eci:, + cavv:, + cavv_algorithm:, + xid:, + enrolled:, + authentication_response_status: + } + ) + stub_comms do + @gateway.authorize(@amount, @credit_card, options_with_3ds1_standalone) + end.check_request do |_endpoint, data, _headers| + assert_equal eci, JSON.parse(data)['mpiData']['eci'] + assert_equal cavv, JSON.parse(data)['mpiData']['cavv'] + assert_equal cavv_algorithm, JSON.parse(data)['mpiData']['cavvAlgorithm'] + assert_equal xid, JSON.parse(data)['mpiData']['xid'] + assert_equal enrolled, JSON.parse(data)['mpiData']['directoryResponse'] + assert_equal authentication_response_status, JSON.parse(data)['mpiData']['authenticationResponse'] + end.respond_with(successful_authorize_response) + end + + def test_adds_3ds2_standalone_fields + version = '2.1.0' + eci = '02' + cavv = 'jJ81HADVRtXfCBATEp01CJUAAAA=' + ds_transaction_id = '97267598-FAE6-48F2-8083-C23433990FBC' + directory_response_status = 'C' + authentication_response_status = 'Y' + options_with_3ds2_standalone = @options.merge( + three_d_secure: { + version:, + eci:, + cavv:, + ds_transaction_id:, + directory_response_status:, + authentication_response_status: + } + ) + stub_comms do + @gateway.authorize(@amount, @credit_card, options_with_3ds2_standalone) + end.check_request do |_endpoint, data, _headers| + assert_equal version, JSON.parse(data)['mpiData']['threeDSVersion'] + assert_equal eci, JSON.parse(data)['mpiData']['eci'] + assert_equal cavv, JSON.parse(data)['mpiData']['cavv'] + assert_equal ds_transaction_id, JSON.parse(data)['mpiData']['dsTransID'] + assert_equal directory_response_status, JSON.parse(data)['mpiData']['directoryResponse'] + assert_equal authentication_response_status, JSON.parse(data)['mpiData']['authenticationResponse'] + end.respond_with(successful_authorize_response) end def test_failed_authorize @@ -74,6 +306,127 @@ def test_failed_authorize assert_failure response end + def test_failure_authorize_with_transient_error + @gateway.instance_variable_set(:@response_headers, { 'transient-error' => 'error_will_robinson' }) + @gateway.expects(:ssl_post).returns(failed_authorize_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert response.params['response_headers']['transient_error'], 'error_will_robinson' + assert response.test? + end + + def test_standard_error_code_mapping + @gateway.expects(:ssl_post).returns(failed_billing_field_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert_equal 'incorrect_address', response.error_code + end + + def test_unknown_error_code_mapping + @gateway.expects(:ssl_post).returns(failed_invalid_delivery_field_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert_equal '702', response.error_code + end + + def test_billing_address_error_code_mapping + @gateway.expects(:ssl_post).returns(failed_billing_address_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert_equal AdyenGateway::STANDARD_ERROR_CODE[:incorrect_address], response.error_code + end + + def test_cvc_length_error_code_mapping + @gateway.expects(:ssl_post).returns(failed_cvc_validation_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert_equal AdyenGateway::STANDARD_ERROR_CODE[:invalid_cvc], response.error_code + end + + def test_invalid_card_number_error_code_mapping + @gateway.expects(:ssl_post).returns(failed_invalid_card_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert_equal AdyenGateway::STANDARD_ERROR_CODE[:incorrect_number], response.error_code + end + + def test_invalid_amount_error_code_mapping + @gateway.expects(:ssl_post).returns(failed_invalid_amount_response) + + response = @gateway.authorize(nil, @credit_card, @options) + assert_failure response + assert_equal AdyenGateway::STANDARD_ERROR_CODE[:invalid_amount], response.error_code + end + + def test_invalid_access_error_code_mapping + @gateway.expects(:ssl_post).returns(failed_not_allowed_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert_equal AdyenGateway::STANDARD_ERROR_CODE[:config_error], response.error_code + end + + def test_unknown_reason_error_code_mapping + @gateway.expects(:ssl_post).returns(failed_unknown_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert_equal AdyenGateway::STANDARD_ERROR_CODE[:processing_error], response.error_code + end + + def test_failed_authorise3d + @gateway.expects(:ssl_post).returns(failed_authorize_response) + + response = @gateway.send(:commit, 'authorise3d', {}, {}) + + assert_equal 'Expired Card', response.message + assert_failure response + end + + def test_failed_authorise3ds2 + @gateway.expects(:ssl_post).returns(failed_authorize_3ds2_response) + + response = @gateway.send(:commit, 'authorise3ds2', {}, {}) + + assert_equal '3D Not Authenticated', response.message + 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_equal '01', response.error_code + assert_failure response + end + + def test_failed_fraud_raw_refusal + @gateway.expects(:ssl_post).returns(failed_fraud_visa_response) + + response = @gateway.send(:commit, 'authorise', {}, {}) + + assert_equal 'N7', response.error_code + 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_equal '01', response.error_code + assert_failure response + end + def test_successful_capture @gateway.expects(:ssl_post).returns(successful_capture_response) response = @gateway.capture(@amount, '7914775043909934') @@ -90,7 +443,31 @@ def test_failed_capture assert_failure response end - def test_successful_purchase + 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_capture_with_localized_shopper_statement + stub_comms do + @gateway.capture(@amount, '7914775043909934', @options.merge(localized_shopper_statement: { 'ja-Kana' => 'ADYEN - セラーA' })) + end.check_request do |_endpoint, data, _headers| + assert_equal 'ADYEN - セラーA', JSON.parse(data)['additionalData']['localizedShopperStatement']['ja-Kana'] + end.respond_with(successful_capture_response) + end + + def test_successful_refund_with_localized_shopper_statement + stub_comms do + @gateway.refund(@amount, '7914775043909934', @options.merge(localized_shopper_statement: { 'ja-Kana' => 'ADYEN - セラーA' })) + end.check_request do |_endpoint, data, _headers| + assert_equal 'ADYEN - セラーA', JSON.parse(data)['additionalData']['localizedShopperStatement']['ja-Kana'] + end.respond_with(successful_capture_response) + end + + def test_successful_purchase_with_credit_card response = stub_comms do @gateway.purchase(@amount, @credit_card, @options) end.respond_with(successful_authorize_response, successful_capture_response) @@ -99,139 +476,1399 @@ def test_successful_purchase assert response.test? end + def test_successful_purchase_with_bank_account + response = stub_comms do + @gateway.purchase(@amount, @bank_account, @options) + end.respond_with(successful_authorize_response, successful_capture_response) + assert_success response + assert_equal '7914775043909934#8814775564188305#', response.authorization + assert response.test? + end + + def test_successful_purchase_with_elo_card + response = stub_comms do + @gateway.purchase(@amount, @elo_credit_card, @options) + end.respond_with(simple_successful_authorize_response, simple_successful_capture_repsonse) + assert_success response + assert_equal '8835511210681145#8835511210689965#', response.authorization + assert response.test? + end + + def test_successful_purchase_with_cabal_card + response = stub_comms do + @gateway.purchase(@amount, @cabal_credit_card, @options) + end.respond_with(simple_successful_authorize_response, simple_successful_capture_repsonse) + assert_success response + assert_equal '8835511210681145#8835511210689965#', response.authorization + assert response.test? + end + + def test_successful_purchase_with_unionpay_card + response = stub_comms do + @gateway.purchase(@amount, @unionpay_credit_card, @options) + end.respond_with(simple_successful_authorize_response, simple_successful_capture_repsonse) + assert_success response + assert_equal '8835511210681145#8835511210689965#', response.authorization + assert response.test? + end + + def test_successful_maestro_purchase + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge({ selected_brand: 'maestro', overwrite_brand: 'true' })) + end.check_request do |endpoint, data, _headers| + if /authorise/.match?(endpoint) + assert_match(/"overwriteBrand":true/, data) + assert_match(/"selectedBrand":"maestro"/, data) + end + end.respond_with(successful_authorize_response, successful_capture_response) + assert_success response + assert_equal '7914775043909934#8814775564188305#', response.authorization + assert response.test? + end + + def test_3ds_2_fields_sent + stub_comms do + @gateway.authorize(@amount, @credit_card, @normalized_3ds_2_options) + end.check_request do |_endpoint, data, _headers| + data = JSON.parse(data) + assert_equal 'browser', data['threeDS2RequestData']['deviceChannel'] + assert_equal 'unknown', data['browserInfo']['acceptHeader'] + assert_equal 100, data['browserInfo']['colorDepth'] + assert_equal false, data['browserInfo']['javaEnabled'] + assert_equal 'US', data['browserInfo']['language'] + assert_equal 1000, data['browserInfo']['screenHeight'] + assert_equal 500, data['browserInfo']['screenWidth'] + assert_equal '-120', data['browserInfo']['timeZoneOffset'] + assert_equal 'unknown', data['browserInfo']['userAgent'] + end.respond_with(successful_authorize_response) + end + def test_installments_sent stub_comms do @gateway.authorize(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_equal 2, JSON.parse(data)['installments']['value'] end.respond_with(successful_authorize_response) end - def test_failed_purchase - @gateway.expects(:ssl_post).returns(failed_purchase_response) + def test_capture_delay_hours_sent + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ capture_delay_hours: 4 })) + end.check_request do |_endpoint, data, _headers| + assert_equal 4, JSON.parse(data)['captureDelayHours'] + end.respond_with(successful_authorize_response) + end - response = @gateway.purchase(@amount, credit_card('400111'), @options) - assert_failure response + def test_custom_routing_sent + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ custom_routing_flag: 'abcdefg' })) + end.check_request do |_endpoint, data, _headers| + assert_equal 'abcdefg', JSON.parse(data)['additionalData']['customRoutingFlag'] + end.respond_with(successful_authorize_response) + end - assert_equal AdyenGateway::STANDARD_ERROR_CODE[:incorrect_number], response.error_code + def test_splits_sent + split_data = [{ + 'amount' => { + 'currency' => 'USD', + 'value' => 50 + }, + 'type' => 'MarketPlace', + 'account' => '163298747', + 'reference' => 'QXhlbFN0b2x0ZW5iZXJnCg' + }, { + 'amount' => { + 'currency' => 'USD', + 'value' => 50 + }, + 'type' => 'Commission', + 'reference' => 'THVjYXNCbGVkc29lCg' + }] + + options = @options.merge({ splits: split_data }) + stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_equal split_data, JSON.parse(data)['splits'] + end.respond_with(successful_authorize_response) end - def test_successful_refund - @gateway.expects(:ssl_post).returns(successful_refund_response) - response = @gateway.refund(@amount, '7914775043909934') - assert_equal '7914775043909934#8514775559925128#', response.authorization - assert_equal '[refund-received]', response.message - assert response.test? + def test_splits_sent_without_amount + split_data = [{ + 'type' => 'MarketPlace', + 'account' => '163298747', + 'reference' => 'QXhlbFN0b2x0ZW5iZXJnCg' + }, { + 'type' => 'Commission', + 'reference' => 'THVjYXNCbGVkc29lCg' + }] + + options = @options.merge({ splits: split_data }) + stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_equal split_data, JSON.parse(data)['splits'] + end.respond_with(successful_authorize_response) end - def test_successful_refund_with_compound_psp_reference - @gateway.expects(:ssl_post).returns(successful_refund_response) - response = @gateway.refund(@amount, '7914775043909934#8514775559000000') - assert_equal '7914775043909934#8514775559925128#', response.authorization - assert_equal '[refund-received]', response.message - assert response.test? + def test_execute_threed_false_with_additional_data + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ execute_threed: false, overwrite_brand: true, selected_brand: 'maestro' })) + end.check_request do |_endpoint, data, _headers| + assert_match(/"additionalData":{"overwriteBrand":true,"executeThreeD":false}/, data) + assert_match(/"selectedBrand":"maestro"/, data) + end.respond_with(successful_authorize_response) end - def test_failed_refund - @gateway.expects(:ssl_post).returns(failed_refund_response) - response = @gateway.refund(@amount, '') - assert_nil response.authorization - assert_equal 'Original pspReference required for this operation', response.message - assert_failure response + def test_execute_threed_false_sent_3ds2 + stub_comms do + @gateway.authorize(@amount, '123', @normalized_3ds_2_options.merge({ execute_threed: false })) + end.check_request do |_endpoint, data, _headers| + refute JSON.parse(data)['additionalData']['scaExemption'] + assert_false JSON.parse(data)['additionalData']['executeThreeD'] + end.respond_with(successful_authorize_response) end - def test_successful_void - @gateway.expects(:ssl_post).returns(successful_void_response) - response = @gateway.void('7914775043909934') - assert_equal '7914775043909934#8614775821628806#', response.authorization - assert_equal '[cancel-received]', response.message - assert response.test? + def test_sca_exemption_not_sent_if_execute_threed_missing_3ds2 + stub_comms do + @gateway.authorize(@amount, '123', @normalized_3ds_2_options.merge({ scaExemption: 'lowValue' })) + end.check_request do |_endpoint, data, _headers| + refute JSON.parse(data)['additionalData']['scaExemption'] + refute JSON.parse(data)['additionalData']['executeThreeD'] + end.respond_with(successful_authorize_response) end - def test_failed_void - @gateway.expects(:ssl_post).returns(failed_void_response) - response = @gateway.void('') - assert_equal 'Original pspReference required for this operation', response.message - assert_failure response + def test_sca_exemption_and_execute_threed_false_sent_3ds2 + stub_comms do + @gateway.authorize(@amount, '123', @normalized_3ds_2_options.merge({ sca_exemption: 'lowValue', execute_threed: false })) + end.check_request do |_endpoint, data, _headers| + assert_equal 'lowValue', JSON.parse(data)['additionalData']['scaExemption'] + assert_false JSON.parse(data)['additionalData']['executeThreeD'] + end.respond_with(successful_authorize_response) end - def test_successful_store - @gateway.expects(:ssl_post).returns(successful_store_response) - response = @gateway.store(@credit_card, @options) + def test_sca_exemption_and_execute_threed_true_sent_3ds2 + stub_comms do + @gateway.authorize(@amount, '123', @normalized_3ds_2_options.merge({ sca_exemption: 'lowValue', execute_threed: true })) + end.check_request do |_endpoint, data, _headers| + assert_equal 'lowValue', JSON.parse(data)['additionalData']['scaExemption'] + assert JSON.parse(data)['additionalData']['executeThreeD'] + end.respond_with(successful_authorize_response) + end + + def test_sca_exemption_not_sent_when_execute_threed_true_3ds1 + stub_comms do + @gateway.authorize(@amount, '123', @options.merge({ sca_exemption: 'lowValue', execute_threed: true })) + end.check_request do |_endpoint, data, _headers| + refute JSON.parse(data)['additionalData']['scaExemption'] + assert JSON.parse(data)['additionalData']['executeThreeD'] + end.respond_with(successful_authorize_response) + end + + def test_sca_exemption_not_sent_when_execute_threed_false_3ds1 + stub_comms do + @gateway.authorize(@amount, '123', @options.merge({ sca_exemption: 'lowValue', execute_threed: false })) + end.check_request do |_endpoint, data, _headers| + refute JSON.parse(data)['additionalData']['scaExemption'] + refute JSON.parse(data)['additionalData']['executeThreeD'] + end.respond_with(successful_authorize_response) + end + + def test_update_shopper_statement_and_industry_usage_sent + stub_comms do + @gateway.adjust(@amount, '123', @options.merge({ update_shopper_statement: 'statement note', industry_usage: 'DelayedCharge' })) + end.check_request do |_endpoint, data, _headers| + assert_equal 'statement note', JSON.parse(data)['additionalData']['updateShopperStatement'] + assert_equal 'DelayedCharge', JSON.parse(data)['additionalData']['industryUsage'] + end.respond_with(successful_adjust_response) + end + + def test_risk_data_sent + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ risk_data: { 'operatingSystem' => 'HAL9000' } })) + end.check_request do |_endpoint, data, _headers| + assert_equal 'HAL9000', JSON.parse(data)['additionalData']['riskdata.operatingSystem'] + end.respond_with(successful_authorize_response) + end + + def test_fund_source_and_fund_destination_sent + fund_options = { + fund_source: { + additional_data: { fundingSource: 'Debit' }, + first_name: 'Payer', + last_name: 'Name', + billing_address: @us_address, + shopper_email: 'john.smith@test.com' + }, + fund_destination: { + additional_data: { walletIdentifier: '12345' } + } + } + + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(fund_options)) + end.check_request do |_endpoint, data, _headers| + fund_source = JSON.parse(data)['fundSource'] + fund_destination = JSON.parse(data)['fundDestination'] + assert_equal 'john.smith@test.com', fund_source['shopperEmail'] + assert_equal 'Payer', fund_source['shopperName']['firstName'] + assert_equal 'Name', fund_source['shopperName']['lastName'] + assert_equal 'Debit', fund_source['additionalData']['fundingSource'] + assert_equal '12345', fund_destination['additionalData']['walletIdentifier'] + end.respond_with(successful_authorize_response) + end + + def test_manual_capture_sent + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(manual_capture: 'true')) + end.check_request do |_endpoint, data, _headers| + assert_equal 'true', JSON.parse(data)['additionalData']['manualCapture'] + end.respond_with(successful_authorize_response) + end + + def test_risk_data_complex_data + stub_comms do + risk_data = { + 'deliveryMethod' => 'express', + 'basket.item.productTitle' => 'Blue T Shirt', + 'promotions.promotion.promotionName' => 'Big Sale promotion' + } + @gateway.authorize(@amount, @credit_card, @options.merge({ risk_data: })) + end.check_request do |_endpoint, data, _headers| + parsed = JSON.parse(data) + assert_equal 'express', parsed['additionalData']['riskdata.deliveryMethod'] + assert_equal 'Blue T Shirt', parsed['additionalData']['riskdata.basket.item.productTitle'] + assert_equal 'Big Sale promotion', parsed['additionalData']['riskdata.promotions.promotion.promotionName'] + end.respond_with(successful_authorize_response) + end + + def test_stored_credential_recurring_cit_initial + options = stored_credential_options(:cardholder, :recurring, :initial) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/"shopperInteraction":"Ecommerce"/, data) + assert_match(/"recurringProcessingModel":"Subscription"/, data) + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_stored_credential_recurring_cit_used + @credit_card.verification_value = nil + options = stored_credential_options(:cardholder, :recurring, ntid: 'abc123') + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/"shopperInteraction":"Ecommerce"/, data) + assert_match(/"recurringProcessingModel":"Subscription"/, data) + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_stored_credential_recurring_mit_initial + options = stored_credential_options(:merchant, :recurring, :initial) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/"shopperInteraction":"Ecommerce"/, data) + assert_match(/"recurringProcessingModel":"Subscription"/, data) + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_stored_credential_recurring_mit_used + @credit_card.verification_value = nil + options = stored_credential_options(:merchant, :recurring, ntid: 'abc123') + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/"shopperInteraction":"ContAuth"/, data) + assert_match(/"recurringProcessingModel":"Subscription"/, data) + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_stored_credential_unscheduled_cit_initial + options = stored_credential_options(:cardholder, :unscheduled, :initial) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/"shopperInteraction":"Ecommerce"/, data) + assert_match(/"recurringProcessingModel":"CardOnFile"/, data) + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_stored_credential_unscheduled_cit_used + @credit_card.verification_value = nil + options = stored_credential_options(:cardholder, :unscheduled, ntid: 'abc123') + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/"shopperInteraction":"Ecommerce"/, data) + assert_match(/"recurringProcessingModel":"CardOnFile"/, data) + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_stored_credential_unscheduled_mit_initial + options = stored_credential_options(:merchant, :unscheduled, :initial) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/"shopperInteraction":"Ecommerce"/, data) + assert_match(/"recurringProcessingModel":"UnscheduledCardOnFile"/, data) + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_stored_credential_unscheduled_mit_used + @credit_card.verification_value = nil + options = stored_credential_options(:merchant, :unscheduled, ntid: 'abc123') + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/"shopperInteraction":"ContAuth"/, data) + assert_match(/"recurringProcessingModel":"UnscheduledCardOnFile"/, data) + end.respond_with(successful_authorize_response) + + 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_passing_shopper_interaction_moto + options = { + stored_credential: { + initiator: 'merchant', + recurring_type: 'unscheduled', + initial_transaction: true + }, + shopper_interaction: 'Moto', + recurring_processing_model: nil, + order_id: '345123' + } + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/"shopperInteraction":"Moto"/, data) + 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')) + end.check_request do |_endpoint, data, _headers| + assert_match(/"amount\":{\"value\":\"2\",\"currency\":\"JPY\"}/, data) + end.respond_with(successful_authorize_response) + + stub_comms do + @gateway.authorize(200, @credit_card, @options.merge(currency: 'CLP')) + end.check_request do |_endpoint, data, _headers| + assert_match(/"amount\":{\"value\":\"200\",\"currency\":\"CLP\"}/, data) + end.respond_with(successful_authorize_response) + end + + def test_failed_purchase + @gateway.expects(:ssl_post).returns(failed_purchase_response) + + response = @gateway.purchase(@amount, credit_card('400111'), @options) + assert_failure response + + assert_equal AdyenGateway::STANDARD_ERROR_CODE[:incorrect_number], response.error_code + end + + def test_successful_refund + @gateway.expects(:ssl_post).returns(successful_refund_response) + response = @gateway.refund(@amount, '7914775043909934') + assert_equal '7914775043909934#8514775559925128#', response.authorization + assert_equal '[refund-received]', response.message + assert response.test? + end + + def test_successful_refund_with_compound_psp_reference + @gateway.expects(:ssl_post).returns(successful_refund_response) + response = @gateway.refund(@amount, '7914775043909934#8514775559000000') + assert_equal '7914775043909934#8514775559925128#', response.authorization + assert_equal '[refund-received]', response.message + assert response.test? + end + + def test_failed_refund + @gateway.expects(:ssl_post).returns(failed_refund_response) + response = @gateway.refund(@amount, '') + assert_nil response.authorization + assert_equal 'Original pspReference required for this operation', response.message + assert_failure response + end + + def test_failed_credit + @gateway.expects(:ssl_post).returns(failed_credit_response) + response = @gateway.refund(@amount, '') + assert_nil response.authorization + assert_equal "Required field 'reference' is not provided.", response.message + assert_failure response + end + + def test_successful_credit + @gateway.expects(:ssl_post).returns(successful_credit_response) + response = @gateway.credit(@amount, '883614109029400G') + assert_equal '#883614109029400G#', response.authorization + assert_equal 'Received', response.message + 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') + assert_equal '7914775043909934#8614775821628806#', response.authorization + assert_equal '[cancel-received]', response.message + assert response.test? + end + + def test_successful_cancel_or_refund + @gateway.expects(:ssl_post).returns(successful_cancel_or_refund_response) + response = @gateway.void('7914775043909934') + assert_equal '7914775043909934#8614775821628806#', response.authorization + assert_equal '[cancelOrRefund-received]', response.message + assert response.test? + end + + def test_failed_void + @gateway.expects(:ssl_post).returns(failed_void_response) + response = @gateway.void('') + assert_equal 'Original pspReference required for this operation', response.message + assert_failure response + end + + def test_successful_adjust + @gateway.expects(:ssl_post).returns(successful_adjust_response) + response = @gateway.adjust(200, '8835544088660594') + assert_equal '8835544088660594#8835544088660594#', response.authorization + assert_equal '[adjustAuthorisation-received]', response.message + end + + def test_failed_adjust + @gateway.expects(:ssl_post).returns(failed_adjust_response) + response = @gateway.adjust(200, '') + assert_equal 'Original pspReference required for this operation', response.message + assert_failure response + end + + def test_successful_synchronous_adjust + @gateway.expects(:ssl_post).returns(successful_synchronous_adjust_response) + response = @gateway.adjust(200, '8835544088660594') + assert_equal '8835544088660594#8835574118820108#', response.authorization + assert_equal 'Authorised', response.message + end + + def test_failed_synchronous_adjust + @gateway.expects(:ssl_post).returns(failed_synchronous_adjust_response) + response = @gateway.adjust(200, '8835544088660594') + assert_equal 'Refused', response.message + assert_failure response + end + + def test_successful_tokenize_only_store + response = stub_comms do + @gateway.store(@credit_card, @options.merge({ tokenize_only: true })) + end.check_request do |_endpoint, data, _headers| + assert_equal 'CardOnFile', JSON.parse(data)['recurringProcessingModel'] + end.respond_with(successful_store_response) + assert_equal '#8835205392522157#', response.authorization + end + + def test_successful_tokenize_only_store_with_ntid + stub_comms do + @gateway.store(@credit_card, @options.merge({ tokenize_only: true, network_transaction_id: '858435661128555' })) + end.check_request do |_endpoint, data, _headers| + assert_equal '858435661128555', JSON.parse(data)['additionalData']['networkTxReference'] + end.respond_with(successful_store_response) + end + + def test_successful_store + response = stub_comms do + @gateway.store(@credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_equal 'CardOnFile', JSON.parse(data)['recurringProcessingModel'] + end.respond_with(successful_store_response) + assert_success response + assert_equal '#8835205392522157#8315202663743702', response.authorization + end + + def test_successful_store_with_bank_account + response = stub_comms do + @gateway.store(@bank_account, @options) + end.check_request do |_endpoint, data, _headers| + assert_equal 'CardOnFile', JSON.parse(data)['recurringProcessingModel'] + end.respond_with(successful_store_response) + assert_success response + assert_equal '#8835205392522157#8315202663743702', response.authorization + end + + def test_successful_store_with_recurring_contract_type + stub_comms do + @gateway.store(@credit_card, @options.merge({ recurring_contract_type: 'ONECLICK' })) + end.check_request do |_endpoint, data, _headers| + assert_equal 'ONECLICK', JSON.parse(data)['recurring']['contract'] + end.respond_with(successful_store_response) + end + + def test_recurring_contract_type_set_for_reference_purchase + stub_comms do + @gateway.store('123', @options) + end.check_request do |_endpoint, data, _headers| + assert_equal 'RECURRING', JSON.parse(data)['recurring']['contract'] + end.respond_with(successful_store_response) + end + + def test_failed_store + @gateway.expects(:ssl_post).returns(failed_store_response) + response = @gateway.store(@credit_card, @options) + assert_failure response + assert_equal 'Refused', response.message + end + + def test_successful_unstore + response = stub_comms do + @gateway.unstore(shopper_reference: 'shopper_reference', + recurring_detail_reference: 'detail_reference') + end.respond_with(successful_unstore_response) + assert_success response + assert_equal '[detail-successfully-disabled]', response.message + end + + def test_failed_unstore + @gateway.expects(:ssl_post).returns(failed_unstore_response) + response = @gateway.unstore(shopper_reference: 'random_reference', + recurring_detail_reference: 'detail_reference') + assert_failure response + assert_equal 'Contract not found', response.message + end + + def test_successful_verify + response = stub_comms do + @gateway.verify(@credit_card, @options) + end.check_request do |endpoint, data, _headers| + assert_equal '0', JSON.parse(data)['amount']['value'] if endpoint.include?('authorise') + end.respond_with(successful_verify_response) + assert_success response + assert_equal '#7914776426645103#', response.authorization + assert_equal 'Authorised', response.message + assert response.test? + end + + def test_successful_verify_with_custom_amount + response = stub_comms do + @gateway.verify(@credit_card, @options.merge({ verify_amount: '500' })) + end.check_request do |endpoint, data, _headers| + assert_equal '500', JSON.parse(data)['amount']['value'] if endpoint.include?('authorise') + end.respond_with(successful_verify_response) + assert_success response + end + + def test_successful_verify_with_bank_account + response = stub_comms do + @gateway.verify(@bank_account, @options) + end.respond_with(successful_verify_response) + assert_success response + assert_equal '#7914776426645103#', response.authorization + assert_equal 'Authorised', response.message + assert response.test? + end + + def test_failed_verify + response = stub_comms do + @gateway.verify(@credit_card, @options) + end.respond_with(failed_verify_response) + assert_failure response + assert_equal '#7914776433387947#', response.authorization + assert_equal 'Refused', response.message + assert response.test? + end + + def test_failed_verify_with_bank_account + response = stub_comms do + @gateway.verify(@bank_account, @options) + end.respond_with(failed_verify_response) + assert_failure response + assert_equal '#7914776433387947#', response.authorization + assert_equal 'Refused', response.message + assert response.test? + end + + def test_failed_avs_check_returns_refusal_reason_raw + @gateway.expects(:ssl_post).returns(failed_authorize_avs_response) + + 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 + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + def test_scrub_bank_account + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed_bank_account), post_scrubbed_bank_account + end + + def test_scrub_network_tokenization_card + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed_network_tokenization_card), post_scrubbed_network_tokenization_card + end + + def test_shopper_data + post = { card: { billingAddress: {} } } + @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, @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 + + def test_add_address + post = { card: { billingAddress: {} } } + @options[:billing_address].delete(:address1) + @options[:billing_address].delete(:address2) + @options[:billing_address].delete(:state) + @options[:shipping_address].delete(:state) + @gateway.send(:add_address, post, @options) + # Billing Address + assert_equal 'NA', post[:billingAddress][:street] + assert_equal 'NA', post[:billingAddress][:houseNumberOrName] + assert_equal 'NA', post[:billingAddress][:stateOrProvince] + assert_equal @options[:billing_address][:zip], post[:billingAddress][:postalCode] + assert_equal @options[:billing_address][:city], post[:billingAddress][:city] + assert_equal @options[:billing_address][:country], post[:billingAddress][:country] + # Shipping Address + assert_equal 'NA', post[:deliveryAddress][:stateOrProvince] + assert_equal @options[:shipping_address][:address1], post[:deliveryAddress][:street] + assert_equal @options[:shipping_address][:address2], post[:deliveryAddress][:houseNumberOrName] + assert_equal @options[:shipping_address][:zip], post[:deliveryAddress][:postalCode] + assert_equal @options[:shipping_address][:city], post[:deliveryAddress][:city] + 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 + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_equal 1234567890, JSON.parse(data)['telephoneNumber'] + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_successful_auth_phone_number + options = @options.merge(billing_address: { phone_number: 987654321, phone: 1234567890 }) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_equal 987654321, JSON.parse(data)['telephoneNumber'] + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_successful_auth_application_info + ActiveMerchant::Billing::AdyenGateway.application_id = { name: 'Acme', version: '1.0' } + + options = @options.merge!( + merchantApplication: { + name: 'Acme Inc.', + version: '2' + } + ) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_equal 'Acme', JSON.parse(data)['applicationInfo']['externalPlatform']['name'] + assert_equal '1.0', JSON.parse(data)['applicationInfo']['externalPlatform']['version'] + assert_equal 'Acme Inc.', JSON.parse(data)['applicationInfo']['merchantApplication']['name'] + assert_equal '2', JSON.parse(data)['applicationInfo']['merchantApplication']['version'] + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_purchase_with_long_order_id + options = @options.merge({ order_id: @long_order_id }) + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_equal @long_order_id[0..79], JSON.parse(data)['reference'] + end.respond_with(successful_authorize_response, successful_capture_response) + assert_success response + end + + def test_authorize_with_credit_card_no_name + credit_card_no_name = ActiveMerchant::Billing::CreditCard.new({ + number: '4111111111111111', + month: 3, + year: 2030, + verification_value: '737', + brand: 'visa' + }) + + response = stub_comms do + @gateway.authorize(@amount, credit_card_no_name, @options) + end.check_request do |_endpoint, data, _headers| + assert_equal 'Not Provided', JSON.parse(data)['card']['holderName'] + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_authorize_with_network_tokenization_credit_card_no_name + @apple_pay_card.first_name = nil + @apple_pay_card.last_name = nil + response = stub_comms do + @gateway.authorize(@amount, @apple_pay_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_equal 'Not Provided', JSON.parse(data)['card']['holderName'] + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_authorize_with_network_tokenization_credit_card + response = stub_comms do + @gateway.authorize(@amount, @apple_pay_card, @options) + end.check_request do |_endpoint, data, _headers| + parsed = JSON.parse(data) + assert_equal 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', parsed['mpiData']['cavv'] + assert_equal '07', parsed['mpiData']['eci'] + assert_equal 'applepay', parsed['additionalData']['paymentdatasource.type'] + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_authorize_with_google_pay + response = stub_comms do + @gateway.authorize(@amount, @google_pay_card, @options.merge(selected_brand: 'visa')) + end.check_request do |_endpoint, data, _headers| + parsed = JSON.parse(data) + assert_equal @google_pay_card.payment_cryptogram, parsed['mpiData']['cavv'] + assert_equal '07', parsed['mpiData']['eci'] + assert_equal 'googlepay', parsed['additionalData']['paymentdatasource.type'] + assert_equal 'googlepay', parsed['selectedBrand'] + assert_equal 'true', parsed['additionalData']['paymentdatasource.tokenized'] + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_authorize_with_google_pay_pan_only + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(wallet_type: :google_pay)) + end.check_request do |_endpoint, data, _headers| + parsed = JSON.parse(data) + assert_equal 'googlepay', parsed['additionalData']['paymentdatasource.type'] + assert_equal 'googlepay', parsed['selectedBrand'] + assert_equal 'false', parsed['additionalData']['paymentdatasource.tokenized'] + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_authorize_with_network_tokenization_credit_card_using_ld_option + response = stub_comms do + @gateway.authorize(@amount, @apple_pay_card, @options) + end.check_request do |_endpoint, data, _headers| + parsed = JSON.parse(data) + assert_equal 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', parsed['mpiData']['cavv'] + assert_equal '07', parsed['mpiData']['eci'] + assert_equal 'applepay', parsed['additionalData']['paymentdatasource.type'] + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_authorize_with_network_tokenization_credit_card_no_apple_no_google + response = stub_comms do + @gateway.authorize(@amount, @nt_credit_card, @options) + end.check_request do |_endpoint, data, _headers| + parsed = JSON.parse(data) + assert_equal 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', parsed['mpiData']['tokenAuthenticationVerificationValue'] + assert_equal '07', parsed['mpiData']['eci'] + assert_nil parsed['additionalData']['paymentdatasource.type'] + assert_equal 'VISATOKENSERVICE', parsed['recurring']['tokenService'] + assert_equal 'EXTERNAL', parsed['recurring']['contract'] + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_authorize_with_network_tokenization_credit_card_and_stored_credentials + stored_credential = stored_credential(:merchant, :recurring) + response = stub_comms do + @gateway.authorize(@amount, @nt_credit_card, @options.merge(stored_credential:)) + end.check_request do |_endpoint, data, _headers| + parsed = JSON.parse(data) + assert_equal 'ContAuth', parsed['shopperInteraction'] + assert_nil parsed['mpiData'] + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_authorize_and_capture_with_network_transaction_id + auth = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.respond_with(successful_authorize_response_with_network_tx_ref) + assert_equal auth.network_transaction_id, '858435661128555' + + response = stub_comms do + @gateway.capture(@amount, auth.authorization, @options.merge(network_transaction_id: auth.network_transaction_id)) + end.check_request do |_, data, _| + assert_match(/"networkTxReference":"#{auth.network_transaction_id}"/, data) + end.respond_with(successful_capture_response) + assert_success response + end + + def test_authorize_and_capture_with_network_transaction_id_from_stored_cred_hash + auth = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.respond_with(successful_authorize_response_with_network_tx_ref) + assert_equal auth.network_transaction_id, '858435661128555' + + response = stub_comms do + @gateway.capture(@amount, auth.authorization, @options.merge(stored_credential: { network_transaction_id: auth.network_transaction_id })) + end.check_request do |_, data, _| + assert_match(/"networkTxReference":"#{auth.network_transaction_id}"/, data) + end.respond_with(successful_capture_response) + assert_success response + end + + def test_authorize_with_network_token + @gateway.expects(:ssl_post).returns(successful_authorize_response) + + response = @gateway.authorize(@amount, @nt_credit_card, @options) + assert_success response + end + + def test_successful_purchase_with_network_token + response = stub_comms do + @gateway.purchase(@amount, @nt_credit_card, @options) + end.respond_with(successful_authorize_response, successful_capture_response) + assert_success response + end + + def test_successful_purchase_with_recurring_detail_reference + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(recurring_detail_reference: '12345')) + end.respond_with(successful_authorize_response, successful_capture_response) + assert_success response + end + + def test_supports_network_tokenization + assert_instance_of TrueClass, @gateway.supports_network_tokenization? + end + + def test_authorize_with_sub_merchant_id + sub_merchant_data = { + sub_merchant_id: '123451234512345', + sub_merchant_name: 'Wildsea', + sub_merchant_street: '1234 Street St', + sub_merchant_city: 'Night City', + sub_merchant_state: 'East Block', + sub_merchant_postal_code: '112233', + sub_merchant_country: 'EUR', + sub_merchant_tax_id: '12345abcde67', + sub_merchant_mcc: '1234' + } + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(sub_merchant_data)) + end.check_request do |_endpoint, data, _headers| + parsed = JSON.parse(data) + assert parsed['additionalData']['subMerchantID'] + assert parsed['additionalData']['subMerchantName'] + assert parsed['additionalData']['subMerchantStreet'] + assert parsed['additionalData']['subMerchantCity'] + assert parsed['additionalData']['subMerchantState'] + assert parsed['additionalData']['subMerchantPostalCode'] + assert parsed['additionalData']['subMerchantCountry'] + assert parsed['additionalData']['subMerchantTaxId'] + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_authorize_with_sub_sellers + 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' + } + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(sub_merchant_data: sub_seller_options)) + end.check_request do |_endpoint, data, _headers| + parsed = JSON.parse(data) + additional_data = parsed['additionalData'] + assert additional_data['subMerchant.numberOfSubSellers'] + assert additional_data['subMerchant.subSeller1.id'] + assert additional_data['subMerchant.subSeller1.name'] + assert additional_data['subMerchant.subSeller1.street'] + assert additional_data['subMerchant.subSeller1.city'] + assert additional_data['subMerchant.subSeller1.state'] + assert additional_data['subMerchant.subSeller1.postalCode'] + assert additional_data['subMerchant.subSeller1.country'] + assert additional_data['subMerchant.subSeller1.taxId'] + assert additional_data['subMerchant.subSeller1.debitSettlementBank'] + assert additional_data['subMerchant.subSeller1.debitSettlementAgency'] + assert additional_data['subMerchant.subSeller1.debitSettlementAccountType'] + assert additional_data['subMerchant.subSeller1.debitSettlementAccount'] + assert additional_data['subMerchant.subSeller1.creditSettlementBank'] + assert additional_data['subMerchant.subSeller1.creditSettlementAgency'] + assert additional_data['subMerchant.subSeller1.creditSettlementAccountType'] + assert additional_data['subMerchant.subSeller1.creditSettlementAccount'] + assert additional_data['subMerchant.subSeller2.id'] + assert additional_data['subMerchant.subSeller2.name'] + assert additional_data['subMerchant.subSeller2.street'] + assert additional_data['subMerchant.subSeller2.city'] + assert additional_data['subMerchant.subSeller2.state'] + assert additional_data['subMerchant.subSeller2.postalCode'] + assert additional_data['subMerchant.subSeller2.country'] + assert additional_data['subMerchant.subSeller2.taxId'] + assert additional_data['subMerchant.subSeller2.debitSettlementBank'] + assert additional_data['subMerchant.subSeller2.debitSettlementAgency'] + assert additional_data['subMerchant.subSeller2.debitSettlementAccountType'] + assert additional_data['subMerchant.subSeller2.debitSettlementAccount'] + assert additional_data['subMerchant.subSeller2.creditSettlementBank'] + assert additional_data['subMerchant.subSeller2.creditSettlementAgency'] + assert additional_data['subMerchant.subSeller2.creditSettlementAccountType'] + assert additional_data['subMerchant.subSeller2.creditSettlementAccount'] + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_level_2_data + level_2_options = { + total_tax_amount: '160', + customer_reference: '101' + } + + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(level_2_data: level_2_options)) + end.check_request do |_endpoint, data, _headers| + parsed = JSON.parse(data) + additional_data = parsed['additionalData'] + assert_equal additional_data['enhancedSchemeData.totalTaxAmount'], level_2_options[:total_tax_amount] + assert_equal additional_data['enhancedSchemeData.customerReference'], level_2_options[:customer_reference] + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_level_3_data + level_3_options = { + total_tax_amount: '12800', + customer_reference: '101', + freight_amount: '300', + destination_state_province_code: 'NYC', + ship_from_postal_code: '1082GM', + order_date: '101216', + destination_postal_code: '1082GM', + destination_country_code: 'NLD', + duty_amount: '500', + items: [ + { + description: 'T16 Test products 1', + product_code: 'TEST120', + commodity_code: 'COMMCODE1', + quantity: '5', + unit_of_measure: 'm', + unit_price: '1000', + discount_amount: '60', + total_amount: '4940' + } + ] + } + + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(level_3_data: level_3_options)) + end.check_request do |_endpoint, data, _headers| + parsed = JSON.parse(data) + additional_data = parsed['additionalData'] + leve_3_keys = ['enhancedSchemeData.freightAmount', 'enhancedSchemeData.destinationStateProvinceCode', + 'enhancedSchemeData.shipFromPostalCode', 'enhancedSchemeData.orderDate', 'enhancedSchemeData.destinationPostalCode', + 'enhancedSchemeData.destinationCountryCode', 'enhancedSchemeData.dutyAmount', + 'enhancedSchemeData.itemDetailLine1.description', 'enhancedSchemeData.itemDetailLine1.productCode', + 'enhancedSchemeData.itemDetailLine1.commodityCode', 'enhancedSchemeData.itemDetailLine1.quantity', + 'enhancedSchemeData.itemDetailLine1.unitOfMeasure', 'enhancedSchemeData.itemDetailLine1.unitPrice', + 'enhancedSchemeData.itemDetailLine1.discountAmount', 'enhancedSchemeData.itemDetailLine1.totalAmount'] + + additional_data_keys = additional_data.keys + assert_all(leve_3_keys) { |item| additional_data_keys.include?(item) } + + mapper = { 'enhancedSchemeData.freightAmount': 'freight_amount', + 'enhancedSchemeData.destinationStateProvinceCode': 'destination_state_province_code', + 'enhancedSchemeData.shipFromPostalCode': 'ship_from_postal_code', + 'enhancedSchemeData.orderDate': 'order_date', + 'enhancedSchemeData.destinationPostalCode': 'destination_postal_code', + 'enhancedSchemeData.destinationCountryCode': 'destination_country_code', + 'enhancedSchemeData.dutyAmount': 'duty_amount' } + + mapper.each do |item| + assert_equal additional_data[item[0]], level_3_options[item[1]] + end + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_succesful_additional_airline_data + airline_data = { + agency_invoice_number: 'BAC123', + agency_plan_name: 'plan name', + airline_code: '434234', + airline_designator_code: '1234', + boarding_fee: '100', + computerized_reservation_system: 'abcd', + customer_reference_number: 'asdf1234', + document_type: 'cc', + leg: { + carrier_code: 'KL' + }, + passenger: { + first_name: 'Joe', + last_name: 'Doe' + } + } + + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(additional_data_airline: airline_data)) + end.check_request do |_endpoint, data, _headers| + parsed = JSON.parse(data) + additional_data = parsed['additionalData'] + assert_equal additional_data['airline.agency_invoice_number'], airline_data[:agency_invoice_number] + assert_equal additional_data['airline.agency_plan_name'], airline_data[:agency_plan_name] + assert_equal additional_data['airline.airline_code'], airline_data[:airline_code] + assert_equal additional_data['airline.airline_designator_code'], airline_data[:airline_designator_code] + assert_equal additional_data['airline.boarding_fee'], airline_data[:boarding_fee] + assert_equal additional_data['airline.computerized_reservation_system'], airline_data[:computerized_reservation_system] + assert_equal additional_data['airline.customer_reference_number'], airline_data[:customer_reference_number] + assert_equal additional_data['airline.document_type'], airline_data[:document_type] + assert_equal additional_data['airline.flight_date'], airline_data[:flight_date] + assert_equal additional_data['airline.ticket_issue_address'], airline_data[:abcqwer] + assert_equal additional_data['airline.ticket_number'], airline_data[:ticket_number] + assert_equal additional_data['airline.travel_agency_code'], airline_data[:travel_agency_code] + assert_equal additional_data['airline.travel_agency_name'], airline_data[:travel_agency_name] + assert_equal additional_data['airline.passenger_name'], airline_data[:passenger_name] + assert_equal additional_data['airline.leg.carrier_code'], airline_data[:leg][:carrier_code] + assert_equal additional_data['airline.leg.class_of_travel'], airline_data[:leg][:class_of_travel] + assert_equal additional_data['airline.passenger.first_name'], airline_data[:passenger][:first_name] + assert_equal additional_data['airline.passenger.last_name'], airline_data[:passenger][:last_name] + assert_equal additional_data['airline.passenger.telephone_number'], airline_data[:passenger][:telephone_number] + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_succesful_additional_airline_data_with_legs + 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', + legs: [ + { + carrier_code: 'KL', + date_of_travel: '2024-10-10' + }, + { + carrier_code: 'KL', + date_of_travel: '2024-10-11' + } + ], + 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.leg1.carrier_code'], airline_data[:legs][0][:carrier_code] + assert_equal additional_data['airline.leg1.date_of_travel'], airline_data[:legs][0][:date_of_travel] + assert_equal additional_data['airline.leg2.carrier_code'], airline_data[:legs][1][:carrier_code] + assert_equal additional_data['airline.leg2.date_of_travel'], airline_data[:legs][1][:date_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 - assert_equal '#8835205392522157#8315202663743702', response.authorization end - def test_failed_store - @gateway.expects(:ssl_post).returns(failed_store_response) - response = @gateway.store(@credit_card, @options) - assert_failure response - assert_equal 'Refused', response.message - 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' + } - def test_successful_verify response = stub_comms do - @gateway.verify(@credit_card, @options) - end.respond_with(successful_verify_response) + @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 - assert_equal '#7914776426645103#', response.authorization - assert_equal 'Authorised', response.message - assert response.test? end - def test_failed_verify + def test_additional_extra_data response = stub_comms do - @gateway.verify(@credit_card, @options) - end.respond_with(failed_verify_response) - assert_failure response - assert_equal '#7914776433387947#', response.authorization - assert_equal 'Refused', response.message - assert response.test? + @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_failed_avs_check_returns_refusal_reason_raw - @gateway.expects(:ssl_post).returns(failed_authorize_avs_response) - - response = @gateway.authorize(@amount, @credit_card, @options) - assert_failure response - assert_equal 'Refused | 05 : Do not honor', response.message + def test_extended_avs_response + response = stub_comms do + @gateway.verify(@credit_card, @options) + end.respond_with(extended_avs_response) + assert_equal 'Card member\'s name, billing address, and billing postal code match.', response.avs_result['message'] end - def test_scrub - assert @gateway.supports_scrubbing? - assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + def test_optional_idempotency_key_header + options = @options.merge(idempotency_key: 'test123') + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, _data, headers| + assert headers['Idempotency-Key'] + end.respond_with(successful_authorize_response) + assert_success response end - def test_scrub_network_tokenization_card - assert @gateway.supports_scrubbing? - assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + def test_three_decimal_places_currency_handling + stub_comms do + @gateway.authorize(1000, @credit_card, @options.merge(currency: 'JOD')) + end.check_request(skip_response: true) do |_endpoint, data| + assert_match(/"amount\":{\"value\":\"1000\",\"currency\":\"JOD\"}/, data) + end end - def test_add_address - post = {:card => {:billingAddress => {}}} - @options[:billing_address].delete(:address1) - @options[:billing_address].delete(:address2) - @gateway.send(:add_address, post, @options) - assert_equal 'N/A', post[:card][:billingAddress][:street] - assert_equal 'N/A', post[:card][:billingAddress][:houseNumberOrName] - assert_equal @options[:billing_address][:zip], post[:card][:billingAddress][:postalCode] - assert_equal @options[:billing_address][:city], post[:card][:billingAddress][:city] - assert_equal @options[:billing_address][:state], post[:card][:billingAddress][:stateOrProvince] - assert_equal @options[:billing_address][:country], post[:card][:billingAddress][:country] - 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' + } - def test_authorize_with_network_tokenization_credit_card response = stub_comms do - @gateway.authorize(@amount, @apple_pay_card, @options) - end.check_request do |endpoint, data, headers| - assert_equal 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', JSON.parse(data)['mpiData']['cavv'] - assert_equal '07', JSON.parse(data)['mpiData']['eci'] + @gateway.authorize(@amount, @credit_card, @options.merge(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) + { + order_id: '#1001', + description: 'AM test', + currency: 'GBP', + customer: '123', + stored_credential: stored_credential(*args, ntid:) + } + end + def pre_scrubbed <<-PRE_SCRUBBED opening connection to pal-test.adyen.com:443... @@ -296,6 +1933,70 @@ def post_scrubbed POST_SCRUBBED end + def pre_scrubbed_bank_account + <<-PRE_SCRUBBED + opening connection to pal-test.adyen.com:443... + opened + starting SSL for pal-test.adyen.com:443... + SSL established + <- "POST /pal/servlet/Payment/v18/authorise HTTP/1.1\r\nContent-Type: application/json\r\nAuthorization: Basic d3NfMTYzMjQ1QENvbXBhbnkuRGFuaWVsYmFra2Vybmw6eXU0aD50ZlxIVEdydSU1PDhxYTVMTkxVUw==\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: pal-test.adyen.com\r\nContent-Length: 308\r\n\r\n" + <- "{\"merchantAccount\":\"DanielbakkernlNL\",\"reference\":\"345123\",\"amount\":{\"value\":\"100\",\"currency\":\"USD\"},\"bankAccount\":{\"bankAccountNumber\":\"15378535\",\"bankLocationId\":\"244183602\",\"ownerName\":\"Jim Smith\",\"shopperEmail\":\"john.smith@test.com\",\"shopperIP\":\"77.110.174.153\",\"shopperReference\":\"John Smith\"}" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Thu, 27 Oct 2016 11:37:13 GMT\r\n" + -> "Server: Apache\r\n" + -> "Set-Cookie: JSESSIONID=C0D66C19173B3491D862B8FDBFD72FD7.test3e; Path=/pal/; Secure; HttpOnly\r\n" + -> "pspReference: 8514775682339577\r\n" + -> "Connection: close\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Content-Type: application/json;charset=utf-8\r\n" + -> "\r\n" + -> "50\r\n" + reading 80 bytes... + -> "" + -> "{\"pspReference\":\"8514775682339577\",\"resultCode\":\"Authorised\",\"authCode\":\"31845\"}" + read 80 bytes + reading 2 bytes... + -> "" + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + PRE_SCRUBBED + end + + def post_scrubbed_bank_account + <<-POST_SCRUBBED + opening connection to pal-test.adyen.com:443... + opened + starting SSL for pal-test.adyen.com:443... + SSL established + <- "POST /pal/servlet/Payment/v18/authorise HTTP/1.1\r\nContent-Type: application/json\r\nAuthorization: Basic [FILTERED]==\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: pal-test.adyen.com\r\nContent-Length: 308\r\n\r\n" + <- "{\"merchantAccount\":\"DanielbakkernlNL\",\"reference\":\"345123\",\"amount\":{\"value\":\"100\",\"currency\":\"USD\"},\"bankAccount\":{\"bankAccountNumber\":\"[FILTERED]\",\"bankLocationId\":\"[FILTERED]\",\"ownerName\":\"Jim Smith\",\"shopperEmail\":\"john.smith@test.com\",\"shopperIP\":\"77.110.174.153\",\"shopperReference\":\"John Smith\"}" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Thu, 27 Oct 2016 11:37:13 GMT\r\n" + -> "Server: Apache\r\n" + -> "Set-Cookie: JSESSIONID=C0D66C19173B3491D862B8FDBFD72FD7.test3e; Path=/pal/; Secure; HttpOnly\r\n" + -> "pspReference: 8514775682339577\r\n" + -> "Connection: close\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Content-Type: application/json;charset=utf-8\r\n" + -> "\r\n" + -> "50\r\n" + reading 80 bytes... + -> "" + -> "{\"pspReference\":\"8514775682339577\",\"resultCode\":\"Authorised\",\"authCode\":\"31845\"}" + read 80 bytes + reading 2 bytes... + -> "" + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + POST_SCRUBBED + end + def pre_scrubbed_network_tokenization_card <<-PRE_SCRUBBED opening connection to pal-test.adyen.com:443... @@ -376,9 +2077,33 @@ def failed_purchase_response RESPONSE end + def simple_successful_authorize_response + <<-RESPONSE + { + "pspReference":"8835511210681145", + "resultCode":"Authorised", + "authCode":"98696" + } + RESPONSE + end + + def simple_successful_capture_repsonse + <<-RESPONSE + { + "pspReference":"8835511210689965", + "response":"[capture-received]" + } + RESPONSE + end + def successful_authorize_response <<-RESPONSE { + "additionalData": { + "cvcResult": "1 Matches", + "avsResult": "0 Unknown", + "cvcResultRaw": "M" + }, "pspReference":"7914775043909934", "resultCode":"Authorised", "authCode":"50055" @@ -386,6 +2111,38 @@ def successful_authorize_response RESPONSE end + def successful_authorize_response_with_network_tx_ref + <<~RESPONSE + { + "additionalData": { + "liabilityShift": "false", + "authCode": "034788", + "avsResult": "2 Neither postal code nor address match", + "adjustAuthorisationData": "BQABAQAd37r69soYRcrrGlBumyPHvhurCKvze1aPCT2fztlUyUZZ0+5YZgh/rlmBjM9FNCm3Emv4awkiFXyaMJ4x+Jc7eGJpCaB9oq1QTkeMIw4yjvblij8nBmj8OIloKN/sKVF1WD4tSSC6ybgz0/ZxVZpn+l4TDcHJfGIYfELax7sMFfjGR6HEGw1Ac0we4FcLltxLL8x/aRRGOaadBO74wpvl8aatVYvgVKh42f09ovChJlDvcoIifAopkp5RxuzN1wqcad+ScHZsriVJVySuXgguAaLmEBpF6y/LQfej1pRW+zEEjYgFzrnbP+giWomBQcyY2mCnf6cBwVaeddavLSv6EMcmuplIfUPGDSr7NygJ2wkAAAEZmz6JwmlAmPoKMsuJPnnRNSBdG2EKTRBU139U2ytJuK8hVXNJc98A7bylLQqRc9zjSxJAOdX+KdaEY4KNASUqovgZ1ylPnRt/FYOqfraZcyQtl9otJjTl9oQkgSdfFeQEKg6OD9VVMzObShBEjuVFuT6HAAujEl79i1eS7QhD0w4/c8zW6tsSF29gbr7CPi/CHudeUuFHBPWGQ/NoIQXYKD+TfU+mKyPq0w8NYRdQyIiTHXHppDfrBJFbyCfE3+Dm80KKt3Kf94jvIs4xawFPURiB73GEELHufROqBQwPThWETrnTC0MwzdGB5r1KwKCtSPcV0V1zKd6pVEbjJjUvuE/9z5KaaSK8CwlHmMQcAlkYEpEmaY5bZ21gghsub9ukn/xcIhoERPi39ahnDya5thX+/+IyihGpRCIq3zMPkGKCqTokDRTv8tOK+6CMUlNbnnF95G4Kkar7lbbhxsHtElCsuVziBuoYt8n/l562uSx669+lkJ0X1w6yDPrsU9gWXkZQ8uozxKVdLIB2n0apQp8syqJ7I5atgyLnFYFnuIxW58D4evPdD5pO1d3DlCTA9DT8Df8kPRdIXNol4+skrTrP8YwMjvm3HZGusffseF0nNhOormhWdBSYIX89mu4uUus=", + "retry.attempt1.acquirerAccount": "TestPmmAcquirerAccount", + "threeDOffered": "false", + "retry.attempt1.avsResultRaw": "2", + "retry.attempt1.acquirer": "TestPmmAcquirer", + "networkTxReference": "858435661128555", + "authorisationMid": "1000", + "acquirerAccountCode": "TestPmmAcquirerAccount", + "cvcResult": "1 Matches", + "retry.attempt1.responseCode": "Approved", + "recurringProcessingModel": "Subscription", + "threeDAuthenticated": "false", + "retry.attempt1.rawResponse": "AUTHORISED" + }, + "pspReference": "853623109930081E", + "resultCode": "Authorised", + "authCode": "034788" + } + RESPONSE + end + + def successful_authorize_with_3ds_response + '{"pspReference":"8835440446784145","resultCode":"RedirectShopper","issuerUrl":"https:\\/\\/test.adyen.com\\/hpp\\/3d\\/validate.shtml","md":"djIhcWk3MUhlVFlyQ1h2UC9NWmhpVm10Zz09IfIxi5eDMZgG72AUXy7PEU86esY68wr2cunaFo5VRyNPuWg3ZSvEIFuielSuoYol5WhjCH+R6EJTjVqY8eCTt+0wiqHd5btd82NstIc8idJuvg5OCu2j8dYo0Pg7nYxW\\/2vXV9Wy\\/RYvwR8tFfyZVC\\/U2028JuWtP2WxrBTqJ6nV2mDoX2chqMRSmX8xrL6VgiLoEfzCC\\/c+14r77+whHP0Mz96IGFf4BIA2Qo8wi2vrTlccH\\/zkLb5hevvV6QH3s9h0\\/JibcUrpoXH6M903ulGuikTr8oqVjEB9w8\\/WlUuxukHmqqXqAeOPA6gScehs6SpRm45PLpLysCfUricEIDhpPN1QCjjgw8+qVf3Ja1SzwfjCVocU","paRequest":"eNpVUctuwjAQ\\/BXaD2Dt4JCHFkspqVQOBChwriJnBanIAyepoF9fG5LS+jQz612PZ3F31ETxllSnSeKSmiY90CjPZs+h709cIZgQU88XXLjPEtfRO50lfpFu8qqUfMzGDsJATbtWx7RsJabq\\/LJIJHcmwp0i9BQL0otY7qhp10URqXOXa9IIdxnLtCC5jz6i+VO4rY2v7HSdr5ZOIBBuNVRVV7b6Kn3BEAaCnT7JY9vWIUDTt41VVSDYAsLD1bqzqDGDLnkmV\\/HhO9lt2DLesORTiSR+ZckmsmeGYG9glrYkHcZ97jB35PCQe6HrI9x0TAvrQO638cgkYRz1Atb2nehOuC38FdBEralUwy8GhnSpq5LMDRPpL0Z4mJ6\\/2WBVa7ISzj1azw+YQZ6N+FawU3ITCg9YcBtjCYJthX570G\\/ZoH\\/b\\/wFlSqpp"}' + end + def failed_authorize_response <<-RESPONSE { @@ -396,6 +2153,97 @@ def failed_authorize_response RESPONSE end + def failed_authorize_3ds2_response + <<-RESPONSE + { + "additionalData": + { + "threeds2.threeDS2Result.dsTransID": "1111-abc-234", + "threeds2.threeDS2Result.eci":"07", + "threeds2.threeDS2Result.threeDSServerTransID":"222-cde-321", + "threeds2.threeDS2Result.transStatusReason":"01", + "threeds2.threeDS2Result.messageVersion":"2.1.0", + "threeds2.threeDS2Result.authenticationValue":"ABCDEFG", + "threeds2.threeDS2Result.transStatus":"N" + }, + "pspReference":"8514775559925128", + "refusalReason":"3D Not Authenticated", + "resultCode":"Refused" + } + 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_fraud_visa_response + <<-RESPONSE + { + "additionalData": + { + "refusalReasonRaw": "N7 : FRAUD" + }, + "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 { @@ -436,6 +2284,51 @@ def failed_refund_response RESPONSE end + def successful_credit_response + <<-RESPONSE + { + "pspReference": "883614109029400G", + "resultCode": "Received" + } + 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 + { + "status":422, + "errorCode":"130", + "message":"Required field 'reference' is not provided.", + "errorType":"validation" + } + RESPONSE + end + def successful_void_response <<-RESPONSE { @@ -445,6 +2338,15 @@ def successful_void_response RESPONSE end + def successful_cancel_or_refund_response + <<-RESPONSE + { + "pspReference":"8614775821628806", + "response":"[cancelOrRefund-received]" + } + RESPONSE + end + def failed_void_response <<-RESPONSE { @@ -456,6 +2358,38 @@ def failed_void_response RESPONSE end + def successful_adjust_response + <<-RESPONSE + { + "pspReference": "8835544088660594", + "response": "[adjustAuthorisation-received]" + } + RESPONSE + end + + def failed_adjust_response + <<-RESPONSE + { + "status":422, + "errorCode":"167", + "message":"Original pspReference required for this operation", + "errorType":"validation" + } + RESPONSE + end + + def successful_synchronous_adjust_response + <<-RESPONSE + {\"additionalData\":{\"authCode\":\"70125\",\"adjustAuthorisationData\":\"BQABAQA9NtGnJAkLXKqW1C+VUeCNMzDf4WwzLFBiuQ8iaA2Yvflz41t0cYxtA7XVzG2pzlJPMnkSK75k3eByNS0\\/m0\\/N2+NnnKv\\/9rYPn8Pjq1jc7CapczdqZNl8P9FwqtIa4Kdeq7ZBNeGalx9oH4reutlFggzWCr+4eYXMRqMgQNI2Bu5XvwkqBbXwbDL05CuNPjjEwO64YrCpVBLrxk4vlW4fvCLFR0u8O68C+Y4swmsPDvGUxWpRgwNVqXsTmvt9z8hlej21BErL8fPEy+fJP4Zab8oyfcLrv9FJkHZq03cyzJpOzqX458Ctn9sIwBawXzNEFN5bCt6eT1rgp0yuHeMGEGwrjNl8rijez7Rd\\/vy1WUYAAMfmZFuJMQ73l1+Hkr0VlHv6crlyP\\/FVTY\\/XIUiGMqa1yM08Zu\\/Gur5N7lU8qnMi2WO9QPyHmmdlfo7+AGsrKrzV4wY\\/wISg0pcv8PypBWVq\\/hYoCqlHsGUuIiyGLIW7A8LtG6\\/JqAA9t\\/0EdnQVz0k06IEEYnBzkQoY8Qv3cVszgPQukGstBraB47gQdVDp9vmuQjMstt8Te56SDRxtfcu0z4nQIURVSkJJNj8RYfwXH9OUbz3Vd2vwoR3lCJFTCKIeW8sidNVB3xAZnddBVQ3P\\/QxPnrrRdCcnoWSGoEOBBIxgF00XwNxJ4P7Xj1bB7oq3M7k99dgPnSdZIjyvG6BWKnCQcGyVRB0yOaYBaOCmN66EgWfXoJR5BA4Jo6gnWnESWV62iUC8OCzmis1VagfaBn0A9vWNcqKFkUr\\/68s3w8ixLJFy+WdpAS\\/flzC3bJbvy9YR9nESKAP40XiNGz9iBROCfPI2bSOvdFf831RdTxWaE+ewAC3w9GsgEKAXxzWsVeSODWRZQA0TEVOfX8SaNVa5w3EXLDsRVnmKgUH8yQnEJQBGhDJXg1sEbowE07CzzdAY5Mc=\",\"refusalReasonRaw\":\"AUTHORISED\"},\"pspReference\":\"8835574118820108\",\"response\":\"Authorised\"} + RESPONSE + end + + def failed_synchronous_adjust_response + <<-RESPONSE + {\"additionalData\":{\"authCode\":\"90745\",\"refusalReasonRaw\":\"2\"},\"pspReference\":\"8835574120337117\",\"response\":\"Refused\"} + RESPONSE + end + def successful_verify_response <<-RESPONSE { @@ -466,6 +2400,94 @@ def successful_verify_response RESPONSE end + def failed_unknown_response + <<~RESPONSE + { + "status": 422, + "errorCode": "0", + "message": "An unknown error occurred", + "errorType": "validation" + } + RESPONSE + end + + def failed_not_allowed_response + <<~RESPONSE + { + "status": 422, + "errorCode": "10", + "message": "You are not allowed to perform this action", + "errorType": "validation" + } + RESPONSE + end + + def failed_invalid_amount_response + <<~RESPONSE + { + "status": 422, + "errorCode": "100", + "message": "There is no amount specified in the request", + "errorType": "validation" + } + RESPONSE + end + + def failed_invalid_card_response + <<~RESPONSE + { + "status": 422, + "errorCode": "101", + "message": "The specified card number is not valid", + "errorType": "validation" + } + RESPONSE + end + + def failed_cvc_validation_response + <<~RESPONSE + { + "status": 422, + "errorCode": "103", + "message": "The length of the CVC code is not correct for the given card number", + "errorType": "validation" + } + RESPONSE + end + + def failed_billing_address_response + <<~RESPONSE + { + "status": 422, + "errorCode": "104", + "message": "There was an error in the specified billing address fields", + "errorType": "validation" + } + RESPONSE + end + + def failed_billing_field_response + <<~RESPONSE + { + "status": 422, + "errorCode": "132", + "message": "Required field 'billingAddress.street' is not provided.", + "errorType": "validation" + } + RESPONSE + end + + def failed_invalid_delivery_field_response + <<~RESPONSE + { + "status": 500, + "errorCode": "702", + "message": "The 'deliveryDate' field is invalid. Invalid date (year)", + "errorType": "validation" + } + RESPONSE + end + def failed_verify_response <<-RESPONSE { @@ -482,6 +2504,12 @@ def failed_authorize_avs_response RESPONSE end + def successful_tokenize_only_store_response + <<-RESPONSE + {"alias":"P481159492341538","aliasType":"Default","pspReference":"881574707964582B","recurringDetailReference":"8415747079647045","result":"Success"} + RESPONSE + end + def successful_store_response <<-RESPONSE {"additionalData":{"recurring.recurringDetailReference":"8315202663743702","recurring.shopperReference":"John Smith"},"pspReference":"8835205392522157","resultCode":"Authorised","authCode":"94571"} @@ -493,4 +2521,22 @@ def failed_store_response {"pspReference":"8835205393394754","refusalReason":"Refused","resultCode":"Refused"} RESPONSE end + + def successful_unstore_response + <<-RESPONSE + {"response":"[detail-successfully-disabled]"} + RESPONSE + end + + def failed_unstore_response + <<-RESPONSE + {"status":422,"errorCode":"800","message":"Contract not found","errorType":"validation"} + RESPONSE + end + + def extended_avs_response + <<-RESPONSE + {\"additionalData\":{\"cvcResult\":\"1 Matches\",\"cvcResultRaw\":\"Y\",\"avsResult\":\"20 Name, address and zip match\",\"avsResultRaw\":\"M\"}} + RESPONSE + end end diff --git a/test/unit/gateways/airwallex_test.rb b/test/unit/gateways/airwallex_test.rb new file mode 100644 index 00000000000..e66baa11401 --- /dev/null +++ b/test/unit/gateways/airwallex_test.rb @@ -0,0 +1,562 @@ +require 'test_helper' + +class AirwallexTest < Test::Unit::TestCase + include CommStub + + def setup + @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 + @declined_amount = 8014 + + @options = { + billing_address: address + } + + @stored_credential_cit_options = { initial_transaction: true, initiator: 'cardholder', reason_type: 'recurring', network_transaction_id: nil } + @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 + + def test_successful_purchase + @gateway.expects(:ssl_post).times(2).returns(successful_purchase_response) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + + assert_equal 'int_hkdmtp6bpg79nn35u43', response.authorization + assert response.test? + end + + def test_failed_purchase_with_declined_card + @gateway.expects(:ssl_post).times(2).returns(successful_payment_intent_response, failed_purchase_response) + + response = @gateway.purchase(@declined_amount, @declined_card, @options) + assert_failure response + assert_equal '14', response.error_code + assert_equal 'The card issuer declined this transaction. Please refer to the original response code.', response.message + end + + def test_successful_authorize + @gateway.expects(:ssl_post).times(2).returns(successful_authorize_response) + + response = @gateway.authorize(@amount, @credit_card, @options.merge(auto_capture: false)) + assert_success response + + assert_equal 'int_hkdmtp6bpg79nqimh2z', response.authorization + assert response.test? + end + + def test_failed_authorize_with_declined_card + @gateway.expects(:ssl_post).times(2).returns(successful_payment_intent_response, failed_authorize_response) + + response = @gateway.authorize(@declined_amount, @declined_card, @options.merge(auto_capture: false)) + assert_failure response + assert_equal '14', response.error_code + assert_equal 'The card issuer declined this transaction. Please refer to the original response code.', response.message + end + + def test_successful_authorize_with_return_url + return_url = 'https://example.com' + + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(return_url:)) + end.check_request do |endpoint, data, _headers| + assert_match(/\"return_url\":\"https:\/\/example.com\"/, data) unless endpoint == setup_endpoint + end.respond_with(successful_authorize_response) + + assert_success response + assert response.test? + assert_equal 'AUTHORIZED', response.message + end + + def test_successful_authorize_with_3ds_v1_options + @options[:three_d_secure] = { + version: '1', + cavv: 'VGhpcyBpcyBhIHRlc3QgYmFzZTY=', + eci: '02', + xid: 'b2h3aDZrd3BJWXVCWEFMbzJqSGQ=' + } + + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |endpoint, data, _headers| + unless endpoint == setup_endpoint + assert_match(/\"version\":\"1.0.0\"/, data) + assert_match(/\"cavv\":\"VGhpcyBpcyBhIHRlc3QgYmFzZTY=\"/, data) + assert_match(/\"eci\":\"02\"/, data) + assert_match(/\"xid\":\"b2h3aDZrd3BJWXVCWEFMbzJqSGQ=\"/, data) + end + end.respond_with(successful_authorize_response) + + assert_success response + assert response.test? + assert_equal 'AUTHORIZED', response.message + end + + def test_successful_authorize_with_3ds_v2_options + @options[:three_d_secure] = { + version: '2.2.0', + cavv: 'MTIzNDU2Nzg5MDA5ODc2NTQzMjE=', + ds_transaction_id: 'f25084f0-5b16-4c0a-ae5d-b24808a95e4b', + eci: '02', + three_ds_server_trans_id: 'df8b9557-e41b-4e17-87e9-2328694a2ea0' + } + + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |endpoint, data, _headers| + unless endpoint == setup_endpoint + assert_match(/\"version\":\"2.2.0\"/, data) + assert_match(/\"authentication_value\":\"MTIzNDU2Nzg5MDA5ODc2NTQzMjE=\"/, data) + assert_match(/\"ds_transaction_id\":\"f25084f0-5b16-4c0a-ae5d-b24808a95e4b\"/, data) + assert_match(/\"eci\":\"02\"/, data) + assert_match(/\"three_ds_server_transaction_id\":\"df8b9557-e41b-4e17-87e9-2328694a2ea0\"/, data) + end + end.respond_with(successful_authorize_response) + + assert_success response + assert response.test? + assert_equal 'AUTHORIZED', response.message + end + + def test_successful_purchase_with_3ds_version_formatting + @options[:three_d_secure] = { + version: '2.0', + cavv: 'MTIzNDU2Nzg5MDA5ODc2NTQzMjE=', + ds_transaction_id: 'f25084f0-5b16-4c0a-ae5d-b24808a95e4b', + eci: '02', + three_ds_server_trans_id: 'df8b9557-e41b-4e17-87e9-2328694a2ea0' + } + + formatted_version = format_three_ds_version(@options[:three_d_secure][:version]) + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |endpoint, data, _headers| + data = JSON.parse(data) + assert_match(data['payment_method_options']['card']['external_three_ds']['version'], formatted_version) unless endpoint == setup_endpoint + end.respond_with(successful_purchase_response) + + assert_success response + assert response.test? + assert_equal 'AUTHORIZED', response.message + end + + def test_successful_skip_3ds_in_payment_intent + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge({ skip_3ds: true })) + end.check_request do |endpoint, data, _headers| + data = JSON.parse(data) + assert_match(data['payment_method_options']['card']['risk_control']['three_ds_action'], 'SKIP_3DS') if endpoint == setup_endpoint + end.respond_with(successful_purchase_response) + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge({ skip_3ds: 'true' })) + end.check_request do |endpoint, data, _headers| + data = JSON.parse(data) + assert_match(data['payment_method_options']['card']['risk_control']['three_ds_action'], 'SKIP_3DS') if endpoint == setup_endpoint + end.respond_with(successful_purchase_response) + end + + def test_successful_capture + @gateway.expects(:ssl_post).returns(successful_capture_response) + + response = @gateway.capture(@amount, @credit_card, @options) + assert_success response + + assert_equal 'int_hkdmtp6bpg79nqimh2z', response.authorization + assert response.test? + end + + def test_failed_capture_with_declined_amount + @gateway.expects(:ssl_post).returns(failed_capture_response) + + response = @gateway.capture(@declined_amount, '12345', @options) + assert_failure response + assert_equal 'not_found', response.error_code + assert_equal 'The requested endpoint does not exist [/api/v1/pa/payment_intents/12345/capture]', response.message + end + + def test_capture_without_auth_raises_error + assert_raise ArgumentError do + @gateway.capture(@amount, '', @options) + end + end + + def test_successful_refund + @gateway.expects(:ssl_post).returns(successful_refund_response) + + response = @gateway.refund(@amount, @credit_card, @options) + assert_success response + + assert_equal 'RECEIVED', response.message + assert_equal response.authorization, 'int_hkdmb6rw6g79o82v60s' + assert response.test? + end + + def test_failed_refund_with_declined_amount + @gateway.expects(:ssl_post).returns(failed_refund_response) + + response = @gateway.refund(@declined_amount, '12345', @options) + assert_failure response + assert_equal 'resource_not_found', response.error_code + assert_equal 'The PaymentIntent with ID 12345 cannot be found.', response.message + end + + def test_successful_void + @gateway.expects(:ssl_post).returns(successful_void_response) + + response = @gateway.void('int_hkdm49cp4g7d5njedty', @options) + assert_success response + + assert_equal 'CANCELLED', response.message + assert response.test? + end + + def test_void_without_auth_raises_error + assert_raise ArgumentError do + @gateway.void('', @options) + end + end + + def test_failed_void + @gateway.expects(:ssl_post).returns(failed_void_response) + response = @gateway.void('12345', @options) + + assert_failure response + end + + def test_successful_verify + @gateway.expects(:ssl_post).times(3).returns(successful_authorize_response, successful_void_response) + response = @gateway.verify(credit_card('4111111111111111'), @options) + + assert_success response + assert_equal 'CANCELLED', response.message + end + + def test_failed_verify + @gateway.expects(:ssl_post).times(2).returns(successful_payment_intent_response, failed_authorize_response) + + response = @gateway.verify(@credit_card, @options) + assert_failure response + end + + def test_refund_passes_both_ids + request_id = "request_#{(Time.now.to_f.round(2) * 100).to_i}" + merchant_order_id = "order_#{(Time.now.to_f.round(2) * 100).to_i}" + stub_comms do + # merchant_order_id is only passed directly on refunds + @gateway.refund(@amount, 'abc123', @options.merge(request_id:, merchant_order_id:)) + end.check_request do |_endpoint, data, _headers| + assert_match(/request_/, data) + assert_match(/order_/, data) + end.respond_with(successful_purchase_response, successful_refund_response) + end + + def test_purchase_passes_appropriate_request_id_per_call + request_id = SecureRandom.uuid + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(request_id:)) + end.check_request do |_endpoint, data, _headers| + if data.include?('payment_method') + # check for this on the purchase call + assert_match(/\"request_id\":\"#{request_id}\"/, data) + else + # check for this on the create_payment_intent calls + assert_match(/\"request_id\":\"#{request_id}_setup\"/, data) + end + end.respond_with(successful_purchase_response) + end + + def test_purchase_passes_appropriate_merchant_order_id + merchant_order_id = SecureRandom.uuid + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(merchant_order_id:)) + end.check_request do |_endpoint, data, _headers| + assert_match(/\"merchant_order_id\":\"#{merchant_order_id}\"/, data) + end.respond_with(successful_purchase_response) + end + + def test_purchase_passes_currency_code + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(currency: 'USD')) + end.check_request do |_endpoint, data, _headers| + # only look for currency code on the create_payment_intent request + assert_match(/USD/, data) if data.include?('_setup') + end.respond_with(successful_purchase_response) + end + + def test_purchase_passes_referrer_data + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + # only look for referrer data on the create_payment_intent request + assert_match(/\"referrer_data\":{\"type\":\"spreedly\"}/, data) if data.include?('_setup') + end.respond_with(successful_purchase_response) + end + + def test_purchase_passes_descriptor + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(description: 'a simple test')) + end.check_request do |_endpoint, data, _headers| + assert_match(/a simple test/, data) + end.respond_with(successful_purchase_response) + end + + def test_purchase_truncates_descriptor + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(description: 'This description is longer than 32 characters.')) + end.check_request do |_endpoint, data, _headers| + refute_match(/This description is longer than 32 characters./, data) + assert_match(/This description is longer than /, data) + end.respond_with(successful_purchase_response) + end + + def test_invalid_login + assert_raise ArgumentError do + AirwallexGateway.new(login: '', password: '') + end + end + + def test_successful_cit_with_stored_credential + auth = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge!({ stored_credential: @stored_credential_cit_options })) + end.check_request do |endpoint, data, _headers| + # This conditional runs assertions after the initial setup call is made + unless endpoint == setup_endpoint + assert_match(/"external_recurring_data\"/, data) + assert_match(/"merchant_trigger_reason\":\"scheduled\"/, data) + assert_match(/"original_transaction_id\":null,/, data) + assert_match(/"triggered_by\":\"customer\"/, data) + end + end.respond_with(successful_authorize_response) + assert_success auth + end + + def test_successful_mit_with_recurring_stored_credential + auth = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge!({ stored_credential: @stored_credential_cit_options })) + end.check_request do |endpoint, data, _headers| + unless endpoint == setup_endpoint + assert_match(/"external_recurring_data\"/, data) + assert_match(/"merchant_trigger_reason\":\"scheduled\"/, data) + assert_match(/"original_transaction_id\":null,/, data) + assert_match(/"triggered_by\":\"customer\"/, data) + end + end.respond_with(successful_authorize_response) + assert_success auth + + add_cit_network_transaction_id_to_stored_credential(auth) + + purchase = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge!({ stored_credential: @stored_credential_mit_options })) + end.check_request do |endpoint, data, _headers| + unless endpoint == setup_endpoint + assert_match(/"external_recurring_data\"/, data) + assert_match(/"merchant_trigger_reason\":\"scheduled\"/, data) + assert_match(/"original_transaction_id\":\"123456789012345\"/, data) + assert_match(/"triggered_by\":\"merchant\"/, data) + end + end.respond_with(successful_purchase_response) + assert_success purchase + end + + def test_successful_mit_with_unscheduled_stored_credential + @stored_credential_cit_options[:reason_type] = 'unscheduled' + @stored_credential_mit_options[:reason_type] = 'unscheduled' + + auth = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge!({ stored_credential: @stored_credential_cit_options })) + end.check_request do |endpoint, data, _headers| + unless endpoint == setup_endpoint + assert_match(/"external_recurring_data\"/, data) + assert_match(/"merchant_trigger_reason\":\"unscheduled\"/, data) + assert_match(/"original_transaction_id\":null,/, data) + assert_match(/"triggered_by\":\"customer\"/, data) + end + end.respond_with(successful_authorize_response) + assert_success auth + + add_cit_network_transaction_id_to_stored_credential(auth) + + purchase = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge!({ stored_credential: @stored_credential_mit_options })) + end.check_request do |endpoint, data, _headers| + unless endpoint == setup_endpoint + assert_match(/"external_recurring_data\"/, data) + assert_match(/"merchant_trigger_reason\":\"unscheduled\"/, data) + assert_match(/"original_transaction_id\":\"123456789012345\"/, data) + assert_match(/"triggered_by\":\"merchant\"/, data) + end + end.respond_with(successful_purchase_response) + assert_success purchase + end + + def test_successful_mit_with_installment_stored_credential + @stored_credential_cit_options[:reason_type] = 'installment' + @stored_credential_mit_options[:reason_type] = 'installment' + + auth = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge!({ stored_credential: @stored_credential_cit_options })) + end.check_request do |endpoint, data, _headers| + unless endpoint == setup_endpoint + assert_match(/"external_recurring_data\"/, data) + assert_match(/"merchant_trigger_reason\":\"scheduled\"/, data) + assert_match(/"original_transaction_id\":null,/, data) + assert_match(/"triggered_by\":\"customer\"/, data) + end + end.respond_with(successful_authorize_response) + assert_success auth + + add_cit_network_transaction_id_to_stored_credential(auth) + + purchase = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge!({ stored_credential: @stored_credential_mit_options })) + end.check_request do |endpoint, data, _headers| + unless endpoint == setup_endpoint + assert_match(/"external_recurring_data\"/, data) + assert_match(/"merchant_trigger_reason\":\"scheduled\"/, data) + assert_match(/"original_transaction_id\":\"123456789012345\"/, data) + assert_match(/"triggered_by\":\"merchant\"/, data) + end + end.respond_with(successful_purchase_response) + assert_success purchase + end + + def test_successful_network_transaction_id_override_with_mastercard + mastercard = credit_card('2223 0000 1018 1375', { brand: 'master' }) + + auth = stub_comms do + @gateway.authorize(@amount, mastercard, @options.merge!({ stored_credential: @stored_credential_cit_options })) + end.check_request do |endpoint, data, _headers| + unless endpoint == setup_endpoint + assert_match(/"external_recurring_data\"/, data) + assert_match(/"merchant_trigger_reason\":\"scheduled\"/, data) + assert_match(/"original_transaction_id\":null,/, data) + assert_match(/"triggered_by\":\"customer\"/, data) + end + end.respond_with(successful_authorize_response) + assert_success auth + + add_cit_network_transaction_id_to_stored_credential(auth) + + purchase = stub_comms do + @gateway.purchase(@amount, mastercard, @options.merge!({ stored_credential: @stored_credential_mit_options })) + end.check_request do |endpoint, data, _headers| + unless endpoint == setup_endpoint + assert_match(/"external_recurring_data\"/, data) + assert_match(/"merchant_trigger_reason\":\"scheduled\"/, data) + assert_match(/"original_transaction_id\":\"MCC123ABC0101\"/, data) + assert_match(/"triggered_by\":\"merchant\"/, data) + end + end.respond_with(successful_purchase_response) + assert_success purchase + end + + def test_failed_mit_with_unapproved_visa_ntid + @gateway.expects(:ssl_post).returns(failed_ntid_response) + assert_raise ArgumentError do + @gateway.authorize(@amount, @credit_card, @options.merge!({ stored_credential: @stored_credential_cit_options })) + end + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + def format_three_ds_version(version) + version = version.split('.') + + version.push('0') until version.length == 3 + version.join('.') + end + + private + + def pre_scrubbed + <<~TRANSCRIPT + opening connection to api-demo.airwallex.com:443...\nopened\nstarting SSL for api-demo.airwallex.com:443...\nSSL established\n<- \"POST /api/v1/pa/payment_intents/create HTTP/1.1\\r\\nContent-Type: application/json\\r\\nAuthorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxNWU1OGQzOS02MWIxLTQ3NzgtYjkzMC1iZWNiYmY3NmMxZjIiLCJzdWIiOiIxZDcyZmI4MC1hMThlLTQyNGEtODFjMC01NGEwZThiZDQzYTQiLCJpYXQiOjE2NDYxMTAxMjMsImV4cCI6MTY0NjEyMjEyMywiYWNjb3VudF9pZCI6IjBhMWE4NzQ3LWM4M2YtNGUwNC05MGQyLTNjZmFjNDkzNTNkYSIsImRhdGFfY2VudGVyX3JlZ2lvbiI6IkhLIiwidHlwZSI6ImNsaWVudCIsImRjIjoiSEsiLCJpc3NkYyI6IlVTIn0.peXgGLfxzJcAxzDpej5fgJqFDEMraF0gh--8s4sUmis\\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-demo.airwallex.com\\r\\nContent-Length: 101\\r\\n\\r\\n\"\n<- \"{\\\"amount\\\":\\\"1.00\\\",\\\"currency\\\":\\\"AUD\\\",\\\"request_id\\\":\\\"164611012286\\\",\\\"merchant_order_id\\\":\\\"mid_164611012286\\\"}\"\n-> \"HTTP/1.1 201 Created\\r\\n\"\n-> \"Content-Type: application/json\\r\\n\"\n-> \"Content-Length: 618\\r\\n\"\n-> \"Date: Tue, 01 Mar 2022 04:48:44 GMT\\r\\n\"\n-> \"x-awx-traceid: ac5e1e9927434d751368bda3d37b89fb\\r\\n\"\n-> \"vary: Origin,Access-Control-Request-Method,Access-Control-Request-Headers\\r\\n\"\n-> \"x-envoy-upstream-service-time: 82\\r\\n\"\n-> \"x-envoy-decorator-operation: patokeninterceptor.airwallex.svc.cluster.local:80/*\\r\\n\"\n-> \"Server: APISIX\\r\\n\"\n-> \"X-B3-TraceId: ac5e1e9927434d751368bda3d37b89fb\\r\\n\"\n-> \"Via: 1.1 google\\r\\n\"\n-> \"Alt-Svc: h3=\\\":443\\\"; ma=2592000,h3-29=\\\":443\\\"; ma=2592000\\r\\n\"\n-> \"Connection: close\\r\\n\"\n-> \"\\r\\n\"\nreading 618 bytes...\n-> \"{\\\"id\\\":\\\"int_hkdmnnq47g7hwjukwk7\\\",\\\"request_id\\\":\\\"164611012286\\\",\\\"amount\\\":1,\\\"currency\\\":\\\"AUD\\\",\\\"merchant_order_id\\\":\\\"mid_164611012286\\\",\\\"status\\\":\\\"REQUIRES_PAYMENT_METHOD\\\",\\\"captured_amount\\\":0,\\\"created_at\\\":\\\"2022-03-01T04:48:44+0000\\\",\\\"updated_at\\\":\\\"2022-03-01T04:48:44+0000\\\",\\\"available_payment_method_types\\\":[\\\"wechatpay\\\",\\\"card\\\"],\\\"client_secret\\\":\\\"eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2NDYxMTAxMjQsImV4cCI6MTY0NjExMzcyNCwiYWNjb3VudF9pZCI6IjBhMWE4NzQ3LWM4M2YtNGUwNC05MGQyLTNjZmFjNDkzNTNkYSIsImRhdGFfY2VudGVyX3JlZ2lvbiI6IkhLIiwiaW50ZW50X2lkIjoiaW50X2hrZG1ubnE0N2c3aHdqdWt3azciLCJwYWRjIjoiSEsifQ.gjWFNQjss2fW0F_afg_Yx0fku-NhzhgERxT0J0he9wU\\\"}\"\nread 618 bytes\nConn close\nopening connection to api-demo.airwallex.com:443...\nopened\nstarting SSL for api-demo.airwallex.com:443...\nSSL established\n<- \"POST /api/v1/pa/payment_intents/int_hkdmnnq47g7hwjukwk7/confirm HTTP/1.1\\r\\nContent-Type: application/json\\r\\nAuthorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxNWU1OGQzOS02MWIxLTQ3NzgtYjkzMC1iZWNiYmY3NmMxZjIiLCJzdWIiOiIxZDcyZmI4MC1hMThlLTQyNGEtODFjMC01NGEwZThiZDQzYTQiLCJpYXQiOjE2NDYxMTAxMjMsImV4cCI6MTY0NjEyMjEyMywiYWNjb3VudF9pZCI6IjBhMWE4NzQ3LWM4M2YtNGUwNC05MGQyLTNjZmFjNDkzNTNkYSIsImRhdGFfY2VudGVyX3JlZ2lvbiI6IkhLIiwidHlwZSI6ImNsaWVudCIsImRjIjoiSEsiLCJpc3NkYyI6IlVTIn0.peXgGLfxzJcAxzDpej5fgJqFDEMraF0gh--8s4sUmis\\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-demo.airwallex.com\\r\\nContent-Length: 278\\r\\n\\r\\n\"\n<- \"{\\\"request_id\\\":\\\"164611012286_purchase\\\",\\\"return_url\\\":\\\"https://example.com\\\",\\\"payment_method\\\":{\\\"type\\\":\\\"card\\\",\\\"card\\\":{\\\"expiry_month\\\":\\\"09\\\",\\\"expiry_year\\\":\\\"2023\\\",\\\"number\\\":\\\"4000100011112224\\\",\\\"name\\\":\\\"Longbob Longsen\\\",\\\"cvc\\\":\\\"123\\\",\\\"billing\\\":{\\\"first_name\\\":\\\"Longbob\\\",\\\"last_name\\\":\\\"Longsen\\\"}}}}\"\n-> \"HTTP/1.1 200 OK\\r\\n\"\n-> \"Content-Type: application/json\\r\\n\"\n-> \"Date: Tue, 01 Mar 2022 04:48:46 GMT\\r\\n\"\n-> \"Vary: Accept-Encoding\\r\\n\"\n-> \"x-awx-traceid: c6bf7d6b1612a8e32c999d3d6ff379a1\\r\\n\"\n-> \"vary: Origin,Access-Control-Request-Method,Access-Control-Request-Headers\\r\\n\"\n-> \"x-envoy-upstream-service-time: 1279\\r\\n\"\n-> \"x-envoy-decorator-operation: patokeninterceptor.airwallex.svc.cluster.local:80/*\\r\\n\"\n-> \"Content-Encoding: gzip\\r\\n\"\n-> \"Server: APISIX\\r\\n\"\n-> \"X-B3-TraceId: c6bf7d6b1612a8e32c999d3d6ff379a1\\r\\n\"\n-> \"Via: 1.1 google\\r\\n\"\n-> \"Alt-Svc: h3=\\\":443\\\"; ma=2592000,h3-29=\\\":443\\\"; ma=2592000\\r\\n\"\n-> \"Connection: close\\r\\n\"\n-> \"Transfer-Encoding: chunked\\r\\n\"\n-> \"\\r\\n\"\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"\\x1F\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"\\x8B\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"\\b\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"\\x00\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"\\x00\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"\\x00\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"\\x00\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"\\x00\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"\\x00\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"\\x03\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"\\xAC\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"S\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"]\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"o\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"\\xDA\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"0\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"\\x14\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"000001\\r\\n\"\nreading 1 bytes...\n-> \"\\xFD\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"264\\r\\n\"\nreading 612 bytes...\n-> \"+S^\\xD7jNpB@\\xDAC\\xD5\\xB2\\xAD\\xD2\\xA6U-L\\xDD^\\\"c\\e\\xE2\\x92\\xD8\\xA9?B\\xA1\\xE2\\xBF\\xF7\\xDE\\x04\\x18\\xD5\\xA6i\\x0F}rr?\\x8E\\xCF9\\xF7\\xFA9R\\\"\\x1AGJ\\xFB\\xA2\\\\\\x89Z\\xEBG:\\\\\\x0E\\xCB\\xF5CX\\xADW\\xC3\\xE8,\\xB2\\xF21H\\xE7\\x8B\\xAE,\\xCEh\\x16\\xC7$N\\x92<+\\x9A`y\\xC9\\x9C\\x84\\\"V\\x9B\\xA0}4\\x8E\\xCF\\\"\\x1E\\xAC\\x95\\x9Ao\\xA0\\xFAbv\\x05\\xB9Zb\\x19\\xE0\\e+\\xA4\\xEDqj%\\x8AS,(\\x13\\xD2q\\xAB\\x1Ao,\\xE4\\x85\\\\\\xB0Py\\b;\\xCF|p\\x10\\xBA\\x9B]^N&W\\x13\\x84\\xE4\\xAC\\xF1\\xC1JQ\\x9C\\xDC[1\\x8F4\\e\\xB6\\xA9%\\\\\\xC6\\xBC\\x97u\\x03\\xA9\\xE7^ \\xFCw\\x02\\xE7s\\xAA\\xAB^\\xE0\\xB6\\xCDMq\\xD4y\\x02u\\xC0\\xA8\\xA5/\\x8D8B\\xD4^\\xFC\\x01\\xF1Xm\\xA1\\xD7o\\x1A\\t\\x05\\x9CY\\xD1\\xB1\\xB3]\\x93|j\\x94\\xDD\\x14\\xB5\\xD1\\xBE\\x84,\\x19An\\x1F\\xDBH\\x862\\x13\\x92\\f \\xA8Y\\x8D\\xED_\\x8D^\\xCE\\xCD\\xFC\\x1D\\x9ENjH\\xCC\\x95\\x868%\\x84\\xC4$B\\x89\\xCESlK\\x12\\x8AY\\xCB4\\xF2j\\x95c\\xF0\\xAB\\x9C\\v\\xE0/G\\x19p\\x057\\x02Agw{F\\xC5\\x81$\\xF8\\xA6\\xD0\\xD9\\x85\\xD2Ki\\e\\xABPut}{\\xDF\\x92\\xF6CZ\\xFE\\xA8\\xB2\\xF5\\xE7\\xA7\\xFC\\xD3\\x83\\xBEw7\\xEB{\\xB5Y\\x7FD\\x84\\x96\\x17\\xBC\\x94|\\x05\\xA5\\rs\\xAE#WU\\x00\\x81J\\x17\\xCA\\x82\\xF5\\xAFe\\xEC\\xF9\\x9EFQ\\xD4nw2\\xD3\\xCB\\xDB\\xC9\\xC5\\xB4\\x9F\\xA8\\x950?\\x18\\xA8\\xEFmI\\xCE\\xC9\\xE0\\x9C\\xC4SB\\xC74\\x1FS\\xFA\\x1E<@\\vB#\\xFE\\xA3n\\xF7{\\x86\\xA0\\xAE;\\xFE\\xBD\\xE4GF\\x17\\xB3\\xE9\\x97\\xEF\\xB7\\xD7\\xBF:R\\x8D5\\xAD\\xC2\\x9D\\xF5\\xE0\\xB4c\\xDC+\\xA3{$\\x9A\\xA4\\xA34\\e&C\\n\\xEF \\xA5Y\\x16S\\x92\\xE6y\\x16\\x0FF\\xF9i\\xA3\\xB1j\\xA94\\xAB\\n+]c\\xB4\\x93\\x87\\xB1tbX\\x80\\xFD\\xB2j\\xCB:\\xE0}f0L\\xB3\\xC1\\xE8oKN\\xF01.\\x82\\x16\\xAFcod]z\\xA8s\\xD2\\xFBJ\\x16\\xADb\\xF8l\\x94]\\xB3\\xAA\\x92O{\\xBA\\xE0\\xA5\\xE2=_@c8|\\xE1\\x0E\\x9F`\\xFB\\xC2\\xB2 \\x8E\\xA9\\xDE2\\xB4\\x15\\xDE\\xEE\\xCD\\x14\\xC1\\xB9\\xB1\\xA82\\xC6\\x19\\xB1\\xD6\\xA11\\xF8\\xD0aQ\\xF7[v\\f\\xFC<\\xAC]\\xEF\\xCB\\xB7nu\\xDEV\\xEC\"\n-> \"\\xEE\\x05\\x00\\x00\\xFF\\xFF\\x03\\x00;d\\xC0x\\xFE\\x04\\x00\\x00\"\nread 612 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"0\\r\\n\"\n-> \"\\r\\n\"\nConn close\n + TRANSCRIPT + end + + def post_scrubbed + <<~TRANSCRIPT + opening connection to api-demo.airwallex.com:443...\nopened\nstarting SSL for api-demo.airwallex.com:443...\nSSL established\n<- \"POST /api/v1/pa/payment_intents/create HTTP/1.1\\r\\nContent-Type: application/json\\r\\nAuthorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxNWU1OGQzOS02MWIxLTQ3NzgtYjkzMC1iZWNiYmY3NmMxZjIiLCJzdWIiOiIxZDcyZmI4MC1hMThlLTQyNGEtODFjMC01NGEwZThiZDQzYTQiLCJpYXQiOjE2NDYxMTAxMjMsImV4cCI6MTY0NjEyMjEyMywiYWNjb3VudF9pZCI6IjBhMWE4NzQ3LWM4M2YtNGUwNC05MGQyLTNjZmFjNDkzNTNkYSIsImRhdGFfY2VudGVyX3JlZ2lvbiI6IkhLIiwidHlwZSI6ImNsaWVudCIsImRjIjoiSEsiLCJpc3NkYyI6IlVTIn0.peXgGLfxzJcAxzDpej5fgJqFDEMraF0gh--8s4sUmis\\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-demo.airwallex.com\\r\\nContent-Length: 101\\r\\n\\r\\n\"\n<- \"{\\\"amount\\\":\\\"1.00\\\",\\\"currency\\\":\\\"AUD\\\",\\\"request_id\\\":\\\"164611012286\\\",\\\"merchant_order_id\\\":\\\"mid_164611012286\\\"}\"\n-> \"HTTP/1.1 201 Created\\r\\n\"\n-> \"Content-Type: application/json\\r\\n\"\n-> \"Content-Length: 618\\r\\n\"\n-> \"Date: Tue, 01 Mar 2022 04:48:44 GMT\\r\\n\"\n-> \"x-awx-traceid: ac5e1e9927434d751368bda3d37b89fb\\r\\n\"\n-> \"vary: Origin,Access-Control-Request-Method,Access-Control-Request-Headers\\r\\n\"\n-> \"x-envoy-upstream-service-time: 82\\r\\n\"\n-> \"x-envoy-decorator-operation: patokeninterceptor.airwallex.svc.cluster.local:80/*\\r\\n\"\n-> \"Server: APISIX\\r\\n\"\n-> \"X-B3-TraceId: ac5e1e9927434d751368bda3d37b89fb\\r\\n\"\n-> \"Via: 1.1 google\\r\\n\"\n-> \"Alt-Svc: h3=\\\":443\\\"; ma=2592000,h3-29=\\\":443\\\"; ma=2592000\\r\\n\"\n-> \"Connection: close\\r\\n\"\n-> \"\\r\\n\"\nreading 618 bytes...\n-> \"{\\\"id\\\":\\\"int_hkdmnnq47g7hwjukwk7\\\",\\\"request_id\\\":\\\"164611012286\\\",\\\"amount\\\":1,\\\"currency\\\":\\\"AUD\\\",\\\"merchant_order_id\\\":\\\"mid_164611012286\\\",\\\"status\\\":\\\"REQUIRES_PAYMENT_METHOD\\\",\\\"captured_amount\\\":0,\\\"created_at\\\":\\\"2022-03-01T04:48:44+0000\\\",\\\"updated_at\\\":\\\"2022-03-01T04:48:44+0000\\\",\\\"available_payment_method_types\\\":[\\\"wechatpay\\\",\\\"card\\\"],\\\"client_secret\\\":\\\"eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2NDYxMTAxMjQsImV4cCI6MTY0NjExMzcyNCwiYWNjb3VudF9pZCI6IjBhMWE4NzQ3LWM4M2YtNGUwNC05MGQyLTNjZmFjNDkzNTNkYSIsImRhdGFfY2VudGVyX3JlZ2lvbiI6IkhLIiwiaW50ZW50X2lkIjoiaW50X2hrZG1ubnE0N2c3aHdqdWt3azciLCJwYWRjIjoiSEsifQ.gjWFNQjss2fW0F_afg_Yx0fku-NhzhgERxT0J0he9wU\\\"}\"\nread 618 bytes\nConn close\nopening connection to api-demo.airwallex.com:443...\nopened\nstarting SSL for api-demo.airwallex.com:443...\nSSL established\n<- \"POST /api/v1/pa/payment_intents/int_hkdmnnq47g7hwjukwk7/confirm HTTP/1.1\\r\\nContent-Type: application/json\\r\\nAuthorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxNWU1OGQzOS02MWIxLTQ3NzgtYjkzMC1iZWNiYmY3NmMxZjIiLCJzdWIiOiIxZDcyZmI4MC1hMThlLTQyNGEtODFjMC01NGEwZThiZDQzYTQiLCJpYXQiOjE2NDYxMTAxMjMsImV4cCI6MTY0NjEyMjEyMywiYWNjb3VudF9pZCI6IjBhMWE4NzQ3LWM4M2YtNGUwNC05MGQyLTNjZmFjNDkzNTNkYSIsImRhdGFfY2VudGVyX3JlZ2lvbiI6IkhLIiwidHlwZSI6ImNsaWVudCIsImRjIjoiSEsiLCJpc3NkYyI6IlVTIn0.peXgGLfxzJcAxzDpej5fgJqFDEMraF0gh--8s4sUmis\\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-demo.airwallex.com\\r\\nContent-Length: 278\\r\\n\\r\\n\"\n<- \"{\\\"request_id\\\":\\\"164611012286_purchase\\\",\\\"return_url\\\":\\\"https://example.com\\\",\\\"payment_method\\\":{\\\"type\\\":\\\"card\\\",\\\"card\\\":{\\\"expiry_month\\\":\\\"09\\\",\\\"expiry_year\\\":\\\"2023\\\",\\\"number\\\":\\\"[REDACTED]\\\",\\\"name\\\":\\\"Longbob Longsen\\\",\\\"cvc\\\":\\\"[REDACTED]\\\",\\\"billing\\\":{\\\"first_name\\\":\\\"Longbob\\\",\\\"last_name\\\":\\\"Longsen\\\"}}}}\"\n-> \"HTTP/1.1 200 OK\\r\\n\"\n-> \"Content-Type: application/json\\r\\n\"\n-> \"Date: Tue, 01 Mar 2022 04:48:46 GMT\\r\\n\"\n-> \"Vary: Accept-Encoding\\r\\n\"\n-> \"x-awx-traceid: c6bf7d6b1612a8e32c999d3d6ff379a1\\r\\n\"\n-> \"vary: Origin,Access-Control-Request-Method,Access-Control-Request-Headers\\r\\n\"\n-> \"x-envoy-upstream-service-time: 1279\\r\\n\"\n-> \"x-envoy-decorator-operation: patokeninterceptor.airwallex.svc.cluster.local:80/*\\r\\n\"\n-> \"Content-Encoding: gzip\\r\\n\"\n-> \"Server: APISIX\\r\\n\"\n-> \"X-B3-TraceId: c6bf7d6b1612a8e32c999d3d6ff379a1\\r\\n\"\n-> \"Via: 1.1 google\\r\\n\"\n-> \"Alt-Svc: h3=\\\":443\\\"; ma=2592000,h3-29=\\\":443\\\"; ma=2592000\\r\\n\"\n-> \"Connection: close\\r\\n\"\n-> \"Transfer-Encoding: chunked\\r\\n\"\n-> \"\\r\\n\"\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"\\x1F\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"\\x8B\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"\\b\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"\\x00\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"\\x00\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"\\x00\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"\\x00\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"\\x00\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"\\x00\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"\\x03\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"\\xAC\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"S\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"]\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"o\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"\\xDA\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"0\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"00000001\\r\\n\"\nreading 1 bytes...\n-> \"\\x14\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"000001\\r\\n\"\nreading 1 bytes...\n-> \"\\xFD\"\nread 1 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"264\\r\\n\"\nreading 612 bytes...\n-> \"+S^\\xD7jNpB@\\xDAC\\xD5\\xB2\\xAD\\xD2\\xA6U-L\\xDD^\\\"c\\e\\xE2\\x92\\xD8\\xA9?B\\xA1\\xE2\\xBF\\xF7\\xDE\\x04\\x18\\xD5\\xA6i\\x0F}rr?\\x8E\\xCF9\\xF7\\xFA9R\\\"\\x1AGJ\\xFB\\xA2\\\\\\x89Z\\xEBG:\\\\\\x0E\\xCB\\xF5CX\\xADW\\xC3\\xE8,\\xB2\\xF21H\\xE7\\x8B\\xAE,\\xCEh\\x16\\xC7$N\\x92<+\\x9A`y\\xC9\\x9C\\x84\\\"V\\x9B\\xA0}4\\x8E\\xCF\\\"\\x1E\\xAC\\x95\\x9Ao\\xA0\\xFAbv\\x05\\xB9Zb\\x19\\xE0\\e+\\xA4\\xEDqj%\\x8AS,(\\x13\\xD2q\\xAB\\x1Ao,\\xE4\\x85\\\\\\xB0Py\\b;\\xCF|p\\x10\\xBA\\x9B]^N&W\\x13\\x84\\xE4\\xAC\\xF1\\xC1JQ\\x9C\\xDC[1\\x8F4\\e\\xB6\\xA9%\\\\\\xC6\\xBC\\x97u\\x03\\xA9\\xE7^ \\xFCw\\x02\\xE7s\\xAA\\xAB^\\xE0\\xB6\\xCDMq\\xD4y\\x02u\\xC0\\xA8\\xA5/\\x8D8B\\xD4^\\xFC\\x01\\xF1Xm\\xA1\\xD7o\\x1A\\t\\x05\\x9CY\\xD1\\xB1\\xB3]\\x93|j\\x94\\xDD\\x14\\xB5\\xD1\\xBE\\x84,\\x19An\\x1F\\xDBH\\x862\\x13\\x92\\f \\xA8Y\\x8D\\xED_\\x8D^\\xCE\\xCD\\xFC\\x1D\\x9ENjH\\xCC\\x95\\x868%\\x84\\xC4$B\\x89\\xCESlK\\x12\\x8AY\\xCB4\\xF2j\\x95c\\xF0\\xAB\\x9C\\v\\xE0/G\\x19p\\x057\\x02Agw{F\\xC5\\x81$\\xF8\\xA6\\xD0\\xD9\\x85\\xD2Ki\\e\\xABPut}{\\xDF\\x92\\xF6CZ\\xFE\\xA8\\xB2\\xF5\\xE7\\xA7\\xFC\\xD3\\x83\\xBEw7\\xEB{\\xB5Y\\x7FD\\x84\\x96\\x17\\xBC\\x94|\\x05\\xA5\\rs\\xAE#WU\\x00\\x81J\\x17\\xCA\\x82\\xF5\\xAFe\\xEC\\xF9\\x9EFQ\\xD4nw2\\xD3\\xCB\\xDB\\xC9\\xC5\\xB4\\x9F\\xA8\\x950?\\x18\\xA8\\xEFmI\\xCE\\xC9\\xE0\\x9C\\xC4SB\\xC74\\x1FS\\xFA\\x1E<@\\vB#\\xFE\\xA3n\\xF7{\\x86\\xA0\\xAE;\\xFE\\xBD\\xE4GF\\x17\\xB3\\xE9\\x97\\xEF\\xB7\\xD7\\xBF:R\\x8D5\\xAD\\xC2\\x9D\\xF5\\xE0\\xB4c\\xDC+\\xA3{$\\x9A\\xA4\\xA34\\e&C\\n\\xEF \\xA5Y\\x16S\\x92\\xE6y\\x16\\x0FF\\xF9i\\xA3\\xB1j\\xA94\\xAB\\n+]c\\xB4\\x93\\x87\\xB1tbX\\x80\\xFD\\xB2j\\xCB:\\xE0}f0L\\xB3\\xC1\\xE8oKN\\xF01.\\x82\\x16\\xAFcod]z\\xA8s\\xD2\\xFBJ\\x16\\xADb\\xF8l\\x94]\\xB3\\xAA\\x92O{\\xBA\\xE0\\xA5\\xE2=_@c8|\\xE1\\x0E\\x9F`\\xFB\\xC2\\xB2 \\x8E\\xA9\\xDE2\\xB4\\x15\\xDE\\xEE\\xCD\\x14\\xC1\\xB9\\xB1\\xA82\\xC6\\x19\\xB1\\xD6\\xA11\\xF8\\xD0aQ\\xF7[v\\f\\xFC<\\xAC]\\xEF\\xCB\\xB7nu\\xDEV\\xEC\"\n-> \"\\xEE\\x05\\x00\\x00\\xFF\\xFF\\x03\\x00;d\\xC0x\\xFE\\x04\\x00\\x00\"\nread 612 bytes\nreading 2 bytes...\n-> \"\\r\\n\"\nread 2 bytes\n-> \"0\\r\\n\"\n-> \"\\r\\n\"\nConn close\n + TRANSCRIPT + end + + def successful_payment_intent_response + %( + {"id":"int_hkdmvldq5g8h5qequao","request_id":"164887285684","amount":1,"currency":"AUD","merchant_order_id":"mid_164887285684","status":"REQUIRES_PAYMENT_METHOD","captured_amount":0,"created_at":"2022-04-02T04:14:17+0000","updated_at":"2022-04-02T04:14:17+0000","available_payment_method_types":["wechatpay","card"],"client_secret":"eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2NDg4NzI4NTcsImV4cCI6MTY0ODg3NjQ1NywiYWNjb3VudF9pZCI6IjBhMWE4NzQ3LWM4M2YtNGUwNC05MGQyLTNjZmFjNDkzNTNkYSIsImRhdGFfY2VudGVyX3JlZ2lvbiI6IkhLIiwiaW50ZW50X2lkIjoiaW50X2hrZG12bGRxNWc4aDVxZXF1YW8iLCJwYWRjIjoiSEsiLCJidXNpbmVzc19uYW1lIjoiU3ByZWVkbHkgRGVtbyBBY2NvdW50In0.kcaBXnCAsIinOUw6iJ0tyTOa3Mv03JsuoyLZWWbmNnI"} + ) + end + + def successful_purchase_response + %( + {"id":"int_hkdmtp6bpg79nn35u43","request_id":"164546381445_purchase","amount":1,"currency":"AUD","merchant_order_id":"mid_164546381445","descriptor":"default","status":"SUCCEEDED","captured_amount":1,"latest_payment_attempt":{"id":"att_hkdmb6rw6g79nn3k7ld_n35u43","amount":1,"payment_method":{"id":"mtd_hkdmb6rw6g79nn3k7lc","type":"card","card":{"expiry_month":"09","expiry_year":"2023","name":"Longbob Longsen","bin":"400010","last4":"2224","brand":"visa","issuer_country_code":"US","card_type":"credit","fingerprint":"IRXv0v/5hVl6wGx8FjnXsPwXiyw=","cvc_check":"pass","billing":{"first_name":"Longbob","last_name":"Longsen"}},"status":"CREATED","created_at":"2022-02-21T17:16:56+0000","updated_at":"2022-02-21T17:16:56+0000"},"payment_intent_id":"int_hkdmtp6bpg79nn35u43","status":"AUTHORIZED","provider_transaction_id":"184716548151_265868712794696","provider_original_response_code":"00","authorization_code":"803478","captured_amount":0,"refunded_amount":0,"created_at":"2022-02-21T17:16:56+0000","updated_at":"2022-02-21T17:16:57+0000","settle_via":"airwallex","authentication_data":{"ds_data":{},"fraud_data":{"action":"ACCEPT","score":"1"},"avs_result":"U","cvc_result":"Y","cvc_code":"M"}},"created_at":"2022-02-21T17:16:55+0000","updated_at":"2022-02-21T17:16:57+0000"} + ) + end + + def failed_purchase_response + %({"code":"issuer_declined","message":"The card issuer declined this transaction. Please refer to the original response code.","provider_original_response_code":"14"}) + end + + def successful_authorize_response + %({"id":"int_hkdmtp6bpg79nqimh2z","request_id":"164546402207_purchase","amount":1,"currency":"AUD","merchant_order_id":"mid_164546402207","descriptor":"default","status":"REQUIRES_CAPTURE","captured_amount":0,"latest_payment_attempt":{"id":"att_hkdmtp6bpg79nqj30rk_qimh2z","amount":1,"payment_method":{"id":"mtd_hkdmtp6bpg79nqj30rj","type":"card","card":{"expiry_month":"09","expiry_year":"2023","name":"Longbob Longsen","bin":"400010","last4":"2224","brand":"visa","issuer_country_code":"US","card_type":"credit","fingerprint":"IRXv0v/5hVl6wGx8FjnXsPwXiyw=","cvc_check":"pass","billing":{"first_name":"Longbob","last_name":"Longsen"}},"status":"CREATED","created_at":"2022-02-21T17:20:23+0000","updated_at":"2022-02-21T17:20:23+0000"},"payment_intent_id":"int_hkdmtp6bpg79nqimh2z","status":"AUTHORIZED","provider_transaction_id":"648365447295_129943849335300","provider_original_response_code":"00","authorization_code":"676405","captured_amount":0,"refunded_amount":0,"created_at":"2022-02-21T17:20:23+0000","updated_at":"2022-02-21T17:20:24+0000","settle_via":"airwallex","authentication_data":{"ds_data":{},"fraud_data":{"action":"ACCEPT","score":"1"},"avs_result":"U","cvc_result":"Y","cvc_code":"M"}},"created_at":"2022-02-21T17:20:22+0000","updated_at":"2022-02-21T17:20:24+0000"}) + end + + def failed_authorize_response + %({"code":"issuer_declined","message":"The card issuer declined this transaction. Please refer to the original response code.","provider_original_response_code":"14"}) + end + + def successful_capture_response + %({"id":"int_hkdmtp6bpg79nqimh2z","request_id":"164546402207_purchase_capture","amount":1,"currency":"AUD","merchant_order_id":"mid_164546402207","descriptor":"default","status":"SUCCEEDED","captured_amount":1,"latest_payment_attempt":{"id":"att_hkdmtp6bpg79nqj30rk_qimh2z","amount":1,"payment_method":{"id":"mtd_hkdmtp6bpg79nqj30rj","type":"card","card":{"expiry_month":"09","expiry_year":"2023","name":"Longbob Longsen","bin":"400010","last4":"2224","brand":"visa","issuer_country_code":"US","card_type":"credit","fingerprint":"IRXv0v/5hVl6wGx8FjnXsPwXiyw=","cvc_check":"pass","billing":{"first_name":"Longbob","last_name":"Longsen"}},"status":"CREATED","created_at":"2022-02-21T17:20:23+0000","updated_at":"2022-02-21T17:20:23+0000"},"payment_intent_id":"int_hkdmtp6bpg79nqimh2z","status":"CAPTURE_REQUESTED","provider_transaction_id":"648365447295_129943849335300","provider_original_response_code":"00","authorization_code":"676405","captured_amount":1,"refunded_amount":0,"created_at":"2022-02-21T17:20:23+0000","updated_at":"2022-02-21T17:20:25+0000","settle_via":"airwallex","authentication_data":{"ds_data":{},"fraud_data":{"action":"ACCEPT","score":"1"},"avs_result":"U","cvc_result":"Y","cvc_code":"M"}},"created_at":"2022-02-21T17:20:22+0000","updated_at":"2022-02-21T17:20:25+0000"}) + end + + def failed_capture_response + %({"code":"not_found","message":"The requested endpoint does not exist [/api/v1/pa/payment_intents/12345/capture]"}) + end + + def successful_refund_response + %({"id":"rfd_hkdmb6rw6g79o84j2nr_82v60s","request_id":"164546508364_purchase_refund","payment_intent_id":"int_hkdmb6rw6g79o82v60s","payment_attempt_id":"att_hkdmtp6bpg79o839j89_82v60s","amount":1,"currency":"AUD","status":"RECEIVED","created_at":"2022-02-21T17:38:07+0000","updated_at":"2022-02-21T17:38:07+0000"}) + end + + def failed_refund_response + %({"code":"resource_not_found","message":"The PaymentIntent with ID 12345 cannot be found."}) + end + + def successful_void_response + %({"id":"int_hkdm49cp4g7d5njedty","request_id":"164573811628_purchase_void","amount":1,"currency":"AUD","merchant_order_id":"mid_164573811628","descriptor":"default","status":"CANCELLED","captured_amount":0,"latest_payment_attempt":{"id":"att_hkdm8kd2fg7d5njty2v_njedty","amount":1,"payment_method":{"id":"mtd_hkdm8kd2fg7d5njtxb2","type":"card","card":{"expiry_month":"09","expiry_year":"2023","name":"Longbob Longsen","bin":"400010","last4":"2224","brand":"visa","issuer_country_code":"US","card_type":"credit","fingerprint":"IRXv0v/5hVl6wGx8FjnXsPwXiyw=","cvc_check":"pass","billing":{"first_name":"Longbob","last_name":"Longsen"}},"status":"CREATED","created_at":"2022-02-24T21:28:38+0000","updated_at":"2022-02-24T21:28:38+0000"},"payment_intent_id":"int_hkdm49cp4g7d5njedty","status":"CANCELLED","provider_transaction_id":"157857893548_031662842463902","provider_original_response_code":"00","authorization_code":"775572","captured_amount":0,"refunded_amount":0,"created_at":"2022-02-24T21:28:38+0000","updated_at":"2022-02-24T21:28:40+0000","settle_via":"airwallex","authentication_data":{"ds_data":{},"fraud_data":{"action":"ACCEPT","score":"1"},"avs_result":"U","cvc_result":"Y","cvc_code":"M"}},"created_at":"2022-02-24T21:28:37+0000","updated_at":"2022-02-24T21:28:40+0000","cancelled_at":"2022-02-24T21:28:40+0000"}) + end + + def failed_void_response + %({"code":"not_found","message":"The requested endpoint does not exist [/api/v1/pa/payment_intents/12345/cancel]"}) + end + + def failed_ntid_response + %({"code":"validation_error","source":"external_recurring_data.original_transaction_id","message":"external_recurring_data.original_transaction_id should be 13-15 characters long"}) + end + + def add_cit_network_transaction_id_to_stored_credential(auth) + @stored_credential_mit_options[:network_transaction_id] = auth.params['latest_payment_attempt']['provider_transaction_id'] + end + + def setup_endpoint + 'https://api-demo.airwallex.com/api/v1/pa/payment_intents/create' + end +end diff --git a/test/unit/gateways/alelo_test.rb b/test/unit/gateways/alelo_test.rb new file mode 100644 index 00000000000..5e8353606cd --- /dev/null +++ b/test/unit/gateways/alelo_test.rb @@ -0,0 +1,380 @@ +require 'test_helper' + +class AleloTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = AleloGateway.new(fixtures(:alelo)) + @credit_card = credit_card + @amount = 100 + + @options = { + order_id: 'f63b625e-331e-490a-b15c-50b4087ca64f', + establishment_code: '000002007690360', + sub_merchant_mcc: '5499', + player_identification: '1', + description: 'Store Purchase', + external_trace_number: '123456', + uuid: '49bc3a5c-2e0f-11ed-a261-0242ac120002' + } + end + + def test_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 + end + + assert_equal 'Missing required parameter: client_id', error.message + end + + def test_supported_card_types + assert_equal AleloGateway.supported_cardtypes, %i[visa master american_express discover] + end + + def test_supported_countries + assert_equal AleloGateway.supported_countries, ['BR'] + end + + def test_support_scrubbing_flag_enabled + assert @gateway.supports_scrubbing? + end + + def test_sucessful_fetch_access_token_with_proper_client_id_client_secret + @gateway = AleloGateway.new(client_id: 'abc123', client_secret: 'def456') + access_token_expectation! @gateway + + resp = @gateway.send(:fetch_access_token) + assert_kind_of Response, resp + assert_equal 'abc123', resp.message + end + + def test_successful_remote_encryption_key + @gateway = AleloGateway.new(client_id: 'abc123', client_secret: 'def456') + encryption_key_expectation! @gateway + + resp = @gateway.send(:remote_encryption_key, 'abc123') + + assert_kind_of Response, resp + assert_equal 'def456', resp.message + assert_equal 'some-uuid', resp.params['uuid'] + end + + def test_successful_purchase_with_provided_credentials + key, secret_key = test_key true + + @gateway.options[:encryption_key] = key + @gateway.options[:access_token] = 'abc123' + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + decrypted = JOSE::JWE.block_decrypt(secret_key, JSON.parse(data)['token']).first + request = JSON.parse(decrypted, symbolize_names: true) + + assert_equal @options[:order_id], request[:requestId] + assert_equal 1.0, request[:amount] + assert_equal @credit_card.number, request[:cardNumber] + assert_equal @credit_card.name, request[:cardholderName] + assert_equal @credit_card.month, request[:expirationMonth] + assert_equal @credit_card.year - 2000, request[:expirationYear] + assert_equal '3', request[:captureType] + assert_equal @credit_card.verification_value, request[:securityCode] + assert_equal @options[:establishment_code], request[:establishmentCode] + assert_equal @options[:player_identification], request[:playerIdentification] + assert_equal @options[:sub_merchant_mcc], request[:subMerchantCode] + assert_equal @options[:external_trace_number], request[:externalTraceNumber] + end.respond_with(successful_capture_response) + + assert_success response + assert_equal 'f63b625e-331e-490a-b15c-50b4087ca64f', response.authorization + + # Check new values for credentials + assert_nil response.params['access_token'] + assert_nil response.params['encryption_key'] + assert_nil response.params['encryption_uuid'] + end + + def test_successful_purchase_with_no_provided_credentials + key = test_key + @gateway.expects(:ssl_post).times(2).returns({ access_token: 'abc123' }.to_json, successful_capture_response) + @gateway.expects(:ssl_get).returns({ publicKey: key, uuid: 'some-uuid' }.to_json) + + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_kind_of MultiResponse, response + assert_equal 3, response.responses.size + assert_equal 'abc123', response.responses.first.message + assert_equal key, response.responses[1].message + + # Check new values for credentials + assert_equal 'abc123', response.params['access_token'] + assert_equal key, response.params['encryption_key'] + assert_equal 'some-uuid', response.params['encryption_uuid'] + end + + def test_sucessful_retry_with_expired_encryption_key + key = test_key + @gateway.options[:encryption_key] = key + @gateway.options[:access_token] = 'abc123' + + # Expectations + # ssl_post purchace => raises a 401 + # ssl_get => key + # ssl_post => Final purchase success + @gateway.expects(:ssl_post). + times(2). + raises(ActiveMerchant::ResponseError.new(stub('401 Response', code: '401'))). + then.returns(successful_capture_response) + + @gateway.expects(:ssl_get).returns({ publicKey: key, uuid: 'some-uuid' }.to_json) + + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_kind_of MultiResponse, response + assert_equal 2, response.responses.size + assert_equal key, response.responses.first.message + end + + def test_sucessful_retry_with_expired_access_token_and_encryption_key + key = test_key + @gateway.options[:encryption_key] = key + @gateway.options[:access_token] = 'abc123' + + # Expectations + # ssl_post purchace => raises a 401 + # ssl_get get key => raise a 401 + # ssl_post => access_token + # ssl_get => key + # ssl_post => Final purchase success + @gateway.expects(:ssl_post). + times(3). + raises(ActiveMerchant::ResponseError.new(stub('401 Response', code: '401'))). + then.returns({ access_token: 'abc123' }.to_json, successful_capture_response) + + @gateway.expects(:ssl_get). + times(2). + raises(ActiveMerchant::ResponseError.new(stub('401 Response', code: '401'))). + then.returns({ publicKey: key, uuid: 'some-uuid' }.to_json) + + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_kind_of MultiResponse, response + assert_equal 3, response.responses.size + assert_equal 'abc123', response.responses.first.message + assert_equal key, response.responses[1].message + end + + def test_sucessful_retry_with_missing_uuid_404 + key = test_key + @gateway.options[:encryption_key] = key + @gateway.options[:access_token] = 'abc123' + + # Expectations + # ssl_post purchace => raises a 401 + # ssl_get get key => raise a 401 + # ssl_post => access_token + # ssl_get => key + # ssl_post => Final purchase success + @gateway.expects(:ssl_post). + times(3). + raises(ActiveMerchant::ResponseError.new(stub('404 Response', code: '404'))). + then.returns({ access_token: 'abc123' }.to_json, successful_capture_response) + + @gateway.expects(:ssl_get). + times(2). + raises(ActiveMerchant::ResponseError.new(stub('404 Response', code: '404'))). + then.returns({ publicKey: key, uuid: 'some-uuid' }.to_json) + + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_kind_of MultiResponse, response + assert_equal 3, response.responses.size + assert_equal 'abc123', response.responses.first.message + assert_equal key, response.responses[1].message + end + + def test_detecting_successfull_response_from_capture + assert @gateway.send :success_from, 'capture/transaction', { status: 'CONFIRMADA' } + end + + def test_detecting_successfull_response_from_refund + assert @gateway.send :success_from, 'capture/transaction/refund', { status: 'ESTORNADA' } + end + + def test_get_response_message_from_messages_key + message = @gateway.send :message_from, { messages: 'hello', messageUser: 'world' } + assert_equal 'hello', message + end + + def test_get_response_message_from_message_user + message = @gateway.send :message_from, { messages: nil, messageUser: 'world' } + assert_equal 'world', message + end + + def test_url_generation_from_action + action = 'test' + assert_equal @gateway.test_url + action, @gateway.send(:url, action) + end + + def test_request_headers_building + gateway = AleloGateway.new(client_id: 'abc123', client_secret: 'def456') + headers = gateway.send :request_headers, 'access_123' + + assert_equal 'application/json', headers['Accept'] + assert_equal 'abc123', headers['X-IBM-Client-Id'] + assert_equal 'def456', headers['X-IBM-Client-Secret'] + assert_equal 'Bearer access_123', headers['Authorization'] + end + + def test_scrub + assert @gateway.supports_scrubbing? + + pre_scrubbed = File.read('test/unit/transcripts/alelo_purchase') + post_scrubbed = File.read('test/unit/transcripts/alelo_purchase_scrubbed') + + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + def test_success_payload_encryption + @gateway.options[:access_token] = 'abc123' + @gateway.options[:encryption_key] = test_key + @gateway.options[:encryption_uuid] = SecureRandom.uuid + + credentials = @gateway.send(:ensure_credentials) + jwe, = @gateway.send(:encrypt_payload, { hello: 'world' }, credentials, {}) + + refute_nil JSON.parse(jwe)['token'] + refute_nil JSON.parse(jwe)['uuid'] + end + + def test_ensure_encryption_format + key, secret_key = test_key true + body = { hello: 'world' } + @gateway.options[:access_token] = 'abc123' + @gateway.options[:encryption_key] = key + credentials = @gateway.send(:ensure_credentials) + + jwe, _cred = @gateway.send(:encrypt_payload, body, credentials, {}) + parsed_jwe = JSON.parse(jwe, symbolize_names: true) + refute_nil parsed_jwe[:token] + + decrypted = JOSE::JWE.block_decrypt(secret_key, parsed_jwe[:token]).first + assert_equal body.to_json, decrypted + end + + def test_ensure_credentials_with_provided_access_token_and_key + @gateway.options[:access_token] = 'abc123' + @gateway.options[:encryption_key] = 'def456' + + credentials = @gateway.send :ensure_credentials + + assert_equal @gateway.options[:access_token], credentials[:access_token] + assert_equal @gateway.options[:encryption_key], credentials[:key] + assert_nil credentials[:multiresp] + end + + def test_ensure_credentials_with_access_token_and_not_key + encryption_key_expectation! @gateway + + @gateway.options[:access_token] = 'abc123' + credentials = @gateway.send :ensure_credentials + + assert_equal @gateway.options[:access_token], credentials[:access_token] + assert_equal 'def456', credentials[:key] + refute_nil credentials[:multiresp] + assert_equal 1, credentials[:multiresp].responses.size + end + + def test_ensure_credentials_with_key_but_not_access_token + @gateway = AleloGateway.new(client_id: 'abc123', client_secret: 'def456') + @gateway.options[:encryption_key] = 'xx_no_key_xx' + + access_token_expectation! @gateway + encryption_key_expectation! @gateway + + credentials = @gateway.send :ensure_credentials + + assert_equal 'abc123', credentials[:access_token] + assert_equal 'def456', credentials[:key] + refute_nil credentials[:multiresp] + assert_equal 2, credentials[:multiresp].responses.size + end + + def test_credit_card_year_should_be_an_integer + post = {} + @gateway.send :add_payment, post, @credit_card + + assert_kind_of Integer, post[:expirationYear] + assert_equal 2, post[:expirationYear].digits.size + end + + private + + def test_key(with_sk = false) + jwk_rsa_sk = JOSE::JWK.generate_key([:rsa, 4096]) + jwk_rsa_pk = JOSE::JWK.to_public(jwk_rsa_sk) + + pem = jwk_rsa_pk.to_pem.split("\n") + pem.pop + pem.shift + + return pem.join unless with_sk + + return pem.join, jwk_rsa_sk + end + + def access_token_expectation!(gateway, access_token = 'abc123') + url = "#{@gateway.class.test_url}captura-oauth-provider/oauth/token" + params = [ + 'grant_type=client_credentials', + 'client_id=abc123', + 'client_secret=def456', + 'scope=%2Fcapture' + ].join('&') + + headers = { + 'Accept' => 'application/json', + 'Content-Type' => 'application/x-www-form-urlencoded' + } + + gateway.expects(:ssl_post).with(url, params, headers).returns({ access_token: }.to_json) + end + + def encryption_key_expectation!(gateway, public_key = 'def456') + url = "#{@gateway.class.test_url}capture/key" + headers = { + 'Accept' => 'application/json', + 'X-IBM-Client-Id' => gateway.options[:client_id], + 'X-IBM-Client-Secret' => gateway.options[:client_secret], + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer abc123' + } + + @gateway.expects(:ssl_get).with(url, headers).returns({ publicKey: public_key, uuid: 'some-uuid' }.to_json) + end + + def successful_capture_response + { + requestId: 'f63b625e-331e-490a-b15c-50b4087ca64f', + dateTime: '211105181958', + returnCode: '00', + nsu: '00123', + amount: '0.10', + maskedCard: '506758******7013', + authorizationCode: '735977', + messages: 'Transação Confirmada com sucesso.', + status: 'CONFIRMADA', + playerIdentification: '4', + captureType: '3' + }.to_json + end +end diff --git a/test/unit/gateways/allied_wallet_test.rb b/test/unit/gateways/allied_wallet_test.rb index c3d1d390fd9..9da275b48bd 100644 --- a/test/unit/gateways/allied_wallet_test.rb +++ b/test/unit/gateways/allied_wallet_test.rb @@ -78,7 +78,7 @@ def test_successful_void void = stub_comms do @gateway.void(response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/123456/, data) end.respond_with(successful_void_response) @@ -88,7 +88,7 @@ def test_successful_void def test_failed_void response = stub_comms do @gateway.void('5d53a33d960c46d00f5dc061947d998c') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/5d53a33d960c46d00f5dc061947d998c/, data) end.respond_with(failed_void_response) @@ -105,7 +105,7 @@ def test_successful_refund refund = stub_comms do @gateway.refund(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/123456/, data) end.respond_with(successful_refund_response) @@ -278,7 +278,6 @@ def failed_refund_response ) end - def empty_purchase_response %( { @@ -297,7 +296,6 @@ def invalid_json_response ) end - def transcript %( <- "POST /merchants/10090/SALEtransactions HTTP/1.1\r\nContent-Type: application/json\r\nAuthorization: Bearer AAEAAHwXaLTYs2APKJW_4TpTDwCw_h9oDx9rA58FR78AFuYCX82Izes1nz9qGBXELGUN_EukKcP5T78Th5guDz4Rw5dQ4Gf0suKw7pz9vWrqa1NpZhrD9Lj9T-SFtOJgfodiwVaBBeSgbJLKA7MOzC9q2dv91HBNP69DygL1oX2L2mtt8fWlKSWhQmtG040E1I43jTueX3L3L9YA7iO6pIwO7CGybE5LnjkQ65KB2K4oYKfXRZosF77hgMJIh-KprFy9cYY3EjfupHeLon9im1BGafrda2N5wj_A_LvdMzfLAD1l1dgj82KlvM_gAzNJ4S19gAicRo9zIbsq36Apt-8jFjS0AQAAAAEAAA9Zr_lVLKMmmtKSo6T_9ulzMCRbYs798EpFD2wMlkb1NCQtA65VrNcM20Ka2FjNQfwOcSMWqDl9zFQhPyFl-npsG1Ww2oyyavA6HSe1HLRLtE_1hNBAlTBPQnLJ6hBf8eR_NTiVa-aQdV2l92-eSwCS59CzrOYGGCY1pLdNMDr_r66kg9l-l94154kRoMBRQSCqZV9iM9M-f3adLJqG6Q79zz1oJpGrH-Zv1kuv8eLaJJNOEFYARb0JbnAC5G1l9-aqxGvBrNkd4sAJIe23XrRx2XJCBIABxuGSQ1xJBTINVlXBXq1mvvd8B1uiYiDNia3c_vIGuSGIjZE0VbUN3oJppfCt1joGdePeUaC2Pyb2vuUN00EBEOaD9RF8IBWMLVJaF9cW2OewDOfBQg94MuOKLdXB_IisRx1ed25VQDVyv0f0CxmkAidvoDN0vvRIJZJr-bgBuL5FZM7gETAeYeiGlh7-Mf2Hzgy7236YNxcC9OnWFEcKEU50nlqog1bJnk8wJgoJWNqG0NUEK4DUzYqknmZ98qQv6rYrg5V-Hey-jAQp_KNf3h-vFHVZdP26Yg\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: api.alliedwallet.com\r\nContent-Length: 464\r\n\r\n" diff --git a/test/unit/gateways/authorize_net_arb_test.rb b/test/unit/gateways/authorize_net_arb_test.rb index 1757c7c74b6..dfcc2d4c9ae 100644 --- a/test/unit/gateways/authorize_net_arb_test.rb +++ b/test/unit/gateways/authorize_net_arb_test.rb @@ -6,8 +6,8 @@ class AuthorizeNetArbTest < Test::Unit::TestCase def setup ActiveMerchant.expects(:deprecated).with('ARB functionality in ActiveMerchant is deprecated and will be removed in a future version. Please contact the ActiveMerchant maintainers if you have an interest in taking ownership of a separate gem that continues support for it.') @gateway = AuthorizeNetArbGateway.new( - :login => 'X', - :password => 'Y' + login: 'X', + password: 'Y' ) @amount = 100 @credit_card = credit_card @@ -18,17 +18,19 @@ def setup def test_successful_recurring @gateway.expects(:ssl_post).returns(successful_recurring_response) - response = @gateway.recurring(@amount, @credit_card, - :billing_address => address.merge(:first_name => 'Jim', :last_name => 'Smith'), - :interval => { - :length => 10, - :unit => :days + response = @gateway.recurring( + @amount, + @credit_card, + billing_address: address.merge(first_name: 'Jim', last_name: 'Smith'), + interval: { + length: 10, + unit: :days }, - :duration => { - :start_date => Time.now.strftime('%Y-%m-%d'), - :occurrences => 30 + duration: { + start_date: Time.now.strftime('%Y-%m-%d'), + occurrences: 30 } - ) + ) assert_instance_of Response, response assert response.success? @@ -39,7 +41,7 @@ def test_successful_recurring def test_successful_update_recurring @gateway.expects(:ssl_post).returns(successful_update_recurring_response) - response = @gateway.update_recurring(:subscription_id => @subscription_id, :amount => @amount * 2) + response = @gateway.update_recurring(subscription_id: @subscription_id, amount: @amount * 2) assert_instance_of Response, response assert response.success? @@ -69,73 +71,73 @@ def test_successful_status_recurring end def test_expdate_formatting - assert_equal '2009-09', @gateway.send(:expdate, credit_card('4111111111111111', :month => '9', :year => '2009')) - assert_equal '2013-11', @gateway.send(:expdate, credit_card('4111111111111111', :month => '11', :year => '2013')) + assert_equal '2009-09', @gateway.send(:expdate, credit_card('4111111111111111', month: '9', year: '2009')) + assert_equal '2013-11', @gateway.send(:expdate, credit_card('4111111111111111', month: '11', year: '2013')) end private def successful_recurring_response - <<-XML - - Sample - - Ok - - I00001 - Successful. - - - #{@subscription_id} - + <<~XML + + Sample + + Ok + + I00001 + Successful. + + + #{@subscription_id} + XML end def successful_update_recurring_response - <<-XML - - Sample - - Ok - - I00001 - Successful. - - - #{@subscription_id} - + <<~XML + + Sample + + Ok + + I00001 + Successful. + + + #{@subscription_id} + XML end def successful_cancel_recurring_response - <<-XML - - Sample - - Ok - - I00001 - Successful. - - - #{@subscription_id} - + <<~XML + + Sample + + Ok + + I00001 + Successful. + + + #{@subscription_id} + XML end def successful_status_recurring_response - <<-XML - - Sample - - Ok - - I00001 - Successful. - - - #{@subscription_status} - + <<~XML + + Sample + + Ok + + I00001 + Successful. + + + #{@subscription_status} + XML end end diff --git a/test/unit/gateways/authorize_net_cim_test.rb b/test/unit/gateways/authorize_net_cim_test.rb index 64f09efd42f..5e4f5182baf 100644 --- a/test/unit/gateways/authorize_net_cim_test.rb +++ b/test/unit/gateways/authorize_net_cim_test.rb @@ -5,8 +5,8 @@ class AuthorizeNetCimTest < Test::Unit::TestCase def setup @gateway = AuthorizeNetCimGateway.new( - :login => 'X', - :password => 'Y' + login: 'X', + password: 'Y' ) @amount = 100 @credit_card = credit_card @@ -15,40 +15,40 @@ def setup @customer_payment_profile_id = '7813' @customer_address_id = '4321' @payment = { - :credit_card => @credit_card + credit_card: @credit_card } @profile = { - :merchant_customer_id => 'Up to 20 chars', # Optional - :description => 'Up to 255 Characters', # Optional - :email => 'Up to 255 Characters', # Optional - :payment_profiles => { # Optional - :customer_type => 'individual or business', # Optional - :bill_to => @address, - :payment => @payment + merchant_customer_id: 'Up to 20 chars', # Optional + description: 'Up to 255 Characters', # Optional + email: 'Up to 255 Characters', # Optional + payment_profiles: { # Optional + customer_type: 'individual or business', # Optional + bill_to: @address, + payment: @payment }, - :ship_to_list => { - :first_name => 'John', - :last_name => 'Doe', - :company => 'Widgets, Inc', - :address1 => '1234 Fake Street', - :city => 'Anytown', - :state => 'MD', - :zip => '12345', - :country => 'USA', - :phone_number => '(123)123-1234', # Optional - Up to 25 digits (no letters) - :fax_number => '(123)123-1234' # Optional - Up to 25 digits (no letters) + ship_to_list: { + first_name: 'John', + last_name: 'Doe', + company: 'Widgets, Inc', + address1: '1234 Fake Street', + city: 'Anytown', + state: 'MD', + zip: '12345', + country: 'USA', + phone_number: '(123)123-1234', # Optional - Up to 25 digits (no letters) + fax_number: '(123)123-1234' # Optional - Up to 25 digits (no letters) } } @options = { - :ref_id => '1234', # Optional - :profile => @profile + ref_id: '1234', # Optional + profile: @profile } end def test_expdate_formatting - assert_equal '2009-09', @gateway.send(:expdate, credit_card('4111111111111111', :month => '9', :year => '2009')) - assert_equal '2013-11', @gateway.send(:expdate, credit_card('4111111111111111', :month => '11', :year => '2013')) - assert_equal 'XXXX', @gateway.send(:expdate, credit_card('XXXX1234', :month => nil, :year => nil)) + assert_equal '2009-09', @gateway.send(:expdate, credit_card('4111111111111111', month: '9', year: '2009')) + assert_equal '2013-11', @gateway.send(:expdate, credit_card('4111111111111111', month: '11', year: '2013')) + assert_equal 'XXXX', @gateway.send(:expdate, credit_card('XXXX1234', month: nil, year: nil)) end def test_should_create_customer_profile_request @@ -65,13 +65,13 @@ def test_should_create_customer_payment_profile_request @gateway.expects(:ssl_post).returns(successful_create_customer_payment_profile_response) assert response = @gateway.create_customer_payment_profile( - :customer_profile_id => @customer_profile_id, - :payment_profile => { - :customer_type => 'individual', - :bill_to => @address, - :payment => @payment + customer_profile_id: @customer_profile_id, + payment_profile: { + customer_type: 'individual', + bill_to: @address, + payment: @payment }, - :validation_mode => :test + validation_mode: :test ) assert_instance_of Response, response assert_success response @@ -83,17 +83,17 @@ def test_should_create_customer_shipping_address_request @gateway.expects(:ssl_post).returns(successful_create_customer_shipping_address_response) assert response = @gateway.create_customer_shipping_address( - :customer_profile_id => @customer_profile_id, - :address => { - :first_name => 'John', - :last_name => 'Doe', - :company => 'Widgets, Inc', - :address1 => '1234 Fake Street', - :city => 'Anytown', - :state => 'MD', - :country => 'USA', - :phone_number => '(123)123-1234', - :fax_number => '(123)123-1234' + customer_profile_id: @customer_profile_id, + address: { + first_name: 'John', + last_name: 'Doe', + company: 'Widgets, Inc', + address1: '1234 Fake Street', + city: 'Anytown', + state: 'MD', + country: 'USA', + phone_number: '(123)123-1234', + fax_number: '(123)123-1234' } ) assert_instance_of Response, response @@ -106,11 +106,11 @@ def test_should_create_customer_profile_transaction_auth_only_and_then_prior_aut @gateway.expects(:ssl_post).returns(successful_create_customer_profile_transaction_response(:auth_only)) assert response = @gateway.create_customer_profile_transaction( - :transaction => { - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => @customer_payment_profile_id, - :type => :auth_only, - :amount => @amount + transaction: { + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: @customer_payment_profile_id, + type: :auth_only, + amount: @amount } ) assert_instance_of Response, response @@ -163,12 +163,12 @@ def test_should_create_customer_profile_transaction_auth_only_and_then_prior_aut @gateway.expects(:ssl_post).returns(successful_create_customer_profile_transaction_response(:prior_auth_capture)) assert response = @gateway.create_customer_profile_transaction( - :transaction => { - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => @customer_payment_profile_id, - :type => :prior_auth_capture, - :amount => @amount, - :trans_id => trans_id + transaction: { + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: @customer_payment_profile_id, + type: :prior_auth_capture, + amount: @amount, + trans_id: } ) assert_instance_of Response, response @@ -187,11 +187,11 @@ def test_should_create_customer_profile_transaction_auth_only_and_then_capture_o @gateway.expects(:ssl_post).returns(successful_create_customer_profile_transaction_response(:auth_only)) assert response = @gateway.create_customer_profile_transaction( - :transaction => { - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => @customer_payment_profile_id, - :type => :auth_only, - :amount => @amount + transaction: { + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: @customer_payment_profile_id, + type: :auth_only, + amount: @amount } ) assert_instance_of Response, response @@ -204,12 +204,12 @@ def test_should_create_customer_profile_transaction_auth_only_and_then_capture_o @gateway.expects(:ssl_post).returns(successful_create_customer_profile_transaction_response(:capture_only)) assert response = @gateway.create_customer_profile_transaction( - :transaction => { - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => @customer_payment_profile_id, - :type => :capture_only, - :amount => @amount, - :approval_code => approval_code + transaction: { + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: @customer_payment_profile_id, + type: :capture_only, + amount: @amount, + approval_code: } ) assert_instance_of Response, response @@ -221,17 +221,17 @@ def test_should_create_customer_profile_transaction_auth_capture_request @gateway.expects(:ssl_post).returns(successful_create_customer_profile_transaction_response(:auth_capture)) assert response = @gateway.create_customer_profile_transaction( - :transaction => { - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => @customer_payment_profile_id, - :type => :auth_capture, - :order => { - :invoice_number => '1234', - :description => 'Test Order Description', - :purchase_order_number => '4321' + transaction: { + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: @customer_payment_profile_id, + type: :auth_capture, + order: { + invoice_number: '1234', + description: 'Test Order Description', + purchase_order_number: '4321' }, - :amount => @amount, - :card_code => '123' + amount: @amount, + card_code: '123' } ) assert_instance_of Response, response @@ -245,16 +245,16 @@ def test_should_create_customer_profile_transaction_auth_capture_request_for_ver @gateway.expects(:ssl_post).returns(successful_create_customer_profile_transaction_response(:auth_capture_version_3_1)) assert response = @gateway.create_customer_profile_transaction( - :transaction => { - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => @customer_payment_profile_id, - :type => :auth_capture, - :order => { - :invoice_number => '1234', - :description => 'Test Order Description', - :purchase_order_number => '4321' + transaction: { + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: @customer_payment_profile_id, + type: :auth_capture, + order: { + invoice_number: '1234', + description: 'Test Order Description', + purchase_order_number: '4321' }, - :amount => @amount + amount: @amount } ) assert_instance_of Response, response @@ -311,7 +311,7 @@ def test_should_create_customer_profile_transaction_auth_capture_request_for_ver def test_should_delete_customer_profile_request @gateway.expects(:ssl_post).returns(successful_delete_customer_profile_response) - assert response = @gateway.delete_customer_profile(:customer_profile_id => @customer_profile_id) + assert response = @gateway.delete_customer_profile(customer_profile_id: @customer_profile_id) assert_instance_of Response, response assert_success response assert_equal @customer_profile_id, response.authorization @@ -320,7 +320,7 @@ def test_should_delete_customer_profile_request def test_should_delete_customer_payment_profile_request @gateway.expects(:ssl_post).returns(successful_delete_customer_payment_profile_response) - assert response = @gateway.delete_customer_payment_profile(:customer_profile_id => @customer_profile_id, :customer_payment_profile_id => @customer_payment_profile_id) + assert response = @gateway.delete_customer_payment_profile(customer_profile_id: @customer_profile_id, customer_payment_profile_id: @customer_payment_profile_id) assert_instance_of Response, response assert_success response assert_nil response.authorization @@ -329,7 +329,7 @@ def test_should_delete_customer_payment_profile_request def test_should_delete_customer_shipping_address_request @gateway.expects(:ssl_post).returns(successful_delete_customer_shipping_address_response) - assert response = @gateway.delete_customer_shipping_address(:customer_profile_id => @customer_profile_id, :customer_address_id => @customer_address_id) + assert response = @gateway.delete_customer_shipping_address(customer_profile_id: @customer_profile_id, customer_address_id: @customer_address_id) assert_instance_of Response, response assert_success response assert_nil response.authorization @@ -338,7 +338,7 @@ def test_should_delete_customer_shipping_address_request def test_should_get_customer_profile_request @gateway.expects(:ssl_post).returns(successful_get_customer_profile_response) - assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert response = @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) assert_instance_of Response, response assert_success response assert_equal @customer_profile_id, response.authorization @@ -355,7 +355,7 @@ def test_should_get_customer_profile_ids_request def test_should_get_customer_profile_request_with_multiple_payment_profiles @gateway.expects(:ssl_post).returns(successful_get_customer_profile_response_with_multiple_payment_profiles) - assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert response = @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) assert_instance_of Response, response assert_success response @@ -367,23 +367,25 @@ def test_should_get_customer_payment_profile_request @gateway.expects(:ssl_post).returns(successful_get_customer_payment_profile_response) assert response = @gateway.get_customer_payment_profile( - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => @customer_payment_profile_id, - :unmask_expiration_date => true + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: @customer_payment_profile_id, + unmask_expiration_date: true, + include_issuer_info: true ) assert_instance_of Response, response assert_success response assert_nil response.authorization assert_equal @customer_payment_profile_id, response.params['profile']['payment_profiles']['customer_payment_profile_id'] assert_equal formatted_expiration_date(@credit_card), response.params['profile']['payment_profiles']['payment']['credit_card']['expiration_date'] + assert_equal @credit_card.first_digits, response.params['profile']['payment_profiles']['payment']['credit_card']['issuer_number'] end def test_should_get_customer_shipping_address_request @gateway.expects(:ssl_post).returns(successful_get_customer_shipping_address_response) assert response = @gateway.get_customer_shipping_address( - :customer_profile_id => @customer_profile_id, - :customer_address_id => @customer_address_id + customer_profile_id: @customer_profile_id, + customer_address_id: @customer_address_id ) assert_instance_of Response, response assert_success response @@ -394,9 +396,9 @@ def test_should_update_customer_profile_request @gateway.expects(:ssl_post).returns(successful_update_customer_profile_response) assert response = @gateway.update_customer_profile( - :profile => { - :customer_profile_id => @customer_profile_id, - :email => 'new email address' + profile: { + customer_profile_id: @customer_profile_id, + email: 'new email address' } ) assert_instance_of Response, response @@ -408,10 +410,10 @@ def test_should_update_customer_payment_profile_request @gateway.expects(:ssl_post).returns(successful_update_customer_payment_profile_response) assert response = @gateway.update_customer_payment_profile( - :customer_profile_id => @customer_profile_id, - :payment_profile => { - :customer_payment_profile_id => @customer_payment_profile_id, - :customer_type => 'business' + customer_profile_id: @customer_profile_id, + payment_profile: { + customer_payment_profile_id: @customer_payment_profile_id, + customer_type: 'business' } ) assert_instance_of Response, response @@ -420,21 +422,21 @@ def test_should_update_customer_payment_profile_request end def test_should_update_customer_payment_profile_request_with_last_four_digits - last_four_credit_card = ActiveMerchant::Billing::CreditCard.new(:number => '4242') #Credit card with only last four digits + last_four_credit_card = ActiveMerchant::Billing::CreditCard.new(number: '4242') # Credit card with only last four digits response = stub_comms do @gateway.update_customer_payment_profile( - :customer_profile_id => @customer_profile_id, - :payment_profile => { - :customer_payment_profile_id => @customer_payment_profile_id, - :bill_to => address(:address1 => '345 Avenue B', - :address2 => 'Apt 101'), - :payment => { - :credit_card => last_four_credit_card + customer_profile_id: @customer_profile_id, + payment_profile: { + customer_payment_profile_id: @customer_payment_profile_id, + bill_to: address(address1: '345 Avenue B', + address2: 'Apt 101'), + payment: { + credit_card: last_four_credit_card } } ) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match %r{XXXX4242}, data end.respond_with(successful_update_customer_payment_profile_response) @@ -447,10 +449,10 @@ def test_should_update_customer_shipping_address_request @gateway.expects(:ssl_post).returns(successful_update_customer_shipping_address_response) assert response = @gateway.update_customer_shipping_address( - :customer_profile_id => @customer_profile_id, - :address => { - :customer_address_id => @customer_address_id, - :city => 'New City' + customer_profile_id: @customer_profile_id, + address: { + customer_address_id: @customer_address_id, + city: 'New City' } ) assert_instance_of Response, response @@ -462,10 +464,10 @@ def test_should_validate_customer_payment_profile_request @gateway.expects(:ssl_post).returns(successful_validate_customer_payment_profile_response) assert response = @gateway.validate_customer_payment_profile( - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => @customer_payment_profile_id, - :customer_address_id => @customer_address_id, - :validation_mode => :live + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: @customer_payment_profile_id, + customer_address_id: @customer_address_id, + validation_mode: :live ) assert_instance_of Response, response assert_success response @@ -478,9 +480,9 @@ def test_should_create_customer_profile_transaction_auth_capture_and_then_void_r @gateway.expects(:ssl_post).returns(successful_create_customer_profile_transaction_response(:void)) assert response = @gateway.create_customer_profile_transaction( - :transaction => { - :type => :void, - :trans_id => response.params['direct_response']['transaction_id'] + transaction: { + type: :void, + trans_id: response.params['direct_response']['transaction_id'] } ) assert_instance_of Response, response @@ -495,12 +497,12 @@ def test_should_create_customer_profile_transaction_auth_capture_and_then_refund @gateway.expects(:ssl_post).returns(unsuccessful_create_customer_profile_transaction_response(:refund)) assert response = @gateway.create_customer_profile_transaction( - :transaction => { - :type => :refund, - :amount => 1, - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => @customer_payment_profile_id, - :trans_id => response.params['direct_response']['transaction_id'] + transaction: { + type: :refund, + amount: 1, + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: @customer_payment_profile_id, + trans_id: response.params['direct_response']['transaction_id'] } ) assert_instance_of Response, response @@ -519,13 +521,13 @@ def test_should_create_customer_profile_transaction_auth_capture_and_then_refund @gateway.expects(:ssl_post).returns(unsuccessful_create_customer_profile_transaction_response(:refund)) assert response = @gateway.create_customer_profile_transaction( - :transaction => { - :type => :refund, - :amount => 1, + transaction: { + type: :refund, + amount: 1, - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => @customer_payment_profile_id, - :trans_id => response.params['direct_response']['transaction_id'] + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: @customer_payment_profile_id, + trans_id: response.params['direct_response']['transaction_id'] } ) assert_instance_of Response, response @@ -559,9 +561,9 @@ def test_should_create_customer_profile_transaction_for_void_request @gateway.expects(:ssl_post).returns(successful_create_customer_profile_transaction_response(:void)) assert response = @gateway.create_customer_profile_transaction_for_void( - :transaction => { - :trans_id => 1 - } + transaction: { + trans_id: 1 + } ) assert_instance_of Response, response assert_success response @@ -573,11 +575,11 @@ def test_should_create_customer_profile_transaction_for_refund_request @gateway.expects(:ssl_post).returns(successful_create_customer_profile_transaction_response(:refund)) assert response = @gateway.create_customer_profile_transaction_for_refund( - :transaction => { - :trans_id => 1, - :amount => '1.00', - :credit_card_number_masked => 'XXXX1234' - } + transaction: { + trans_id: 1, + amount: '1.00', + credit_card_number_masked: 'XXXX1234' + } ) assert_instance_of Response, response assert_success response @@ -588,21 +590,21 @@ def test_should_create_customer_profile_transaction_for_refund_request def test_should_create_customer_profile_transaction_passing_recurring_flag response = stub_comms do @gateway.create_customer_profile_transaction( - :transaction => { - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => @customer_payment_profile_id, - :type => :auth_capture, - :order => { - :invoice_number => '1234', - :description => 'Test Order Description', - :purchase_order_number => '4321' + transaction: { + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: @customer_payment_profile_id, + type: :auth_capture, + order: { + invoice_number: '1234', + description: 'Test Order Description', + purchase_order_number: '4321' }, - :amount => @amount, - :card_code => '123', - :recurring_billing => true + amount: @amount, + card_code: '123', + recurring_billing: true } ) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match %r{true}, data end.respond_with(successful_create_customer_profile_transaction_response(:auth_capture)) @@ -623,13 +625,13 @@ def test_full_or_masked_card_number def test_multiple_errors_when_creating_customer_profile @gateway.expects(:ssl_post).returns(unsuccessful_create_customer_profile_transaction_response_with_multiple_errors(:refund)) assert response = @gateway.create_customer_profile_transaction( - :transaction => { - :type => :refund, - :amount => 1, + transaction: { + type: :refund, + amount: 1, - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => @customer_payment_profile_id, - :trans_id => 1 + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: @customer_payment_profile_id, + trans_id: 1 } ) assert_equal 'The transaction was unsuccessful.', response.message @@ -642,11 +644,11 @@ def get_auth_only_response @gateway.expects(:ssl_post).returns(successful_create_customer_profile_transaction_response(:auth_only)) assert response = @gateway.create_customer_profile_transaction( - :transaction => { - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => @customer_payment_profile_id, - :type => :auth_only, - :amount => @amount + transaction: { + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: @customer_payment_profile_id, + type: :auth_only, + amount: @amount } ) assert_instance_of Response, response @@ -662,16 +664,16 @@ def get_and_validate_auth_capture_response @gateway.expects(:ssl_post).returns(successful_create_customer_profile_transaction_response(:auth_capture)) assert response = @gateway.create_customer_profile_transaction( - :transaction => { - :customer_profile_id => @customer_profile_id, - :customer_payment_profile_id => @customer_payment_profile_id, - :type => :auth_capture, - :order => { - :invoice_number => '1234', - :description => 'Test Order Description', - :purchase_order_number => '4321' + transaction: { + customer_profile_id: @customer_profile_id, + customer_payment_profile_id: @customer_payment_profile_id, + type: :auth_capture, + order: { + invoice_number: '1234', + description: 'Test Order Description', + purchase_order_number: '4321' }, - :amount => @amount + amount: @amount } ) assert_instance_of Response, response @@ -823,6 +825,7 @@ def successful_get_customer_profile_response #{@credit_card.number} #{@gateway.send(:expdate, @credit_card)} + #{@credit_card.first_digits} @@ -874,6 +877,7 @@ def successful_get_customer_profile_response_with_multiple_payment_profiles #{@credit_card.number} #{@gateway.send(:expdate, @credit_card)} + #{@credit_card.first_digits} @@ -884,6 +888,7 @@ def successful_get_customer_profile_response_with_multiple_payment_profiles XXXX1234 XXXX + 424242 @@ -914,6 +919,7 @@ def successful_get_customer_payment_profile_response #{@credit_card.number} #{@gateway.send(:expdate, @credit_card)} + #{@credit_card.first_digits} @@ -1003,16 +1009,16 @@ def successful_update_customer_shipping_address_response end SUCCESSFUL_DIRECT_RESPONSE = { - :auth_only => '1,1,1,This transaction has been approved.,Gw4NGI,Y,508223659,,,100.00,CC,auth_only,Up to 20 chars,,,,,,,,,,,Up to 255 Characters,,,,,,,,,,,,,,6E5334C13C78EA078173565FD67318E4,,2,,,,,,,,,,,,,,,,,,,,,,,,,,,,', - :capture_only => '1,1,1,This transaction has been approved.,,Y,508223660,,,100.00,CC,capture_only,Up to 20 chars,,,,,,,,,,,Up to 255 Characters,,,,,,,,,,,,,,6E5334C13C78EA078173565FD67318E4,,2,,,,,,,,,,,,,,,,,,,,,,,,,,,,', - :auth_capture => '1,1,1,This transaction has been approved.,d1GENk,Y,508223661,32968c18334f16525227,Store purchase,1.00,CC,auth_capture,,Longbob,Longsen,,,,,,,,,,,,,,,,,,,,,,,269862C030129C1173727CC10B1935ED,M,2,,,,,,,,,,,,,,,,,,,,,,,,,,,,', - :void => '1,1,1,This transaction has been approved.,nnCMEx,P,2149222068,1245879759,,0.00,CC,void,1245879759,,,,,,,K1C2N6,,,,,,,,,,,,,,,,,,F240D65BB27ADCB8C80410B92342B22C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,', - :refund => '1,1,1,This transaction has been approved.,nnCMEx,P,2149222068,1245879759,,0.00,CC,refund,1245879759,,,,,,,K1C2N6,,,,,,,,,,,,,,,,,,F240D65BB27ADCB8C80410B92342B22C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,', - :prior_auth_capture => '1,1,1,This transaction has been approved.,VR0lrD,P,2149227870,1245958544,,1.00,CC,prior_auth_capture,1245958544,,,,,,,K1C2N6,,,,,,,,,,,,,,,,,,0B8BFE0A0DE6FDB69740ED20F79D04B0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,', - :auth_capture_version_3_1 => '1,1,1,This transaction has been approved.,CSYM0K,Y,2163585627,1234,Test Order Description,100.00,CC,auth_capture,Up to 20 chars,,,Widgets Inc,1234 My Street,Ottawa,ON,K1C2N6,CA,,,Up to 255 Characters,,,,,,,,,,,,,4321,02DFBD7934AD862AB16688D44F045D31,,2,,,,,,,,,,,XXXX4242,Visa,,,,,,,,,,,,,,,,' + auth_only: '1,1,1,This transaction has been approved.,Gw4NGI,Y,508223659,,,100.00,CC,auth_only,Up to 20 chars,,,,,,,,,,,Up to 255 Characters,,,,,,,,,,,,,,6E5334C13C78EA078173565FD67318E4,,2,,,,,,,,,,,,,,,,,,,,,,,,,,,,', + capture_only: '1,1,1,This transaction has been approved.,,Y,508223660,,,100.00,CC,capture_only,Up to 20 chars,,,,,,,,,,,Up to 255 Characters,,,,,,,,,,,,,,6E5334C13C78EA078173565FD67318E4,,2,,,,,,,,,,,,,,,,,,,,,,,,,,,,', + auth_capture: '1,1,1,This transaction has been approved.,d1GENk,Y,508223661,32968c18334f16525227,Store purchase,1.00,CC,auth_capture,,Longbob,Longsen,,,,,,,,,,,,,,,,,,,,,,,269862C030129C1173727CC10B1935ED,M,2,,,,,,,,,,,,,,,,,,,,,,,,,,,,', + void: '1,1,1,This transaction has been approved.,nnCMEx,P,2149222068,1245879759,,0.00,CC,void,1245879759,,,,,,,K1C2N6,,,,,,,,,,,,,,,,,,F240D65BB27ADCB8C80410B92342B22C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,', + refund: '1,1,1,This transaction has been approved.,nnCMEx,P,2149222068,1245879759,,0.00,CC,refund,1245879759,,,,,,,K1C2N6,,,,,,,,,,,,,,,,,,F240D65BB27ADCB8C80410B92342B22C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,', + prior_auth_capture: '1,1,1,This transaction has been approved.,VR0lrD,P,2149227870,1245958544,,1.00,CC,prior_auth_capture,1245958544,,,,,,,K1C2N6,,,,,,,,,,,,,,,,,,0B8BFE0A0DE6FDB69740ED20F79D04B0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,', + auth_capture_version_3_1: '1,1,1,This transaction has been approved.,CSYM0K,Y,2163585627,1234,Test Order Description,100.00,CC,auth_capture,Up to 20 chars,,,Widgets Inc,1234 My Street,Ottawa,ON,K1C2N6,CA,,,Up to 255 Characters,,,,,,,,,,,,,4321,02DFBD7934AD862AB16688D44F045D31,,2,,,,,,,,,,,XXXX4242,Visa,,,,,,,,,,,,,,,,' } UNSUCCESSUL_DIRECT_RESPONSE = { - :refund => '3,2,54,The referenced transaction does not meet the criteria for issuing a credit.,,P,0,,,1.00,CC,credit,1245952682,,,Widgets Inc,1245952682 My Street,Ottawa,ON,K1C2N6,CA,,,bob1245952682@email.com,,,,,,,,,,,,,,207BCBBF78E85CF174C87AE286B472D2,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,447250,406104' + refund: '3,2,54,The referenced transaction does not meet the criteria for issuing a credit.,,P,0,,,1.00,CC,credit,1245952682,,,Widgets Inc,1245952682 My Street,Ottawa,ON,K1C2N6,CA,,,bob1245952682@email.com,,,,,,,,,,,,,,207BCBBF78E85CF174C87AE286B472D2,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,447250,406104' } def successful_create_customer_profile_transaction_response(transaction_type) diff --git a/test/unit/gateways/authorize_net_test.rb b/test/unit/gateways/authorize_net_test.rb index 9b3516bb25d..704e0f85e2e 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 = { @@ -29,6 +33,14 @@ def setup description: 'Store Purchase' } + @level_3_options = { + ship_from_address: { + zip: 'origin27701', + country: 'originUS' + }, + summary_commodity_code: 'CODE' + } + @additional_options = { line_items: [ { @@ -47,13 +59,28 @@ def setup } ] } + + @level_3_line_item_options = { + line_items: [ + { + item_id: '1', + name: 'mug', + description: 'coffee', + quantity: '100', + unit_price: '10', + unit_of_measure: 'yards', + total_amount: '1000', + product_code: 'coupon' + } + ] + } end def test_add_swipe_data_with_bad_data @credit_card.track_data = BAD_TRACK_DATA stub_comms do @gateway.purchase(@amount, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_nil doc.at_xpath('//track1') assert_nil doc.at_xpath('//track2') @@ -66,7 +93,7 @@ def test_add_swipe_data_with_track_1 @credit_card.track_data = TRACK1_DATA stub_comms do @gateway.purchase(@amount, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_equal '%B378282246310005^LONGSON/LONGBOB^1705101130504392?', doc.at_xpath('//track1').content assert_nil doc.at_xpath('//track2') @@ -79,7 +106,7 @@ def test_add_swipe_data_with_track_2 @credit_card.track_data = TRACK2_DATA stub_comms do @gateway.purchase(@amount, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_nil doc.at_xpath('//track1') assert_equal ';4111111111111111=1803101000020000831?', doc.at_xpath('//track2').content @@ -93,7 +120,7 @@ def test_retail_market_type_device_type_included_in_swipe_transactions_with_vali @credit_card.track_data = track stub_comms do @gateway.purchase(@amount, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_nil doc.at_xpath('//retail') end @@ -104,7 +131,7 @@ def test_retail_market_type_device_type_included_in_swipe_transactions_with_vali @credit_card.track_data = track stub_comms do @gateway.purchase(@amount, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_not_nil doc.at_xpath('//retail') assert_equal '2', doc.at_xpath('//retail/marketType').content @@ -118,8 +145,8 @@ def test_device_type_used_from_options_if_included_with_valid_track_data [TRACK1_DATA, TRACK2_DATA].each do |track| @credit_card.track_data = track stub_comms do - @gateway.purchase(@amount, @credit_card, {device_type: 1}) - end.check_request do |endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, { device_type: 1 }) + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_not_nil doc.at_xpath('//retail') assert_equal '2', doc.at_xpath('//retail/marketType').content @@ -130,10 +157,10 @@ 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| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_nil doc.at_xpath('//retail') end @@ -145,7 +172,7 @@ def test_moto_market_type_included_when_card_is_entered_manually @credit_card.manual_entry = true stub_comms do @gateway.purchase(@amount, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_not_nil doc.at_xpath('//retail') assert_equal '1', doc.at_xpath('//retail/marketType').content @@ -156,7 +183,7 @@ def test_moto_market_type_included_when_card_is_entered_manually def test_market_type_can_be_specified stub_comms do @gateway.purchase(@amount, @credit_card, market_type: 0) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_equal '0', doc.at_xpath('//retail/marketType').content end @@ -166,7 +193,7 @@ def test_market_type_can_be_specified def test_successful_echeck_authorization response = stub_comms do @gateway.authorize(@amount, @check) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_not_nil doc.at_xpath('//payment/bankAccount') assert_equal '244183602', doc.at_xpath('//routingNumber').content @@ -184,12 +211,13 @@ def test_successful_echeck_authorization assert_equal '508141794', response.authorization.split('#')[0] end - def test_successful_echeck_purchase + def test_successful_echeck_purchase_with_checking_account_type response = stub_comms do @gateway.purchase(@amount, @check) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_not_nil doc.at_xpath('//payment/bankAccount') + assert_equal 'checking', doc.at_xpath('//accountType').content assert_equal '244183602', doc.at_xpath('//routingNumber').content assert_equal '15378535', doc.at_xpath('//accountNumber').content assert_equal 'Bank of Elbonia', doc.at_xpath('//bankName').content @@ -205,10 +233,27 @@ def test_successful_echeck_purchase assert_equal '508141795', response.authorization.split('#')[0] end + def test_successful_echeck_purchase_with_savings_account_type + savings_account = check(account_type: 'savings') + response = stub_comms do + @gateway.purchase(@amount, savings_account) + end.check_request do |_endpoint, data, _headers| + parse(data) do |doc| + assert_not_nil doc.at_xpath('//payment/bankAccount') + assert_equal 'savings', doc.at_xpath('//accountType').content + end + end.respond_with(successful_purchase_response) + + assert response + assert_instance_of Response, response + assert_success response + assert_equal '508141795', response.authorization.split('#')[0] + end + def test_echeck_passing_recurring_flag response = stub_comms do @gateway.purchase(@amount, @check, recurring: true) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_equal settings_from_doc(parse(data))['recurringBilling'], 'true' end.respond_with(successful_purchase_response) @@ -224,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) - 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 + @gateway.authorize(@amount, @payment_token) + end.check_request do |_endpoint, data, _headers| + assert_match(/true<\/isPaymentToken>/, data) + assert_no_match(//, data) end.respond_with(successful_authorize_response) assert response @@ -240,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) - 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 + @gateway.purchase(@amount, @payment_token, {}) + end.check_request do |_endpoint, data, _headers| + assert_match(/true<\/isPaymentToken>/, data) + assert_no_match(//, data) end.respond_with(successful_purchase_response) assert response @@ -262,6 +303,7 @@ def test_successful_authorization assert_equal 'M', response.cvv_result['code'] assert_equal 'CVV matches', response.cvv_result['message'] + assert_equal 'I00001', response.params['full_response_code'] assert_equal '508141794', response.authorization.split('#')[0] assert response.test? @@ -286,7 +328,7 @@ def test_successful_purchase def test_successful_purchase_with_utf_character stub_comms do @gateway.purchase(@amount, credit_card('4000100011112224', last_name: 'Wåhlin')) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/Wåhlin/, data) end.respond_with(successful_purchase_response) end @@ -294,7 +336,7 @@ def test_successful_purchase_with_utf_character def test_passes_partial_auth stub_comms do @gateway.purchase(@amount, credit_card, disable_partial_auth: true) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/allowPartialAuth<\/settingName>/, data) assert_match(/false<\/settingValue>/, data) end.respond_with(successful_purchase_response) @@ -303,14 +345,14 @@ def test_passes_partial_auth def test_passes_email_customer stub_comms do @gateway.purchase(@amount, credit_card, email_customer: true) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/emailCustomer<\/settingName>/, data) assert_match(/true<\/settingValue>/, data) end.respond_with(successful_purchase_response) stub_comms do @gateway.purchase(@amount, credit_card, email_customer: false) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/emailCustomer<\/settingName>/, data) assert_match(/false<\/settingValue>/, data) end.respond_with(successful_purchase_response) @@ -319,16 +361,45 @@ def test_passes_email_customer def test_passes_header_email_receipt stub_comms do @gateway.purchase(@amount, credit_card, header_email_receipt: 'yet another field') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/headerEmailReceipt<\/settingName>/, data) assert_match(/yet another field<\/settingValue>/, data) 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)) + end.check_request do |_endpoint, data, _headers| + assert_match(//, data) + assert_match(/#{@level_3_options[:summary_commodity_code]}<\/summaryCommodityCode>/, data) + assert_match(/<\/order>/, data) + assert_match(//, data) + assert_match(/#{@level_3_options[:ship_from_address][:zip]}<\/zip>/, data) + assert_match(/#{@level_3_options[:ship_from_address][:country]}<\/country>/, data) + assert_match(/<\/shipFrom>/, data) + end.respond_with(successful_purchase_response) + end + def test_passes_line_items stub_comms do @gateway.purchase(@amount, credit_card, @options.merge(@additional_options)) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(//, data) assert_match(//, data) assert_match(/#{@additional_options[:line_items][0][:item_id]}<\/itemId>/, data) @@ -346,6 +417,24 @@ def test_passes_line_items end.respond_with(successful_purchase_response) end + def test_passes_level_3_line_items + stub_comms do + @gateway.purchase(@amount, credit_card, @options.merge(@level_3_line_item_options)) + end.check_request do |_endpoint, data, _headers| + assert_match(//, data) + assert_match(//, data) + assert_match(/#{@level_3_line_item_options[:line_items][0][:item_id]}<\/itemId>/, data) + assert_match(/#{@level_3_line_item_options[:line_items][0][:name]}<\/name>/, data) + assert_match(/#{@level_3_line_item_options[:line_items][0][:description]}<\/description>/, data) + assert_match(/#{@level_3_line_item_options[:line_items][0][:quantity]}<\/quantity>/, data) + assert_match(/#{@level_3_line_item_options[:line_items][0][:unit_price]}<\/unitPrice>/, data) + assert_match(/#{@level_3_line_item_options[:line_items][0][:unit_of_measure]}<\/unitOfMeasure>/, data) + assert_match(/#{@level_3_line_item_options[:line_items][0][:total_amount]}<\/totalAmount>/, data) + assert_match(/#{@level_3_line_item_options[:line_items][0][:product_code]}<\/productCode>/, data) + assert_match(/<\/lineItems>/, data) + end.respond_with(successful_purchase_response) + end + def test_failed_purchase @gateway.expects(:ssl_post).returns(failed_purchase_response) @@ -393,6 +482,41 @@ def test_successful_purchase_using_stored_card assert_equal 'Street address and 5-digit postal code match.', response.avs_result['message'] end + def test_successful_purchase_using_stored_card_and_custom_delimiter + @gateway.expects(:ssl_post).returns(successful_store_response) + store = @gateway.store(@credit_card, @options) + assert_success store + + @gateway.expects(:ssl_post).returns(successful_purchase_using_stored_card_response_with_pipe_delimiter) + + response = @gateway.purchase(@amount, store.authorization, { delimiter: '|', description: 'description, with, commas' }) + assert_success response + + assert_equal '2235700270#XXXX2224#cim_purchase', response.authorization + assert_equal 'Y', response.avs_result['code'] + assert response.avs_result['street_match'] + assert response.avs_result['postal_match'] + assert_equal 'Street address and 5-digit postal code match.', response.avs_result['message'] + assert_equal 'description, with, commas', response.params['order_description'] + end + + def test_successful_purchase_using_stored_card_and_custom_delimiter_with_quotes + @gateway.expects(:ssl_post).returns(successful_store_response) + store = @gateway.store(@credit_card, @options) + assert_success store + + @gateway.expects(:ssl_post).returns(successful_purchase_using_stored_card_response_with_pipe_delimiter_and_quotes) + + response = @gateway.purchase(@amount, store.authorization, { delimiter: '|', description: 'description, with, commas' }) + assert_success response + + assert_equal '12345667#XXXX1111#cim_purchase', response.authorization + assert_equal 'Y', response.avs_result['code'] + assert response.avs_result['street_match'] + assert response.avs_result['postal_match'] + assert_equal 'Street address and 5-digit postal code match.', response.avs_result['message'] + end + def test_failed_purchase_using_stored_card @gateway.expects(:ssl_post).returns(successful_store_response) store = @gateway.store(@credit_card, @options) @@ -430,6 +554,167 @@ def test_successful_authorize_and_capture_using_stored_card assert_equal 'This transaction has been approved.', capture.message end + def test_successful_auth_with_initial_reccuring_stored_credential + stored_credential_params = { + initial_transaction: true, + reason_type: 'recurring', + initiator: 'cardholder', + network_transaction_id: nil + } + auth = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_params })) + end.check_request do |_endpoint, data, _headers| + doc = parse(data) + assert_equal 'true', doc.at_xpath('//processingOptions/isFirstRecurringPayment').content + assert_not_match(/isFirstSubsequentAuth/, doc) + end.respond_with(successful_authorize_response) + assert_success auth + assert auth.authorization + end + + def test_successful_auth_with_initial_unscheduled_stored_credential + stored_credential_params = { + initial_transaction: true, + reason_type: 'unscheduled', + initiator: 'cardholder', + network_transaction_id: nil + } + auth = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_params })) + end.check_request do |_endpoint, data, _headers| + doc = parse(data) + assert_equal 'true', doc.at_xpath('//processingOptions/isFirstSubsequentAuth').content + assert_not_match(/isFirstRecurringPayment/, doc) + end.respond_with(successful_authorize_response) + assert_success auth + assert auth.authorization + end + + def test_successful_auth_with_initial_installment_stored_credential + stored_credential_params = { + initial_transaction: true, + reason_type: 'installment', + initiator: 'cardholder', + network_transaction_id: nil + } + auth = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_params })) + end.check_request do |_endpoint, data, _headers| + doc = parse(data) + assert_equal 'true', doc.at_xpath('//processingOptions/isFirstSubsequentAuth').content + assert_not_match(/isFirstRecurringPayment/, doc) + end.respond_with(successful_authorize_response) + assert_success auth + assert auth.authorization + end + + def test_successful_auth_with_subsequent_installment_stored_credential + stored_credential_params = { + initial_transaction: false, + reason_type: 'installment', + initiator: 'merchant', + network_transaction_id: '0123' + } + auth = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_params })) + end.check_request do |_endpoint, data, _headers| + doc = parse(data) + assert_equal 'true', doc.at_xpath('//processingOptions/isSubsequentAuth').content + assert_equal '0123', doc.at_xpath('//subsequentAuthInformation/originalNetworkTransId').content + end.respond_with(successful_authorize_response) + assert_success auth + assert auth.authorization + end + + def test_successful_auth_with_subsequent_unscheduled_stored_credential + stored_credential_params = { + initial_transaction: false, + reason_type: 'unscheduled', + initiator: 'merchant', + network_transaction_id: '0123' + } + auth = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_params })) + end.check_request do |_endpoint, data, _headers| + doc = parse(data) + assert_equal 'true', doc.at_xpath('//processingOptions/isSubsequentAuth').content + assert_equal '0123', doc.at_xpath('//subsequentAuthInformation/originalNetworkTransId').content + end.respond_with(successful_authorize_response) + assert_success auth + assert auth.authorization + end + + def test_successful_auth_with_subsequent_recurring_stored_credential + stored_credential_params = { + initial_transaction: false, + reason_type: 'recurring', + initiator: 'merchant', + network_transaction_id: '0123' + } + auth = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_params })) + end.check_request do |_endpoint, data, _headers| + doc = parse(data) + assert_equal 'true', doc.at_xpath('//processingOptions/isSubsequentAuth').content + assert_equal '0123', doc.at_xpath('//subsequentAuthInformation/originalNetworkTransId').content + assert_equal 'recurringBilling', doc.at_xpath('//transactionSettings/setting/settingName').content + assert_equal 'true', doc.at_xpath('//transactionSettings/setting/settingValue').content + end.respond_with(successful_authorize_response) + assert_success auth + assert auth.authorization + end + + def test_successful_auth_with_subsequent_installment_stored_credential_and_cardholder_initiator + stored_credential_params = { + initial_transaction: false, + reason_type: 'installment', + initiator: 'cardholder', + network_transaction_id: '0123' + } + auth = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_params })) + end.check_request do |_endpoint, data, _headers| + doc = parse(data) + assert_equal 'true', doc.at_xpath('//processingOptions/isStoredCredentials').content + end.respond_with(successful_authorize_response) + assert_success auth + assert auth.authorization + end + + def test_successful_auth_with_subsequent_unscheduled_stored_credential_and_cardholder_initiator + stored_credential_params = { + initial_transaction: false, + reason_type: 'unscheduled', + initiator: 'cardholder', + network_transaction_id: '0123' + } + auth = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_params })) + end.check_request do |_endpoint, data, _headers| + doc = parse(data) + assert_equal 'true', doc.at_xpath('//processingOptions/isStoredCredentials').content + end.respond_with(successful_authorize_response) + assert_success auth + assert auth.authorization + end + + def test_successful_auth_with_subsequent_recurring_stored_credential_and_cardholder_initiator + stored_credential_params = { + initial_transaction: false, + reason_type: 'recurring', + initiator: 'cardholder', + network_transaction_id: '0123' + } + auth = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_params })) + end.check_request do |_endpoint, data, _headers| + doc = parse(data) + assert_equal 'true', doc.at_xpath('//processingOptions/isStoredCredentials').content + end.respond_with(successful_authorize_response) + assert_success auth + assert auth.authorization + end + def test_failed_authorize_using_stored_card @gateway.expects(:ssl_post).returns(successful_store_response) store = @gateway.store(@credit_card, @options) @@ -519,7 +804,10 @@ def test_failed_void_using_stored_card def test_successful_verify response = stub_comms do - @gateway.verify(@credit_card) + @gateway.verify(@credit_card, @options) + end.check_request do |_endpoint, data, _headers| + doc = parse(data) + assert_equal '1.00', doc.at_xpath('//transactionRequest/amount').content if doc.at_xpath('//transactionRequest/transactionType').content == 'authOnlyTransaction' end.respond_with(successful_authorize_response, successful_void_response) assert_success response end @@ -532,6 +820,20 @@ def test_successful_verify_failed_void assert_match %r{This transaction has been approved}, response.message end + def test_successful_verify_with_0_auth_card + options = { + verify_amount: 0, + billing_address: { + address1: '123 St', + zip: '88888' + } + } + response = stub_comms do + @gateway.verify(@credit_card, options) + end.respond_with(successful_authorize_response) + assert_success response + end + def test_unsuccessful_verify response = stub_comms do @gateway.verify(@credit_card, @options) @@ -601,7 +903,7 @@ def test_failed_store def test_successful_unstore response = stub_comms do @gateway.unstore('35959426#32506918#cim_store') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| doc = parse(data) assert_equal '35959426', doc.at_xpath('//deleteCustomerProfileRequest/customerProfileId').content end.respond_with(successful_unstore_response) @@ -641,8 +943,22 @@ def test_failed_store_new_payment_profile def test_address stub_comms do - @gateway.authorize(@amount, @credit_card, billing_address: {address1: '164 Waverley Street', country: 'US', state: 'CO', phone: '(555)555-5555', fax: '(555)555-4444'}) - end.check_request do |endpoint, data, headers| + @gateway.authorize(@amount, @credit_card, billing_address: { address1: '164 Waverley Street', country: 'US', state: 'CO', phone: '(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_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 @@ -656,7 +972,7 @@ def test_address def test_address_with_empty_billing_address stub_comms do @gateway.authorize(@amount, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_equal '', doc.at_xpath('//billTo/address').content, data assert_equal '', doc.at_xpath('//billTo/city').content, data @@ -669,8 +985,8 @@ def test_address_with_empty_billing_address def test_address_with_address2_present stub_comms do - @gateway.authorize(@amount, @credit_card, billing_address: {address1: '164 Waverley Street', address2: 'Apt 1234', country: 'US', state: 'CO', phone: '(555)555-5555', fax: '(555)555-4444'}) - end.check_request do |endpoint, data, headers| + @gateway.authorize(@amount, @credit_card, billing_address: { address1: '164 Waverley Street', address2: 'Apt 1234', country: 'US', state: 'CO', phone: '(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 Apt 1234', doc.at_xpath('//billTo/address').content, data @@ -683,8 +999,8 @@ def test_address_with_address2_present def test_address_north_america_with_defaults stub_comms do - @gateway.authorize(@amount, @credit_card, billing_address: {address1: '164 Waverley Street', country: 'US'}) - end.check_request do |endpoint, data, headers| + @gateway.authorize(@amount, @credit_card, billing_address: { address1: '164 Waverley Street', country: 'US' }) + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_equal 'NC', doc.at_xpath('//billTo/state').content, data assert_equal '164 Waverley Street', doc.at_xpath('//billTo/address').content, data @@ -695,8 +1011,8 @@ def test_address_north_america_with_defaults def test_address_outsite_north_america stub_comms do - @gateway.authorize(@amount, @credit_card, billing_address: {address1: '164 Waverley Street', country: 'DE'}) - end.check_request do |endpoint, data, headers| + @gateway.authorize(@amount, @credit_card, billing_address: { address1: '164 Waverley Street', country: 'DE' }) + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_equal 'n/a', doc.at_xpath('//billTo/state').content, data assert_equal '164 Waverley Street', doc.at_xpath('//billTo/address').content, data @@ -707,8 +1023,8 @@ def test_address_outsite_north_america def test_address_outsite_north_america_with_address2_present stub_comms do - @gateway.authorize(@amount, @credit_card, billing_address: {address1: '164 Waverley Street', address2: 'Apt 1234', country: 'DE'}) - end.check_request do |endpoint, data, headers| + @gateway.authorize(@amount, @credit_card, billing_address: { address1: '164 Waverley Street', address2: 'Apt 1234', country: 'DE' }) + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_equal 'n/a', doc.at_xpath('//billTo/state').content, data assert_equal '164 Waverley Street Apt 1234', doc.at_xpath('//billTo/address').content, data @@ -720,7 +1036,7 @@ def test_address_outsite_north_america_with_address2_present def test_duplicate_window stub_comms do @gateway.purchase(@amount, @credit_card, duplicate_window: 0) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_equal settings_from_doc(parse(data))['duplicateWindow'], '0' end.respond_with(successful_purchase_response) end @@ -739,7 +1055,7 @@ def test_duplicate_window_class_attribute_deprecated def test_add_cardholder_authentication_value stub_comms do @gateway.purchase(@amount, @credit_card, cardholder_authentication_value: 'E0Mvq8AAABEiMwARIjNEVWZ3iJk=', authentication_indicator: '2') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_equal 'E0Mvq8AAABEiMwARIjNEVWZ3iJk=', doc.at_xpath('//cardholderAuthentication/cardholderAuthenticationValue').content assert_equal '2', doc.at_xpath('//cardholderAuthentication/authenticationIndicator').content @@ -752,7 +1068,7 @@ def test_alternative_three_d_secure_options three_d_secure_opts = { cavv: 'E0Mvq8AAABEiMwARIjNEVWZ3iJk=', eci: '2' } stub_comms do @gateway.purchase(@amount, @credit_card, three_d_secure: three_d_secure_opts) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_equal 'E0Mvq8AAABEiMwARIjNEVWZ3iJk=', doc.at_xpath('//cardholderAuthentication/cardholderAuthenticationValue').content assert_equal '2', doc.at_xpath('//cardholderAuthentication/authenticationIndicator').content @@ -771,7 +1087,7 @@ def test_prioritize_authentication_value_params authentication_indicator: '2', three_d_secure: three_d_secure_opts ) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_equal 'E0Mvq8AAABEiMwARIjNEVWZ3iJk=', doc.at_xpath('//cardholderAuthentication/cardholderAuthenticationValue').content assert_equal '2', doc.at_xpath('//cardholderAuthentication/authenticationIndicator').content @@ -780,10 +1096,20 @@ def test_prioritize_authentication_value_params end.respond_with(successful_purchase_response) end + def test_does_not_add_cardholder_authentication_element_without_relevant_values + stub_comms do + @gateway.purchase(@amount, @credit_card) + end.check_request do |_endpoint, data, _headers| + parse(data) do |doc| + assert !doc.at_xpath('//cardholderAuthentication'), data + end + end.respond_with(successful_purchase_response) + end + def test_capture_passing_extra_info response = stub_comms do @gateway.capture(50, '123456789', description: 'Yo', order_id: 'Sweetness') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_not_nil doc.at_xpath('//order/description'), data assert_equal 'Yo', doc.at_xpath('//order/description').content, data @@ -803,10 +1129,38 @@ def test_successful_refund assert_equal '2214602071#2224#refund', refund.authorization end + def test_successful_bank_refund + response = stub_comms do + @gateway.refund(50, '12345667', account_type: 'checking', routing_number: '123450987', account_number: '12345667', first_name: 'Louise', last_name: 'Belcher') + 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', doc.at_xpath('//transactionRequest/payment/bankAccount/nameOnAccount').content + end + end.respond_with(successful_refund_response) + 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') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_equal 'Bob', doc.at_xpath('//billTo/firstName').content, data assert_equal 'Smith', doc.at_xpath('//billTo/lastName').content, data @@ -847,11 +1201,11 @@ def test_failed_credit end def test_supported_countries - assert_equal 4, (['US', 'CA', 'AU', 'VA'] & AuthorizeNetGateway.supported_countries).size + assert_equal 3, (%w[US CA AU] & AuthorizeNetGateway.supported_countries).size end def test_supported_card_types - assert_equal [:visa, :master, :american_express, :discover, :diners_club, :jcb, :maestro], AuthorizeNetGateway.supported_cardtypes + assert_equal %i[visa master american_express discover diners_club jcb maestro], AuthorizeNetGateway.supported_cardtypes end def test_failure_without_response_reason_text @@ -878,7 +1232,6 @@ def test_avs_result assert_equal 'Y', response.avs_result['street_match'] assert_equal 'Y', response.avs_result['postal_match'] - @gateway.expects(:ssl_post).returns(address_not_provided_avs_response) response = @gateway.purchase(@amount, @credit_card) @@ -903,7 +1256,7 @@ def test_message response = stub_comms do @gateway.purchase(@amount, @credit_card) end.respond_with(no_match_avs_response) - assert_equal 'Street address matches, but 5-digit and 9-digit postal code do not match.', response.message + assert_equal 'Street address matches, but postal code does not match.', response.message response = stub_comms do @gateway.purchase(@amount, @credit_card) @@ -915,7 +1268,7 @@ def test_solution_id_is_added_to_post_data_parameters @gateway.class.application_id = 'A1000000' stub_comms do @gateway.authorize(@amount, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| doc = parse(data) assert_equal 'A1000000', fields_from_doc(doc)['x_solution_id'], data assert_equal '1.00', doc.at_xpath('//transactionRequest/amount').content @@ -938,7 +1291,7 @@ def assert_no_has_customer_id(data) def test_include_cust_id_for_numeric_values stub_comms do @gateway.purchase(@amount, @credit_card, customer: '123') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_not_nil doc.at_xpath('//customer/id'), data assert_equal '123', doc.at_xpath('//customer/id').content, data @@ -950,7 +1303,7 @@ def test_include_cust_id_for_numeric_values def test_include_cust_id_for_word_character_values stub_comms do @gateway.purchase(@amount, @credit_card, customer: '4840_TT') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_not_nil doc.at_xpath('//customer/id'), data assert_equal '4840_TT', doc.at_xpath('//customer/id').content, data @@ -962,7 +1315,7 @@ def test_include_cust_id_for_word_character_values def test_dont_include_cust_id_for_email_addresses stub_comms do @gateway.purchase(@amount, @credit_card, customer: 'bob@test.com') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| doc = parse(data) assert !doc.at_xpath('//customer/id'), data assert_equal '1.00', doc.at_xpath('//transactionRequest/amount').content @@ -972,7 +1325,7 @@ def test_dont_include_cust_id_for_email_addresses def test_dont_include_cust_id_for_phone_numbers stub_comms do @gateway.purchase(@amount, @credit_card, customer: '111-123-1231') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| doc = parse(data) assert !doc.at_xpath('//customer/id'), data assert_equal '1.00', doc.at_xpath('//transactionRequest/amount').content @@ -980,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, @@ -992,7 +1343,7 @@ def test_includes_shipping_name_when_different_from_billing_name stub_comms do @gateway.purchase(@amount, card, options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_equal 'billing', doc.at_xpath('//billTo/firstName').text assert_equal 'name', doc.at_xpath('//billTo/lastName').text @@ -1003,21 +1354,19 @@ 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) options = { order_id: 'a' * 21, billing_address: address(name: 'billing name'), - shipping_address: shipping_address + shipping_address: } stub_comms do @gateway.purchase(@amount, card, options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_equal 'billing', doc.at_xpath('//billTo/firstName').text assert_equal 'name', doc.at_xpath('//billTo/lastName').text @@ -1028,10 +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, @@ -1042,7 +1388,7 @@ def test_truncation city: 'a' * 41, state: 'a' * 41, zip: 'a' * 21, - country: 'a' * 61, + country: 'a' * 61 ), shipping_address: address( name: ['a' * 51, 'a' * 51].join(' '), @@ -1051,13 +1397,13 @@ def test_truncation city: 'a' * 41, state: 'a' * 41, zip: 'a' * 21, - country: 'a' * 61, + country: 'a' * 61 ) } stub_comms do @gateway.purchase(@amount, card, options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_truncated(data, 20, '//refId') assert_truncated(data, 255, '//description') assert_address_truncated(data, 50, 'firstName') @@ -1077,7 +1423,7 @@ def test_invalid_cvv card = credit_card(@credit_card.number, { verification_value: cvv }) stub_comms do @gateway.purchase(@amount, card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) { |doc| assert_nil doc.at_xpath('//cardCode') } end.respond_with(successful_purchase_response) end @@ -1087,7 +1433,7 @@ def test_card_number_truncation card = credit_card(@credit_card.number + '0123456789') stub_comms do @gateway.purchase(@amount, card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_equal @credit_card.number, doc.at_xpath('//cardNumber').text end @@ -1103,13 +1449,11 @@ 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) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_equal credit_card.payment_cryptogram, doc.at_xpath('//creditCard/cryptogram').content assert_equal credit_card.number, doc.at_xpath('//creditCard/cardNumber').content @@ -1123,13 +1467,11 @@ 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) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_equal credit_card.payment_cryptogram, doc.at_xpath('//creditCard/cryptogram').content assert_equal credit_card.number, doc.at_xpath('//creditCard/cardNumber').content @@ -1142,7 +1484,7 @@ def test_failed_apple_pay_authorization_with_network_tokenization_not_supported def test_supports_network_tokenization_true response = stub_comms do @gateway.supports_network_tokenization? - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_equal 'authOnlyTransaction', doc.at_xpath('//transactionType').content assert_equal '0.01', doc.at_xpath('//amount').content @@ -1157,7 +1499,7 @@ def test_supports_network_tokenization_true def test_supports_network_tokenization_false response = stub_comms do @gateway.supports_network_tokenization? - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_equal 'authOnlyTransaction', doc.at_xpath('//transactionType').content assert_equal '0.01', doc.at_xpath('//amount').content @@ -1179,7 +1521,85 @@ def test_verify_bad_credentials assert !@gateway.verify_credentials end - private + def test_0_amount_verify_with_no_zip + @options[:verify_amount] = 0 + @options[:billing_address] = { zip: nil, address1: 'XYZ' } + + response = @gateway.verify(@credit_card, @options) + + assert_failure response + assert_equal 'Billing address including zip code is required for a 0 amount verify', response.message + end + + def test_verify_transcript_with_0_auth + stub_comms do + @options[:verify_amount] = 0 + @gateway.verify(@credit_card, @options) + end.check_request(skip_response: true) do |_endpoint, data, _headers| + doc = parse(data) + assert_equal '0.00', doc.at_xpath('//transactionRequest/amount').content if doc.at_xpath('//transactionRequest/transactionType').content == 'authOnlyTransaction' + end + end + + def test_verify_amount_with_bad_string + error = assert_raises(ArgumentError) do + @gateway.send :amount_for_verify, verify_amount: 'dog' + end + + assert_equal 'verify_amount value must be an integer', error.message + end + + def test_verify_amount_with_boolean + error = assert_raises(ArgumentError) do + @gateway.send :amount_for_verify, verify_amount: true + end + + assert_equal 'verify_amount value must be an integer', error.message + end + + def test_verify_amount_with_decimal + error = assert_raises(ArgumentError) do + @gateway.send :amount_for_verify, verify_amount: 0.125 + end + + assert_equal 'verify_amount value must be an integer', error.message + end + + def test_verify_amount_with_negative + error = assert_raises(ArgumentError) do + @gateway.send :amount_for_verify, verify_amount: -100 + end + + assert_equal 'verify_amount value must be an integer', error.message + end + + def test_verify_amount_with_string_as_number + assert_equal 200, @gateway.send(:amount_for_verify, verify_amount: '200') + end + + def test_verify_amount_with_zero_without_zip + error = assert_raises(ArgumentError) do + @gateway.send :amount_for_verify, verify_amount: 0, billing_address: { address1: 'street' } + end + + assert_equal 'Billing address including zip code is required for a 0 amount verify', error.message + end + + def test_verify_amount_with_zero_without_address + error = assert_raises(ArgumentError) do + @gateway.send :amount_for_verify, verify_amount: 0, billing_address: { zip: '051052' } + end + + assert_equal 'Billing address including zip code is required for a 0 amount verify', error.message + end + + def test_verify_amount_without_gsf + assert_equal 100, @gateway.send(:amount_for_verify, {}) + end + + def test_verify_amount_with_nil_value + assert_equal 100, @gateway.send(:amount_for_verify, { verify_amount: nil }) + end def pre_scrubbed <<-PRE_SCRUBBED @@ -1270,7 +1690,7 @@ def assert_address_truncated(data, expected_size, field) end def successful_purchase_response - <<-eos + <<-XML - eos + XML end def fraud_review_response - <<-eos + <<-XML - eos + XML end def address_not_provided_avs_response - <<-eos + <<-XML - eos + XML end def no_match_cvv_response - <<-eos + <<-XML - eos + XML end def no_match_avs_response - <<-eos + <<-XML - eos + XML end def failed_purchase_response - <<-eos + <<-XML @@ -1497,11 +1917,11 @@ def failed_purchase_response - eos + XML end def no_message_response - <<-eos + <<-XML @@ -1533,11 +1953,11 @@ def no_message_response - eos + XML end def successful_purchase_response_test_mode - <<-eos + <<-XML - eos + XML end def successful_authorize_response - <<-eos + <<-XML @@ -1616,11 +2036,11 @@ def successful_authorize_response - eos + XML end def failed_authorize_response - <<-eos + <<-XML @@ -1662,11 +2082,11 @@ def failed_authorize_response - eos + XML end def successful_capture_response - <<-eos + <<-XML @@ -1697,11 +2117,11 @@ def successful_capture_response - eos + XML end def already_actioned_capture_response - <<-eos + <<-XML @@ -1732,11 +2152,11 @@ def already_actioned_capture_response - eos + XML end def failed_capture_response - <<-eos + <<-XML Error @@ -1765,11 +2185,11 @@ def failed_capture_response - eos + XML end def successful_refund_response - <<-eos + <<-XML @@ -1799,11 +2219,11 @@ def successful_refund_response - eos + XML end def failed_refund_response - <<-eos + <<-XML @@ -1833,11 +2253,11 @@ def failed_refund_response - eos + XML end def successful_void_response - <<-eos + <<-XML @@ -1868,11 +2288,11 @@ def successful_void_response - eos + XML end def failed_void_response - <<-eos + <<-XML @@ -1904,11 +2324,11 @@ def failed_void_response - eos + XML end def successful_credit_response - <<-eos + <<-XML 1 @@ -1945,11 +2365,11 @@ def successful_credit_response - eos + XML end def failed_credit_response - <<-eos + <<-XML 1 @@ -1986,11 +2406,11 @@ def failed_credit_response - eos + XML end def successful_store_response - <<-eos + <<-XML @@ -2007,11 +2427,11 @@ def successful_store_response - eos + XML end def failed_store_response - <<-eos + <<-XML @@ -2025,11 +2445,11 @@ def failed_store_response - eos + XML end def successful_unstore_response - <<-eos + <<-XML @@ -2040,11 +2460,11 @@ def successful_unstore_response - eos + XML end def failed_unstore_response - <<-eos + <<-XML @@ -2055,11 +2475,11 @@ def failed_unstore_response - eos + XML end def successful_store_new_payment_profile_response - <<-eos + <<-XML @@ -2072,11 +2492,11 @@ def successful_store_new_payment_profile_response 38392170 34896759 - eos + XML end def failed_store_new_payment_profile_response - <<-eos + <<-XML @@ -2089,11 +2509,11 @@ def failed_store_new_payment_profile_response 38392767 34897359 - eos + XML end def successful_purchase_using_stored_card_response - <<-eos + <<-XML 1 @@ -2106,11 +2526,32 @@ def successful_purchase_using_stored_card_response 1,1,1,This transaction has been approved.,8HUT72,Y,2235700270,1,Store Purchase,1.01,CC,auth_capture,e385c780422f4bd182c4,Longbob,Longsen,,,,n/a,,,,,,,,,,,,,,,,,,,4A20EEAF89018FF075899DDB332E9D35,,2,,,,,,,,,,,XXXX2224,Visa,,,,,,,,,,,,,,,, - eos + XML + end + + def successful_purchase_using_stored_card_response_with_pipe_delimiter + <<-XML + + + 1 + + Ok + + I00001 + Successful. + + + 1|1|1|This transaction has been approved.|8HUT72|Y|2235700270|1|description, with, commas|1.01|CC|auth_capture|e385c780422f4bd182c4|Longbob|Longsen||||n/a|||||||||||||||||||4A20EEAF89018FF075899DDB332E9D35||2|||||||||||XXXX2224|Visa|||||||||||||||| + + XML + end + + def successful_purchase_using_stored_card_response_with_pipe_delimiter_and_quotes + "\xEF\xBB\xBF12345OkI00001Successful.\"1\"|\"1\"|\"1\"|\"This transaction has been approved.\"|\"001234\"|\"Y\"|\"12345667\"|\"654321\"|\"\"|\"39.95\"|\"CC\"|\"auth_capture\"|\"54321\"|\"Jane\"|\"Doe\"|\"\"|\"1 Main St.\"|\"Durham\"|\"NC\"|\"27707\"|\"US\"|\"\"|\"\"|\"test@example.com\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"XXXX1111\"|\"Visa\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"|\"\"" end def failed_purchase_using_stored_card_response - <<-eos + <<-XML 1 @@ -2123,11 +2564,11 @@ def failed_purchase_using_stored_card_response 3,1,6,The credit card number is invalid.,,P,0,1,Store Purchase,1.01,CC,auth_capture,2da01d7b583c706106e2,Longbob,Longsen,,,,n/a,,,,,,,,,,,,,,,,,,,13BA28EEA3593C13E2E3BC109D16E5D2,,,,,,,,,,,,,XXXX1222,,,,,,,,,,,,,,,,, - eos + XML end def successful_authorize_using_stored_card_response - <<-eos + <<-XML 1 @@ -2140,11 +2581,11 @@ def successful_authorize_using_stored_card_response 1,1,1,This transaction has been approved.,GGHQ5R,Y,2235700640,1,Store Purchase,1.01,CC,auth_only,0bde9d39f8eb9443f2af,Longbob,Longsen,,,,n/a,,,,,,,,,,,,,,,,,,,E47E5CA4F1239B00D39A7F8C147215D3,,2,,,,,,,,,,,XXXX2224,Visa,,,,,,,,,,,,,,,, - eos + XML end def failed_authorize_using_stored_card_response - <<-eos + <<-XML 1 @@ -2157,11 +2598,11 @@ def failed_authorize_using_stored_card_response 3,1,6,The credit card number is invalid.,,P,0,1,Store Purchase,1.01,CC,auth_only,f632442ce056f9f82ee9,Longbob,Longsen,,,,n/a,,,,,,,,,,,,,,,,,,,13BA28EEA3593C13E2E3BC109D16E5D2,,,,,,,,,,,,,XXXX1222,,,,,,,,,,,,,,,,, - eos + XML end def successful_capture_using_stored_card_response - <<-eos + <<-XML @@ -2174,11 +2615,11 @@ def successful_capture_using_stored_card_response 1,1,1,This transaction has been approved.,GGHQ5R,P,2235700640,1,,1.01,CC,prior_auth_capture,0bde9d39f8eb9443f2af,,,,,,,,,,,,,,,,,,,,,,,,,E47E5CA4F1239B00D39A7F8C147215D3,,,,,,,,,,,,,XXXX2224,Visa,,,,,,,,,,,,,,,, - eos + XML end def failed_capture_using_stored_card_response - <<-eos + <<-XML @@ -2191,11 +2632,11 @@ def failed_capture_using_stored_card_response 3,2,47,The amount requested for settlement cannot be greater than the original amount authorized.,,P,0,,,41.01,CC,prior_auth_capture,,,,,,,,,,,,,,,,,,,,,,,,,,8A556B125A1DA070AF5A84B798B7FBF7,,,,,,,,,,,,,,Visa,,,,,,,,,,,,,,,, - eos + XML end def failed_refund_using_stored_card_response - <<-eos + <<-XML 1 @@ -2207,12 +2648,11 @@ def failed_refund_using_stored_card_response - eos - + XML end def successful_void_using_stored_card_response - <<-eos + <<-XML @@ -2225,11 +2665,11 @@ def successful_void_using_stored_card_response 1,1,1,This transaction has been approved.,3R9YE2,P,2235701141,1,,0.00,CC,void,becdb509b35a32c30e97,,,,,,,,,,,,,,,,,,,,,,,,,C3C4B846B9D5A37D14462C2BF5B924FD,,,,,,,,,,,,,XXXX2224,Visa,,,,,,,,,,,,,,,, - eos + XML end def failed_void_using_stored_card_response - <<-eos + <<-XML @@ -2242,11 +2682,11 @@ def failed_void_using_stored_card_response 1,1,310,This transaction has already been voided.,,P,0,,,0.00,CC,void,,,,,,,,,,,,,,,,,,,,,,,,,,FD9FAE70BEF461997A6C15D7D597658D,,,,,,,,,,,,,,Visa,,,,,,,,,,,,,,,, - eos + XML end def network_tokenization_not_supported_response - <<-eos + <<-XML @@ -2288,11 +2728,11 @@ def network_tokenization_not_supported_response - eos + XML end def credentials_are_legit_response - <<-eos + <<-XML @@ -2303,11 +2743,11 @@ def credentials_are_legit_response - eos + XML end def credentials_are_bogus_response - <<-eos + <<-XML @@ -2318,11 +2758,11 @@ def credentials_are_bogus_response - eos + XML end def failed_refund_for_unsettled_payment_response - <<-eos + <<-XML @@ -2354,6 +2794,48 @@ def failed_refund_for_unsettled_payment_response - eos + XML + end + + def unsuccessful_authorize_response_for_jcb_card + <<-XML + + + 1 + + Error + + E00027 + The transaction was unsuccessful. + + + + 3 + + P + + + 0 + + + 0 + XXXX0017 + JCB + + + 289 + This processor does not accept zero dollar authorization for this card type. + + + + + x_currency_code + USD + + + + + + XML end end diff --git a/test/unit/gateways/axcessms_test.rb b/test/unit/gateways/axcessms_test.rb index e87a092498b..c4b0c9bfd12 100644 --- a/test/unit/gateways/axcessms_test.rb +++ b/test/unit/gateways/axcessms_test.rb @@ -21,12 +21,12 @@ def setup ip: '0.0.0.0', mode: @mode, billing_address: { - :address1 => '10 Marklar St', - :address2 => 'Musselburgh', - :city => 'Dunedin', - :zip => '9013', - :state => 'Otago', - :country => 'NZ' + address1: '10 Marklar St', + address2: 'Musselburgh', + city: 'Dunedin', + zip: '9013', + state: 'Otago', + country: 'NZ' } } end @@ -106,13 +106,13 @@ def test_failed_void @gateway.expects(:ssl_post).returns(failed_void_response) response = @gateway.refund(@amount - 30, 'authorization', @options) assert_failure response - assert_equal 'Reference Error - reversal needs at least one successful transaction of type (CP or DB or RB or PA)', response.message + assert_equal 'Reference Error - reversal needs at least one successful transaction of type (CP or DB or RB or PA)', response.message end def test_authorize_using_reference_sets_proper_elements stub_comms do @gateway.authorize(@amount, 'MY_AUTHORIZE_VALUE', @options) - end.check_request do |endpoint, body, headers| + end.check_request do |_endpoint, body, _headers| assert_xpath_text(body, '//ReferenceID', 'MY_AUTHORIZE_VALUE') assert_no_match(//, body) end.respond_with(successful_authorize_response) @@ -121,7 +121,7 @@ def test_authorize_using_reference_sets_proper_elements def test_purchase_using_reference_sets_proper_elements stub_comms do @gateway.purchase(@amount, 'MY_AUTHORIZE_VALUE', @options) - end.check_request do |endpoint, body, headers| + end.check_request do |_endpoint, body, _headers| assert_xpath_text(body, '//ReferenceID', 'MY_AUTHORIZE_VALUE') assert_no_match(//, body) end.respond_with(successful_authorize_response) @@ -129,8 +129,8 @@ def test_purchase_using_reference_sets_proper_elements def test_setting_mode_sets_proper_element stub_comms do - @gateway.purchase(@amount, 'MY_AUTHORIZE_VALUE', {mode: 'CRAZY_TEST_MODE'}) - end.check_request do |endpoint, body, headers| + @gateway.purchase(@amount, 'MY_AUTHORIZE_VALUE', { mode: 'CRAZY_TEST_MODE' }) + end.check_request do |_endpoint, body, _headers| assert_xpath_text(body, '//Transaction/@mode', 'CRAZY_TEST_MODE') end.respond_with(successful_authorize_response) end @@ -138,7 +138,7 @@ def test_setting_mode_sets_proper_element def test_defaults_to_integrator_test stub_comms do @gateway.purchase(@amount, 'MY_AUTHORIZE_VALUE', {}) - end.check_request do |endpoint, body, headers| + end.check_request do |_endpoint, body, _headers| assert_xpath_text(body, '//Transaction/@mode', 'INTEGRATOR_TEST') end.respond_with(successful_authorize_response) end diff --git a/test/unit/gateways/balanced_test.rb b/test/unit/gateways/balanced_test.rb index 652b27fd084..86d16dfc067 100644 --- a/test/unit/gateways/balanced_test.rb +++ b/test/unit/gateways/balanced_test.rb @@ -33,7 +33,7 @@ def test_successful_purchase def test_successful_purchase_with_outside_token response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, '/cards/CCVOX2d7Ar6Ze5TOxHsebeH', @options) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, endpoint, _data, _headers| assert_equal('https://api.balancedpayments.com/cards/CCVOX2d7Ar6Ze5TOxHsebeH/debits', endpoint) end.respond_with(debits_response) @@ -211,7 +211,7 @@ def test_store new_email_address = '%d@example.org' % Time.now assert response = @gateway.store(@credit_card, { - email: new_email_address + email: new_email_address }) assert_instance_of String, response.authorization end @@ -221,7 +221,7 @@ def test_successful_purchase_with_legacy_outside_token response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, legacy_outside_token, @options) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, endpoint, _data, _headers| assert_equal('https://api.balancedpayments.com/cards/CC7m1Mtqk6rVJo5tcD1qitAC/debits', endpoint) end.respond_with(debits_response) @@ -237,7 +237,7 @@ def test_capturing_legacy_authorizations [v1_authorization, v11_authorization].each do |authorization| stub_comms(@gateway, :ssl_request) do @gateway.capture(@amount, authorization) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, endpoint, _data, _headers| assert_equal('https://api.balancedpayments.com/card_holds/HL7dYMhpVBcqAYqxLF5mZtQ5/debits', endpoint) end.respond_with(authorized_debits_response) end @@ -250,7 +250,7 @@ def test_voiding_legacy_authorizations [v1_authorization, v11_authorization].each do |authorization| stub_comms(@gateway, :ssl_request) do @gateway.void(authorization) - end.check_request do |method, endpoint, data, headers| + end.check_request do |method, endpoint, data, _headers| assert_equal :put, method assert_equal('https://api.balancedpayments.com/card_holds/HL7dYMhpVBcqAYqxLF5mZtQ5', endpoint) assert_match %r{\bis_void=true\b}, data @@ -265,7 +265,7 @@ def test_refunding_legacy_purchases [v1_authorization, v11_authorization].each do |authorization| stub_comms(@gateway, :ssl_request) do @gateway.refund(nil, authorization) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, endpoint, _data, _headers| assert_equal('https://api.balancedpayments.com/debits/WD2x6vLS7RzHYEcdymqRyNAO/refunds', endpoint) end.respond_with(refunds_response) end @@ -275,9 +275,10 @@ def test_passing_address a = address response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card, address: a) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, endpoint, data, _headers| next if endpoint =~ /debits/ - clean = proc{|s| Regexp.escape(CGI.escape(s))} + + clean = proc { |s| Regexp.escape(CGI.escape(s)) } assert_match(%r{address\[line1\]=#{clean[a[:address1]]}}, data) assert_match(%r{address\[line2\]=#{clean[a[:address2]]}}, data) assert_match(%r{address\[city\]=#{clean[a[:city]]}}, data) @@ -292,8 +293,9 @@ def test_passing_address def test_passing_address_without_zip response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card, address: address(zip: nil)) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, endpoint, data, _headers| next if endpoint =~ /debits/ + assert_no_match(%r{address}, data) end.respond_with(cards_response, debits_response) @@ -303,8 +305,9 @@ def test_passing_address_without_zip def test_passing_address_with_blank_zip response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card, address: address(zip: ' ')) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, endpoint, data, _headers| next if endpoint =~ /debits/ + assert_no_match(%r{address}, data) end.respond_with(cards_response, debits_response) @@ -331,540 +334,539 @@ def invalid_login_response end def marketplace_response - <<-RESPONSE -{ - "meta": { - "last": "/marketplaces?limit=10&offset=0", - "next": null, - "href": "/marketplaces?limit=10&offset=0", - "limit": 10, - "offset": 0, - "previous": null, - "total": 1, - "first": "/marketplaces?limit=10&offset=0" - }, - "marketplaces": [ - { - "in_escrow": 47202, - "domain_url": "example.com", - "name": "Test Marketplace", - "links": { - "owner_customer": "AC73SN17anKkjk6Y1sVe2uaq" - }, - "href": "/marketplaces/TEST-MP73SaFdpQePv9dOaG5wXOGO", - "created_at": "2012-07-19T17:33:51.974238Z", - "support_email_address": "support@example.com", - "updated_at": "2012-07-19T17:33:52.848042Z", - "support_phone_number": "+16505551234", - "production": false, - "meta": {}, - "unsettled_fees": 0, - "id": "TEST-MP73SaFdpQePv9dOaG5wXOGO" - } - ], - "links": { - "marketplaces.debits": "/debits", - "marketplaces.reversals": "/reversals", - "marketplaces.customers": "/customers", - "marketplaces.credits": "/credits", - "marketplaces.cards": "/cards", - "marketplaces.card_holds": "/card_holds", - "marketplaces.refunds": "/refunds", - "marketplaces.owner_customer": "/customers/{marketplaces.owner_customer}", - "marketplaces.transactions": "/transactions", - "marketplaces.bank_accounts": "/bank_accounts", - "marketplaces.callbacks": "/callbacks", - "marketplaces.events": "/events" - } -} -RESPONSE + <<~RESPONSE + { + "meta": { + "last": "/marketplaces?limit=10&offset=0", + "next": null, + "href": "/marketplaces?limit=10&offset=0", + "limit": 10, + "offset": 0, + "previous": null, + "total": 1, + "first": "/marketplaces?limit=10&offset=0" + }, + "marketplaces": [ + { + "in_escrow": 47202, + "domain_url": "example.com", + "name": "Test Marketplace", + "links": { + "owner_customer": "AC73SN17anKkjk6Y1sVe2uaq" + }, + "href": "/marketplaces/TEST-MP73SaFdpQePv9dOaG5wXOGO", + "created_at": "2012-07-19T17:33:51.974238Z", + "support_email_address": "support@example.com", + "updated_at": "2012-07-19T17:33:52.848042Z", + "support_phone_number": "+16505551234", + "production": false, + "meta": {}, + "unsettled_fees": 0, + "id": "TEST-MP73SaFdpQePv9dOaG5wXOGO" + } + ], + "links": { + "marketplaces.debits": "/debits", + "marketplaces.reversals": "/reversals", + "marketplaces.customers": "/customers", + "marketplaces.credits": "/credits", + "marketplaces.cards": "/cards", + "marketplaces.card_holds": "/card_holds", + "marketplaces.refunds": "/refunds", + "marketplaces.owner_customer": "/customers/{marketplaces.owner_customer}", + "marketplaces.transactions": "/transactions", + "marketplaces.bank_accounts": "/bank_accounts", + "marketplaces.callbacks": "/callbacks", + "marketplaces.events": "/events" + } + } + RESPONSE end def cards_response - <<-RESPONSE -{ - "cards": [ - { - "cvv_match": null, - "links": { - "customer": null - }, - "name": "Longbob Longsen", - "expiration_year": 2015, - "avs_street_match": null, - "is_verified": true, - "created_at": "2014-02-06T23:19:27.146436Z", - "cvv_result": null, - "brand": "Visa", - "number": "xxxxxxxxxxxx1111", - "updated_at": "2014-02-06T23:19:27.146441Z", - "id": "CCXfdppSxXOGzaMUHp9EQyI", - "expiration_month": 9, - "cvv": null, - "meta": {}, - "href": "/cards/CCXfdppSxXOGzaMUHp9EQyI", - "address": { - "city": null, - "line2": null, - "line1": null, - "state": null, - "postal_code": null, - "country_code": null - }, - "fingerprint": "e0928a7fe2233bf6697413f663b3d94114358e6ac027fcd58ceba0bb37f05039", - "avs_postal_match": null, - "avs_result": null - } - ], - "links": { - "cards.card_holds": "/cards/{cards.id}/card_holds", - "cards.customer": "/customers/{cards.customer}", - "cards.debits": "/cards/{cards.id}/debits" - } -} -RESPONSE + <<~RESPONSE + { + "cards": [ + { + "cvv_match": null, + "links": { + "customer": null + }, + "name": "Longbob Longsen", + "expiration_year": 2015, + "avs_street_match": null, + "is_verified": true, + "created_at": "2014-02-06T23:19:27.146436Z", + "cvv_result": null, + "brand": "Visa", + "number": "xxxxxxxxxxxx1111", + "updated_at": "2014-02-06T23:19:27.146441Z", + "id": "CCXfdppSxXOGzaMUHp9EQyI", + "expiration_month": 9, + "cvv": null, + "meta": {}, + "href": "/cards/CCXfdppSxXOGzaMUHp9EQyI", + "address": { + "city": null, + "line2": null, + "line1": null, + "state": null, + "postal_code": null, + "country_code": null + }, + "fingerprint": "e0928a7fe2233bf6697413f663b3d94114358e6ac027fcd58ceba0bb37f05039", + "avs_postal_match": null, + "avs_result": null + } + ], + "links": { + "cards.card_holds": "/cards/{cards.id}/card_holds", + "cards.customer": "/customers/{cards.customer}", + "cards.debits": "/cards/{cards.id}/debits" + } + } + RESPONSE end def debits_response - <<-RESPONSE -{ - "debits": [ - { - "status": "succeeded", - "description": "Shopify Purchase", - "links": { - "customer": null, - "source": "CCXfdppSxXOGzaMUHp9EQyI", - "order": null, - "dispute": null - }, - "updated_at": "2014-02-06T23:19:29.690815Z", - "created_at": "2014-02-06T23:19:28.709143Z", - "transaction_number": "W250-112-1883", - "failure_reason": null, - "currency": "USD", - "amount": 100, - "failure_reason_code": null, - "meta": {}, - "href": "/debits/WDYZhc3mWCkxvOwIokeUz6M", - "appears_on_statement_as": "BAL*example.com", - "id": "WDYZhc3mWCkxvOwIokeUz6M" - } - ], - "links": { - "debits.customer": "/customers/{debits.customer}", - "debits.dispute": "/disputes/{debits.dispute}", - "debits.source": "/resources/{debits.source}", - "debits.order": "/orders/{debits.order}", - "debits.refunds": "/debits/{debits.id}/refunds", - "debits.events": "/debits/{debits.id}/events" - } -} -RESPONSE + <<~RESPONSE + { + "debits": [ + { + "status": "succeeded", + "description": "Shopify Purchase", + "links": { + "customer": null, + "source": "CCXfdppSxXOGzaMUHp9EQyI", + "order": null, + "dispute": null + }, + "updated_at": "2014-02-06T23:19:29.690815Z", + "created_at": "2014-02-06T23:19:28.709143Z", + "transaction_number": "W250-112-1883", + "failure_reason": null, + "currency": "USD", + "amount": 100, + "failure_reason_code": null, + "meta": {}, + "href": "/debits/WDYZhc3mWCkxvOwIokeUz6M", + "appears_on_statement_as": "BAL*example.com", + "id": "WDYZhc3mWCkxvOwIokeUz6M" + } + ], + "links": { + "debits.customer": "/customers/{debits.customer}", + "debits.dispute": "/disputes/{debits.dispute}", + "debits.source": "/resources/{debits.source}", + "debits.order": "/orders/{debits.order}", + "debits.refunds": "/debits/{debits.id}/refunds", + "debits.events": "/debits/{debits.id}/events" + } + } + RESPONSE end def authorized_debits_response - <<-RESPONSE -{ - "debits": [ - { - "status": "succeeded", - "description": "Shopify Purchase", - "links": { - "customer": null, - "source": "CC2uKKcUhaSFRrl2mnGPSbDO", - "order": null, - "dispute": null - }, - "updated_at": "2014-02-06T23:19:29.690815Z", - "created_at": "2014-02-06T23:19:28.709143Z", - "transaction_number": "W250-112-1883", - "failure_reason": null, - "currency": "USD", - "amount": 100, - "failure_reason_code": null, - "meta": {}, - "href": "/debits/WDYZhc3mWCkxvOwIokeUz6M", - "appears_on_statement_as": "BAL*example.com", - "id": "WDYZhc3mWCkxvOwIokeUz6M" - } - ], - "links": { - "debits.customer": "/customers/{debits.customer}", - "debits.dispute": "/disputes/{debits.dispute}", - "debits.source": "/resources/{debits.source}", - "debits.order": "/orders/{debits.order}", - "debits.refunds": "/debits/{debits.id}/refunds", - "debits.events": "/debits/{debits.id}/events" - } -} -RESPONSE + <<~RESPONSE + { + "debits": [ + { + "status": "succeeded", + "description": "Shopify Purchase", + "links": { + "customer": null, + "source": "CC2uKKcUhaSFRrl2mnGPSbDO", + "order": null, + "dispute": null + }, + "updated_at": "2014-02-06T23:19:29.690815Z", + "created_at": "2014-02-06T23:19:28.709143Z", + "transaction_number": "W250-112-1883", + "failure_reason": null, + "currency": "USD", + "amount": 100, + "failure_reason_code": null, + "meta": {}, + "href": "/debits/WDYZhc3mWCkxvOwIokeUz6M", + "appears_on_statement_as": "BAL*example.com", + "id": "WDYZhc3mWCkxvOwIokeUz6M" + } + ], + "links": { + "debits.customer": "/customers/{debits.customer}", + "debits.dispute": "/disputes/{debits.dispute}", + "debits.source": "/resources/{debits.source}", + "debits.order": "/orders/{debits.order}", + "debits.refunds": "/debits/{debits.id}/refunds", + "debits.events": "/debits/{debits.id}/events" + } + } + RESPONSE end def authorized_partial_debits_response - <<-RESPONSE -{ - "debits": [ - { - "status": "succeeded", - "description": "Shopify Purchase", - "links": { - "customer": null, - "source": "CC2uKKcUhaSFRrl2mnGPSbDO", - "order": null, - "dispute": null - }, - "updated_at": "2014-02-06T23:19:29.690815Z", - "created_at": "2014-02-06T23:19:28.709143Z", - "transaction_number": "W250-112-1883", - "failure_reason": null, - "currency": "USD", - "amount": 50, - "failure_reason_code": null, - "meta": {}, - "href": "/debits/WDYZhc3mWCkxvOwIokeUz6M", - "appears_on_statement_as": "BAL*example.com", - "id": "WDYZhc3mWCkxvOwIokeUz6M" - } - ], - "links": { - "debits.customer": "/customers/{debits.customer}", - "debits.dispute": "/disputes/{debits.dispute}", - "debits.source": "/resources/{debits.source}", - "debits.order": "/orders/{debits.order}", - "debits.refunds": "/debits/{debits.id}/refunds", - "debits.events": "/debits/{debits.id}/events" - } -} -RESPONSE + <<~RESPONSE + { + "debits": [ + { + "status": "succeeded", + "description": "Shopify Purchase", + "links": { + "customer": null, + "source": "CC2uKKcUhaSFRrl2mnGPSbDO", + "order": null, + "dispute": null + }, + "updated_at": "2014-02-06T23:19:29.690815Z", + "created_at": "2014-02-06T23:19:28.709143Z", + "transaction_number": "W250-112-1883", + "failure_reason": null, + "currency": "USD", + "amount": 50, + "failure_reason_code": null, + "meta": {}, + "href": "/debits/WDYZhc3mWCkxvOwIokeUz6M", + "appears_on_statement_as": "BAL*example.com", + "id": "WDYZhc3mWCkxvOwIokeUz6M" + } + ], + "links": { + "debits.customer": "/customers/{debits.customer}", + "debits.dispute": "/disputes/{debits.dispute}", + "debits.source": "/resources/{debits.source}", + "debits.order": "/orders/{debits.order}", + "debits.refunds": "/debits/{debits.id}/refunds", + "debits.events": "/debits/{debits.id}/events" + } + } + RESPONSE end - def declined_response - <<-RESPONSE -{ - "errors": [ - { - "status": "Payment Required", - "category_code": "card-declined", - "additional": "Customer call bank", - "status_code": 402, - "category_type": "banking", - "extras": {}, - "request_id": "OHMc8d80eb4903011e390c002a1fe53e539", - "description": "R530: Customer call bank. Your request id is OHMc8d80eb4903011e390c002a1fe53e539." - } - ] -} -RESPONSE + <<~RESPONSE + { + "errors": [ + { + "status": "Payment Required", + "category_code": "card-declined", + "additional": "Customer call bank", + "status_code": 402, + "category_type": "banking", + "extras": {}, + "request_id": "OHMc8d80eb4903011e390c002a1fe53e539", + "description": "R530: Customer call bank. Your request id is OHMc8d80eb4903011e390c002a1fe53e539." + } + ] + } + RESPONSE end def bad_email_response - <<-'RESPONSE' -{ - "errors": [ - { - "status": "Bad Request", - "category_code": "request", - "additional": null, - "status_code": 400, - "category_type": "request", - "extras": { - "email": "\"invalid_email\" must be a valid email address as specified by RFC-2822" - }, - "request_id": "OHM9107a4bc903111e390c002a1fe53e539", - "description": "Invalid field [email] - \"invalid_email\" must be a valid email address as specified by RFC-2822 Your request id is OHM9107a4bc903111e390c002a1fe53e539." - } - ] -} -RESPONSE + <<~'RESPONSE' + { + "errors": [ + { + "status": "Bad Request", + "category_code": "request", + "additional": null, + "status_code": 400, + "category_type": "request", + "extras": { + "email": "\"invalid_email\" must be a valid email address as specified by RFC-2822" + }, + "request_id": "OHM9107a4bc903111e390c002a1fe53e539", + "description": "Invalid field [email] - \"invalid_email\" must be a valid email address as specified by RFC-2822 Your request id is OHM9107a4bc903111e390c002a1fe53e539." + } + ] + } + RESPONSE end def account_frozen_response - <<-RESPONSE -{ - "errors": [ - { - "status": "Payment Required", - "category_code": "card-declined", - "additional": "Account Frozen", - "status_code": 402, - "category_type": "banking", - "extras": {}, - "request_id": "OHMec50b6be903c11e387cb026ba7cac9da", - "description": "R758: Account Frozen. Your request id is OHMec50b6be903c11e387cb026ba7cac9da." - } - ], - "links": { - "debits.customer": "/customers/{debits.customer}", - "debits.dispute": "/disputes/{debits.dispute}", - "debits.source": "/resources/{debits.source}", - "debits.order": "/orders/{debits.order}", - "debits.refunds": "/debits/{debits.id}/refunds", - "debits.events": "/debits/{debits.id}/events" - }, - "debits": [ - { - "status": "failed", - "description": "Shopify Purchase", - "links": { - "customer": null, - "source": "CC7a41DYIaSSyGoau6rZ8VcG", - "order": null, - "dispute": null - }, - "updated_at": "2014-02-07T21:15:10.107464Z", - "created_at": "2014-02-07T21:15:09.206335Z", - "transaction_number": "W202-883-1157", - "failure_reason": "R758: Account Frozen.", - "currency": "USD", - "amount": 100, - "failure_reason_code": "card-declined", - "meta": {}, - "href": "/debits/WD7cjQ5gizGWMDWbxDndgm7w", - "appears_on_statement_as": "BAL*example.com", - "id": "WD7cjQ5gizGWMDWbxDndgm7w" - } - ] -} -RESPONSE + <<~RESPONSE + { + "errors": [ + { + "status": "Payment Required", + "category_code": "card-declined", + "additional": "Account Frozen", + "status_code": 402, + "category_type": "banking", + "extras": {}, + "request_id": "OHMec50b6be903c11e387cb026ba7cac9da", + "description": "R758: Account Frozen. Your request id is OHMec50b6be903c11e387cb026ba7cac9da." + } + ], + "links": { + "debits.customer": "/customers/{debits.customer}", + "debits.dispute": "/disputes/{debits.dispute}", + "debits.source": "/resources/{debits.source}", + "debits.order": "/orders/{debits.order}", + "debits.refunds": "/debits/{debits.id}/refunds", + "debits.events": "/debits/{debits.id}/events" + }, + "debits": [ + { + "status": "failed", + "description": "Shopify Purchase", + "links": { + "customer": null, + "source": "CC7a41DYIaSSyGoau6rZ8VcG", + "order": null, + "dispute": null + }, + "updated_at": "2014-02-07T21:15:10.107464Z", + "created_at": "2014-02-07T21:15:09.206335Z", + "transaction_number": "W202-883-1157", + "failure_reason": "R758: Account Frozen.", + "currency": "USD", + "amount": 100, + "failure_reason_code": "card-declined", + "meta": {}, + "href": "/debits/WD7cjQ5gizGWMDWbxDndgm7w", + "appears_on_statement_as": "BAL*example.com", + "id": "WD7cjQ5gizGWMDWbxDndgm7w" + } + ] + } + RESPONSE end def appears_on_response - <<-RESPONSE -{ - "debits": [ - { - "status": "succeeded", - "description": "Shopify Purchase", - "links": { - "customer": null, - "source": "CC4SKo0WY3lhfWc6CgMyPo34", - "order": null, - "dispute": null - }, - "updated_at": "2014-02-07T21:20:13.950392Z", - "created_at": "2014-02-07T21:20:12.737821Z", - "transaction_number": "W337-477-3752", - "failure_reason": null, - "currency": "USD", - "amount": 100, - "failure_reason_code": null, - "meta": {}, - "href": "/debits/WD4UDDm6iqtYMEd21UBaa50H", - "appears_on_statement_as": "BAL*Homer Electric", - "id": "WD4UDDm6iqtYMEd21UBaa50H" - } - ], - "links": { - "debits.customer": "/customers/{debits.customer}", - "debits.dispute": "/disputes/{debits.dispute}", - "debits.source": "/resources/{debits.source}", - "debits.order": "/orders/{debits.order}", - "debits.refunds": "/debits/{debits.id}/refunds", - "debits.events": "/debits/{debits.id}/events" - } -} -RESPONSE + <<~RESPONSE + { + "debits": [ + { + "status": "succeeded", + "description": "Shopify Purchase", + "links": { + "customer": null, + "source": "CC4SKo0WY3lhfWc6CgMyPo34", + "order": null, + "dispute": null + }, + "updated_at": "2014-02-07T21:20:13.950392Z", + "created_at": "2014-02-07T21:20:12.737821Z", + "transaction_number": "W337-477-3752", + "failure_reason": null, + "currency": "USD", + "amount": 100, + "failure_reason_code": null, + "meta": {}, + "href": "/debits/WD4UDDm6iqtYMEd21UBaa50H", + "appears_on_statement_as": "BAL*Homer Electric", + "id": "WD4UDDm6iqtYMEd21UBaa50H" + } + ], + "links": { + "debits.customer": "/customers/{debits.customer}", + "debits.dispute": "/disputes/{debits.dispute}", + "debits.source": "/resources/{debits.source}", + "debits.order": "/orders/{debits.order}", + "debits.refunds": "/debits/{debits.id}/refunds", + "debits.events": "/debits/{debits.id}/events" + } + } + RESPONSE end def holds_response - <<-RESPONSE -{ - "card_holds": [ - { - "status": "succeeded", - "description": "Shopify Purchase", - "links": { - "card": "CC2uKKcUhaSFRrl2mnGPSbDO", - "debit": null - }, - "updated_at": "2014-02-07T21:46:39.678439Z", - "created_at": "2014-02-07T21:46:39.303526Z", - "transaction_number": "HL343-028-3032", - "expires_at": "2014-02-14T21:46:39.532363Z", - "failure_reason": null, - "currency": "USD", - "amount": 100, - "meta": {}, - "href": "/card_holds/HL2wPXf6ByqkLMiWGab7QRsq", - "failure_reason_code": null, - "voided_at": null, - "id": "HL2wPXf6ByqkLMiWGab7QRsq" - } - ], - "links": { - "card_holds.events": "/card_holds/{card_holds.id}/events", - "card_holds.card": "/resources/{card_holds.card}", - "card_holds.debits": "/card_holds/{card_holds.id}/debits", - "card_holds.debit": "/debits/{card_holds.debit}" - } -} -RESPONSE + <<~RESPONSE + { + "card_holds": [ + { + "status": "succeeded", + "description": "Shopify Purchase", + "links": { + "card": "CC2uKKcUhaSFRrl2mnGPSbDO", + "debit": null + }, + "updated_at": "2014-02-07T21:46:39.678439Z", + "created_at": "2014-02-07T21:46:39.303526Z", + "transaction_number": "HL343-028-3032", + "expires_at": "2014-02-14T21:46:39.532363Z", + "failure_reason": null, + "currency": "USD", + "amount": 100, + "meta": {}, + "href": "/card_holds/HL2wPXf6ByqkLMiWGab7QRsq", + "failure_reason_code": null, + "voided_at": null, + "id": "HL2wPXf6ByqkLMiWGab7QRsq" + } + ], + "links": { + "card_holds.events": "/card_holds/{card_holds.id}/events", + "card_holds.card": "/resources/{card_holds.card}", + "card_holds.debits": "/card_holds/{card_holds.id}/debits", + "card_holds.debit": "/debits/{card_holds.debit}" + } + } + RESPONSE end def method_not_allowed_response - <<-RESPONSE -{ - "errors": [ - { - "status": "Method Not Allowed", - "category_code": "method-not-allowed", - "description": "Your request id is OHMfaf5570a904211e3bcab026ba7f8ec28.", - "status_code": 405, - "category_type": "request", - "request_id": "OHMfaf5570a904211e3bcab026ba7f8ec28" - } - ] -} -RESPONSE + <<~RESPONSE + { + "errors": [ + { + "status": "Method Not Allowed", + "category_code": "method-not-allowed", + "description": "Your request id is OHMfaf5570a904211e3bcab026ba7f8ec28.", + "status_code": 405, + "category_type": "request", + "request_id": "OHMfaf5570a904211e3bcab026ba7f8ec28" + } + ] + } + RESPONSE end def unauthorized_response - <<-RESPONSE -{ - "errors": [ - { - "status": "Unauthorized", - "category_code": "authentication-required", - "description": "

The server could not verify that you are authorized to access the URL requested. You either supplied the wrong credentials (e.g. a bad password), or your browser doesn't understand how to supply the credentials required.

In case you are allowed to request the document, please check your user-id and password and try again.

Your request id is OHM56702560904311e3988c026ba7cd33d0.", - "status_code": 401, - "category_type": "permission", - "request_id": "OHM56702560904311e3988c026ba7cd33d0" - } - ] -} -RESPONSE + <<~RESPONSE + { + "errors": [ + { + "status": "Unauthorized", + "category_code": "authentication-required", + "description": "

The server could not verify that you are authorized to access the URL requested. You either supplied the wrong credentials (e.g. a bad password), or your browser doesn't understand how to supply the credentials required.

In case you are allowed to request the document, please check your user-id and password and try again.

Your request id is OHM56702560904311e3988c026ba7cd33d0.", + "status_code": 401, + "category_type": "permission", + "request_id": "OHM56702560904311e3988c026ba7cd33d0" + } + ] + } + RESPONSE end def voided_hold_response - <<-RESPONSE -{ - "card_holds": [ - { - "status": "succeeded", - "description": "Shopify Purchase", - "links": { - "card": "CC52ACcRnrG5eupOERKK4OAq", - "debit": null - }, - "updated_at": "2014-02-07T22:10:28.923304Z", - "created_at": "2014-02-07T22:10:27.904233Z", - "transaction_number": "HL728-165-8425", - "expires_at": "2014-02-14T22:10:28.045745Z", - "failure_reason": null, - "currency": "USD", - "amount": 100, - "meta": {}, - "href": "/card_holds/HL54qindwhlErSujLo5IcP5J", - "failure_reason_code": null, - "voided_at": "2014-02-07T22:10:28.923308Z", - "id": "HL54qindwhlErSujLo5IcP5J" - } - ], - "links": { - "card_holds.events": "/card_holds/{card_holds.id}/events", - "card_holds.card": "/resources/{card_holds.card}", - "card_holds.debits": "/card_holds/{card_holds.id}/debits", - "card_holds.debit": "/debits/{card_holds.debit}" - } -} -RESPONSE + <<~RESPONSE + { + "card_holds": [ + { + "status": "succeeded", + "description": "Shopify Purchase", + "links": { + "card": "CC52ACcRnrG5eupOERKK4OAq", + "debit": null + }, + "updated_at": "2014-02-07T22:10:28.923304Z", + "created_at": "2014-02-07T22:10:27.904233Z", + "transaction_number": "HL728-165-8425", + "expires_at": "2014-02-14T22:10:28.045745Z", + "failure_reason": null, + "currency": "USD", + "amount": 100, + "meta": {}, + "href": "/card_holds/HL54qindwhlErSujLo5IcP5J", + "failure_reason_code": null, + "voided_at": "2014-02-07T22:10:28.923308Z", + "id": "HL54qindwhlErSujLo5IcP5J" + } + ], + "links": { + "card_holds.events": "/card_holds/{card_holds.id}/events", + "card_holds.card": "/resources/{card_holds.card}", + "card_holds.debits": "/card_holds/{card_holds.id}/debits", + "card_holds.debit": "/debits/{card_holds.debit}" + } + } + RESPONSE end def refunds_response - <<-RESPONSE -{ - "links": { - "refunds.dispute": "/disputes/{refunds.dispute}", - "refunds.events": "/refunds/{refunds.id}/events", - "refunds.debit": "/debits/{refunds.debit}", - "refunds.order": "/orders/{refunds.order}" - }, - "refunds": [ - { - "status": "succeeded", - "description": "Shopify Purchase", - "links": { - "debit": "WDAtJcbjh3EJLW0tp7CUxAk", - "order": null, - "dispute": null - }, - "href": "/refunds/RFJ4N00zLaQFrfBkC8cbN68", - "created_at": "2014-02-07T22:35:06.424855Z", - "transaction_number": "RF424-240-3258", - "updated_at": "2014-02-07T22:35:07.655276Z", - "currency": "USD", - "amount": 100, - "meta": {}, - "id": "RFJ4N00zLaQFrfBkC8cbN68" - } - ] -} -RESPONSE + <<~RESPONSE + { + "links": { + "refunds.dispute": "/disputes/{refunds.dispute}", + "refunds.events": "/refunds/{refunds.id}/events", + "refunds.debit": "/debits/{refunds.debit}", + "refunds.order": "/orders/{refunds.order}" + }, + "refunds": [ + { + "status": "succeeded", + "description": "Shopify Purchase", + "links": { + "debit": "WDAtJcbjh3EJLW0tp7CUxAk", + "order": null, + "dispute": null + }, + "href": "/refunds/RFJ4N00zLaQFrfBkC8cbN68", + "created_at": "2014-02-07T22:35:06.424855Z", + "transaction_number": "RF424-240-3258", + "updated_at": "2014-02-07T22:35:07.655276Z", + "currency": "USD", + "amount": 100, + "meta": {}, + "id": "RFJ4N00zLaQFrfBkC8cbN68" + } + ] + } + RESPONSE end def partial_refunds_response - <<-RESPONSE -{ - "links": { - "refunds.dispute": "/disputes/{refunds.dispute}", - "refunds.events": "/refunds/{refunds.id}/events", - "refunds.debit": "/debits/{refunds.debit}", - "refunds.order": "/orders/{refunds.order}" - }, - "refunds": [ - { - "status": "succeeded", - "description": "Shopify Purchase", - "links": { - "debit": "WDAtJcbjh3EJLW0tp7CUxAk", - "order": null, - "dispute": null - }, - "href": "/refunds/RFJ4N00zLaQFrfBkC8cbN68", - "created_at": "2014-02-07T22:35:06.424855Z", - "transaction_number": "RF424-240-3258", - "updated_at": "2014-02-07T22:35:07.655276Z", - "currency": "USD", - "amount": 50, - "meta": {}, - "id": "RFJ4N00zLaQFrfBkC8cbN68" - } - ] -} -RESPONSE + <<~RESPONSE + { + "links": { + "refunds.dispute": "/disputes/{refunds.dispute}", + "refunds.events": "/refunds/{refunds.id}/events", + "refunds.debit": "/debits/{refunds.debit}", + "refunds.order": "/orders/{refunds.order}" + }, + "refunds": [ + { + "status": "succeeded", + "description": "Shopify Purchase", + "links": { + "debit": "WDAtJcbjh3EJLW0tp7CUxAk", + "order": null, + "dispute": null + }, + "href": "/refunds/RFJ4N00zLaQFrfBkC8cbN68", + "created_at": "2014-02-07T22:35:06.424855Z", + "transaction_number": "RF424-240-3258", + "updated_at": "2014-02-07T22:35:07.655276Z", + "currency": "USD", + "amount": 50, + "meta": {}, + "id": "RFJ4N00zLaQFrfBkC8cbN68" + } + ] + } + RESPONSE end def refunds_pending_response - <<-RESPONSE -{ - "links": { - "refunds.dispute": "/disputes/{refunds.dispute}", - "refunds.events": "/refunds/{refunds.id}/events", - "refunds.debit": "/debits/{refunds.debit}", - "refunds.order": "/orders/{refunds.order}" - }, - "refunds": [ - { - "status": "pending", - "description": null, - "links": { - "debit": "WD7AT5AGKI0jccoElAEEqiuL", - "order": null, - "dispute": null - }, - "href": "/refunds/RF46a5p6ZVMK4qVIeCJ8u2LE", - "created_at": "2014-05-22T20:20:32.956467Z", - "transaction_number": "RF485-302-2551", - "updated_at": "2014-05-22T20:20:35.991553Z", - "currency": "USD", - "amount": 100, - "meta": {}, - "id": "RF46a5p6ZVMK4qVIeCJ8u2LE" - } - ] -} -RESPONSE + <<~RESPONSE + { + "links": { + "refunds.dispute": "/disputes/{refunds.dispute}", + "refunds.events": "/refunds/{refunds.id}/events", + "refunds.debit": "/debits/{refunds.debit}", + "refunds.order": "/orders/{refunds.order}" + }, + "refunds": [ + { + "status": "pending", + "description": null, + "links": { + "debit": "WD7AT5AGKI0jccoElAEEqiuL", + "order": null, + "dispute": null + }, + "href": "/refunds/RF46a5p6ZVMK4qVIeCJ8u2LE", + "created_at": "2014-05-22T20:20:32.956467Z", + "transaction_number": "RF485-302-2551", + "updated_at": "2014-05-22T20:20:35.991553Z", + "currency": "USD", + "amount": 100, + "meta": {}, + "id": "RF46a5p6ZVMK4qVIeCJ8u2LE" + } + ] + } + RESPONSE end end diff --git a/test/unit/gateways/bambora_apac_test.rb b/test/unit/gateways/bambora_apac_test.rb new file mode 100644 index 00000000000..c31335dfdc4 --- /dev/null +++ b/test/unit/gateways/bambora_apac_test.rb @@ -0,0 +1,233 @@ +require 'test_helper' + +class BamboraApacTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = BamboraApacGateway.new( + username: 'username', + password: 'password' + ) + + @amount = 100 + @credit_card = credit_card + end + + def test_successful_purchase + response = stub_comms do + @gateway.purchase(@amount, @credit_card, order_id: 1) + end.check_request do |_endpoint, data, _headers| + assert_match(%r{username<}, data) + assert_match(%r{password<}, data) + assert_match(%r{1<}, data) + assert_match(%r{100<}, data) + assert_match(%r{1<}, data) + assert_match(%r{#{@credit_card.number}<}, data) + assert_match(%r{#{"%02d" % @credit_card.month}<}, data) + assert_match(%r{#{@credit_card.year}<}, data) + assert_match(%r{#{@credit_card.verification_value}<}, data) + assert_match(%r{#{@credit_card.name}<}, data) + end.respond_with(successful_purchase_response) + + assert_success response + assert_equal '89435577', response.authorization + end + + def test_failed_purchase + response = stub_comms do + @gateway.purchase(@amount, @credit_card) + end.respond_with(failed_purchase_response) + + assert_failure response + assert_equal 'Do Not Honour', response.message + assert_equal Gateway::STANDARD_ERROR_CODE[:card_declined], response.error_code + assert_equal '', response.authorization + end + + def test_successful_authorize + response = stub_comms do + @gateway.authorize(@amount, @credit_card, order_id: 1) + end.check_request do |_endpoint, data, _headers| + assert_match(%r{1<}, data) + assert_match(%r{2<}, data) + end.respond_with(successful_authorize_response) + + assert_success response + assert_equal '89435583', response.authorization + end + + def test_successful_capture + response = stub_comms do + @gateway.capture(@amount, 'receipt') + end.check_request do |_endpoint, data, _headers| + assert_match(%r{receipt<}, data) + assert_match(%r{100<}, data) + end.respond_with(successful_capture_response) + + assert_success response + end + + def test_successful_refund + response = stub_comms do + @gateway.refund(@amount, 'receipt') + end.check_request do |_endpoint, data, _headers| + assert_match(%r{receipt<}, data) + assert_match(%r{100<}, data) + end.respond_with(successful_refund_response) + + assert_success response + end + + def test_successful_void + response = stub_comms do + @gateway.void('receipt', amount: 200) + end.check_request do |_endpoint, data, _headers| + assert_match(%r{200<}, data) + end.respond_with(successful_void_response) + + assert_success response + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + private + + def pre_scrubbed + <<~'PRE_SCRUBBED' + opening connection to demo.ippayments.com.au:443... + opened + starting SSL for demo.ippayments.com.au:443... + SSL established + <- "POST /interface/api/dts.asmx HTTP/1.1\r\nContent-Type: text/xml; charset=utf-8\r\nSoapaction: http://www.ippayments.com.au/interface/api/dts/SubmitSinglePayment\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: demo.ippayments.com.au\r\nContent-Length: 822\r\n\r\n" + <- "\n\n \n \n \n \n 1\n \n 1\n \n 4005550000000001\n 09\n 2015\n 123\n Longbob Longsen\n \n \n nmi.api\n qwerty123\n \n \n\n]]>\n \n \n \n\n" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: Microsoft-IIS/6.0\r\n" + -> "X-Robots-Tag: noindex\r\n" + -> "X-Powered-By: ASP.NET\r\n" + -> "Cache-Control: private, max-age=0\r\n" + -> "Content-Type: text/xml; charset=utf-8\r\n" + -> "Content-Length: 767\r\n" + -> "Date: Fri, 19 Dec 2014 19:55:13 GMT\r\n" + -> "Connection: close\r\n" + -> "\r\n" + reading 767 bytes... + -> "<Response>\r\n\t<ResponseCode>1</ResponseCode>\r\n\t<Timestamp>20-Dec-2014 06:55:17</Timestamp>\r\n\t<Receipt></Receipt>\r\n\t<SettlementDate></SettlementDate>\r\n\t<DeclinedCode>183</DeclinedCode>\r\n\t<DeclinedMessage>Exception parsing transaction XML</DeclinedMessage>\r\n</Response>\r\n" + read 767 bytes + Conn close + PRE_SCRUBBED + end + + def post_scrubbed + <<~'POST_SCRUBBED' + opening connection to demo.ippayments.com.au:443... + opened + starting SSL for demo.ippayments.com.au:443... + SSL established + <- "POST /interface/api/dts.asmx HTTP/1.1\r\nContent-Type: text/xml; charset=utf-8\r\nSoapaction: http://www.ippayments.com.au/interface/api/dts/SubmitSinglePayment\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: demo.ippayments.com.au\r\nContent-Length: 822\r\n\r\n" + <- "\n\n \n \n \n \n 1\n \n 1\n \n [FILTERED]\n 09\n 2015\n [FILTERED]\n Longbob Longsen\n \n \n nmi.api\n [FILTERED]\n \n \n\n]]>\n \n \n \n\n" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: Microsoft-IIS/6.0\r\n" + -> "X-Robots-Tag: noindex\r\n" + -> "X-Powered-By: ASP.NET\r\n" + -> "Cache-Control: private, max-age=0\r\n" + -> "Content-Type: text/xml; charset=utf-8\r\n" + -> "Content-Length: 767\r\n" + -> "Date: Fri, 19 Dec 2014 19:55:13 GMT\r\n" + -> "Connection: close\r\n" + -> "\r\n" + reading 767 bytes... + -> "<Response>\r\n\t<ResponseCode>1</ResponseCode>\r\n\t<Timestamp>20-Dec-2014 06:55:17</Timestamp>\r\n\t<Receipt></Receipt>\r\n\t<SettlementDate></SettlementDate>\r\n\t<DeclinedCode>183</DeclinedCode>\r\n\t<DeclinedMessage>Exception parsing transaction XML</DeclinedMessage>\r\n</Response>\r\n" + read 767 bytes + Conn close + POST_SCRUBBED + end + + def successful_purchase_response + <<~XML + <Response> + <ResponseCode>0</ResponseCode> + <Timestamp>20-Dec-2014 04:07:39</Timestamp> + <Receipt>89435577</Receipt> + <SettlementDate>22-Dec-2014</SettlementDate> + <DeclinedCode></DeclinedCode> + <DeclinedMessage></DeclinedMessage> + </Response> + + XML + end + + def failed_purchase_response + <<~XML + <Response> + <ResponseCode>1</ResponseCode> + <Timestamp>20-Dec-2014 04:14:56</Timestamp> + <Receipt></Receipt> + <SettlementDate>22-Dec-2014</SettlementDate> + <DeclinedCode>05</DeclinedCode> + <DeclinedMessage>Do Not Honour</DeclinedMessage> + </Response> + + XML + end + + def successful_authorize_response + <<~XML + <Response> + <ResponseCode>0</ResponseCode> + <Timestamp>20-Dec-2014 04:18:13</Timestamp> + <Receipt>89435583</Receipt> + <SettlementDate>22-Dec-2014</SettlementDate> + <DeclinedCode></DeclinedCode> + <DeclinedMessage></DeclinedMessage> + </Response> + + XML + end + + def successful_capture_response + <<~XML + <Response> + <ResponseCode>0</ResponseCode> + <Timestamp>20-Dec-2014 04:18:15</Timestamp> + <Receipt>89435584</Receipt> + <SettlementDate>22-Dec-2014</SettlementDate> + <DeclinedCode></DeclinedCode> + <DeclinedMessage></DeclinedMessage> + </Response> + + XML + end + + def successful_refund_response + <<~XML + <Response> + <ResponseCode>0</ResponseCode> + <Timestamp>20-Dec-2014 04:24:51</Timestamp> + <Receipt>89435596</Receipt> + <SettlementDate>22-Dec-2014</SettlementDate> + <DeclinedCode></DeclinedCode> + <DeclinedMessage></DeclinedMessage> + </Response> + + XML + end + + def successful_void_response + <<~XML + <Response> + <ResponseCode>0</ResponseCode> + <DeclinedCode></DeclinedCode> + <DeclinedMessage></DeclinedMessage> + </Response> + + XML + end +end diff --git a/test/unit/gateways/bank_frick_test.rb b/test/unit/gateways/bank_frick_test.rb index 8ab20b5e9be..4c49abc68c5 100644 --- a/test/unit/gateways/bank_frick_test.rb +++ b/test/unit/gateways/bank_frick_test.rb @@ -6,7 +6,7 @@ def setup sender: 'sender-uuid', channel: 'channel-uuid', userid: 'user-uuid', - userpwd: 'password', + userpwd: 'password' ) @credit_card = credit_card diff --git a/test/unit/gateways/banwire_test.rb b/test/unit/gateways/banwire_test.rb index e7466ba51bc..5bbc177913a 100644 --- a/test/unit/gateways/banwire_test.rb +++ b/test/unit/gateways/banwire_test.rb @@ -5,32 +5,37 @@ class BanwireTest < Test::Unit::TestCase def setup @gateway = BanwireGateway.new( - :login => 'desarrollo', - :currency => 'MXN') - - @credit_card = credit_card('5204164299999999', - :month => 11, - :year => 2012, - :verification_value => '999') + login: 'desarrollo', + currency: 'MXN' + ) + + @credit_card = credit_card( + '5204164299999999', + month: 11, + year: 2012, + verification_value: '999' + ) @amount = 100 @options = { - :order_id => '1', - :email => 'test@email.com', - :billing_address => address, - :description => 'Store purchase' + order_id: '1', + email: 'test@email.com', + billing_address: address, + description: 'Store purchase' } - @amex_credit_card = credit_card('375932134599999', - :month => 3, - :year => 2017, - :first_name => 'Banwire', - :last_name => 'Test Card') + @amex_credit_card = credit_card( + '375932134599999', + month: 3, + year: 2017, + first_name: 'Banwire', + last_name: 'Test Card' + ) @amex_options = { - :order_id => '2', - :email => 'test@email.com', - :billing_address => address(:address1 => 'Horacio', :zip => 11560), - :description => 'Store purchase amex' + order_id: '2', + email: 'test@email.com', + billing_address: address(address1: 'Horacio', zip: 11560), + description: 'Store purchase amex' } end @@ -61,11 +66,11 @@ def test_invalid_json assert_match %r{Invalid response received from the Banwire API}, response.message end - #American Express requires address and zipcode + # American Express requires address and zipcode def test_successful_amex_purchase response = stub_comms do @gateway.purchase(@amount, @amex_credit_card, @amex_options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/post_code=11560/, data) end.respond_with(successful_purchase_amex_response) @@ -75,7 +80,7 @@ def test_successful_amex_purchase assert response.test? end - #American Express requires address and zipcode + # American Express requires address and zipcode def test_unsuccessful_amex_request @gateway.expects(:ssl_post).returns(failed_purchase_amex_response) @@ -89,7 +94,6 @@ def test_transcript_scrubbing assert_equal scrubbed_transcript, @gateway.scrub(transcript) end - private def failed_purchase_response diff --git a/test/unit/gateways/barclaycard_smartpay_test.rb b/test/unit/gateways/barclaycard_smartpay_test.rb index 08d4f38efb0..fd36afab2bf 100644 --- a/test/unit/gateways/barclaycard_smartpay_test.rb +++ b/test/unit/gateways/barclaycard_smartpay_test.rb @@ -46,37 +46,38 @@ def setup } @options_with_shipping_house_number_and_shipping_street = { - order_id: '1', - street: 'Top Level Drive', - house_number: '1000', - billing_address: address, - shipping_house_number: '999', - shipping_street: 'Downtown Loop', - shipping_address: { - name: 'PU JOI SO', - address1: '新北市店溪路3579號139樓', - company: 'Widgets Inc', - city: '新北市', - zip: '231509', - country: 'TW', - phone: '(555)555-5555', - fax: '(555)555-6666' - }, - description: 'Store Purchase' + order_id: '1', + street: 'Top Level Drive', + house_number: '1000', + billing_address: address, + shipping_house_number: '999', + shipping_street: 'Downtown Loop', + shipping_address: { + name: 'PU JOI SO', + address1: '新北市店溪路3579號139樓', + company: 'Widgets Inc', + city: '新北市', + zip: '231509', + country: 'TW', + phone: '(555)555-5555', + fax: '(555)555-6666' + }, + description: 'Store Purchase' } @options_with_credit_fields = { order_id: '1', - billing_address: { - name: 'Jim Smith', - address1: '100 Street', - company: 'Widgets Inc', - city: 'Ottawa', - state: 'ON', - zip: 'K1C2N6', - country: 'CA', - phone: '(555)555-5555', - fax: '(555)555-6666'}, + billing_address: { + name: 'Jim Smith', + address1: '100 Street', + company: 'Widgets Inc', + city: 'Ottawa', + state: 'ON', + zip: 'K1C2N6', + country: 'CA', + phone: '(555)555-5555', + fax: '(555)555-6666' + }, email: 'long@bob.com', customer: 'Longbob Longsen', description: 'Store Purchase', @@ -92,14 +93,38 @@ def setup @avs_address = @options.clone @avs_address.update(billing_address: { - name: 'Jim Smith', - street: 'Test AVS result', - houseNumberOrName: '2', - city: 'Cupertino', - state: 'CA', - zip: '95014', - country: 'US' - }) + name: 'Jim Smith', + street: 'Test AVS result', + houseNumberOrName: '2', + city: 'Cupertino', + state: 'CA', + zip: '95014', + country: 'US' + }) + + @normalized_3ds_2_options = { + reference: '345123', + shopper_email: 'john.smith@test.com', + shopper_ip: '77.110.174.153', + shopper_reference: 'John Smith', + billing_address: address(), + order_id: '123', + stored_credential: { reason_type: 'unscheduled' }, + three_ds_2: { + channel: 'browser', + browser_info: { + accept_header: 'unknown', + depth: 100, + java: false, + language: 'US', + height: 1000, + width: 500, + timezone: '-120', + user_agent: 'unknown' + }, + notification_url: 'https://example.com/notification' + } + } end def test_successful_purchase @@ -115,7 +140,7 @@ def test_successful_purchase def test_successful_authorize_with_alternate_address response = stub_comms do @gateway.authorize(@amount, @credit_card, @options_with_alternate_address) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/billingAddress.houseNumberOrName=%E6%96%B0%E5%8C%97%E5%B8%82%E5%BA%97%E6%BA%AA%E8%B7%AF3579%E8%99%9F139%E6%A8%93/, data) assert_match(/billingAddress.street=Not\+Provided/, data) end.respond_with(successful_authorize_response) @@ -127,10 +152,8 @@ 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) - end.check_request do |endpoint, data, headers| + @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) end.respond_with(successful_authorize_response) @@ -142,10 +165,8 @@ 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) - end.check_request do |endpoint, data, headers| + @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) assert_match(/deliveryAddress.street=Downtown\+Loop/, data) @@ -158,10 +179,24 @@ def test_successful_authorize_with_shipping_house_number_and_street end def test_successful_authorize_with_extra_options + shopper_interaction = 'ContAuth' + shopper_statement = 'One-year premium subscription' + device_fingerprint = 'abcde123' + response = stub_comms do - @gateway.authorize(@amount, @credit_card, @options.merge(shopper_interaction: 'ContAuth')) - end.check_request do |endpoint, data, headers| - assert_match(/shopperInteraction=ContAuth/, data) + @gateway.authorize( + @amount, + @credit_card, + @options.merge( + shopper_interaction:, + device_fingerprint:, + shopper_statement: + ) + ) + end.check_request do |_endpoint, data, _headers| + assert_match(/shopperInteraction=#{shopper_interaction}/, data) + assert_match(/shopperStatement=#{Regexp.quote(CGI.escape(shopper_statement))}/, data) + assert_match(/deviceFingerprint=#{device_fingerprint}/, data) end.respond_with(successful_authorize_response) assert_success response @@ -188,6 +223,18 @@ def test_successful_authorize_with_3ds assert response.test? end + def test_successful_authorize_with_3ds2_browser_client_data + @gateway.stubs(:ssl_post).returns(successful_authorize_with_3ds2_response) + + assert response = @gateway.authorize(@amount, @three_ds_enrolled_card, @normalized_3ds_2_options) + assert response.test? + assert_equal '8815609737078177', response.authorization + assert_equal response.params['resultCode'], 'IdentifyShopper' + refute response.params['additionalData']['threeds2.threeDS2Token'].blank? + refute response.params['additionalData']['threeds2.threeDSServerTransID'].blank? + refute response.params['additionalData']['threeds2.threeDSMethodURL'].blank? + end + def test_failed_authorize @gateway.stubs(:ssl_post).returns(failed_authorize_response) @@ -206,17 +253,18 @@ def test_successful_capture end def test_failed_capture - @gateway.stubs(:ssl_post).raises(ActiveMerchant::ResponseError.new(stub(:code => '500', :body => failed_capture_response))) + @gateway.stubs(:ssl_post).raises(ActiveMerchant::ResponseError.new(stub(code: '422', body: failed_capture_response))) response = @gateway.capture(@amount, '0000000000000000', @options) assert_failure response + assert_equal('167: Original pspReference required for this operation', response.message) assert response.test? end def test_legacy_capture_psp_reference_passed_for_refund response = stub_comms do @gateway.refund(@amount, '8814002632606717', @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/originalReference=8814002632606717/, data) end.respond_with(successful_refund_response) @@ -227,7 +275,7 @@ def test_legacy_capture_psp_reference_passed_for_refund def test_successful_refund response = stub_comms do @gateway.refund(@amount, '7914002629995504#8814002632606717', @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/originalReference=7914002629995504&/, data) assert_no_match(/8814002632606717/, data) end.respond_with(successful_refund_response) @@ -237,10 +285,11 @@ def test_successful_refund end def test_failed_refund - @gateway.stubs(:ssl_post).raises(ActiveMerchant::ResponseError.new(stub(:code => '500', :body => failed_refund_response))) + @gateway.stubs(:ssl_post).raises(ActiveMerchant::ResponseError.new(stub(code: '422', body: failed_refund_response))) response = @gateway.refund(@amount, '0000000000000000', @options) assert_failure response + assert_equal('137: Invalid amount specified', response.message) assert response.test? end @@ -261,7 +310,8 @@ def test_failed_credit def test_credit_contains_all_fields response = stub_comms do @gateway.credit(@amount, @credit_card, @options_with_credit_fields) - end.check_request do |endpoint, data, headers| + end.check_request do |endpoint, data, _headers| + assert_match(%r{/refundWithData}, endpoint) assert_match(/dateOfBirth=1990-10-11&/, data) assert_match(/entityType=NaturalPerson&/, data) assert_match(/nationality=US&/, data) @@ -272,6 +322,26 @@ def test_credit_contains_all_fields assert response.test? end + def test_successful_third_party_payout + response = stub_comms do + @gateway.credit(@amount, @credit_card, @options_with_credit_fields.merge({ third_party_payout: true })) + end.check_request do |endpoint, data, _headers| + if /storeDetailAndSubmitThirdParty/.match?(endpoint) + assert_match(%r{/storeDetailAndSubmitThirdParty}, endpoint) + assert_match(/dateOfBirth=1990-10-11&/, data) + assert_match(/entityType=NaturalPerson&/, data) + assert_match(/nationality=US&/, data) + assert_match(/shopperName.firstName=Longbob&/, data) + assert_match(/recurring\.contract=PAYOUT/, data) + else + assert_match(/originalReference=/, data) + end + end.respond_with(successful_payout_store_response, successful_payout_confirm_response) + + assert_success response + assert response.test? + end + def test_successful_void @gateway.expects(:ssl_post).returns(successful_void_response) @@ -298,9 +368,9 @@ def test_unsuccessful_verify def test_authorize_nonfractional_currency response = stub_comms do @gateway.authorize(@amount, @credit_card, @options.merge(currency: 'JPY')) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/amount.value=1/, data) - assert_match(/amount.currency=JPY/, data) + assert_match(/amount.currency=JPY/, data) end.respond_with(successful_authorize_response) assert_success response @@ -309,9 +379,9 @@ def test_authorize_nonfractional_currency def test_authorize_three_decimal_currency response = stub_comms do @gateway.authorize(@amount, @credit_card, @options.merge(currency: 'OMR')) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/amount.value=100/, data) - assert_match(/amount.currency=OMR/, data) + assert_match(/amount.currency=OMR/, data) end.respond_with(successful_authorize_response) assert_success response @@ -325,13 +395,67 @@ def test_successful_store end def test_failed_store - @gateway.stubs(:ssl_post).raises(ActiveMerchant::ResponseError.new(stub(:code => '422', :body => failed_store_response))) + @gateway.stubs(:ssl_post).raises(ActiveMerchant::ResponseError.new(stub(code: '422', body: failed_store_response))) response = @gateway.store(@credit_card, @options) assert_failure response assert response.test? end + def test_execute_threed_false_sent_3ds2 + stub_comms do + @gateway.authorize(@amount, @credit_card, @normalized_3ds_2_options.merge({ execute_threed: false })) + end.check_request do |_endpoint, data, _headers| + refute_match(/additionalData.scaExemption/, data) + assert_match(/additionalData.executeThreeD=false/, data) + end.respond_with(successful_authorize_response) + end + + def test_sca_exemption_not_sent_if_execute_threed_missing_3ds2 + stub_comms do + @gateway.authorize(@amount, @credit_card, @normalized_3ds_2_options.merge({ scaExemption: 'lowValue' })) + end.check_request do |_endpoint, data, _headers| + refute_match(/additionalData.scaExemption/, data) + refute_match(/additionalData.executeThreeD=false/, data) + end.respond_with(successful_authorize_response) + end + + def test_sca_exemption_and_execute_threed_false_sent_3ds2 + stub_comms do + @gateway.authorize(@amount, @credit_card, @normalized_3ds_2_options.merge({ sca_exemption: 'lowValue', execute_threed: false })) + end.check_request do |_endpoint, data, _headers| + assert_match(/additionalData.scaExemption=lowValue/, data) + assert_match(/additionalData.executeThreeD=false/, data) + end.respond_with(successful_authorize_response) + end + + def test_sca_exemption_and_execute_threed_true_sent_3ds2 + stub_comms do + @gateway.authorize(@amount, @credit_card, @normalized_3ds_2_options.merge({ sca_exemption: 'lowValue', execute_threed: true })) + end.check_request do |_endpoint, data, _headers| + assert_match(/additionalData.scaExemption=lowValue/, data) + assert_match(/additionalData.executeThreeD=true/, data) + end.respond_with(successful_authorize_response) + end + + def test_sca_exemption_not_sent_when_execute_threed_true_3ds1 + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ sca_exemption: 'lowValue', execute_threed: true })) + end.check_request do |_endpoint, data, _headers| + refute_match(/additionalData.scaExemption/, data) + assert_match(/additionalData.executeThreeD=true/, data) + end.respond_with(successful_authorize_response) + end + + def test_sca_exemption_not_sent_when_execute_threed_false_3ds1 + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ sca_exemption: 'lowValue', execute_threed: false })) + end.check_request do |_endpoint, data, _headers| + refute_match(/additionalData.scaExemption/, data) + refute_match(/additionalData.executeThreeD/, data) + end.respond_with(successful_authorize_response) + end + def test_avs_result @gateway.expects(:ssl_post).returns(failed_avs_response) @@ -345,6 +469,22 @@ def test_transcript_scrubbing assert_equal scrubbed_transcript, @gateway.scrub(transcript) end + def test_proper_error_response_handling + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.respond_with(configuration_error_response) + + message = "#{response.params['errorCode']}: #{response.params['message']}" + assert_equal('905: Payment details are not supported', message) + + response2 = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.respond_with(validation_error_response) + + message2 = "#{response2.params['errorCode']}: #{response2.params['message']}" + assert_equal('702: Internal error', message2) + end + private def successful_authorize_response @@ -355,6 +495,10 @@ def successful_authorize_with_3ds_response 'pspReference=8815161318854998&resultCode=RedirectShopper&issuerUrl=https%3A%2F%2Ftest.adyen.com%2Fhpp%2F3d%2Fvalidate.shtml&md=WIFa2sF3CuPyN53Txjt3U%2F%2BDuCsddzywiY5NLgEAdUAXPksHUzXL5E%2BsfvdpolkGWR8b1oh%2FNA3jNaUP9UCgfjhXqRslGFy9OGqcZ1ITMz54HHm%2FlsCKN9bTftKnYA4F7GqvOgcIIrinUZjbMvW9doGifwzSqYLo6ASOm6bARL5n7cIFV8IWtA2yPlO%2FztKSTRJt1glN4s8sMcpE57z4soWKMuycbdXdpp6d4ZRSa%2F1TPF0MnJF0zNaSAAkw9JpXqGMOz5sFF2Smpc38HXJzM%2FV%2B1mmoDhhWmXXOb5YQ0QSCS7DXKIcr8ZtuGuGmFp0QOfZiO41%2B2I2N7VhONVx8xSn%2BLu4m6vaDIg5qsnd9saxaWwbJpl9okKm6pB2MJap9ScuBCcvI496BPCrjQ2LHxvDWhk6M3Exemtv942NQIGlsiPaW0KXoC2dQvBsxWh0K&paRequest=eNpVUtuOgjAQ%2FRXj%2B1KKoIWMTVgxWR%2B8RNkPaMpEycrFUlb8%2B20B190%2BnXPm0pnTQnpRiMkJZauQwxabRpxxkmfLacQYDeiczihjgR%2BGbMrhEB%2FxxuEbVZNXJaeO63hAntSUK3kRpeYg5O19s%2BPUm%2FnBHMhIoUC1SXiKjT4URSxvba5QARlkKEWB%2FFSbgbLr41QIpXFVFUB6HWTVllo9OPNMwyeBVl35Reu6iQi53%2B9OM5Y7sipMVqmF1G9tA8QmAnlNeGgtakzjLs%2F4Pjl3u3TtbdNtZzDdJV%2FBPu7PEojNgExo5J5LmUvpfELDyPcjPwDS6yAKOxFffx4nxhXXrDwIUNt74oFQG%2FgrgLFdYSkfPFwws9WTAXZ1VaLJMPb%2BYiCvoVcf1mSpjW%2B%2BN9i8YKFr0MLa3Qdsl9yYREM37NtYAsSWkvElyfjiBv37CT9ySbE1' end + def successful_authorize_with_3ds2_response + 'additionalData.threeds2.threeDS2Token=BQABAQB9sBAzFS%2BrvT1fuY78N4P5BA5DO6s9Y6jCIzvMcH%2Bk5%2B0ms8dRPEZZhO8CYx%2Fa5NCl8r4vyJj0nI0HZ9CBl%2FQLxtGLYfVu6sNxZc9xZry%2Bm24pBGTtHsd4vunorPNPAGlYWHBXtf4h0Sj9Qy0bzlau7a%2Feayi1cpjbfV%2B8Eqw%2FAod1B80heU8sX2DKm5SHlR4o0qTu0WQUSJfKRxjdJ1AntgAxjYo3uFUlU%2FyhNpdRiAxgauLImbllfQTGVTcYBQXsY9FSakfAZRW1kT7bNMraCvRUpp4o1Z5ZezJxPcksfCEzFVPyJYcTvcV4odQK4tT6imRLRvG1OgUVNzNAuDBnEJtFOC%2BE5YwAwfKuloCqB9oAAOzL5ZHXOXPASY2ehJ3RaCZjqj5vmAX8L9GY35FV8q49skYZpzIvlMICWjErI2ayKMCiXHFDE54f2GJEhVRKpY9s506740UGQc0%2FMgbKyLyqtU%2BRG30BwA9bSt3NQKchm9xoOL7U%2Bzm6OIeikmw94TBq%2BmBN7SdQi%2BK2W4yfMkqFsl7hc7HHBa%2BOc6At7wxxdxCLg6wksQmDxElXeQfFkWvoBuR96fIHaXILnVHKjWcTbeulXBhVPA5Y47MLEtZL3G8k%2BzKTFUCW7O0MN2WxUoMBT8foan1%2B9QhZejEqiamreIs56PLQkJvhigyRQmiqwnVjXiFOv%2FEcWn0Z6IM2TnAfw3Kd2KwZ9JaePLtZ2Ck7%2FUEsdt1Kj2HYeE86WM4PESystER5oBT12xWXvbp8CEA7Mulmpd3bkiMl5IVRoSBL5pl4qZd1CrnG%2FeuvtXYTsN%2FdA%2BIcWwiLiXpmSwqaRB8DfChwouuNMAAkfKhQ6b3vLAToc3o%2B3Xa1QetsK8GI1pmjkoZRvLd2xfGhVe%2FmCl23wzQsAicwB9ZXXMgWbaS2OwdwsISQGOmsWrajzp7%2FvR0T4aHqJlrFvKnc9BrWEWbDi8g%2BDFZ2E2ifhFYSYhrHVA7yOIIDdTQnH3CIzaevxUAnbIyFsxrhy8USdP6R6CdJZ%2Bg0rIJ5%2FeZ5P8JjDiYJWi5FDJwy%2BNP9PQIFFim6psbELCtnAaW1m7pU1FeNwjYUGIdVD2f%2BVYJe4cWHPCaWAAsARNXTzjrfUEq%2BpEYDcs%2FLyTB8f69qSrmTSDGsCETsNNy27LY%2BtodGDKsxtW35jIqoV8l2Dra3wucman8nIZp3VTNtNvZDCqWetLXxBbFVZN6ecuoMPwhER5MBFUrkkXCSSFBK%2FNGp%2FXaEDP6A2hmUKvXikL3F9S7MIKQCUYC%2FI7K4DFYFBjTBzN4%3D&additionalData.threeds2.threeDSServerTransID=efbf9d05-5e6b-4659-a64e-f1dfa5d846c4&additionalData.threeds2.threeDSMethodURL=https%3A%2F%2Fpal-test.adyen.com%2Fthreeds2simulator%2Facs%2FstartMethod.shtml&pspReference=8815609737078177&resultCode=IdentifyShopper' + end + def failed_authorize_response 'pspReference=7914002630895750&refusalReason=Refused&resultCode=Refused' end @@ -364,7 +508,7 @@ def successful_capture_response end def failed_capture_response - 'validation 100 No amount specified' + 'errorType=validation&errorCode=167&message=Original+pspReference+required+for+this+operation&status=422' end def successful_refund_response @@ -372,13 +516,21 @@ def successful_refund_response end def failed_refund_response - 'validation 100 No amount specified' + 'errorType=validation&errorCode=137&message=Invalid+amount+specified&status=422' end def successful_credit_response 'fraudResult.accountScore=70&fraudResult.results.0.accountScore=20&fraudResult.results.0.checkId=2&fraudResult.results.0.name=CardChunkUsage&fraudResult.results.1.accountScore=25&fraudResult.results.1.checkId=4&fraudResult.results.1.name=HolderNameUsage&fraudResult.results.2.accountScore=25&fraudResult.results.2.checkId=8&fraudResult.results.2.name=ShopperEmailUsage&fraudResult.results.3.accountScore=0&fraudResult.results.3.checkId=1&fraudResult.results.3.name=PaymentDetailRefCheck&fraudResult.results.4.accountScore=0&fraudResult.results.4.checkId=13&fraudResult.results.4.name=IssuerRefCheck&fraudResult.results.5.accountScore=0&fraudResult.results.5.checkId=15&fraudResult.results.5.name=IssuingCountryReferral&fraudResult.results.6.accountScore=0&fraudResult.results.6.checkId=26&fraudResult.results.6.name=ShopperEmailRefCheck&fraudResult.results.7.accountScore=0&fraudResult.results.7.checkId=27&fraudResult.results.7.name=PmOwnerRefCheck&fraudResult.results.8.accountScore=0&fraudResult.results.8.checkId=56&fraudResult.results.8.name=ShopperReferenceTrustCheck&fraudResult.results.9.accountScore=0&fraudResult.results.9.checkId=10&fraudResult.results.9.name=HolderNameContainsNumber&fraudResult.results.10.accountScore=0&fraudResult.results.10.checkId=11&fraudResult.results.10.name=HolderNameIsOneWord&fraudResult.results.11.accountScore=0&fraudResult.results.11.checkId=21&fraudResult.results.11.name=EmailDomainValidation&pspReference=8514743049239955&resultCode=Received' end + def successful_payout_store_response + 'pspReference=8815391117417347&resultCode=%5Bpayout-submit-received%5D' + end + + def successful_payout_confirm_response + 'pspReference=8815391117421182&response=%5Bpayout-confirm-received%5D' + end + def failed_credit_response 'errorType=validation&errorCode=137&message=Invalid+amount+specified&status=422' end @@ -399,6 +551,14 @@ def failed_avs_response 'additionalData.liabilityShift=false&additionalData.authCode=3115&additionalData.avsResult=2+Neither+postal+code+nor+address+match&additionalData.cardHolderName=Longbob+Longsen&additionalData.threeDOffered=false&additionalData.refusalReasonRaw=AUTHORISED&additionalData.issuerCountry=US&additionalData.cvcResult=1+Matches&additionalData.avsResultRaw=2&additionalData.threeDAuthenticated=false&additionalData.cvcResultRaw=1&additionalData.acquirerCode=SmartPayTestPmmAcquirer&additionalData.acquirerReference=7F50RDN2L06&fraudResult.accountScore=170&fraudResult.results.0.accountScore=20&fraudResult.results.0.checkId=2&fraudResult.results.0.name=CardChunkUsage&fraudResult.results.1.accountScore=25&fraudResult.results.1.checkId=4&fraudResult.results.1.name=HolderNameUsage&fraudResult.results.2.accountScore=25&fraudResult.results.2.checkId=8&fraudResult.results.2.name=ShopperEmailUsage&fraudResult.results.3.accountScore=0&fraudResult.results.3.checkId=1&fraudResult.results.3.name=PaymentDetailRefCheck&fraudResult.results.4.accountScore=0&fraudResult.results.4.checkId=13&fraudResult.results.4.name=IssuerRefCheck&fraudResult.results.5.accountScore=0&fraudResult.results.5.checkId=15&fraudResult.results.5.name=IssuingCountryReferral&fraudResult.results.6.accountScore=0&fraudResult.results.6.checkId=26&fraudResult.results.6.name=ShopperEmailRefCheck&fraudResult.results.7.accountScore=0&fraudResult.results.7.checkId=27&fraudResult.results.7.name=PmOwnerRefCheck&fraudResult.results.8.accountScore=0&fraudResult.results.8.checkId=10&fraudResult.results.8.name=HolderNameContainsNumber&fraudResult.results.9.accountScore=0&fraudResult.results.9.checkId=11&fraudResult.results.9.name=HolderNameIsOneWord&fraudResult.results.10.accountScore=0&fraudResult.results.10.checkId=21&fraudResult.results.10.name=EmailDomainValidation&fraudResult.results.11.accountScore=100&fraudResult.results.11.checkId=20&fraudResult.results.11.name=AVSAuthResultCheck&fraudResult.results.12.accountScore=0&fraudResult.results.12.checkId=25&fraudResult.results.12.name=CVCAuthResultCheck&pspReference=8814591938804745&refusalReason=FRAUD-CANCELLED&resultCode=Cancelled&authCode=3115' end + def validation_error_response + 'errorType=validation&errorCode=702&message=Internal+error&status=500' + end + + def configuration_error_response + 'errorType=configuration&errorCode=905&message=Payment+details+are+not+supported&pspReference=4315391674762857&status=500' + end + def transcript %( opening connection to pal-test.barclaycardsmartpay.com:443... @@ -488,5 +648,4 @@ def scrubbed_transcript Conn close ) end - end diff --git a/test/unit/gateways/barclays_epdq_extra_plus_test.rb b/test/unit/gateways/barclays_epdq_extra_plus_test.rb index 2155d23a374..9289b3bb0ea 100644 --- a/test/unit/gateways/barclays_epdq_extra_plus_test.rb +++ b/test/unit/gateways/barclays_epdq_extra_plus_test.rb @@ -2,21 +2,21 @@ class BarclaysEpdqExtraPlusTest < Test::Unit::TestCase def setup - @credentials = { :login => 'pspid', - :user => 'username', - :password => 'password', - :signature => 'mynicesig', - :signature_encryptor => 'sha512' } + @credentials = { login: 'pspid', + user: 'username', + password: 'password', + signature: 'mynicesig', + signature_encryptor: 'sha512' } @gateway = BarclaysEpdqExtraPlusGateway.new(@credentials) @credit_card = credit_card - @mastercard = credit_card('5399999999999999', :brand => 'mastercard') + @mastercard = credit_card('5399999999999999', brand: 'mastercard') @amount = 100 @identification = '3014726' @billing_id = 'myalias' @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase' + order_id: '1', + billing_address: address, + description: 'Store Purchase' } @parameters = { 'orderID' => '1', @@ -54,7 +54,7 @@ def test_successful_purchase_with_action_param @gateway.expects(:add_pair).at_least(1) @gateway.expects(:add_pair).with(anything, 'ECI', '7') @gateway.expects(:ssl_post).returns(successful_purchase_response) - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(:action => 'SAS')) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(action: 'SAS')) assert_success response assert_equal '3014726;SAS', response.authorization assert response.params['HTML_ANSWER'].nil? @@ -74,7 +74,7 @@ def test_successful_purchase_with_custom_eci @gateway.expects(:add_pair).at_least(1) @gateway.expects(:add_pair).with(anything, 'ECI', '4') @gateway.expects(:ssl_post).returns(successful_purchase_response) - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(:eci => 4)) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(eci: 4)) assert_success response assert_equal '3014726;SAL', response.authorization assert response.test? @@ -82,7 +82,7 @@ def test_successful_purchase_with_custom_eci def test_successful_purchase_with_3dsecure @gateway.expects(:ssl_post).returns(successful_3dsecure_purchase_response) - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(:d3d => true)) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(d3d: true)) assert_success response assert_equal '3014726;SAL', response.authorization assert response.params['HTML_ANSWER'] @@ -115,7 +115,7 @@ def test_successful_authorize_with_custom_eci @gateway.expects(:add_pair).at_least(1) @gateway.expects(:add_pair).with(anything, 'ECI', '4') @gateway.expects(:ssl_post).returns(successful_purchase_response) - assert response = @gateway.authorize(@amount, @credit_card, @options.merge(:eci => 4)) + assert response = @gateway.authorize(@amount, @credit_card, @options.merge(eci: 4)) assert_success response assert_equal '3014726;RES', response.authorization assert response.test? @@ -123,7 +123,7 @@ def test_successful_authorize_with_custom_eci def test_successful_authorize_with_3dsecure @gateway.expects(:ssl_post).returns(successful_3dsecure_purchase_response) - assert response = @gateway.authorize(@amount, @credit_card, @options.merge(:d3d => true)) + assert response = @gateway.authorize(@amount, @credit_card, @options.merge(d3d: true)) assert_success response assert_equal '3014726;RES', response.authorization assert response.params['HTML_ANSWER'] @@ -141,7 +141,7 @@ def test_successful_capture def test_successful_capture_with_action_option @gateway.expects(:ssl_post).returns(successful_capture_response) - assert response = @gateway.capture(@amount, '3048326', :action => 'SAS') + assert response = @gateway.capture(@amount, '3048326', action: 'SAS') assert_success response assert_equal '3048326;SAS', response.authorization assert response.test? @@ -185,7 +185,7 @@ def test_successful_store @gateway.expects(:add_pair).at_least(1) @gateway.expects(:add_pair).with(anything, 'ECI', '7') @gateway.expects(:ssl_post).times(2).returns(successful_purchase_response) - assert response = @gateway.store(@credit_card, :billing_id => @billing_id) + assert response = @gateway.store(@credit_card, billing_id: @billing_id) assert_success response assert_equal '3014726;RES', response.authorization assert_equal '2', response.billing_id @@ -197,7 +197,7 @@ def test_deprecated_store_option @gateway.expects(:add_pair).with(anything, 'ECI', '7') @gateway.expects(:ssl_post).times(2).returns(successful_purchase_response) assert_deprecation_warning(BarclaysEpdqExtraPlusGateway::OGONE_STORE_OPTION_DEPRECATION_MESSAGE) do - assert response = @gateway.store(@credit_card, :store => @billing_id) + assert response = @gateway.store(@credit_card, store: @billing_id) assert_success response assert_equal '3014726;RES', response.authorization assert response.test? @@ -225,7 +225,7 @@ def test_supported_countries end def test_supported_card_types - assert_equal [:visa, :master, :american_express, :diners_club, :discover, :jcb, :maestro], BarclaysEpdqExtraPlusGateway.supported_cardtypes + assert_equal %i[visa master american_express diners_club discover jcb maestro], BarclaysEpdqExtraPlusGateway.supported_cardtypes end def test_default_currency @@ -239,7 +239,7 @@ def test_default_currency end def test_custom_currency_at_gateway_level - gateway = BarclaysEpdqExtraPlusGateway.new(@credentials.merge(:currency => 'USD')) + gateway = BarclaysEpdqExtraPlusGateway.new(@credentials.merge(currency: 'USD')) gateway.expects(:add_pair).at_least(1) gateway.expects(:add_pair).with(anything, 'currency', 'USD') gateway.expects(:ssl_post).returns(successful_purchase_response) @@ -247,11 +247,11 @@ def test_custom_currency_at_gateway_level end def test_local_custom_currency_overwrite_gateway_level - gateway = BarclaysEpdqExtraPlusGateway.new(@credentials.merge(:currency => 'USD')) + gateway = BarclaysEpdqExtraPlusGateway.new(@credentials.merge(currency: 'USD')) gateway.expects(:add_pair).at_least(1) gateway.expects(:add_pair).with(anything, 'currency', 'EUR') gateway.expects(:ssl_post).returns(successful_purchase_response) - gateway.purchase(@amount, @credit_card, @options.merge(:currency => 'EUR')) + gateway.purchase(@amount, @credit_card, @options.merge(currency: 'EUR')) end def test_avs_result @@ -310,13 +310,13 @@ def test_format_error_message_with_no_separator end def test_without_signature - gateway = BarclaysEpdqExtraPlusGateway.new(@credentials.merge(:signature => nil, :signature_encryptor => nil)) + gateway = BarclaysEpdqExtraPlusGateway.new(@credentials.merge(signature: nil, signature_encryptor: nil)) gateway.expects(:ssl_post).returns(successful_purchase_response) assert_deprecation_warning(BarclaysEpdqExtraPlusGateway::OGONE_NO_SIGNATURE_DEPRECATION_MESSAGE) do gateway.purchase(@amount, @credit_card, @options) end - gateway = BarclaysEpdqExtraPlusGateway.new(@credentials.merge(:signature => nil, :signature_encryptor => 'none')) + gateway = BarclaysEpdqExtraPlusGateway.new(@credentials.merge(signature: nil, signature_encryptor: 'none')) gateway.expects(:ssl_post).returns(successful_purchase_response) assert_no_deprecation_warning do gateway.purchase(@amount, @credit_card, @options) @@ -324,27 +324,27 @@ def test_without_signature end def test_signature_for_accounts_created_before_10_may_20101 - gateway = BarclaysEpdqExtraPlusGateway.new(@credentials.merge(:signature_encryptor => nil)) + gateway = BarclaysEpdqExtraPlusGateway.new(@credentials.merge(signature_encryptor: nil)) assert signature = gateway.send(:add_signature, @parameters) assert_equal Digest::SHA1.hexdigest('1100EUR4111111111111111MrPSPIDRES2mynicesig').upcase, signature end def test_signature_for_accounts_with_signature_encryptor_to_sha1 - gateway = BarclaysEpdqExtraPlusGateway.new(@credentials.merge(:signature_encryptor => 'sha1')) + gateway = BarclaysEpdqExtraPlusGateway.new(@credentials.merge(signature_encryptor: 'sha1')) assert signature = gateway.send(:add_signature, @parameters) assert_equal Digest::SHA1.hexdigest(string_to_digest).upcase, signature end def test_signature_for_accounts_with_signature_encryptor_to_sha256 - gateway = BarclaysEpdqExtraPlusGateway.new(@credentials.merge(:signature_encryptor => 'sha256')) + gateway = BarclaysEpdqExtraPlusGateway.new(@credentials.merge(signature_encryptor: 'sha256')) assert signature = gateway.send(:add_signature, @parameters) assert_equal Digest::SHA256.hexdigest(string_to_digest).upcase, signature end def test_signature_for_accounts_with_signature_encryptor_to_sha512 - gateway = BarclaysEpdqExtraPlusGateway.new(@credentials.merge(:signature_encryptor => 'sha512')) + gateway = BarclaysEpdqExtraPlusGateway.new(@credentials.merge(signature_encryptor: 'sha512')) assert signature = gateway.send(:add_signature, @parameters) assert_equal Digest::SHA512.hexdigest(string_to_digest).upcase, signature end @@ -359,13 +359,13 @@ def test_3dsecure_win_3ds_option post = {} gateway = BarclaysEpdqExtraPlusGateway.new(@credentials) - gateway.send(:add_d3d, post, { :win_3ds => :pop_up }) + gateway.send(:add_d3d, post, { win_3ds: :pop_up }) assert 'POPUP', post['WIN3DS'] - gateway.send(:add_d3d, post, { :win_3ds => :pop_ix }) + gateway.send(:add_d3d, post, { win_3ds: :pop_ix }) assert 'POPIX', post['WIN3DS'] - gateway.send(:add_d3d, post, { :win_3ds => :invalid }) + gateway.send(:add_d3d, post, { win_3ds: :invalid }) assert 'MAINW', post['WIN3DS'] end @@ -374,14 +374,14 @@ def test_3dsecure_additional_options gateway = BarclaysEpdqExtraPlusGateway.new(@credentials) gateway.send(:add_d3d, post, { - :http_accept => 'text/html', - :http_user_agent => 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)', - :accept_url => 'https://accept_url', - :decline_url => 'https://decline_url', - :exception_url => 'https://exception_url', - :paramsplus => 'params_plus', - :complus => 'com_plus', - :language => 'fr_FR' + http_accept: 'text/html', + http_user_agent: 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)', + accept_url: 'https://accept_url', + decline_url: 'https://decline_url', + exception_url: 'https://exception_url', + paramsplus: 'params_plus', + complus: 'com_plus', + language: 'fr_FR' }) assert 'HTTP_ACCEPT', 'text/html' assert 'HTTP_USER_AGENT', 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)' @@ -414,20 +414,20 @@ def test_transcript_scrubbing private def string_to_digest - 'ALIAS=2mynicesigAMOUNT=100mynicesigCARDNO=4111111111111111mynicesig'+ - 'CN=Client NamemynicesigCURRENCY=EURmynicesigOPERATION=RESmynicesig'+ - 'ORDERID=1mynicesigPSPID=MrPSPIDmynicesig' + 'ALIAS=2mynicesigAMOUNT=100mynicesigCARDNO=4111111111111111mynicesig'\ + 'CN=Client NamemynicesigCURRENCY=EURmynicesigOPERATION=RESmynicesig'\ + 'ORDERID=1mynicesigPSPID=MrPSPIDmynicesig' end def d3d_string_to_digest - 'ALIAS=2mynicesigAMOUNT=100mynicesigCARDNO=4111111111111111mynicesig'+ - 'CN=Client NamemynicesigCURRENCY=EURmynicesigFLAG3D=Ymynicesig'+ - 'HTTP_ACCEPT=*/*mynicesigOPERATION=RESmynicesigORDERID=1mynicesig'+ - 'PSPID=MrPSPIDmynicesigWIN3DS=MAINWmynicesig' + 'ALIAS=2mynicesigAMOUNT=100mynicesigCARDNO=4111111111111111mynicesig'\ + 'CN=Client NamemynicesigCURRENCY=EURmynicesigFLAG3D=Ymynicesig'\ + 'HTTP_ACCEPT=*/*mynicesigOPERATION=RESmynicesigORDERID=1mynicesig'\ + 'PSPID=MrPSPIDmynicesigWIN3DS=MAINWmynicesig' end def successful_authorize_response - <<-END + <<-XML - END + XML end def successful_purchase_response - <<-END + <<-XML - END + XML end def successful_3dsecure_purchase_response - <<-END + <<-XML - END + XML end def failed_purchase_response - <<-END + <<-XML - END + XML end def successful_capture_response - <<-END + <<-XML - END + XML end def successful_void_response - <<-END + <<-XML - END + XML end def successful_referenced_credit_response - <<-END + <<-XML - END + XML end def successful_unreferenced_credit_response - <<-END + <<-XML - END + XML end def test_failed_authorization_due_to_unknown_order_number - <<-END + <<-XML - END + XML end def transcript diff --git a/test/unit/gateways/be2bill_test.rb b/test/unit/gateways/be2bill_test.rb index d83f42e42b5..e4ca81f6381 100644 --- a/test/unit/gateways/be2bill_test.rb +++ b/test/unit/gateways/be2bill_test.rb @@ -3,17 +3,17 @@ class Be2billTest < Test::Unit::TestCase def setup @gateway = Be2billGateway.new( - :login => 'login', - :password => 'password' - ) + login: 'login', + password: 'password' + ) @credit_card = credit_card @amount = 100 @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase' + order_id: '1', + billing_address: address, + description: 'Store Purchase' } end @@ -41,11 +41,11 @@ def test_unsuccessful_request # Place raw successful response from gateway here def successful_purchase_response - {'OPERATIONTYPE'=>'payment', 'TRANSACTIONID'=>'A189063', 'EXECCODE'=>'0000', 'MESSAGE'=>'The transaction has been accepted.', 'ALIAS'=>'A189063', 'DESCRIPTOR'=>'RENTABILITEST'}.to_json + { 'OPERATIONTYPE' => 'payment', 'TRANSACTIONID' => 'A189063', 'EXECCODE' => '0000', 'MESSAGE' => 'The transaction has been accepted.', 'ALIAS' => 'A189063', 'DESCRIPTOR' => 'RENTABILITEST' }.to_json end # Place raw failed response from gateway here def failed_purchase_response - {'OPERATIONTYPE'=>'payment', 'TRANSACTIONID'=>'A189063', 'EXECCODE'=>'1001', 'MESSAGE'=>"The parameter \"CARDCODE\" is missing.\n", 'DESCRIPTOR'=>'RENTABILITEST'}.to_json + { 'OPERATIONTYPE' => 'payment', 'TRANSACTIONID' => 'A189063', 'EXECCODE' => '1001', 'MESSAGE' => "The parameter \"CARDCODE\" is missing.\n", 'DESCRIPTOR' => 'RENTABILITEST' }.to_json end end diff --git a/test/unit/gateways/beanstream_interac_test.rb b/test/unit/gateways/beanstream_interac_test.rb index c1158396746..71f7d3f9dda 100644 --- a/test/unit/gateways/beanstream_interac_test.rb +++ b/test/unit/gateways/beanstream_interac_test.rb @@ -3,19 +3,19 @@ class BeanstreamInteracTest < Test::Unit::TestCase def setup @gateway = BeanstreamInteracGateway.new( - :login => 'login', - :password => 'password' - ) + login: 'login', + password: 'password' + ) @amount = 100 - - @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase' + + @options = { + order_id: '1', + billing_address: address, + description: 'Store Purchase' } end - + def test_successful_purchase @gateway.expects(:ssl_post).returns(successful_purchase_response) response = @gateway.purchase(@amount, @options) @@ -25,7 +25,7 @@ def test_successful_purchase assert response.params['pageContents'] assert_equal response.params['pageContents'], response.redirect end - + def test_successful_confirmation @gateway.expects(:ssl_post).returns(successful_confirmation_response) @@ -36,15 +36,15 @@ def test_successful_confirmation end private - + def successful_purchase_response 'responseType=R&pageContents=%3CHTML%3E%3CHEAD%3E%3C%2FHEAD%3E%3CBODY%3E%3CFORM%20action%3D%22https%3A%2F%2Fpayments%2Ebeanstream%2Ecom%2FiOnlineEmulator%2Fgateway%2Easp%22%20method%3DPOST%20id%3DfrmIOnline%20name%3DfrmIOnline%3E%3Cinput%20type%3D%22hidden%22%20name%3D%22IDEBIT%5FMERCHNUM%22%20%20value%3D%2210010162199999%22%3E%3Cinput%20type%3D%22hidden%22%20name%3D%22IDEBIT%5FAMOUNT%22%20%20value%3D%221500%22%3E%3Cinput%20type%3D%22hidden%22%20name%3D%22IDEBIT%5FTERMID%22%20value%3D%2262199999%22%3E%3Cinput%20type%3D%22hidden%22%20name%3D%22IDEBIT%5FCURRENCY%22%20value%3D%22CAD%22%3E%3Cinput%20type%3D%22hidden%22%20name%3D%22IDEBIT%5FINVOICE%22%20value%3D%221be7db7a129b07ac5f7e%22%3E%3Cinput%20type%3D%22hidden%22%20name%3D%22IDEBIT%5FMERCHDATA%22%20value%3D%226CE36AF7%2D5013%2D4B94%2DB740153714A41962%22%3E%3Cinput%20type%3D%22hidden%22%20name%3D%22IDEBIT%5FFUNDEDURL%22%20value%3D%22https%3A%2F%2Fwww%2Ebeanstream%2Ecom%2Fscripts%2Fprocess%5Ftransaction%5Fauth%2Easp%3F%26funded%3D1%22%3E%3Cinput%20type%3D%22hidden%22%20name%3D%22IDEBIT%5FNOTFUNDEDURL%22%20value%3D%22https%3A%2F%2Fwww%2Ebeanstream%2Ecom%2Fscripts%2Fprocess%5Ftransaction%5Fauth%2Easp%3F%26funded%3D0%22%3E%3Cinput%20type%3D%22hidden%22%20name%3D%22merchant%5Fname%22%20value%3D%22Cody%20Fauser%22%3E%3Cinput%20type%3D%22hidden%22%20name%3D%22referHost%22%20value%3D%22https%3A%2F%2Fwww%2Ebeanstream%2Ecom%2Fscripts%2Fprocess%5Ftransaction%2Easp%22%3E%3Cinput%20type%3D%22hidden%22%20name%3D%22referHost2%22%20value%3D%22https%3A%2F%2Fwww%2Ecatnrose%2Ecom%2Fioxml%2Easp%22%3E%3Cinput%20type%3D%22hidden%22%20name%3D%22referHost3%22%20value%3D%22%22%3E%3Cinput%20type%3D%22hidden%22%20name%3D%22IDEBIT%5FMERCHLANG%22%20value%3D%22en%22%3E%3Cinput%20type%3D%22hidden%22%20name%3D%22IDEBIT%5FVERSION%22%20value%3D%221%22%3E%3C%2FFORM%3E%3CSCRIPT%20language%3D%22JavaScript%22%3Edocument%2EfrmIOnline%2Esubmit%28%29%3B%3C%2FSCRIPT%3E%3C%2FBODY%3E%3C%2FHTML%3E' end - + def successful_return_from_interac_online 'bank_choice=1&merchant_name=Billing+Boss+IO+SB&confirmValue=&headerText=&IDEBIT_MERCHDATA=C4B50A48-6E11-4C21-A31EF4A602BC0099&IDEBIT_INVOICE=18face21593b59c7bb7e&IDEBIT_AMOUNT=1500&IDEBIT_FUNDEDURL=http%3A%2F%2Febay.massapparel.com%3A8000%2Finterac%2Ffunded%3Ffunded%3D1&IDEBIT_NOTFUNDEDURL=http%3A%2F%2Febay.massapparel.com%3A8000%2Finterac%2Fnotfunded%3Ffunded%3D0&IDEBIT_ISSLANG=en&IDEBIT_TRACK2=3728024906540591214%3D12010123456789XYZ&IDEBIT_ISSCONF=CONF%23TEST&IDEBIT_ISSNAME=TestBank1&IDEBIT_VERSION=1&accountType=Chequing' end - + def successful_confirmation_response 'trnApproved=1&trnId=10000029&messageId=1&messageText=Approved&trnOrderNumber=f29d2406b49b239b6dfb5db1f642b2&authCode=TEST&errorType=N&errorFields=&responseType=T&trnAmount=5%2E00&trnDate=6%2F8%2F2008+3%3A17%3A12+PM&avsProcessed=0&avsId=0&avsResult=0&avsAddrMatch=0&avsPostalMatch=0&avsMessage=Address+Verification+not+performed+for+this+transaction%2E&trnType=P&paymentMethod=IO&ioConfCode=CONF%23TEST&ioInstName=TestBank1&ref1=reference+one&ref2=&ref3=&ref4=&ref5=' end diff --git a/test/unit/gateways/beanstream_test.rb b/test/unit/gateways/beanstream_test.rb index be07b0c9e4a..fa9f748c9ff 100644 --- a/test/unit/gateways/beanstream_test.rb +++ b/test/unit/gateways/beanstream_test.rb @@ -7,10 +7,11 @@ def setup Base.mode = :test @gateway = BeanstreamGateway.new( - :login => 'merchant id', - :user => 'username', - :password => 'password' - ) + login: 'merchant id', + user: 'username', + password: 'password', + api_key: 'api_key' + ) @credit_card = credit_card @@ -21,45 +22,46 @@ def setup number: '4030000010001234', payment_cryptogram: 'cryptogram goes here', eci: 'an ECI value', - transaction_id: 'transaction ID', + transaction_id: 'transaction ID' ) - @check = check( - :institution_number => '001', - :transit_number => '26729' - ) + @check = check( + institution_number: '001', + transit_number: '26729' + ) @amount = 1000 @options = { - :order_id => '1234', - :billing_address => { - :name => 'xiaobo zzz', - :phone => '555-555-5555', - :address1 => '1234 Levesque St.', - :address2 => 'Apt B', - :city => 'Montreal', - :state => 'QC', - :country => 'CA', - :zip => 'H2C1X8' + order_id: '1234', + billing_address: { + name: 'xiaobo zzz', + phone: '555-555-5555', + address1: '1234 Levesque St.', + address2: 'Apt B', + city: 'Montreal', + state: 'QC', + country: 'CA', + zip: 'H2C1X8' }, - :email => 'xiaobozzz@example.com', - :subtotal => 800, - :shipping => 100, - :tax1 => 100, - :tax2 => 100, - :custom => 'reference one' + email: 'xiaobozzz@example.com', + subtotal: 800, + shipping: 100, + tax1: 100, + tax2: 100, + custom: 'reference one' } @recurring_options = @options.merge( - :interval => { :unit => :months, :length => 1 }, - :occurrences => 5) + interval: { unit: :months, length: 1 }, + occurrences: 5 + ) end def test_successful_purchase response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @decrypted_credit_card, @options) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| refute_match(/recurringPayment=true/, data) end.respond_with(successful_purchase_response) @@ -70,8 +72,8 @@ def test_successful_purchase def test_successful_purchase_with_recurring response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @decrypted_credit_card, @options.merge(recurring: true)) - end.check_request do |method, endpoint, data, headers| - assert_match(/recurringPayment=true/, data) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/recurringPayment=1/, data) end.respond_with(successful_purchase_response) assert_success response @@ -80,8 +82,8 @@ def test_successful_purchase_with_recurring def test_successful_authorize_with_recurring response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@amount, @decrypted_credit_card, @options.merge(recurring: true)) - end.check_request do |method, endpoint, data, headers| - assert_match(/recurringPayment=true/, data) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/recurringPayment=1/, data) end.respond_with(successful_purchase_response) assert_success response @@ -138,7 +140,7 @@ def test_successful_purchase_with_check def test_successful_purchase_with_vault @gateway.expects(:ssl_post).returns(successful_purchase_response) - vault = rand(100000)+10001 + vault = rand(10001..110000) assert response = @gateway.purchase(@amount, vault, @options) assert_success response @@ -161,7 +163,6 @@ def test_failed_verify assert_equal 'DECLINE', response.message end - # Testing Non-American countries def test_german_address_sets_state_to_the_required_dummy_value @@ -210,7 +211,7 @@ def test_successful_update_recurring @gateway.expects(:ssl_post).returns(successful_update_recurring_response) response = assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.update_recurring(@amount, @credit_card, @recurring_options.merge(:account_id => response.params['rbAccountId'])) + @gateway.update_recurring(@amount, @credit_card, @recurring_options.merge(account_id: response.params['rbAccountId'])) end assert_success response assert_equal 'Request successful', response.message @@ -228,14 +229,14 @@ def test_successful_cancel_recurring @gateway.expects(:ssl_post).returns(successful_cancel_recurring_response) response = assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.cancel_recurring(:account_id => response.params['rbAccountId']) + @gateway.cancel_recurring(account_id: response.params['rbAccountId']) end assert_success response assert_equal 'Request successful', response.message end def test_ip_is_being_sent - @gateway.expects(:ssl_post).with do |url, data| + @gateway.expects(:ssl_post).with do |_url, data| data =~ /customerIp=123\.123\.123\.123/ end.returns(successful_purchase_response) @@ -246,7 +247,7 @@ def test_ip_is_being_sent def test_includes_network_tokenization_fields response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @decrypted_credit_card, @options) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/3DSecureXID/, data) assert_match(/3DSecureECI/, data) assert_match(/3DSecureCAVV/, data) @@ -261,7 +262,7 @@ def test_defaults_state_and_zip_with_country @options[:shipping_address] = address response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @decrypted_credit_card, @options) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/ordProvince=--/, data) assert_match(/ordPostalCode=000000/, data) assert_match(/shipProvince=--/, data) @@ -272,12 +273,12 @@ def test_defaults_state_and_zip_with_country end def test_no_state_and_zip_default_with_missing_country - address = { } + address = {} @options[:billing_address] = address @options[:shipping_address] = address response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @decrypted_credit_card, @options) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_no_match(/ordProvince=--/, data) assert_no_match(/ordPostalCode=000000/, data) assert_no_match(/shipProvince=--/, data) @@ -293,7 +294,7 @@ def test_sends_email_without_addresses @options[:shipping_email] = 'ship@mail.com' response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @decrypted_credit_card, @options) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/ordEmailAddress=xiaobozzz%40example.com/, data) assert_match(/shipEmailAddress=ship%40mail.com/, data) end.respond_with(successful_purchase_response) @@ -301,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 @@ -340,11 +354,11 @@ def unsuccessful_void_response end def brazilian_address_params_without_zip_and_state - { :shipProvince => '--', :shipPostalCode => '000000', :ordProvince => '--', :ordPostalCode => '000000', :ordCountry => 'BR', :trnCardOwner => 'Longbob Longsen', :shipCity => 'Rio de Janeiro', :ordAddress1 => '1234 Levesque St.', :ordShippingPrice => '1.00', :deliveryEstimate => nil, :shipName => 'xiaobo zzz', :trnCardNumber => '4242424242424242', :trnAmount => '10.00', :trnType => 'P', :ordAddress2 => 'Apt B', :ordTax1Price => '1.00', :shipEmailAddress => 'xiaobozzz@example.com', :trnExpMonth => '09', :ordCity => 'Rio de Janeiro', :shipPhoneNumber => '555-555-5555', :ordName => 'xiaobo zzz', :trnExpYear => next_year, :trnOrderNumber => '1234', :shipCountry => 'BR', :ordTax2Price => '1.00', :shipAddress1 => '1234 Levesque St.', :ordEmailAddress => 'xiaobozzz@example.com', :trnCardCvd => '123', :trnComments => nil, :shippingMethod => nil, :ref1 => 'reference one', :shipAddress2 => 'Apt B', :ordPhoneNumber => '555-555-5555', :ordItemPrice => '8.00' } + { shipProvince: '--', shipPostalCode: '000000', ordProvince: '--', ordPostalCode: '000000', ordCountry: 'BR', trnCardOwner: 'Longbob Longsen', shipCity: 'Rio de Janeiro', ordAddress1: '1234 Levesque St.', ordShippingPrice: '1.00', deliveryEstimate: nil, shipName: 'xiaobo zzz', trnCardNumber: '4242424242424242', trnAmount: '10.00', trnType: 'P', ordAddress2: 'Apt B', ordTax1Price: '1.00', shipEmailAddress: 'xiaobozzz@example.com', trnExpMonth: '09', ordCity: 'Rio de Janeiro', shipPhoneNumber: '555-555-5555', ordName: 'xiaobo zzz', trnExpYear: next_year, trnOrderNumber: '1234', shipCountry: 'BR', ordTax2Price: '1.00', shipAddress1: '1234 Levesque St.', ordEmailAddress: 'xiaobozzz@example.com', trnCardCvd: '123', trnComments: nil, shippingMethod: nil, ref1: 'reference one', shipAddress2: 'Apt B', ordPhoneNumber: '555-555-5555', ordItemPrice: '8.00' } end def german_address_params_without_state - { :shipProvince => '--', :shipPostalCode => '12345', :ordProvince => '--', :ordPostalCode => '12345', :ordCountry => 'DE', :trnCardOwner => 'Longbob Longsen', :shipCity => 'Berlin', :ordAddress1 => '1234 Levesque St.', :ordShippingPrice => '1.00', :deliveryEstimate => nil, :shipName => 'xiaobo zzz', :trnCardNumber => '4242424242424242', :trnAmount => '10.00', :trnType => 'P', :ordAddress2 => 'Apt B', :ordTax1Price => '1.00', :shipEmailAddress => 'xiaobozzz@example.com', :trnExpMonth => '09', :ordCity => 'Berlin', :shipPhoneNumber => '555-555-5555', :ordName => 'xiaobo zzz', :trnExpYear => next_year, :trnOrderNumber => '1234', :shipCountry => 'DE', :ordTax2Price => '1.00', :shipAddress1 => '1234 Levesque St.', :ordEmailAddress => 'xiaobozzz@example.com', :trnCardCvd => '123', :trnComments => nil, :shippingMethod => nil, :ref1 => 'reference one', :shipAddress2 => 'Apt B', :ordPhoneNumber => '555-555-5555', :ordItemPrice => '8.00' } + { shipProvince: '--', shipPostalCode: '12345', ordProvince: '--', ordPostalCode: '12345', ordCountry: 'DE', trnCardOwner: 'Longbob Longsen', shipCity: 'Berlin', ordAddress1: '1234 Levesque St.', ordShippingPrice: '1.00', deliveryEstimate: nil, shipName: 'xiaobo zzz', trnCardNumber: '4242424242424242', trnAmount: '10.00', trnType: 'P', ordAddress2: 'Apt B', ordTax1Price: '1.00', shipEmailAddress: 'xiaobozzz@example.com', trnExpMonth: '09', ordCity: 'Berlin', shipPhoneNumber: '555-555-5555', ordName: 'xiaobo zzz', trnExpYear: next_year, trnOrderNumber: '1234', shipCountry: 'DE', ordTax2Price: '1.00', shipAddress1: '1234 Levesque St.', ordEmailAddress: 'xiaobozzz@example.com', trnCardCvd: '123', trnComments: nil, shippingMethod: nil, ref1: 'reference one', shipAddress2: 'Apt B', ordPhoneNumber: '555-555-5555', ordItemPrice: '8.00' } end def next_year @@ -370,5 +384,4 @@ def transcript def scrubbed_transcript 'ref1=reference+one&trnCardOwner=Longbob+Longsen&trnCardNumber=[FILTERED]&trnExpMonth=09&trnExpYear=16&trnCardCvd=[FILTERED]&ordName=xiaobo+zzz&ordEmailAddress=xiaobozzz%40example.com&username=awesomesauce&password=[FILTERED]' end - end diff --git a/test/unit/gateways/blue_pay_test.rb b/test/unit/gateways/blue_pay_test.rb index ce498445007..d1bc53db5c8 100644 --- a/test/unit/gateways/blue_pay_test.rb +++ b/test/unit/gateways/blue_pay_test.rb @@ -1,11 +1,11 @@ require 'test_helper' RSP = { - :approved_auth => 'AUTH_CODE=XCADZ&PAYMENT_ACCOUNT_MASK=xxxxxxxxxxxx4242&CARD_TYPE=VISA&TRANS_TYPE=AUTH&REBID=&STATUS=1&AVS=_&TRANS_ID=100134203758&CVV2=_&MESSAGE=Approved%20Auth', - :approved_capture => 'AUTH_CODE=CHTHX&PAYMENT_ACCOUNT_MASK=xxxxxxxxxxxx4242&CARD_TYPE=VISA&TRANS_TYPE=CAPTURE&REBID=&STATUS=1&AVS=_&TRANS_ID=100134203760&CVV2=_&MESSAGE=Approved%20Capture', - :approved_void => 'AUTH_CODE=KTMHB&PAYMENT_ACCOUNT_MASK=xxxxxxxxxxxx4242&CARD_TYPE=VISA&TRANS_TYPE=VOID&REBID=&STATUS=1&AVS=_&TRANS_ID=100134203763&CVV2=_&MESSAGE=Approved%20Void', - :declined => 'TRANS_ID=100000000150&STATUS=0&AVS=0&CVV2=7&MESSAGE=Declined&REBID=', - :approved_purchase => 'AUTH_CODE=GYRUY&PAYMENT_ACCOUNT_MASK=xxxxxxxxxxxx4242&CARD_TYPE=VISA&TRANS_TYPE=SALE&REBID=&STATUS=1&AVS=_&TRANS_ID=100134203767&CVV2=_&MESSAGE=Approved%20Sale' + approved_auth: 'AUTH_CODE=XCADZ&PAYMENT_ACCOUNT_MASK=xxxxxxxxxxxx4242&CARD_TYPE=VISA&TRANS_TYPE=AUTH&REBID=&STATUS=1&AVS=_&TRANS_ID=100134203758&CVV2=_&MESSAGE=Approved%20Auth', + approved_capture: 'AUTH_CODE=CHTHX&PAYMENT_ACCOUNT_MASK=xxxxxxxxxxxx4242&CARD_TYPE=VISA&TRANS_TYPE=CAPTURE&REBID=&STATUS=1&AVS=_&TRANS_ID=100134203760&CVV2=_&MESSAGE=Approved%20Capture', + approved_void: 'AUTH_CODE=KTMHB&PAYMENT_ACCOUNT_MASK=xxxxxxxxxxxx4242&CARD_TYPE=VISA&TRANS_TYPE=VOID&REBID=&STATUS=1&AVS=_&TRANS_ID=100134203763&CVV2=_&MESSAGE=Approved%20Void', + declined: 'TRANS_ID=100000000150&STATUS=0&AVS=0&CVV2=7&MESSAGE=Declined&REBID=', + approved_purchase: 'AUTH_CODE=GYRUY&PAYMENT_ACCOUNT_MASK=xxxxxxxxxxxx4242&CARD_TYPE=VISA&TRANS_TYPE=SALE&REBID=&STATUS=1&AVS=_&TRANS_ID=100134203767&CVV2=_&MESSAGE=Approved%20Sale' } class BluePayTest < Test::Unit::TestCase @@ -13,37 +13,51 @@ class BluePayTest < Test::Unit::TestCase def setup @gateway = BluePayGateway.new( - :login => 'X', - :password => 'Y' + login: 'X', + password: 'Y' ) @amount = 100 @credit_card = credit_card + @check = check @rebill_id = '100096219669' @rebill_status = 'active' + @options = { ip: '192.168.0.1' } end def test_successful_authorization - #@gateway.expects(:ssl_post).returns(successful_authorization_response) - @gateway.expects(:ssl_post).returns(RSP[:approved_auth]) - assert response = @gateway.authorize(@amount, @credit_card) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/CUSTOMER_IP=192.168.0.1/, data) + end.respond_with(RSP[:approved_auth]) + + assert response assert_instance_of Response, response assert_success response assert_equal '100134203758', response.authorization end def test_successful_purchase - @gateway.expects(:ssl_post).returns(RSP[:approved_purchase]) + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/CUSTOMER_IP=192.168.0.1/, data) + end.respond_with(RSP[:approved_purchase]) - assert response = @gateway.purchase(@amount, @credit_card) + assert response assert_instance_of Response, response assert_success response assert_equal '100134203767', response.authorization end def test_failed_authorization - @gateway.expects(:ssl_post).returns(RSP[:declined]) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/CUSTOMER_IP=192.168.0.1/, data) + end.respond_with(RSP[:declined]) - assert response = @gateway.authorize(@amount, @credit_card) + assert response assert_instance_of Response, response assert_failure response assert_equal '100000000150', response.authorization @@ -52,8 +66,8 @@ def test_failed_authorization def test_add_address_outsite_north_america result = {} - @gateway.send(:add_address, result, :billing_address => {:address1 => '123 Test St.', :address2 => '5F', :city => 'Testville', :company => 'Test Company', :country => 'DE', :state => ''} ) - assert_equal ['ADDR1', 'ADDR2', 'CITY', 'COMPANY_NAME', 'COUNTRY', 'PHONE', 'STATE', 'ZIP'], result.stringify_keys.keys.sort + @gateway.send(:add_address, result, billing_address: { address1: '123 Test St.', address2: '5F', city: 'Testville', company: 'Test Company', country: 'DE', state: '' }) + assert_equal %w[ADDR1 ADDR2 CITY COMPANY_NAME COUNTRY PHONE STATE ZIP], result.stringify_keys.keys.sort assert_equal 'n/a', result[:STATE] assert_equal '123 Test St.', result[:ADDR1] assert_equal 'DE', result[:COUNTRY] @@ -62,20 +76,19 @@ def test_add_address_outsite_north_america def test_add_address result = {} - @gateway.send(:add_address, result, :billing_address => {:address1 => '123 Test St.', :address2 => '5F', :city => 'Testville', :company => 'Test Company', :country => 'US', :state => 'AK'} ) + @gateway.send(:add_address, result, billing_address: { address1: '123 Test St.', address2: '5F', city: 'Testville', company: 'Test Company', country: 'US', state: 'AK' }) - assert_equal ['ADDR1', 'ADDR2', 'CITY', 'COMPANY_NAME', 'COUNTRY', 'PHONE', 'STATE', 'ZIP'], result.stringify_keys.keys.sort + assert_equal %w[ADDR1 ADDR2 CITY COMPANY_NAME COUNTRY PHONE STATE ZIP], result.stringify_keys.keys.sort assert_equal 'AK', result[:STATE] assert_equal '123 Test St.', result[:ADDR1] assert_equal 'US', result[:COUNTRY] - end def test_name_comes_from_payment_method result = {} @gateway.send(:add_creditcard, result, @credit_card) - @gateway.send(:add_address, result, :billing_address => {:address1 => '123 Test St.', :address2 => '5F', :city => 'Testville', :company => 'Test Company', :country => 'US', :state => 'AK'} ) + @gateway.send(:add_address, result, billing_address: { address1: '123 Test St.', address2: '5F', city: 'Testville', company: 'Test Company', country: 'US', state: 'AK' }) assert_equal @credit_card.first_name, result[:NAME1] assert_equal @credit_card.last_name, result[:NAME2] @@ -83,19 +96,19 @@ def test_name_comes_from_payment_method def test_add_invoice result = {} - @gateway.send(:add_invoice, result, :order_id => '#1001') + @gateway.send(:add_invoice, result, order_id: '#1001') assert_equal '#1001', result[:invoice_num] end def test_add_description result = {} - @gateway.send(:add_invoice, result, :description => 'My Purchase is great') + @gateway.send(:add_invoice, result, description: 'My Purchase is great') assert_equal 'My Purchase is great', result[:description] end def test_purchase_meets_minimum_requirements params = { - :amount => '1.01', + amount: '1.01' } @gateway.send(:add_creditcard, params, @credit_card) @@ -107,26 +120,39 @@ def test_purchase_meets_minimum_requirements end def test_successful_refund - @gateway.expects(:ssl_post).returns(successful_refund_response) - assert response = @gateway.refund(@amount, '100134230412', :card_number => @credit_card.number) + response = stub_comms do + @gateway.refund(@amount, '100134230412', @options.merge({ card_number: @credit_card.number })) + end.check_request do |_endpoint, data, _headers| + assert_match(/CUSTOMER_IP=192\.168\.0\.1/, data) + end.respond_with(successful_refund_response) + + assert response assert_success response assert_equal 'This transaction has been approved', response.message 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') - end.check_request do |endpoint, data, headers| + @gateway.refund(50, '123456789', @options.merge({ card_number: @credit_card.number, first_name: 'Bob', last_name: 'Smith', zip: '12345', doc_type: 'WEB' })) + end.check_request do |_endpoint, data, _headers| assert_match(/NAME1=Bob/, data) assert_match(/NAME2=Smith/, data) assert_match(/ZIP=12345/, data) + assert_match(/CUSTOMER_IP=192\.168\.0\.1/, data) + assert_match(/DOC_TYPE=WEB/, data) end.respond_with(successful_purchase_response) + assert_success response end def test_failed_refund - @gateway.expects(:ssl_post).returns(failed_refund_response) - assert response = @gateway.refund(@amount, '123456789', :card_number => @credit_card.number) + response = stub_comms do + @gateway.refund(@amount, '123456789', @options.merge({ card_number: @credit_card.number })) + end.check_request do |_endpoint, data, _headers| + assert_match(/CUSTOMER_IP=192\.168\.0\.1/, data) + end.respond_with(failed_refund_response) + + assert response assert_failure response assert_equal 'The referenced transaction does not meet the criteria for issuing a credit', response.message end @@ -134,18 +160,34 @@ def test_failed_refund def test_deprecated_credit @gateway.expects(:ssl_post).returns(successful_purchase_response) assert_deprecation_warning('credit should only be used to credit a payment method') do - assert response = @gateway.credit(@amount, '123456789', :card_number => @credit_card.number) + response = stub_comms do + @gateway.credit(@amount, '123456789', @options.merge({ card_number: @credit_card.number })) + end.check_request do |_endpoint, data, _headers| + assert_match(/CUSTOMER_IP=192\.168\.0\.1/, data) + end.respond_with(failed_refund_response) + + assert response assert_success response assert_equal 'This transaction has been approved', response.message end end + def test_successful_credit_with_check + response = stub_comms do + @gateway.credit(50, @check, @options.merge({ doc_type: 'PPD' })) + end.check_request do |_endpoint, data, _headers| + assert_match(/DOC_TYPE=PPD/, data) + end.respond_with(successful_credit_response) + + assert_success response + end + def test_supported_countries - assert_equal ['US', 'CA'], BluePayGateway.supported_countries + assert_equal %w[US CA], BluePayGateway.supported_countries end def test_supported_card_types - assert_equal [:visa, :master, :american_express, :discover, :diners_club, :jcb], BluePayGateway.supported_cardtypes + assert_equal %i[visa master american_express discover diners_club jcb], BluePayGateway.supported_cardtypes end def test_parser_extracts_exactly_the_keys_in_gateway_response @@ -177,13 +219,38 @@ def test_cvv_result end 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 + end - def get_msg(query) - @gateway.send(:parse, query).message - end - assert_equal 'CVV does not match', get_msg('STATUS=2&CVV2=N&AVS=A&MESSAGE=FAILURE') - assert_equal 'Street address matches, but 5-digit and 9-digit postal code do not match.', - get_msg('STATUS=2&CVV2=M&AVS=A&MESSAGE=FAILURE') + def test_passing_stored_credentials_data_for_mit_transaction + options = @options.merge({ stored_credential: { initiator: 'merchant', reason_type: 'installment' } }) + stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/cof=M/, data) + assert_match(/cofscheduled=Y/, data) + end.respond_with(RSP[:approved_auth]) + end + + def test_passing_stored_credentials_for_cit_transaction + options = @options.merge({ stored_credential: { initiator: 'cardholder', reason_type: 'unscheduled' } }) + stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/cof=C/, data) + assert_match(/cofscheduled=N/, data) + end.respond_with(RSP[:approved_auth]) + end + + def test_passes_nothing_for_unrecognized_stored_credentials_values + options = @options.merge({ stored_credential: { initiator: 'unknown', reason_type: 'something weird' } }) + stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/cof=&/, data) + assert_match(/cofscheduled=&/, data) + end.respond_with(RSP[:approved_auth]) end # Recurring Billing Unit Tests @@ -191,13 +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, - :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 - ) + @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 + ) end assert_instance_of Response, response @@ -210,7 +279,7 @@ def test_successful_update_recurring @gateway.expects(:ssl_post).returns(successful_update_recurring_response) response = assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.update_recurring(:rebill_id => @rebill_id, :rebill_amount => @amount * 2) + @gateway.update_recurring(rebill_id: @rebill_id, rebill_amount: @amount * 2) end assert_instance_of Response, response @@ -298,6 +367,10 @@ def successful_status_recurring_response 'last_date=2012-04-13%2009%3A49%3A27&usual_date=2012-04-13%2000%3A00%3A00&template_id=100096219668&status=active&account_id=100096218902&rebill_id=100096219669&reb_amount=2.00&creation_date=2012-04-13%2009%3A49%3A19&sched_expr=1%20DAY&next_date=2012-04-13%2000%3A00%3A00&next_amount=&user_id=100096218903&cycles_remain=4' end + def successful_credit_response + 'REBID=&AVS=_&TRANS_TYPE=CREDIT&STATUS=1&PAYMENT_ACCOUNT_MASK=C%3A244183602%3Axxxx8535&AUTH_CODE=&CARD_TYPE=ACH&MESSAGE=App%20ACH%20Credit&CVV2=_&TRANS_ID=100786598799' + end + def transcript 'card_num=4111111111111111&exp_date=1212&MASTER_ID=&PAYMENT_TYPE=CREDIT&PAYMENT_ACCOUNT=4242424242424242&CARD_CVV2=123&CARD_EXPIRE=0916&NAME1=Longbob&NAME2=Longsen&ORDER_ID=78c40687dd55dbdc140df777b0e8ece3&INVOICE_ID=&invoice_num=78c40687dd55dbdc140df777b0e8ece3&EMAIL=&CUSTOM_ID=&DUPLICATE_OVERRIDE=&TRANS_TYPE=SALE&AMOUNT=1.00&MODE=TEST&ACCOUNT_ID=100096218902&TAMPER_PROOF_SEAL=55624458ce3e15fa8e33e6f2d784bbcb' end diff --git a/test/unit/gateways/blue_snap_test.rb b/test/unit/gateways/blue_snap_test.rb index dffaf3d9565..b4578d3d11a 100644 --- a/test/unit/gateways/blue_snap_test.rb +++ b/test/unit/gateways/blue_snap_test.rb @@ -1,13 +1,59 @@ +# coding: utf-8 + require 'test_helper' +class BlueSnapCurrencyDocMock + attr_accessor :received_amount + + def currency(currency); end + + def amount(amount) + @received_amount = amount + end +end + class BlueSnapTest < Test::Unit::TestCase include CommStub def setup @gateway = BlueSnapGateway.new(api_username: 'login', api_password: 'password') @credit_card = credit_card + @check = check @amount = 100 - @options = { order_id: '1' } + + # BlueSnap may require support contact to activate fraud checking on sandbox accounts. + # Specific merchant-configurable thresholds were set and are reflected in the + # recorded responses: + # Order Total Amount Decline Threshold = 3728 + # Payment Country Decline List = Brazil + @fraudulent_amount = 3729 + @fraudulent_card = credit_card('4007702835532454') + + @options = { order_id: '1', personal_identification_number: 'CNPJ' } + @options_3ds2 = @options.merge( + three_d_secure: { + eci: '05', + cavv: 'AAABAWFlmQAAAABjRWWZEEFgFz+A', + xid: 'MGpHWm5ZWVpKclo0aUk0VmltVDA=', + ds_transaction_id: 'jhg34-sdgds87-sdg87-sdfg7', + version: '2.2.0' + } + ) + @valid_check_options = { + billing_address: { + address1: '123 Street', + address2: 'Apt 1', + city: 'Happy City', + state: 'CA', + zip: '94901' + }, + authorized_by_shopper: true + } + @option_fraud_info = @options.merge( + transaction_fraud_info: { + fraud_session_id: 'fbcc094208f54c0e974d56875c73af7a' + } + ) end def test_successful_purchase @@ -18,6 +64,258 @@ def test_successful_purchase assert_equal '1012082839', response.authorization end + def test_successful_purchase_with_shipping_contact_info + more_options = @options.merge({ + shipping_address: { + address1: '123 Main St', + adress2: 'Apt B', + city: 'Springfield', + state: 'NC', + country: 'US', + zip: '27701' + } + }) + response = stub_comms(@gateway, :raw_ssl_request) do + @gateway.purchase(@amount, @credit_card, more_options) + end.check_request do |_method, _url, data| + assert_match(/shipping-contact-info/, data) + assert_match(/123 Main St/, data) + assert_match(/Springfield/, data) + assert_match(/NC/, data) + assert_match(/US/, data) + assert_match(/27701/, data) + end.respond_with(successful_purchase_response_with_metadata) + + assert_success response + assert_equal '1012082839', response.authorization + end + + def test_successful_purchase_with_card_holder_info + more_options = @options.merge({ + billing_address: { + address1: '123 Street', + address2: 'Apt 1', + city: 'Happy City', + state: 'CA', + zip: '94901' + }, + phone_number: '555 888 0000' + }) + response = stub_comms(@gateway, :raw_ssl_request) do + @gateway.purchase(@amount, @credit_card, more_options) + end.check_request do |_method, _url, data| + assert_match(/card-holder-info/, data) + assert_match(/
123 Street/, data) + assert_match(/Apt 1/, data) + assert_match(/555 888 0000/, data) + end.respond_with(successful_purchase_response_with_metadata) + + assert_success response + assert_equal '1012082839', response.authorization + end + + def test_successful_purchase_with_metadata + # description option should become meta-data field + + more_options = @options.merge({ + order_id: '1', + ip: '127.0.0.1', + email: 'joe@example.com', + transaction_meta_data: [ + { + meta_key: 'stateTaxAmount', + meta_value: '20.00', + meta_description: 'State Tax Amount' + }, + { + meta_key: 'cityTaxAmount', + meta_value: 10.00, + meta_description: 'City Tax Amount' + }, + { + meta_key: 'websiteInfo', + meta_value: 'www.info.com', + meta_description: 'Website' + } + ], + description: 'Legacy Product Desc', + soft_descriptor: 'OnCardStatement', + personal_identification_number: 'CNPJ' + }) + + response = stub_comms(@gateway, :raw_ssl_request) do + @gateway.purchase(@amount, @credit_card, more_options) + end.check_request do |_method, _url, data| + assert_match(/transaction-meta-data/, data) + assert_match(/Legacy Product Desc<\/meta-value>/, data) + assert_match(/description<\/meta-key>/, data) + assert_match(/cityTaxAmount<\/meta-key>/, data) + assert_match(/stateTaxAmount<\/meta-key>/, data) + assert_match(/websiteInfo<\/meta-key>/, data) + end.respond_with(successful_purchase_response_with_metadata) + + assert_success response + assert_equal '1012082839', response.authorization + + assert_equal 4, response.params['transaction-meta-data'].length + + response.params['transaction-meta-data'].each { |m| + assert_true m['meta-key'].length > 0 + assert_true m['meta-value'].length > 0 + assert_true m['meta-description'].length > 0 + + case m['meta-key'] + when 'description' + assert_equal 'Product ABC', m['meta-value'] + assert_equal 'Product Description', m['meta-description'] + when 'cityTaxAmount' + assert_equal '10.00', m['meta-value'] + assert_equal 'City Tax Amount', m['meta-description'] + when 'stateTaxAmount' + assert_equal '20.00', m['meta-value'] + assert_equal 'State Tax Amount', m['meta-description'] + end + } + end + + def test_successful_purchase_with_metadata_empty + more_options = @options.merge({ + order_id: '1', + ip: '127.0.0.1', + email: 'joe@example.com', + transaction_meta_data: [], + soft_descriptor: 'OnCardStatement', + personal_identification_number: 'CNPJ' + }) + + response = stub_comms(@gateway, :raw_ssl_request) do + @gateway.purchase(@amount, @credit_card, more_options) + end.check_request do |_method, _url, data| + assert_not_match(/transaction-meta-data/, data) + assert_not_match(/meta-key/, data) + end.respond_with(successful_purchase_response) + + assert_success response + assert_nil response.params['transaction-meta-data'] + end + + def test_successful_purchase_with_metadata_nil + more_options = @options.merge({ + order_id: '1', + ip: '127.0.0.1', + email: 'joe@example.com', + transaction_meta_data: nil, + soft_descriptor: 'OnCardStatement', + personal_identification_number: 'CNPJ' + }) + + response = stub_comms(@gateway, :raw_ssl_request) do + @gateway.purchase(@amount, @credit_card, more_options) + end.check_request do |_method, _url, data| + assert_not_match(/transaction-meta-data/, data) + assert_not_match(/meta-key/, data) + end.respond_with(successful_purchase_response) + + assert_success response + assert_nil response.params['transaction-meta-data'] + end + + def test_successful_purchase_with_unused_state_code + unrecognized_state_code_options = { + billing_address: { + city: 'Dresden', + state: 'Sachsen', + country: 'DE', + zip: '01069' + } + } + + @gateway.expects(:raw_ssl_request).returns(successful_stateless_purchase_response) + + response = @gateway.purchase(@amount, @credit_card, unrecognized_state_code_options) + assert_success response + assert_equal '1021645629', response.authorization + assert_not_includes(response.params, 'state') + end + + def test_successful_purchase_with_fraud_info + fraud_info = @option_fraud_info.merge({ ip: '123.12.134.1' }) + response = stub_comms(@gateway, :raw_ssl_request) do + @gateway.purchase(@amount, @credit_card, fraud_info) + end.check_request do |_method, _url, data| + assert_match(/fbcc094208f54c0e974d56875c73af7a<\/fraud-session-id>/, data) + assert_match(/123.12.134.1<\/shopper-ip-address>/, data) + end.respond_with(successful_purchase_response) + + assert_success response + assert_equal '1012082839', response.authorization + end + + def test_successful_echeck_purchase + @gateway.expects(:raw_ssl_request).returns(successful_echeck_purchase_response) + + response = @gateway.purchase(@amount, @check, @options.merge(@valid_check_options)) + assert_success response + assert_equal '1019803029', response.authorization + end + + def test_successful_purchase_with_3ds_auth + response = stub_comms(@gateway, :raw_ssl_request) do + @gateway.purchase(@amount, @credit_card, @options_3ds2) + end.check_request do |_method, _url, data| + assert_match(//, data) + assert_match(/#{Regexp.quote(@options_3ds2[:three_d_secure][:eci])}<\/eci>/, data) + assert_match(/#{Regexp.quote(@options_3ds2[:three_d_secure][:cavv])}<\/cavv>/, data) + assert_match(/#{Regexp.quote(@options_3ds2[:three_d_secure][:xid])}<\/xid>/, data) + assert_match(/#{Regexp.quote(@options_3ds2[:three_d_secure][:version])}<\/three-d-secure-version>/, data) + assert_match(/#{Regexp.quote(@options_3ds2[:three_d_secure][:ds_transaction_id])}<\/ds-transaction-id>/, data) + end.respond_with(successful_purchase_with_3ds_auth_response) + + assert_success response + assert_equal '1024951831', response.authorization + assert_equal '019082915501456', response.params['original-network-transaction-id'] + assert_equal '019082915501456', response.params['network-transaction-id'] + end + + def test_successful_purchase_with_cit_stored_credential_fields + cit_stored_credentials = { + initiator: 'cardholder', + network_transaction_id: 'ABC123' + } + stub_comms(@gateway, :raw_ssl_request) do + @gateway.purchase(@amount, @credit_card, @options_3ds2.merge({ stored_credential: cit_stored_credentials })) + end.check_request do |_method, _url, data| + assert_match 'SHOPPER', data + assert_match 'ABC123', data + end.respond_with(successful_purchase_with_3ds_auth_response) + end + + def test_successful_purchase_with_mit_stored_credential_fields + cit_stored_credentials = { + initiator: 'merchant', + network_transaction_id: 'QER100' + } + stub_comms(@gateway, :raw_ssl_request) do + @gateway.purchase(@amount, @credit_card, @options_3ds2.merge({ stored_credential: cit_stored_credentials })) + end.check_request do |_method, _url, data| + assert_match 'MERCHANT', data + assert_match 'QER100', data + end.respond_with(successful_purchase_with_3ds_auth_response) + end + + def test_does_not_send_3ds_auth_when_empty + stub_comms(@gateway, :raw_ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_method, _url, data| + assert_not_match(//, data) + assert_not_match(//, data) + assert_not_match(//, data) + assert_not_match(//, data) + assert_not_match(//, data) + assert_not_match(//, data) + end.respond_with(successful_purchase_response) + end + def test_failed_purchase @gateway.expects(:raw_ssl_request).returns(failed_purchase_response) @@ -26,14 +324,56 @@ def test_failed_purchase assert_equal '14002', response.error_code end - def test_successful_authorize - @gateway.expects(:raw_ssl_request).returns(successful_authorize_response) + def test_failed_echeck_purchase + @gateway.expects(:raw_ssl_request).returns(failed_echeck_purchase_response) - response = @gateway.authorize(@amount, @credit_card, @options) + response = @gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_equal '16004', response.error_code + end + + def test_successful_authorize + response = stub_comms(@gateway, :raw_ssl_request) do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_type, _endpoint, data, _headers| + assert_match 'false', data + assert_match 'CNPJ', data + end.respond_with(successful_authorize_response) assert_success response 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) + end.check_request do |_type, _endpoint, data, _headers| + assert_match(//, data) + assert_match(/#{Regexp.quote(@options_3ds2[:three_d_secure][:eci])}<\/eci>/, data) + assert_match(/#{Regexp.quote(@options_3ds2[:three_d_secure][:cavv])}<\/cavv>/, data) + assert_match(/#{Regexp.quote(@options_3ds2[:three_d_secure][:xid])}<\/xid>/, data) + assert_match(/#{Regexp.quote(@options_3ds2[:three_d_secure][:version])}<\/three-d-secure-version>/, data) + assert_match(/#{Regexp.quote(@options_3ds2[:three_d_secure][:ds_transaction_id])}<\/ds-transaction-id>/, data) + end.respond_with(successful_authorize_with_3ds_auth_response) + + assert_success response + assert_equal '1024951833', response.authorization + assert_equal 'MCC8929120829', response.params['original-network-transaction-id'] + assert_equal 'MCC8929120829', response.params['network-transaction-id'] + end + def test_failed_authorize @gateway.expects(:raw_ssl_request).returns(failed_authorize_response) @@ -43,9 +383,25 @@ def test_failed_authorize end def test_successful_capture - @gateway.expects(:raw_ssl_request).returns(successful_capture_response) + response = stub_comms(@gateway, :raw_ssl_request) do + @gateway.capture(@amount, @credit_card, @options) + end.check_request do |_method, _url, data| + assert_not_match(/1.00<\/amount>/, data) + assert_not_match(/USD<\/currency>/, data) + end.respond_with(successful_capture_response) + + assert_success response + assert_equal '1012082881', response.authorization + end + + def test_successful_partial_capture + response = stub_comms(@gateway, :raw_ssl_request) do + @gateway.capture(@amount, @credit_card, @options.merge(include_capture_amount: true)) + end.check_request do |_method, _url, data| + assert_match(/1.00<\/amount>/, data) + assert_match(/USD<\/currency>/, data) + end.respond_with(successful_capture_response) - response = @gateway.capture(@amount, 'Authorization') assert_success response assert_equal '1012082881', response.authorization end @@ -59,9 +415,58 @@ def test_failed_capture end def test_successful_refund - @gateway.expects(:raw_ssl_request).returns(successful_refund_response) + options = { + reason: 'Refund for order #1992', + cancel_subscription: 'false', + tax_amount: 0.05, + transaction_meta_data: [ + { + meta_key: 'refundedItems', + meta_value: '1552,8832', + meta_description: 'Refunded Items', + meta_is_visible: 'false' + }, + { + meta_key: 'Number2', + meta_value: 'KTD', + meta_description: 'Metadata 2', + meta_is_visible: 'true' + } + ] + } + transaction_id = '1286' + response = stub_comms(@gateway, :ssl_request) do + @gateway.refund(@amount, transaction_id, options) + end.check_request do |_action, endpoint, data, _headers| + doc = REXML::Document.new(data) - response = @gateway.refund(@amount, 'Authorization') + assert_includes endpoint, "/refund/#{transaction_id}" + assert_match(/1.00<\/amount>/, data) + assert_match(/0.05<\/tax-amount>/, data) + assert_match(/false<\/cancel-subscription>/, data) + assert_match(/Refund for order #1992<\/reason>/, data) + assert_match(/refundedItems<\/meta-key>/, data) + assert_match(/KTD<\/meta-value>/, data) + assert_match(/Metadata 2<\/meta-description>/, data) + transaction_meta_data = doc.root.elements['transaction-meta-data'].elements.to_a + transaction_meta_data.each_with_index do |item, index| + assert_match item.elements['meta-key'].text, options[:transaction_meta_data][index][:meta_key] + assert_match item.elements['meta-value'].text, options[:transaction_meta_data][index][:meta_value] + assert_match item.elements['meta-description'].text, options[:transaction_meta_data][index][:meta_description] + assert_match item.elements['is-visible'].text, options[:transaction_meta_data][index][:meta_is_visible] + end + end.respond_with(successful_refund_without_merchant_transaction_id_response) + assert_success response + assert_equal '1061398943', response.authorization + end + + def test_successful_refund_with_merchant_id + merchant_transaction_id = '12678' + response = stub_comms(@gateway, :ssl_request) do + @gateway.refund(@amount, '', @options.merge({ merchant_transaction_id: })) + end.check_request do |_action, endpoint, _data, _headers| + assert_includes endpoint, "/refund/merchant/#{merchant_transaction_id}" + end.respond_with(successful_refund_response) assert_success response assert_equal '1012082907', response.authorization end @@ -114,6 +519,14 @@ def test_successful_store assert_equal '20936441', response.authorization end + def test_successful_echeck_store + @gateway.expects(:raw_ssl_request).returns(successful_echeck_store_response) + + response = @gateway.store(@check, @options) + assert_success response + assert_equal '23844081|check', response.authorization + end + def test_failed_store @gateway.expects(:raw_ssl_request).returns(failed_store_response) @@ -122,10 +535,18 @@ def test_failed_store assert_equal '14002', response.error_code end + def test_failed_echeck_store + @gateway.expects(:raw_ssl_request).returns(failed_echeck_store_response) + + response = @gateway.store(@check, @options) + assert_failure response + assert_equal '10001', response.error_code + end + def test_currency_added_correctly stub_comms(@gateway, :raw_ssl_request) do @gateway.purchase(@amount, @credit_card, @options.merge(currency: 'CAD')) - end.check_request do |method, url, data| + end.check_request do |_method, _url, data| assert_match(/CAD<\/currency>/, data) end.respond_with(successful_purchase_response) end @@ -140,13 +561,94 @@ def test_verify_bad_credentials assert !@gateway.verify_credentials end + def test_failed_forbidden_response + @gateway.expects(:raw_ssl_request).returns(forbidden_response) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_equal 'You are not authorized to perform this request due to inappropriate role permissions.', response.message + end + + def test_failed_rate_limit_response + @gateway.expects(:raw_ssl_request).returns(rate_limit_response) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_equal 'Client request rate is too high', response.message + end + + def test_does_not_send_level_3_when_empty + response = stub_comms(@gateway, :raw_ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_type, _endpoint, data, _headers| + assert_not_match(/level-3-data/, data) + end.respond_with(successful_purchase_response) + assert_success response + end + + def test_fraud_response_handling + @gateway.expects(:raw_ssl_request).returns(fraudulent_purchase_response) + + response = @gateway.purchase(@fraudulent_amount, @credit_card, @options) + assert_failure response + assert_match(/fraud-reference-id/, response.message) + assert_match(/fraud-event/, response.message) + end + + def test_fraud_response_handling_multiple_triggers + @gateway.expects(:raw_ssl_request).returns(fraudulent_purchase_response_multiple_triggers) + + response = @gateway.purchase(@fraudulent_amount, @fraudulent_card, @options) + assert_failure response + assert_match(/orderTotalDecline/, response.message) + assert_match(/blacklistPaymentCountryDecline/, response.message) + end + def test_scrub assert @gateway.supports_scrubbing? assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed end + def test_echeck_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed_echeck), post_scrubbed_echeck + end + + def test_localizes_currencies + amount = 1234 + + # Check a 2 decimal place currency + assert_equal '12.34', check_amount_registered(amount, 'USD') + + # Check all 0 decimal currencies + ActiveMerchant::Billing::BlueSnapGateway.currencies_without_fractions.each do |currency| + assert_equal '12', check_amount_registered(amount, currency) + end + + # Check all 3 decimal currencies + ActiveMerchant::Billing::BlueSnapGateway.currencies_with_three_decimal_places.each do |currency| + assert_equal '1.234', check_amount_registered(amount, currency) + end + end + + def test_optional_idempotency_key_header + stub_comms(@gateway, :raw_ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge({ idempotency_key: 'test123' })) + end.check_request do |_method, _url, _data, headers| + assert_equal 'test123', headers['Idempotency-Key'] + end.respond_with(successful_authorize_response) + end + private + def check_amount_registered(amount, currency) + doc = BlueSnapCurrencyDocMock.new + options = @options.merge(currency:) + @gateway.send(:add_amount, doc, amount, options) + + doc.received_amount + end + def pre_scrubbed %q{ opening connection to sandbox.bluesnap.com:443... @@ -161,6 +663,20 @@ def pre_scrubbed } end + def pre_scrubbed_echeck + %q{ + opening connection to sandbox.bluesnap.com:443... + opened + starting SSL for sandbox.bluesnap.com:443... + SSL established + <- "POST /services/2/alt-transactions HTTP/1.1\r\nContent-Type: application/xml\r\nAuthorization: Basic QVBJXzE0NjExNzM3MTY2NTc2NzM0MDQyMzpuZll3VHg4ZkZBdkpxQlhjeHF3Qzg=\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: sandbox.bluesnap.com\r\nContent-Length: 973\r\n\r\n" + <- "\n 1.00\n USD\n \n Jim\n Smith\n CA\n Happy City\n 94901\n Jim Smith\n \n \n 15378535\n 244183602\n CORPORATE_CHECKING\n \n true\n \n " + -> "HTTP/1.1 200 200\r\n" + -> "Set-Cookie: JSESSIONID=65D503B9785EA6641D4757EA568A6532; Path=/services; Secure; HttpOnly\r\n" + -> "Connection: close\r\n" + } + end + def post_scrubbed %q{ opening connection to sandbox.bluesnap.com:443... @@ -175,6 +691,20 @@ def post_scrubbed } end + def post_scrubbed_echeck + %q{ + opening connection to sandbox.bluesnap.com:443... + opened + starting SSL for sandbox.bluesnap.com:443... + SSL established + <- "POST /services/2/alt-transactions HTTP/1.1\r\nContent-Type: application/xml\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: sandbox.bluesnap.com\r\nContent-Length: 973\r\n\r\n" + <- "\n 1.00\n USD\n \n Jim\n Smith\n CA\n Happy City\n 94901\n Jim Smith\n \n \n [FILTERED]\n [FILTERED]\n CORPORATE_CHECKING\n \n true\n \n " + -> "HTTP/1.1 200 200\r\n" + -> "Set-Cookie: JSESSIONID=65D503B9785EA6641D4757EA568A6532; Path=/services; Secure; HttpOnly\r\n" + -> "Connection: close\r\n" + } + end + def successful_purchase_response MockResponse.succeeded <<-XML @@ -192,6 +722,7 @@ def successful_purchase_response ON Ottawa K1C2N6 + CNPJ 9299 @@ -209,6 +740,175 @@ def successful_purchase_response XML end + def successful_purchase_response_with_metadata + MockResponse.succeeded <<-XML + + + AUTH_CAPTURE + 1012082839 + ECOMMERCE + BLS*Spreedly + 1.00 + USD + + Longbob + Longsen + CA + ON + Ottawa + K1C2N6 + CNPJ + + + 9299 + VISA + CREDIT + + + + stateTaxAmount + 20.00 + State Tax Amount + + + cityTaxAmount + 10.00 + City Tax Amount + + + shippingAmount + 150.00 + Shipping Amount + + + websiteInfo + www.info.com + Website + + + + success + ND + U + U + U + + + XML + end + + def successful_purchase_with_3ds_auth_response + MockResponse.succeeded <<-XML + + + AUTH_CAPTURE + 1024951831 + ECOMMERCE + BLS*Spreedly + 1.00 + 1.00 + USD + N + + Longbob + Longsen + CA + ON + Ottawa + K1C2N6 + + 25105083 + + 1091 + VISA + CREDIT + CONSUMER + N + us + + + 019082915501456 + 019082915501456 + + + success + NR + N + N + U + 019082915501456 + + + XML + end + + def successful_echeck_purchase_response + MockResponse.succeeded <<-XML + + + 1019803029 + 1.00 + USD + + Jim + Smith + CA + Happy City + 94901 + Jim Smith + + + 15378535 + 244183602 + CORPORATE_CHECKING + + + PENDING + + + XML + end + + def successful_stateless_purchase_response + MockResponse.succeeded <<-XML + + + AUTH_CAPTURE + 1021645629 + ECOMMERCE + BLS*Spreedly + 1.00 + 1.00 + USD + + Longbob + Longsen + DE + Dresden + 01069 + + 24449087 + + 9299 + VISA + CREDIT + PLATINUM + CONSUMER + N + ALLIED IRISH BANKS PLC + ie + + + success + ND + U + U + U + + + XML + end + def failed_purchase_response body = <<-XML @@ -224,6 +924,21 @@ def failed_purchase_response MockResponse.failed(body, 400) end + def failed_echeck_purchase_response + body = <<-XML + + + + PAYMENT_NOT_AUTHORIZED_BY_SHOPPER + 16004 + The payment was not authorized by shopper. Missing/Invalid 'authorized-by-shopper' element. + + + XML + + MockResponse.failed(body, 400) + end + def successful_authorize_response MockResponse.succeeded <<-XML @@ -241,6 +956,7 @@ def successful_authorize_response ON Ottawa K1C2N6 + CNPJ 9299 @@ -258,6 +974,53 @@ def successful_authorize_response XML end + def successful_authorize_with_3ds_auth_response + MockResponse.succeeded <<-XML + + + AUTH_ONLY + 1024951833 + ECOMMERCE + BLS*Spreedly + 1.00 + 1.00 + USD + S + + Longbob + Longsen + CA + ON + Ottawa + K1C2N6 + + 25105085 + + 1096 + MASTERCARD + CREDIT + STANDARD + CONSUMER + N + PUBLIC BANK BERHAD + my + + + MCC8929120829 + MCC8929120829 + + + success + NC + U + U + U + MCC8929120829 + + + XML + end + def failed_authorize_response body = <<-XML @@ -289,6 +1052,7 @@ def successful_capture_response ON Ottawa K1C2N6 + CNPJ 9299 @@ -337,6 +1101,7 @@ def successful_refund_response ON Ottawa K1C2N6 + CNPJ 9299 @@ -368,6 +1133,32 @@ def failed_refund_response MockResponse.failed(body, 400) end + def successful_refund_without_merchant_transaction_id_response + MockResponse.succeeded <<-XML + + + 1061398943 + 1.00 + 0.05 + + + refundedItems + 1552,8832 + Refunded Items + false + + + Number2 + KTD + Metadata 2 + true + + + Refund for order #1992 + + XML + end + def successful_void_response MockResponse.succeeded <<-XML @@ -385,6 +1176,7 @@ def successful_void_response ON Ottawa K1C2N6 + CNPJ 9299 @@ -433,6 +1225,7 @@ def successful_verify_response ON Ottawa K1C2N6 + CNPJ 9299 @@ -474,6 +1267,7 @@ def successful_store_response ON Ottawa K1C2N6 + CNPJ USD @@ -502,6 +1296,39 @@ def successful_store_response response end + def successful_echeck_store_response + response = MockResponse.succeeded <<-XML + + + 23844081 + Jim + Smith + Happy City + 94901 + Jim Smith + USD + + + + Jim + Smith + + Jim Smith + + + 15378535 + 244183602 + CORPORATE_CHECKING + + + + + XML + + response.headers = { 'content-location' => 'https://sandbox.bluesnap.com/services/2/vaulted-shoppers/23844081' } + response + end + def failed_store_response body = <<-XML @@ -516,12 +1343,82 @@ def failed_store_response MockResponse.failed(body, 400) end + def failed_echeck_store_response + body = <<-XML + + + + VALIDATION_GENERAL_FAILURE + 10001 + ECP data validity check failed + + + XML + MockResponse.failed(body, 400) + end + + def forbidden_response + MockResponse.new(403, 'You are not authorized to perform this request due to inappropriate role permissions.') + end + + def rate_limit_response + MockResponse.new(429, 'Client request rate is too high') + end + + def fraudulent_purchase_response + body = <<-XML + + + + FRAUD_DETECTED + 15011 + The request cannot be fulfilled for the current shopper. Please contact BlueSnap support for further details. + + 6270209 + + orderTotalDecline + D + 3729 > 3728 + + + + + XML + MockResponse.new(400, body) + end + + def fraudulent_purchase_response_multiple_triggers + body = <<-XML + + + + FRAUD_DETECTED + 15011 + The request cannot be fulfilled for the current shopper. Please contact BlueSnap support for further details. + + 6270189 + + blacklistPaymentCountryDecline + D + BR is in list: [BR] + + + orderTotalDecline + D + 3729 > 3728 + + + + + XML + MockResponse.new(400, body) + end + def credentials_are_legit_response MockResponse.new(400, 'Server Error') end def credentials_are_bogus_response - MockResponse.new(401, %{Apache Tomcat/8.0.24 - Error report

HTTP Status 401 - Bad credentials

type Status report

message Bad credentials

description This request requires HTTP authentication.


Apache Tomcat/8.0.24

}) + MockResponse.new(401, %{HTTP Status 401 – Unauthorized

HTTP Status 401 – Unauthorized


Type Status Report

Message Bad credentials

Description The request has not been applied because it lacks valid authentication credentials for the target resource.


Apache Tomcat Version X

}) end - end diff --git a/test/unit/gateways/bogus_test.rb b/test/unit/gateways/bogus_test.rb index ddd8a1dcd31..301ed2ddfa6 100644 --- a/test/unit/gateways/bogus_test.rb +++ b/test/unit/gateways/bogus_test.rb @@ -8,17 +8,17 @@ class BogusTest < Test::Unit::TestCase def setup @gateway = BogusGateway.new( - :login => 'bogus', - :password => 'bogus' + login: 'bogus', + password: 'bogus' ) @creditcard = credit_card(CC_SUCCESS_PLACEHOLDER) - @response = ActiveMerchant::Billing::Response.new(true, 'Transaction successful', :transid => BogusGateway::AUTHORIZATION) + @response = ActiveMerchant::Billing::Response.new(true, 'Transaction successful', transid: BogusGateway::AUTHORIZATION) end def test_authorize - assert @gateway.authorize(1000, credit_card(CC_SUCCESS_PLACEHOLDER)).success? + assert @gateway.authorize(1000, credit_card(CC_SUCCESS_PLACEHOLDER)).success? response = @gateway.authorize(1000, credit_card(CC_FAILURE_PLACEHOLDER)) refute response.success? assert_equal Gateway::STANDARD_ERROR_CODE[:processing_error], response.error_code @@ -34,7 +34,7 @@ def test_authorize_using_credit_card_token end def test_purchase - assert @gateway.purchase(1000, credit_card(CC_SUCCESS_PLACEHOLDER)).success? + assert @gateway.purchase(1000, credit_card(CC_SUCCESS_PLACEHOLDER)).success? response = @gateway.purchase(1000, credit_card(CC_FAILURE_PLACEHOLDER)) refute response.success? assert_equal Gateway::STANDARD_ERROR_CODE[:processing_error], response.error_code @@ -47,7 +47,7 @@ def test_purchase def test_capture assert @gateway.capture(1000, '1337').success? assert @gateway.capture(1000, @response.params['transid']).success? - response = @gateway.capture(1000, CC_FAILURE_PLACEHOLDER) + response = @gateway.capture(1000, CC_FAILURE_PLACEHOLDER) refute response.success? assert_equal Gateway::STANDARD_ERROR_CODE[:processing_error], response.error_code assert_raises(ActiveMerchant::Billing::Error) do @@ -56,8 +56,8 @@ def test_capture end def test_credit - assert @gateway.credit(1000, credit_card(CC_SUCCESS_PLACEHOLDER)).success? - response = @gateway.credit(1000, credit_card(CC_FAILURE_PLACEHOLDER)) + assert @gateway.credit(1000, credit_card(CC_SUCCESS_PLACEHOLDER)).success? + response = @gateway.credit(1000, 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 @@ -78,7 +78,7 @@ def test_refund end def test_credit_uses_refund - options = {:foo => :bar} + options = { foo: :bar } @gateway.expects(:refund).with(1000, '1337', options) assert_deprecation_warning(Gateway::CREDIT_DEPRECATION_MESSAGE) do @gateway.credit(1000, '1337', options) @@ -96,8 +96,19 @@ 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? + assert @gateway.store(credit_card(CC_SUCCESS_PLACEHOLDER)).success? response = @gateway.store(credit_card(CC_FAILURE_PLACEHOLDER)) refute response.success? assert_equal Gateway::STANDARD_ERROR_CODE[:processing_error], response.error_code @@ -125,71 +136,71 @@ def test_supported_card_types end def test_authorize_with_check - assert @gateway.authorize(1000, check(:account_number => CHECK_SUCCESS_PLACEHOLDER, :number => nil)).success? - assert !@gateway.authorize(1000, check(:account_number => CHECK_FAILURE_PLACEHOLDER, :number => nil)).success? + assert @gateway.authorize(1000, check(account_number: CHECK_SUCCESS_PLACEHOLDER, number: nil)).success? + assert !@gateway.authorize(1000, check(account_number: CHECK_FAILURE_PLACEHOLDER, number: nil)).success? e = assert_raises(ActiveMerchant::Billing::Error) do - @gateway.authorize(1000, check(:account_number => '123', :number => nil)) + @gateway.authorize(1000, check(account_number: '123', number: nil)) end assert_equal('Bogus Gateway: Use bank account number ending in 1 for success, 2 for exception and anything else for error', e.message) end def test_purchase_with_check # use account number if number isn't given - assert @gateway.purchase(1000, check(:account_number => CHECK_SUCCESS_PLACEHOLDER, :number => nil)).success? - assert !@gateway.purchase(1000, check(:account_number => CHECK_FAILURE_PLACEHOLDER, :number => nil)).success? + assert @gateway.purchase(1000, check(account_number: CHECK_SUCCESS_PLACEHOLDER, number: nil)).success? + assert !@gateway.purchase(1000, check(account_number: CHECK_FAILURE_PLACEHOLDER, number: nil)).success? # give priority to number over account_number if given - assert !@gateway.purchase(1000, check(:account_number => CHECK_SUCCESS_PLACEHOLDER, :number => CHECK_FAILURE_PLACEHOLDER)).success? - assert @gateway.purchase(1000, check(:account_number => CHECK_FAILURE_PLACEHOLDER, :number => CHECK_SUCCESS_PLACEHOLDER)).success? + assert !@gateway.purchase(1000, check(account_number: CHECK_SUCCESS_PLACEHOLDER, number: CHECK_FAILURE_PLACEHOLDER)).success? + assert @gateway.purchase(1000, check(account_number: CHECK_FAILURE_PLACEHOLDER, number: CHECK_SUCCESS_PLACEHOLDER)).success? e = assert_raises(ActiveMerchant::Billing::Error) do - @gateway.purchase(1000, check(:account_number => '123', :number => nil)) + @gateway.purchase(1000, check(account_number: '123', number: nil)) end assert_equal('Bogus Gateway: Use bank account number ending in 1 for success, 2 for exception and anything else for error', e.message) end def test_store_with_check - assert @gateway.store(check(:account_number => CHECK_SUCCESS_PLACEHOLDER, :number => nil)).success? - assert !@gateway.store(check(:account_number => CHECK_FAILURE_PLACEHOLDER, :number => nil)).success? + assert @gateway.store(check(account_number: CHECK_SUCCESS_PLACEHOLDER, number: nil)).success? + assert !@gateway.store(check(account_number: CHECK_FAILURE_PLACEHOLDER, number: nil)).success? e = assert_raises(ActiveMerchant::Billing::Error) do - @gateway.store(check(:account_number => '123', :number => nil)) + @gateway.store(check(account_number: '123', number: nil)) end assert_equal('Bogus Gateway: Use bank account number ending in 1 for success, 2 for exception and anything else for error', e.message) end def test_credit_with_check - assert @gateway.credit(1000, check(:account_number => CHECK_SUCCESS_PLACEHOLDER, :number => nil)).success? - assert !@gateway.credit(1000, check(:account_number => CHECK_FAILURE_PLACEHOLDER, :number => nil)).success? + assert @gateway.credit(1000, check(account_number: CHECK_SUCCESS_PLACEHOLDER, number: nil)).success? + assert !@gateway.credit(1000, check(account_number: CHECK_FAILURE_PLACEHOLDER, number: nil)).success? e = assert_raises(ActiveMerchant::Billing::Error) do - @gateway.credit(1000, check(:account_number => '123', :number => nil)) + @gateway.credit(1000, check(account_number: '123', number: nil)) end assert_equal('Bogus Gateway: Use bank account number ending in 1 for success, 2 for exception and anything else for error', e.message) end def test_store_then_purchase_with_check - reference = @gateway.store(check(:account_number => CHECK_SUCCESS_PLACEHOLDER, :number => nil)) + reference = @gateway.store(check(account_number: CHECK_SUCCESS_PLACEHOLDER, number: nil)) assert @gateway.purchase(1000, reference.authorization).success? end def test_authorize_emv - approve_response = @gateway.authorize(1000, credit_card('123', {icc_data: 'DEADBEEF'})) + approve_response = @gateway.authorize(1000, credit_card('123', { icc_data: 'DEADBEEF' })) assert approve_response.success? assert_equal '8A023030', approve_response.emv_authorization - decline_response = @gateway.authorize(1005, credit_card('123', {icc_data: 'DEADBEEF'})) + decline_response = @gateway.authorize(1005, credit_card('123', { icc_data: 'DEADBEEF' })) refute decline_response.success? assert_equal Gateway::STANDARD_ERROR_CODE[:processing_error], decline_response.error_code assert_equal '8A023035', decline_response.emv_authorization e = assert_raises(ActiveMerchant::Billing::Error) do - @gateway.authorize(1001, credit_card('123', {icc_data: 'DEADBEEF'})) + @gateway.authorize(1001, credit_card('123', { icc_data: 'DEADBEEF' })) end assert_equal('Bogus Gateway: Use amount ending in 00 for success, 05 for failure and anything else for exception', e.message) end def test_purchase_emv - assert @gateway.purchase(1000, credit_card('123', {icc_data: 'DEADBEEF'})).success? - response = @gateway.purchase(1005, credit_card('123', {icc_data: 'DEADBEEF'})) + assert @gateway.purchase(1000, credit_card('123', { icc_data: 'DEADBEEF' })).success? + response = @gateway.purchase(1005, credit_card('123', { icc_data: 'DEADBEEF' })) refute response.success? assert_equal Gateway::STANDARD_ERROR_CODE[:processing_error], response.error_code e = assert_raises(ActiveMerchant::Billing::Error) do - @gateway.purchase(1001, credit_card('123', {icc_data: 'DEADBEEF'})) + @gateway.purchase(1001, credit_card('123', { icc_data: 'DEADBEEF' })) end assert_equal('Bogus Gateway: Use amount ending in 00 for success, 05 for failure and anything else for exception', e.message) end diff --git a/test/unit/gateways/borgun_test.rb b/test/unit/gateways/borgun_test.rb index bd38587664a..61eb5d23937 100644 --- a/test/unit/gateways/borgun_test.rb +++ b/test/unit/gateways/borgun_test.rb @@ -49,13 +49,72 @@ def test_authorize_and_capture capture = stub_comms do @gateway.capture(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/140601083732/, data) end.respond_with(successful_capture_response) assert_success capture end + def test_failed_preauth_3ds + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge({ redirect_url: 'http://localhost/index.html', apply_3d_secure: '1', sale_description: 'product description' })) + end.check_request do |_endpoint, data, _headers| + assert_match(/MerchantReturnURL>#{@options[:redirect_url]}/, data) + assert_match(/SaleDescription>#{@options[:sale_description]}/, data) + assert_match(/TrCurrencyExponent>2/, data) + end.respond_with(failed_get_3ds_authentication_response) + + assert_failure response + assert_equal response.message, 'Exception in PostEnrollmentRequest.' + assert response.authorization.blank? + end + + def test_successful_preauth_3ds + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge({ redirect_url: 'http://localhost/index.html', apply_3d_secure: '1', sale_description: 'product description' })) + end.check_request do |_endpoint, data, _headers| + assert_match(/MerchantReturnURL>#{@options[:redirect_url]}/, data) + assert_match(/SaleDescription>#{@options[:sale_description]}/, data) + assert_match(/TrCurrencyExponent>2/, data) + end.respond_with(successful_get_3ds_authentication_response) + + assert_success response + assert !response.params['redirecttoacsform'].blank? + assert !response.params['acsformfields_actionurl'].blank? + assert !response.params['acsformfields_pareq'].blank? + assert !response.params['threedsmessageid'].blank? + assert response.authorization.blank? + end + + def test_successful_purchase_after_3ds + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge({ three_ds_message_id: '98324_zzi_1234353' })) + end.check_request do |_endpoint, data, _headers| + assert_match(/ThreeDSMessageId>#{@options[:three_ds_message_id]}/, data) + assert_match(/TrCurrencyExponent>0/, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_authorize_airline_data + # itinerary data abbreviated for brevity + passenger_itinerary_data = { + 'MessageNumber' => '1111111', + 'TrDate' => '20120222', + 'TrTime' => '151515', + 'PassengerName' => 'Jane Doe' + } + response = stub_comms do + @gateway.authorize(@amount, @credit_card, { passenger_itinerary_data: }) + end.check_request do |_endpoint, data, _headers| + assert_match('PassengerItineraryData', data) + end.respond_with(successful_authorize_response) + + assert_success response + end + def test_refund response = stub_comms do @gateway.purchase(@amount, @credit_card) @@ -66,7 +125,7 @@ def test_refund refund = stub_comms do @gateway.refund(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/140216103700/, data) end.respond_with(successful_refund_response) @@ -83,7 +142,7 @@ def test_void refund = stub_comms do @gateway.void(response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/140216103700/, data) end.respond_with(successful_void_response) @@ -93,7 +152,7 @@ def test_void def test_passing_cvv stub_comms do @gateway.purchase(@amount, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/#{@credit_card.verification_value}/, data) end.respond_with(successful_purchase_response) end @@ -101,7 +160,7 @@ def test_passing_cvv def test_passing_terminal_id stub_comms do @gateway.purchase(@amount, @credit_card, { terminal_id: '3' }) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/TerminalID>3/, data) end.respond_with(successful_purchase_response) end @@ -295,6 +354,55 @@ def successful_void_response ) end + def successful_get_3ds_authentication_response + <<~RESPONSE + + + + + + 0 + + + 1000 + 23 + 23 + 23_20201408041400003 + 9 + ToberedirectedtoACS + Y + 3C21444F43545950452068746D6C2053595354454D202261626F75743A6C65676163792D636F6D706174223E0D0D0A3C68746D6C20636C6173733D226E6F2D6A7322206C616E673D22656E2220786D6C6E733D22687474703A2F2F7777772E77332E6F72672F313939392F7868746D6C223E0D0D0A3C686561643E0D0D0A3C6D65746120687474702D65717569763D22436F6E74656E742D547970652220636F6E74656E743D22746578742F68746D6C3B20636861727365743D7574662D38222F3E0D0D0A3C6D65746120636861727365743D227574662D38222F3E0D0D0A3C7469746C653E3344205365637572652050726F63657373696E673C2F7469746C653E0D0D0A3C6C696E6B20687265663D2268747470733A2F2F6D70692E626F7267756E2E69732F6D647061796D70692F7374617469632F6D70692E637373222072656C3D227374796C6573686565742220747970653D22746578742F637373222F3E0D0D0A3C2F686561643E0D0D0A3C626F64793E0D0D0A3C6469762069643D226D61696E223E0D0D0A3C6469762069643D22636F6E74656E74223E0D0D0A3C6469762069643D226F72646572223E0D0D0A3C68323E3344205365637572652050726F63657373696E673C2F68323E0D0D0A3C696D67207372633D2268747470733A2F2F6D70692E626F7267756E2E69732F6D647061796D70692F7374617469632F7072656C6F616465722E6769662220616C743D22506C6561736520776169742E2E222F3E0D0D0A3C696D67207372633D2268747470733A2F2F6D70692E626F7267756E2E69732F6D647061796D70692F7374617469632F6D635F6964636865636B5F68727A5F6C74645F706F735F31303370782E706E672220616C743D224D61737465724361726420494420436865636B222F3E0D0D0A3C6469762069643D22666F726D646976223E0D0D0A3C73637269707420747970653D22746578742F6A617661736372697074223E0D0D0A66756E6374696F6E2068696465416E645375626D697454696D656428666F726D6964290D0D0A7B0D0D0A7661722074696D65723D73657454696D656F7574282268696465416E645375626D69742827222B666F726D69642B2227293B222C313030293B0D0D0A7D0D0D0A0D0D0A66756E6374696F6E2068696465416E645375626D697428666F726D6964290D0D0A7B0D0D0A76617220666F726D783D646F63756D656E742E676574456C656D656E744279496428666F726D6964293B0D0D0A0969662028666F726D78213D6E756C6C290D0D0A097B0D0D0A09666F726D782E7374796C652E7669736962696C6974793D2268696464656E223B0D0D0A09666F726D782E7375626D697428293B0D0D0A097D0D0D0A7D0D0D0A3C2F7363726970743E0D0D0A3C6469763E0D0D0A3C666F726D2069643D22776562666F726D3022206E616D653D2272656432414353763122206D6574686F643D22504F53542220616374696F6E3D2268747470733A2F2F616373312E3364732E6D6F646972756D2E636F6D2F6D647061796163732F706172657122206163636570745F636861727365743D225554462D38223E0D0D0A3C696E70757420747970653D2268696464656E22206E616D653D225F636861727365745F222076616C75653D225554462D38222F3E0D0D0A3C696E70757420747970653D2268696464656E22206E616D653D225061526571222076616C75653D22654A785655753175676A4155665258692F396D57723443354E7346684D724C676D4F7746574C6C427A437861697445392F56715575663237353979766E6E4D4C487A75466D4A596F426F556363757A37716B476E725A657A5976764F614F4146455976634759636932654B4A77786C5633336153737A6D647530416D614471563246565363366A45615A5674654F417A502F4B4133434563554755706A39306744434D6679413243724137495635317142756B384F586D52505A56765365466F374855724779426A486B5133534B32753341764D79676E416F4C37345475766A6770445063634B383759465946736A6A4F6356676F39354D756251317A2F664A4E552B5449452B627932612F706E6D6166322F53684F587065676E45566B42646165517564536D4E61655377634D4838425456435268367167313350732F4C56595A51616554634D5237736D75594578385A6341343635434B53594645774B384844754A707349302F4D5A51597939346F627036454E6F70555A31626755625A53414E354348702B7357344C6259786B4E4B4978382B4B5157636448796735766A5538756F3279636267455132305475787954336535766F337A2F34415552317277553D222F3E0D0D0A3C696E70757420747970653D2268696464656E22206E616D653D224D44222076616C75653D2232335F3230323031343038303431343030303033222F3E0D0D0A3C696E70757420747970653D2268696464656E22206E616D653D225465726D55726C222076616C75653D22687474703A2F2F6C6F63616C686F73742F696E6465782E68746D6C222F3E0D0D0A3C696E70757420747970653D227375626D697422206E616D653D227375626D697442746E222076616C75653D22506C6561736520636C69636B206865726520746F20636F6E74696E7565222F3E0D0D0A3C2F666F726D3E0D0D0A3C2F6469763E0D0D0A3C2F6469763E0D0D0A3C73637269707420747970653D22746578742F6A617661736372697074223E0D0D0A09090968696465416E645375626D697454696D65642827776562666F726D3027293B0D0D0A09093C2F7363726970743E0D0D0A3C6E6F7363726970743E0D0D0A3C64697620616C69676E3D2263656E746572223E0D0D0A3C623E4A617661736372697074206973207475726E6564206F6666206F72206E6F7420737570706F72746564213C2F623E0D0D0A3C62722F3E0D0D0A3C2F6469763E0D0D0A3C2F6E6F7363726970743E0D0D0A3C2F6469763E0D0D0A3C6469762069643D22636F6E74656E742D666F6F746572223E0D0D0A3C62722F3E0D0D0A3C696D67206865696768743D22323022207372633D2268747470733A2F2F6D70692E626F7267756E2E69732F6D647061796D70692F7374617469632F706F77657265642D62792D6D6F646972756D2E7376672220616C743D22506F7765726564206279204D6F646972756D222F3E0D0D0A3C2F6469763E0D0D0A3C2F6469763E0D0D0A3C2F6469763E0D0D0A3C2F626F64793E0D0D0A3C2F68746D6C3E0D0D0A + + + https://acs1.3ds.modirum.com/mdpayacs/pareq + eJxVUu1ugjAUfRXi/9mWr4C5NsFhMrLgmOwFWLlBzCxaitE9/VqUuf2759yvnnMLHzuFmJYoBoUccuz7qkGnrZezYvvOaOAFEYvcGYci2eKJwxlV33aSszmdu0AmaDqV2FVSc6jEaZVteOAzP/KA3CEcUGUpj90gDCMfyA2CrA7IV51qBuk8OXmRPZVvSeFo7HUrGyBjHkQ3SK2u3AvMygnAoL74TuvjgpDPccK87YFYFsjjOcVgo95MubQ1z/fJNU+TIE+by2a/pnmaf2/ShOXpegnEVkBdaeQudSmNaeSwcMH8BTVCRh6qg13Ps/LVYZQaeTcMR7smuYEx8ZcA465CKSYFEwK8HDuJpsI0/MZQYy94obp6ENopUZ1bgUbZSAN5CHp+sW4LbYxkNKIx8+KQWcdHyg5vjU8uo2ycbgEQ20TuxyT3e5vo3z/4AUR1rwU= + http://localhost/index.html + + + + + RESPONSE + end + + def failed_get_3ds_authentication_response + %( + + + + + <?xml version="1.0" encoding="iso-8859-1"?> + <get3DSAuthenticationReply> + <Status> + <ResultCode>30</ResultCode> + <ResultText>MPI returns error</ResultText> + <ErrorMessage>Exception in PostEnrollmentRequest.</ErrorMessage> + </Status> + </get3DSAuthenticationReply> + + + ) + end + def transcript <<-PRE_SCRUBBED <- "POST /ws/Heimir.pub.ws:Authorization HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAuthorization: Basic yyyyyyyyyyyyyyyyyyyyyyyyyy==\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: gateway01.borgun.is\r\nContent-Length: 1220\r\n\r\n" diff --git a/test/unit/gateways/bpoint_test.rb b/test/unit/gateways/bpoint_test.rb index acdad65c45d..1c156a6212e 100644 --- a/test/unit/gateways/bpoint_test.rb +++ b/test/unit/gateways/bpoint_test.rb @@ -88,13 +88,23 @@ def test_failed_refund def test_successful_void @gateway.expects(:ssl_post).returns(successful_void_response) - response = @gateway.void(@amount, '') + response = @gateway.void('', amount: 300) assert_success response end + def test_void_passes_correct_transaction_reference + stub_comms do + # transaction number from successful authorize response + @gateway.void('219388558', amount: 300) + end.check_request do |_endpoint, data, _headers| + assert_match(%r(219388558)m, data) + assert_match(%r(300)m, data) + end.respond_with(successful_void_response) + end + def test_failed_void @gateway.expects(:ssl_post).returns(failed_void_response) - response = @gateway.void(@amount, '') + response = @gateway.void('') assert_failure response end @@ -125,11 +135,20 @@ def test_scrub def test_passing_biller_code stub_comms do @gateway.authorize(@amount, @credit_card, { biller_code: '1234' }) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(%r(1234)m, data) end.respond_with(successful_authorize_response) end + def test_passing_reference_and_crn + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ crn1: 'ref' })) + end.check_request do |_endpoint, data, _headers| + assert_match(%r(1)m, data) + assert_match(%r(ref)m, data) + end.respond_with(successful_authorize_response) + end + private def pre_scrubbed @@ -225,7 +244,7 @@ def successful_authorize_response ) end - alias_method :successful_verify_response, :successful_authorize_response + alias successful_verify_response successful_authorize_response def failed_authorize_response %( @@ -252,7 +271,7 @@ def failed_authorize_response ) end - alias_method :failed_verify_response, :failed_authorize_response + alias failed_verify_response failed_authorize_response def successful_capture_response %( @@ -399,23 +418,23 @@ def failed_void_response end def successful_store_response - %( - - - - - - 5999992142370790 - 498765...769 - VC - - - SUCCESS - - - - - ) + %( + + + + + + 5999992142370790 + 498765...769 + VC + + + SUCCESS + + + + + ) end def failed_store_response diff --git a/test/unit/gateways/braintree_blue_test.rb b/test/unit/gateways/braintree_blue_test.rb index ff67f2dc983..9acac32a1b3 100644 --- a/test/unit/gateways/braintree_blue_test.rb +++ b/test/unit/gateways/braintree_blue_test.rb @@ -5,37 +5,42 @@ def setup @old_verbose, $VERBOSE = $VERBOSE, false @gateway = BraintreeBlueGateway.new( - :merchant_id => 'test', - :public_key => 'test', - :private_key => 'test' + merchant_id: 'test', + public_key: 'test', + private_key: 'test', + test: true ) - @internal_gateway = @gateway.instance_variable_get( :@braintree_gateway ) + @internal_gateway = @gateway.instance_variable_get(:@braintree_gateway) end def teardown $VERBOSE = @old_verbose end + def test_api_version + assert_equal '6', @gateway.fetch_version + end + def test_refund_legacy_method_signature Braintree::TransactionGateway.any_instance.expects(:refund). with('transaction_id', nil). - returns(braintree_result(:id => 'refund_transaction_id')) - response = @gateway.refund('transaction_id', :test => true) + returns(braintree_result(id: 'refund_transaction_id')) + response = @gateway.refund('transaction_id', test: true) assert_equal 'refund_transaction_id', response.authorization end def test_refund_method_signature Braintree::TransactionGateway.any_instance.expects(:refund). with('transaction_id', '10.00'). - returns(braintree_result(:id => 'refund_transaction_id')) - response = @gateway.refund(1000, 'transaction_id', :test => true) + returns(braintree_result(id: 'refund_transaction_id')) + response = @gateway.refund(1000, 'transaction_id', test: true) assert_equal 'refund_transaction_id', response.authorization end def test_transaction_uses_customer_id_by_default Braintree::TransactionGateway.any_instance.expects(:sale). - with(has_entries(:customer_id => 'present')). + with(has_entries(customer_id: 'present')). returns(braintree_result) assert response = @gateway.purchase(10, 'present', {}) @@ -45,7 +50,7 @@ def test_transaction_uses_customer_id_by_default def test_transaction_uses_payment_method_token_when_option Braintree::TransactionGateway.any_instance.expects(:sale). - with(has_entries(:payment_method_token => 'present')). + with(has_entries(payment_method_token: 'present')). returns(braintree_result) assert response = @gateway.purchase(10, 'present', { payment_method_token: true }) @@ -55,7 +60,7 @@ def test_transaction_uses_payment_method_token_when_option def test_transaction_uses_payment_method_nonce_when_option Braintree::TransactionGateway.any_instance.expects(:sale). - with(has_entries(:payment_method_nonce => 'present')). + with(all_of(has_entries(payment_method_nonce: 'present'), has_key(:customer))). returns(braintree_result) assert response = @gateway.purchase(10, 'present', { payment_method_nonce: true }) @@ -63,13 +68,63 @@ def test_transaction_uses_payment_method_nonce_when_option assert_success response end + def test_authorize_transaction + Braintree::TransactionGateway.any_instance.expects(:sale). + returns(braintree_result) + + response = @gateway.authorize(100, credit_card('41111111111111111111')) + + assert_equal 'transaction_id', response.authorization + assert_equal true, response.test + end + + def test_purchase_transaction + Braintree::TransactionGateway.any_instance.expects(:sale). + returns(braintree_result) + + response = @gateway.purchase(100, credit_card('41111111111111111111')) + + assert_equal 'transaction_id', response.authorization + assert_equal true, response.test + end + + def test_capture_transaction + Braintree::TransactionGateway.any_instance.expects(:submit_for_settlement). + returns(braintree_result(id: 'capture_transaction_id')) + + response = @gateway.capture(100, 'transaction_id') + + assert_equal 'capture_transaction_id', response.authorization + assert_equal true, response.test + end + + def test_partial_capture_transaction + Braintree::TransactionGateway.any_instance.expects(:submit_for_partial_settlement). + returns(braintree_result(id: 'capture_transaction_id')) + + response = @gateway.capture(100, 'transaction_id', { partial_capture: true }) + + assert_equal 'capture_transaction_id', response.authorization + assert_equal true, response.test + end + + def test_refund_transaction + Braintree::TransactionGateway.any_instance.expects(:refund). + returns(braintree_result(id: 'refund_transaction_id')) + + response = @gateway.refund(1000, 'transaction_id') + assert_equal 'refund_transaction_id', response.authorization + assert_equal true, response.test + end + def test_void_transaction Braintree::TransactionGateway.any_instance.expects(:void). with('transaction_id'). - returns(braintree_result(:id => 'void_transaction_id')) + returns(braintree_result(id: 'void_transaction_id')) - response = @gateway.void('transaction_id', :test => true) + response = @gateway.void('transaction_id') assert_equal 'void_transaction_id', response.authorization + assert_equal true, response.test end def test_verify_good_credentials @@ -86,20 +141,61 @@ def test_verify_bad_credentials assert !@gateway.verify_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') + options = { + allow_card_verification: true, + billing_address: { + zip: '10000' + } + } + response = @gateway.verify(card, options) + assert_success response + assert_equal 'transaction_id', response.params['authorization'] + assert_equal 'M', response.cvv_result['code'] + assert_equal 'P', response.avs_result['code'] + end + + def test_failed_verification_transaction + Braintree::CreditCardVerificationGateway.any_instance.expects(:create). + returns(braintree_error_result(message: 'CVV must be 4 digits for American Express and 3 digits for other card types. (81707)')) + + card = credit_card('4111111111111111') + options = { + allow_card_verification: true, + billing_address: { + zip: '10000' + } + } + response = @gateway.verify(card, options) + assert_failure response + end + def test_user_agent_includes_activemerchant_version assert @internal_gateway.config.user_agent.include?("(ActiveMerchant #{ActiveMerchant::VERSION})") end def test_merchant_account_id_present_when_provided_on_gateway_initialization @gateway = BraintreeBlueGateway.new( - :merchant_id => 'test', - :merchant_account_id => 'present', - :public_key => 'test', - :private_key => 'test' + merchant_id: 'test', + merchant_account_id: 'present', + public_key: 'test', + private_key: 'test' ) Braintree::TransactionGateway.any_instance.expects(:sale). - with(has_entries(:merchant_account_id => 'present')). + with(has_entries(merchant_account_id: 'present')). returns(braintree_result) @gateway.authorize(100, credit_card('41111111111111111111')) @@ -107,33 +203,87 @@ def test_merchant_account_id_present_when_provided_on_gateway_initialization def test_merchant_account_id_on_transaction_takes_precedence @gateway = BraintreeBlueGateway.new( - :merchant_id => 'test', - :merchant_account_id => 'present', - :public_key => 'test', - :private_key => 'test' + merchant_id: 'test', + merchant_account_id: 'present', + public_key: 'test', + private_key: 'test' ) Braintree::TransactionGateway.any_instance.expects(:sale). - with(has_entries(:merchant_account_id => 'account_on_transaction')). + with(has_entries(merchant_account_id: 'account_on_transaction')). returns(braintree_result) - @gateway.authorize(100, credit_card('41111111111111111111'), :merchant_account_id => 'account_on_transaction') + @gateway.authorize(100, credit_card('41111111111111111111'), merchant_account_id: 'account_on_transaction') end def test_merchant_account_id_present_when_provided Braintree::TransactionGateway.any_instance.expects(:sale). - with(has_entries(:merchant_account_id => 'present')). + with(has_entries(merchant_account_id: 'present')). returns(braintree_result) - @gateway.authorize(100, credit_card('41111111111111111111'), :merchant_account_id => 'present') + @gateway.authorize(100, credit_card('41111111111111111111'), merchant_account_id: 'present') end def test_service_fee_amount_can_be_specified Braintree::TransactionGateway.any_instance.expects(:sale). - with(has_entries(:service_fee_amount => '2.31')). + with(has_entries(service_fee_amount: '2.31')). returns(braintree_result) - @gateway.authorize(100, credit_card('41111111111111111111'), :service_fee_amount => '2.31') + @gateway.authorize(100, credit_card('41111111111111111111'), service_fee_amount: '2.31') + end + + def test_venmo_profile_id_can_be_specified + Braintree::TransactionGateway.any_instance.expects(:sale).with do |params| + (params[:options][:venmo][:profile_id] == 'profile_id') + end.returns(braintree_result) + + @gateway.authorize(100, credit_card('41111111111111111111'), venmo_profile_id: 'profile_id') + end + + def test_customer_has_default_payment_method + options = { + payment_method_nonce: 'fake-paypal-future-nonce', + store: true, + device_data: 'device_data', + paypal: { + paypal_flow_type: 'checkout_with_vault' + } + } + + Braintree::TransactionGateway.any_instance.expects(:sale).returns(braintree_result(paypal: { implicitly_vaulted_payment_method_token: 'abc123' })) + + Braintree::CustomerGateway.any_instance.expects(:update).with(nil, { default_payment_method_token: 'abc123' }).returns(nil) + + @gateway.authorize(100, 'fake-paypal-future-nonce', options) + end + + def test_not_adding_default_payment_method_to_customer + options = { + prevent_default_payment_method: true, + payment_method_nonce: 'fake-paypal-future-nonce', + store: true, + device_data: 'device_data', + paypal: { + paypal_flow_type: 'checkout_with_vault' + } + } + + Braintree::TransactionGateway.any_instance.expects(:sale).returns(braintree_result(paypal: { implicitly_vaulted_payment_method_token: 'abc123' })) + + Braintree::CustomerGateway.any_instance.expects(:update).with(nil, { default_payment_method_token: 'abc123' }).never + + @gateway.authorize(100, 'fake-paypal-future-nonce', options) + end + + def test_risk_data_can_be_specified + risk_data = { + customer_browser: 'User-Agent Header', + customer_ip: '127.0.0.1' + } + Braintree::TransactionGateway.any_instance.expects(:sale). + with(has_entries(risk_data:)).returns(braintree_result) + + @gateway.authorize(100, credit_card('4111111111111111'), risk_data:) end def test_hold_in_escrow_can_be_specified @@ -141,7 +291,16 @@ def test_hold_in_escrow_can_be_specified (params[:options][:hold_in_escrow] == true) end.returns(braintree_result) - @gateway.authorize(100, credit_card('41111111111111111111'), :hold_in_escrow => true) + @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 @@ -154,61 +313,61 @@ def test_merchant_account_id_absent_if_not_provided def test_verification_merchant_account_id_exists_when_verify_card_and_merchant_account_id gateway = BraintreeBlueGateway.new( - :merchant_id => 'merchant_id', - :merchant_account_id => 'merchant_account_id', - :public_key => 'public_key', - :private_key => 'private_key' + merchant_id: 'merchant_id', + merchant_account_id: 'merchant_account_id', + public_key: 'public_key', + private_key: 'private_key' ) customer = stub( - :credit_cards => [stub_everything], - :email => 'email', - :phone => '321-654-0987', - :first_name => 'John', - :last_name => 'Smith' + credit_cards: [stub_everything], + email: 'email', + phone: '321-654-0987', + first_name: 'John', + last_name: 'Smith' ) customer.stubs(:id).returns('123') - result = Braintree::SuccessfulResult.new(:customer => customer) + result = Braintree::SuccessfulResult.new(customer:) Braintree::CustomerGateway.any_instance.expects(:create).with do |params| params[:credit_card][:options][:verification_merchant_account_id] == 'merchant_account_id' end.returns(result) - gateway.store(credit_card('41111111111111111111'), :verify_card => true) + gateway.store(credit_card('41111111111111111111'), verify_card: true) end def test_merchant_account_id_can_be_set_by_options gateway = BraintreeBlueGateway.new( - :merchant_id => 'merchant_id', - :merchant_account_id => 'merchant_account_id', - :public_key => 'public_key', - :private_key => 'private_key' + merchant_id: 'merchant_id', + merchant_account_id: 'merchant_account_id', + public_key: 'public_key', + private_key: 'private_key' ) customer = stub( - :credit_cards => [stub_everything], - :email => 'email', - :phone => '321-654-0987', - :first_name => 'John', - :last_name => 'Smith' + credit_cards: [stub_everything], + email: 'email', + phone: '321-654-0987', + first_name: 'John', + last_name: 'Smith' ) customer.stubs(:id).returns('123') - result = Braintree::SuccessfulResult.new(:customer => customer) + result = Braintree::SuccessfulResult.new(customer:) Braintree::CustomerGateway.any_instance.expects(:create).with do |params| params[:credit_card][:options][:verification_merchant_account_id] == 'value_from_options' end.returns(result) - gateway.store(credit_card('41111111111111111111'), :verify_card => true, :verification_merchant_account_id => 'value_from_options') + gateway.store(credit_card('41111111111111111111'), verify_card: true, verification_merchant_account_id: 'value_from_options') end def test_store_with_verify_card_true customer = stub( - :credit_cards => [stub_everything], - :email => 'email', - :phone => '321-654-0987', - :first_name => 'John', - :last_name => 'Smith' + credit_cards: [stub_everything], + email: 'email', + phone: '321-654-0987', + first_name: 'John', + last_name: 'Smith' ) customer.stubs(:id).returns('123') - result = Braintree::SuccessfulResult.new(:customer => customer) + result = Braintree::SuccessfulResult.new(customer:) Braintree::CustomerGateway.any_instance.expects(:create).with do |params| params[:credit_card][:options].has_key?(:verify_card) assert_equal true, params[:credit_card][:options][:verify_card] @@ -216,119 +375,171 @@ def test_store_with_verify_card_true params end.returns(result) - response = @gateway.store(credit_card('41111111111111111111'), :verify_card => true) + response = @gateway.store(credit_card('41111111111111111111'), verify_card: true) assert_equal '123', response.params['customer_vault_id'] assert_equal response.params['customer_vault_id'], response.authorization end def test_passes_email customer = stub( - :credit_cards => [stub_everything], - :email => 'bob@example.com', - :phone => '321-654-0987', - :first_name => 'John', - :last_name => 'Smith', + credit_cards: [stub_everything], + email: 'bob@example.com', + phone: '321-654-0987', + first_name: 'John', + last_name: 'Smith', id: '123' ) - result = Braintree::SuccessfulResult.new(:customer => customer) + result = Braintree::SuccessfulResult.new(customer:) Braintree::CustomerGateway.any_instance.expects(:create).with do |params| assert_equal 'bob@example.com', params[:email] params end.returns(result) - response = @gateway.store(credit_card('41111111111111111111'), :email => 'bob@example.com') + response = @gateway.store(credit_card('41111111111111111111'), email: 'bob@example.com') assert_success response end def test_scrubs_invalid_email customer = stub( - :credit_cards => [stub_everything], - :email => nil, - :phone => '321-654-0987', - :first_name => 'John', - :last_name => 'Smith', - :id => '123' + credit_cards: [stub_everything], + email: nil, + phone: '321-654-0987', + first_name: 'John', + last_name: 'Smith', + id: '123' ) - result = Braintree::SuccessfulResult.new(:customer => customer) + result = Braintree::SuccessfulResult.new(customer:) Braintree::CustomerGateway.any_instance.expects(:create).with do |params| assert_equal nil, params[:email] params end.returns(result) - response = @gateway.store(credit_card('41111111111111111111'), :email => 'bogus') + response = @gateway.store(credit_card('41111111111111111111'), email: 'bogus') assert_success response end def test_store_with_verify_card_false customer = stub( - :credit_cards => [stub_everything], - :email => 'email', - :phone => '321-654-0987', - :first_name => 'John', - :last_name => 'Smith' + credit_cards: [stub_everything], + email: 'email', + phone: '321-654-0987', + first_name: 'John', + last_name: 'Smith' ) customer.stubs(:id).returns('123') - result = Braintree::SuccessfulResult.new(:customer => customer) + result = Braintree::SuccessfulResult.new(customer:) Braintree::CustomerGateway.any_instance.expects(:create).with do |params| params[:credit_card][:options].has_key?(:verify_card) assert_equal false, params[:credit_card][:options][:verify_card] params end.returns(result) - response = @gateway.store(credit_card('41111111111111111111'), :verify_card => false) + response = @gateway.store(credit_card('41111111111111111111'), verify_card: false) assert_equal '123', response.params['customer_vault_id'] assert_equal response.params['customer_vault_id'], response.authorization end def test_store_with_billing_address_options customer_attributes = { - :credit_cards => [stub_everything], - :email => 'email', - :phone => '321-654-0987', - :first_name => 'John', - :last_name => 'Smith' + credit_cards: [stub_everything], + email: 'email', + phone: '321-654-0987', + first_name: 'John', + last_name: 'Smith' } billing_address = { - :address1 => '1 E Main St', - :address2 => 'Suite 403', - :city => 'Chicago', - :state => 'Illinois', - :zip => '60622', - :country_name => 'US' + address1: '1 E Main St', + address2: 'Suite 403', + city: 'Chicago', + state: 'Illinois', + zip: '60622', + country_name: 'US' } customer = stub(customer_attributes) customer.stubs(:id).returns('123') - result = Braintree::SuccessfulResult.new(:customer => customer) + result = Braintree::SuccessfulResult.new(customer:) Braintree::CustomerGateway.any_instance.expects(:create).with do |params| assert_not_nil params[:credit_card][:billing_address] - [:street_address, :extended_address, :locality, :region, :postal_code, :country_name].each do |billing_attribute| + %i[street_address extended_address locality region postal_code country_name].each do |billing_attribute| params[:credit_card][:billing_address].has_key?(billing_attribute) if params[:billing_address] end params end.returns(result) - @gateway.store(credit_card('41111111111111111111'), :billing_address => billing_address) + @gateway.store(credit_card('41111111111111111111'), billing_address:) + end + + def test_store_with_phone_only_billing_address_option + customer_attributes = { + credit_cards: [stub_everything], + email: 'email', + first_name: 'John', + last_name: 'Smith', + phone: '123-456-7890' + } + billing_address = { + phone: '123-456-7890' + } + customer = stub(customer_attributes) + customer.stubs(:id).returns('123') + result = Braintree::SuccessfulResult.new(customer:) + Braintree::CustomerGateway.any_instance.expects(:create).with do |params| + assert_nil params[:credit_card][:billing_address] + params + end.returns(result) + + @gateway.store(credit_card('41111111111111111111'), billing_address:) + end + + def test_store_with_nil_billing_address_options + customer_attributes = { + credit_cards: [stub_everything], + email: 'email', + first_name: 'John', + last_name: 'Smith', + phone: '123-456-7890' + } + billing_address = { + name: 'John Smith', + phone: '123-456-7890', + company: nil, + address1: nil, + address2: '', + city: nil, + state: nil, + zip: nil, + country_name: nil + } + customer = stub(customer_attributes) + customer.stubs(:id).returns('123') + result = Braintree::SuccessfulResult.new(customer:) + Braintree::CustomerGateway.any_instance.expects(:create).with do |params| + assert_nil params[:credit_card][:billing_address] + params + end.returns(result) + + @gateway.store(credit_card('41111111111111111111'), billing_address:) end def test_store_with_credit_card_token customer = stub( - :email => 'email', - :phone => '321-654-0987', - :first_name => 'John', - :last_name => 'Smith' + email: 'email', + phone: '321-654-0987', + first_name: 'John', + last_name: 'Smith' ) customer.stubs(:id).returns('123') braintree_credit_card = stub_everything(token: 'cctoken') customer.stubs(:credit_cards).returns([braintree_credit_card]) - result = Braintree::SuccessfulResult.new(:customer => customer) + result = Braintree::SuccessfulResult.new(customer:) Braintree::CustomerGateway.any_instance.expects(:create).with do |params| assert_equal 'cctoken', params[:credit_card][:token] params end.returns(result) - response = @gateway.store(credit_card('41111111111111111111'), :credit_card_token => 'cctoken') + response = @gateway.store(credit_card('41111111111111111111'), credit_card_token: 'cctoken') assert_success response assert_equal 'cctoken', response.params['braintree_customer']['credit_cards'][0]['token'] assert_equal 'cctoken', response.params['credit_card_token'] @@ -336,15 +547,15 @@ def test_store_with_credit_card_token def test_store_with_customer_id customer = stub( - :email => 'email', - :phone => '321-654-0987', - :first_name => 'John', - :last_name => 'Smith', - :credit_cards => [stub_everything] + email: 'email', + phone: '321-654-0987', + first_name: 'John', + last_name: 'Smith', + credit_cards: [stub_everything] ) customer.stubs(:id).returns('customerid') - result = Braintree::SuccessfulResult.new(:customer => customer) + result = Braintree::SuccessfulResult.new(customer:) Braintree::CustomerGateway.any_instance.expects(:find). with('customerid'). raises(Braintree::NotFoundError) @@ -353,7 +564,7 @@ def test_store_with_customer_id params end.returns(result) - response = @gateway.store(credit_card('41111111111111111111'), :customer => 'customerid') + response = @gateway.store(credit_card('41111111111111111111'), customer: 'customerid') assert_success response assert_equal 'customerid', response.params['braintree_customer']['id'] end @@ -364,7 +575,7 @@ def test_store_with_existing_customer_id token: 'cctoken' ) - result = Braintree::SuccessfulResult.new(credit_card: credit_card) + result = Braintree::SuccessfulResult.new(credit_card:) Braintree::CustomerGateway.any_instance.expects(:find).with('customerid') Braintree::CreditCardGateway.any_instance.expects(:create).with do |params| assert_equal 'customerid', params[:customer_id] @@ -380,88 +591,125 @@ def test_store_with_existing_customer_id assert_equal 'cctoken', response.params['credit_card_token'] end + def test_store_with_existing_customer_id_and_nil_billing_address_options + credit_card = stub( + customer_id: 'customerid', + token: 'cctoken' + ) + options = { + customer: 'customerid', + billing_address: { + name: 'John Smith', + phone: '123-456-7890', + company: nil, + address1: nil, + address2: nil, + city: nil, + state: nil, + zip: nil, + country_name: nil + } + } + + result = Braintree::SuccessfulResult.new(credit_card:) + Braintree::CustomerGateway.any_instance.expects(:find).with('customerid') + Braintree::CreditCardGateway.any_instance.expects(:create).with do |params| + assert_equal 'customerid', params[:customer_id] + assert_equal '41111111111111111111', params[:number] + assert_equal 'Longbob Longsen', params[:cardholder_name] + params + end.returns(result) + + response = @gateway.store(credit_card('41111111111111111111'), options) + assert_success response + assert_nil response.params['braintree_customer'] + assert_equal 'customerid', response.params['customer_vault_id'] + assert_equal 'cctoken', response.params['credit_card_token'] + end + def test_update_with_cvv - stored_credit_card = mock(:token => 'token', :default? => true) - customer = mock(:credit_cards => [stored_credit_card], :id => '123') + stored_credit_card = mock(token: 'token', default?: true) + customer = mock(credit_cards: [stored_credit_card], id: '123') Braintree::CustomerGateway.any_instance.stubs(:find).with('vault_id').returns(customer) BraintreeBlueGateway.any_instance.stubs(:customer_hash) - result = Braintree::SuccessfulResult.new(:customer => customer) + result = Braintree::SuccessfulResult.new(customer:) Braintree::CustomerGateway.any_instance.expects(:update).with do |vault, params| assert_equal '567', params[:credit_card][:cvv] assert_equal 'Longbob Longsen', params[:credit_card][:cardholder_name] [vault, params] end.returns(result) - @gateway.update('vault_id', credit_card('41111111111111111111', :verification_value => '567')) + @gateway.update('vault_id', credit_card('41111111111111111111', verification_value: '567')) end def test_update_with_verify_card_true - stored_credit_card = stub(:token => 'token', :default? => true) - customer = stub(:credit_cards => [stored_credit_card], :id => '123') + stored_credit_card = stub(token: 'token', default?: true) + customer = stub(credit_cards: [stored_credit_card], id: '123') Braintree::CustomerGateway.any_instance.stubs(:find).with('vault_id').returns(customer) BraintreeBlueGateway.any_instance.stubs(:customer_hash) - result = Braintree::SuccessfulResult.new(:customer => customer) + result = Braintree::SuccessfulResult.new(customer:) Braintree::CustomerGateway.any_instance.expects(:update).with do |vault, params| assert_equal true, params[:credit_card][:options][:verify_card] [vault, params] end.returns(result) - @gateway.update('vault_id', credit_card('41111111111111111111'), :verify_card => true) + @gateway.update('vault_id', credit_card('41111111111111111111'), verify_card: true) end def test_merge_credit_card_options_ignores_bad_option - params = {:first_name => 'John', :credit_card => {:cvv => '123'}} - options = {:verify_card => true, :bogus => 'ignore me'} + params = { first_name: 'John', credit_card: { cvv: '123' } } + options = { verify_card: true, bogus: 'ignore me' } merged_params = @gateway.send(:merge_credit_card_options, params, options) - expected_params = {:first_name => 'John', :credit_card => {:cvv => '123', :options => {:verify_card => true}}} + expected_params = { first_name: 'John', credit_card: { cvv: '123', options: { verify_card: true } } } assert_equal expected_params, merged_params end def test_merge_credit_card_options_handles_nil_credit_card - params = {:first_name => 'John'} - options = {:verify_card => true, :bogus => 'ignore me'} + params = { first_name: 'John' } + options = { verify_card: true, bogus: 'ignore me' } merged_params = @gateway.send(:merge_credit_card_options, params, options) - expected_params = {:first_name => 'John', :credit_card => {:options => {:verify_card => true}}} + expected_params = { first_name: 'John', credit_card: { options: { verify_card: true } } } assert_equal expected_params, merged_params end def test_merge_credit_card_options_handles_billing_address billing_address = { - :address1 => '1 E Main St', - :city => 'Chicago', - :state => 'Illinois', - :zip => '60622', - :country => 'US' + address1: '1 E Main St', + city: 'Chicago', + state: 'Illinois', + zip: '60622', + country: 'US' } - params = {:first_name => 'John'} - options = {:billing_address => billing_address} + params = { first_name: 'John' } + options = { billing_address: } expected_params = { - :first_name => 'John', - :credit_card => { - :billing_address => { - :street_address => '1 E Main St', - :extended_address => nil, - :company => nil, - :locality => 'Chicago', - :region => 'Illinois', - :postal_code => '60622', - :country_code_alpha2 => 'US' + first_name: 'John', + credit_card: { + billing_address: { + street_address: '1 E Main St', + extended_address: nil, + company: nil, + locality: 'Chicago', + region: 'Illinois', + postal_code: '60622', + country_code_alpha2: 'US', + country_code_alpha3: 'USA' }, - :options => {} + options: {} } } assert_equal expected_params, @gateway.send(:merge_credit_card_options, params, options) end def test_merge_credit_card_options_only_includes_billing_address_when_present - params = {:first_name => 'John'} + params = { first_name: 'John' } options = {} expected_params = { - :first_name => 'John', - :credit_card => { - :options => {} + first_name: 'John', + credit_card: { + options: {} } } assert_equal expected_params, @gateway.send(:merge_credit_card_options, params, options) @@ -471,83 +719,182 @@ def test_address_country_handling Braintree::TransactionGateway.any_instance.expects(:sale).with do |params| (params[:billing][:country_code_alpha2] == 'US') end.returns(braintree_result) - @gateway.purchase(100, credit_card('41111111111111111111'), :billing_address => {:country => 'US'}) + @gateway.purchase(100, credit_card('41111111111111111111'), billing_address: { country: 'US' }) Braintree::TransactionGateway.any_instance.expects(:sale).with do |params| (params[:billing][:country_code_alpha2] == 'US') end.returns(braintree_result) - @gateway.purchase(100, credit_card('41111111111111111111'), :billing_address => {:country_code_alpha2 => 'US'}) + @gateway.purchase(100, credit_card('41111111111111111111'), billing_address: { country_code_alpha2: 'US' }) Braintree::TransactionGateway.any_instance.expects(:sale).with do |params| (params[:billing][:country_name] == 'United States of America') end.returns(braintree_result) - @gateway.purchase(100, credit_card('41111111111111111111'), :billing_address => {:country_name => 'United States of America'}) + @gateway.purchase(100, credit_card('41111111111111111111'), billing_address: { country_name: 'United States of America' }) Braintree::TransactionGateway.any_instance.expects(:sale).with do |params| (params[:billing][:country_code_alpha3] == 'USA') end.returns(braintree_result) - @gateway.purchase(100, credit_card('41111111111111111111'), :billing_address => {:country_code_alpha3 => 'USA'}) + @gateway.purchase(100, credit_card('41111111111111111111'), billing_address: { country_code_alpha3: 'USA' }) Braintree::TransactionGateway.any_instance.expects(:sale).with do |params| (params[:billing][:country_code_numeric] == 840) end.returns(braintree_result) - @gateway.purchase(100, credit_card('41111111111111111111'), :billing_address => {:country_code_numeric => 840}) + @gateway.purchase(100, credit_card('41111111111111111111'), billing_address: { country_code_numeric: 840 }) end def test_address_zip_handling Braintree::TransactionGateway.any_instance.expects(:sale).with do |params| (params[:billing][:postal_code] == '12345') end.returns(braintree_result) - @gateway.purchase(100, credit_card('41111111111111111111'), :billing_address => {:zip => '12345'}) + @gateway.purchase(100, credit_card('41111111111111111111'), billing_address: { zip: '12345' }) Braintree::TransactionGateway.any_instance.expects(:sale).with do |params| (params[:billing][:postal_code] == nil) end.returns(braintree_result) - @gateway.purchase(100, credit_card('41111111111111111111'), :billing_address => {:zip => '1234567890'}) + @gateway.purchase(100, credit_card('41111111111111111111'), billing_address: { zip: '1234567890' }) end def test_cardholder_name_passing_with_card Braintree::TransactionGateway.any_instance.expects(:sale).with do |params| (params[:credit_card][:cardholder_name] == 'Longbob Longsen') end.returns(braintree_result) - @gateway.purchase(100, credit_card('41111111111111111111'), :customer => {:first_name => 'Longbob', :last_name => 'Longsen'}) + @gateway.purchase(100, credit_card('41111111111111111111'), customer: { first_name: 'Longbob', last_name: 'Longsen' }) end - def test_three_d_secure_pass_thru_handling + def test_three_d_secure_pass_thru_handling_version_1 Braintree::TransactionGateway. any_instance. expects(:sale). with(has_entries(three_d_secure_pass_thru: { cavv: 'cavv', eci_flag: 'eci', - xid: 'xid', - })). - returns(braintree_result) + xid: 'xid' + })). + returns(braintree_result) + + @gateway.purchase(100, credit_card('41111111111111111111'), three_d_secure: { cavv: 'cavv', eci: 'eci', xid: 'xid' }) + end + + def test_three_d_secure_pass_thru_handling_version_2 + 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: 'eci', + ds_transaction_id: 'trans_id', + cavv_algorithm: 'algorithm', + 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 + Braintree::TransactionGateway. + any_instance. + expects(:sale). + with(has_entries(three_d_secure_pass_thru: has_entries( + three_d_secure_version: '2.0', + cavv: 'cavv', + eci_flag: 'eci', + ds_transaction_id: 'trans_id' + ))). + returns(braintree_result) - @gateway.purchase(100, credit_card('41111111111111111111'), three_d_secure: {cavv: 'cavv', eci: 'eci', xid: 'xid'}) + @gateway.purchase(100, credit_card('41111111111111111111'), three_d_secure: { version: '2.0', cavv: 'cavv', eci: 'eci', ds_transaction_id: 'trans_id' }) + end + + def test_purchase_string_based_payment_method_nonce_removes_customer + Braintree::TransactionGateway. + any_instance. + expects(:sale). + with(Not(has_key(:customer))). + returns(braintree_result) + + @gateway.purchase(100, credit_card('41111111111111111111'), payment_method_nonce: '1234') + end + + def test_authorize_string_based_payment_method_nonce_removes_customer + Braintree::TransactionGateway. + any_instance. + expects(:sale). + with(Not(has_key(:customer))). + returns(braintree_result) + + @gateway.authorize(100, credit_card('41111111111111111111'), payment_method_nonce: '1234') end def test_passes_recurring_flag @gateway = BraintreeBlueGateway.new( - :merchant_id => 'test', - :merchant_account_id => 'present', - :public_key => 'test', - :private_key => 'test' + merchant_id: 'test', + merchant_account_id: 'present', + public_key: 'test', + private_key: 'test' ) Braintree::TransactionGateway.any_instance.expects(:sale). - with(has_entries(:recurring => true)). + with(has_entries(transaction_source: 'recurring')). returns(braintree_result) - @gateway.purchase(100, credit_card('41111111111111111111'), :recurring => true) + @gateway.purchase(100, credit_card('41111111111111111111'), recurring: true) Braintree::TransactionGateway.any_instance.expects(:sale). - with(Not(has_entries(:recurring => true))). + with(Not(has_entries(recurring: true))). returns(braintree_result) @gateway.purchase(100, credit_card('41111111111111111111')) end + def test_passes_transaction_source + Braintree::TransactionGateway.any_instance.expects(:sale).with do |params| + (params[:transaction_source] == 'recurring') && (params[:recurring] == nil) + end.returns(braintree_result) + @gateway.purchase(100, credit_card('41111111111111111111'), transaction_source: 'recurring', recurring: true) + end + + def test_passes_skip_avs + Braintree::TransactionGateway.any_instance.expects(:sale).with do |params| + (params[:options][:skip_avs] == true) + end.returns(braintree_result(avs_postal_code_response_code: 'B', avs_street_address_response_code: 'B')) + + response = @gateway.purchase(100, credit_card('41111111111111111111'), skip_avs: true) + assert_equal 'B', response.avs_result['code'] + end + + def test_passes_skip_cvv + Braintree::TransactionGateway.any_instance.expects(:sale).with do |params| + (params[:options][:skip_cvv] == true) + end.returns(braintree_result(cvv_response_code: 'B')) + + response = @gateway.purchase(100, credit_card('41111111111111111111'), skip_cvv: true) + assert_equal 'B', response.cvv_result['code'] + end + + def test_successful_purchase_with_account_type + Braintree::TransactionGateway.any_instance.expects(:sale).with do |params| + params[:options][:credit_card][:account_type] == 'credit' + end.returns(braintree_result) + + @gateway.purchase(100, credit_card('41111111111111111111'), account_type: 'credit') + end + def test_configured_logger_has_a_default # The default is actually provided by the Braintree gem, but we # assert its presence in order to show ActiveMerchant need not @@ -566,9 +913,9 @@ def test_default_logger_sets_warn_level_without_overwriting_global # Re-instantiate a gateway to show it doesn't touch the global gateway = BraintreeBlueGateway.new( - :merchant_id => 'test', - :public_key => 'test', - :private_key => 'test' + merchant_id: 'test', + public_key: 'test', + private_key: 'test' ) internal_gateway = gateway.instance_variable_get(:@braintree_gateway) @@ -585,9 +932,9 @@ def test_that_setting_a_wiredump_device_on_the_gateway_sets_the_braintree_logger assert_not_equal logger, Braintree::Configuration.logger gateway = BraintreeBlueGateway.new( - :merchant_id => 'test', - :public_key => 'test', - :private_key => 'test' + merchant_id: 'test', + public_key: 'test', + private_key: 'test' ) internal_gateway = gateway.instance_variable_get(:@braintree_gateway) @@ -596,88 +943,226 @@ def test_that_setting_a_wiredump_device_on_the_gateway_sets_the_braintree_logger end end - def test_solution_id_is_added_to_create_transaction_parameters - assert_nil @gateway.send(:create_transaction_parameters, 100, credit_card('41111111111111111111'),{})[:channel] + def test_channel_is_added_to_create_transaction_parameters + assert_nil @gateway.send(:create_transaction_parameters, 100, credit_card('41111111111111111111'), {})[:channel] ActiveMerchant::Billing::BraintreeBlueGateway.application_id = 'ABC123' - assert_equal @gateway.send(:create_transaction_parameters, 100, credit_card('41111111111111111111'),{})[:channel], 'ABC123' + assert_equal @gateway.send(:create_transaction_parameters, 100, credit_card('41111111111111111111'), {})[:channel], 'ABC123' - gateway = BraintreeBlueGateway.new(:merchant_id => 'test', :public_key => 'test', :private_key => 'test', channel: 'overidden-channel') - assert_equal gateway.send(:create_transaction_parameters, 100, credit_card('41111111111111111111'),{})[:channel], 'overidden-channel' + gateway = BraintreeBlueGateway.new(merchant_id: 'test', public_key: 'test', private_key: 'test', channel: 'overidden-channel') + assert_equal gateway.send(:create_transaction_parameters, 100, credit_card('41111111111111111111'), {})[:channel], 'overidden-channel' ensure ActiveMerchant::Billing::BraintreeBlueGateway.application_id = nil end + def test_override_application_id_is_sent_to_channel + gateway = BraintreeBlueGateway.new(merchant_id: 'test', public_key: 'test', private_key: 'test', channel: 'overidden-channel') + gateway_response = gateway.send(:create_transaction_parameters, 100, credit_card('41111111111111111111'), {}) + assert_equal gateway_response[:channel], 'overidden-channel' + + gateway_response = gateway.send(:create_transaction_parameters, 100, credit_card('41111111111111111111'), { override_application_id: 'override-application-id' }) + assert_equal gateway_response[:channel], 'override-application-id' + end + def test_successful_purchase_with_descriptor Braintree::TransactionGateway.any_instance.expects(:sale).with do |params| (params[:descriptor][:name] == 'wow*productname') && - (params[:descriptor][:phone] == '4443331112') && - (params[:descriptor][:url] == 'wow.com') + (params[:descriptor][:phone] == '4443331112') && + (params[:descriptor][:url] == 'wow.com') end.returns(braintree_result) @gateway.purchase(100, credit_card('41111111111111111111'), descriptor_name: 'wow*productname', descriptor_phone: '4443331112', descriptor_url: 'wow.com') end + def test_successful_purchase_with_device_data + Braintree::TransactionGateway.any_instance.expects(:sale).with do |params| + (params[:device_data] == 'device data string') + end.returns(braintree_result({ risk_data: { id: 123456, decision: 'Decline', device_data_captured: true, fraud_service_provider: 'kount' } })) + + response = @gateway.purchase(100, credit_card('41111111111111111111'), device_data: 'device data string') + + assert transaction = response.params['braintree_transaction'] + assert transaction['risk_data'] + assert_equal 123456, transaction['risk_data']['id'] + assert_equal 'Decline', transaction['risk_data']['decision'] + assert_equal true, transaction['risk_data']['device_data_captured'] + assert_equal 'kount', transaction['risk_data']['fraud_service_provider'] + end + + def test_successful_purchase_with_travel_data + Braintree::TransactionGateway.any_instance.expects(:sale).with do |params| + (params[:industry][:industry_type] == Braintree::Transaction::IndustryType::TravelAndCruise) && + (params[:industry][:data][:travel_package] == 'flight') && + (params[:industry][:data][:departure_date] == '2050-07-22') && + (params[:industry][:data][:lodging_check_in_date] == '2050-07-22') && + (params[:industry][:data][:lodging_check_out_date] == '2050-07-25') && + (params[:industry][:data][:lodging_name] == 'Best Hotel Ever') + end.returns(braintree_result) + + @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 + Braintree::TransactionGateway.any_instance.expects(:sale).with do |params| + (params[:industry][:industry_type] == Braintree::Transaction::IndustryType::Lodging) && + (params[:industry][:data][:folio_number] == 'ABC123') && + (params[:industry][:data][:check_in_date] == '2050-12-22') && + (params[:industry][:data][:check_out_date] == '2050-12-25') && + (params[:industry][:data][:room_rate] == '80.00') + end.returns(braintree_result) + + @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 Braintree::TransactionGateway.any_instance.expects(:sale). with( - :amount => '1.00', - :order_id => '1', - :customer => {:id => nil, :email => nil, :phone => nil, - :first_name => 'Longbob', :last_name => 'Longsen'}, - :options => {:store_in_vault => false, :submit_for_settlement => nil, :hold_in_escrow => nil}, - :custom_fields => nil, - :apple_pay_card => { - :number => '4111111111111111', - :expiration_month => '09', - :expiration_year => (Time.now.year + 1).to_s, - :cardholder_name => 'Longbob Longsen', - :cryptogram => '111111111100cryptogram', - :eci_indicator => '05' + amount: '1.00', + order_id: '1', + customer: { id: nil, email: nil, phone: nil, + first_name: 'Longbob', last_name: 'Longsen' }, + options: { store_in_vault: false, submit_for_settlement: nil, hold_in_escrow: nil }, + custom_fields: nil, + apple_pay_card: { + number: '4111111111111111', + expiration_month: '09', + expiration_year: (Time.now.year + 1).to_s, + cardholder_name: 'Longbob Longsen', + cryptogram: '111111111100cryptogram', + eci_indicator: '05' } ). - returns(braintree_result(:id => 'transaction_id')) + returns(braintree_result(id: 'transaction_id')) + + credit_card = network_tokenization_credit_card( + '4111111111111111', + brand: 'visa', + transaction_id: '123', + eci: '05', + payment_cryptogram: '111111111100cryptogram' + ) - credit_card = network_tokenization_credit_card('4111111111111111', - :brand => 'visa', - :transaction_id => '123', - :eci => '05', - :payment_cryptogram => '111111111100cryptogram' + response = @gateway.authorize(100, credit_card, test: true, order_id: '1') + assert_equal 'transaction_id', response.authorization + end + + def test_apple_pay_card_recurring + Braintree::TransactionGateway.any_instance.expects(:sale). + with( + amount: '1.00', + order_id: '1', + customer: { id: nil, email: nil, phone: nil, + first_name: 'Longbob', last_name: 'Longsen' }, + options: { store_in_vault: false, submit_for_settlement: nil, hold_in_escrow: nil }, + custom_fields: nil, + apple_pay_card: { + number: '4111111111111111', + expiration_month: '09', + expiration_year: (Time.now.year + 1).to_s, + cardholder_name: 'Longbob Longsen', + cryptogram: 'cryptogram' + }, + external_vault: { + status: 'vaulted', + previous_network_transaction_id: '123ABC' + }, + transaction_source: 'recurring' + ). + returns(braintree_result(id: 'transaction_id')) + + apple_pay = network_tokenization_credit_card( + '4111111111111111', + brand: 'visa', + transaction_id: '123', + payment_cryptogram: 'some_other_value', + source: :apple_pay + ) + + response = @gateway.authorize(100, apple_pay, { test: true, order_id: '1', stored_credential: stored_credential(:merchant, :recurring, id: '123ABC') }) + assert_equal 'transaction_id', response.authorization + end + + def test_google_pay_card + Braintree::TransactionGateway.any_instance.expects(:sale). + with( + amount: '1.00', + order_id: '1', + customer: { id: nil, email: nil, phone: nil, + 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: { + 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' + } + ). + 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' ) - response = @gateway.authorize(100, credit_card, :test => true, :order_id => '1') + 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_network_token_card Braintree::TransactionGateway.any_instance.expects(:sale). with( - :amount => '1.00', - :order_id => '1', - :customer => {:id => nil, :email => nil, :phone => nil, - :first_name => 'Longbob', :last_name => 'Longsen'}, - :options => {:store_in_vault => false, :submit_for_settlement => nil, :hold_in_escrow => nil}, - :custom_fields => nil, - :android_pay_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' + amount: '1.00', + order_id: '1', + customer: { id: nil, email: nil, phone: nil, + first_name: 'Longbob', last_name: 'Longsen' }, + options: { store_in_vault: false, submit_for_settlement: nil, hold_in_escrow: nil }, + custom_fields: nil, + credit_card: { + number: '4111111111111111', + expiration_month: '09', + expiration_year: (Time.now.year + 1).to_s, + cardholder_name: 'Longbob Longsen', + network_tokenization_attributes: { + cryptogram: '111111111100cryptogram', + ecommerce_indicator: '05' + } } ). - returns(braintree_result(:id => 'transaction_id')) + returns(braintree_result(id: 'transaction_id')) credit_card = network_tokenization_credit_card('4111111111111111', - :brand => 'visa', - :eci => '05', - :payment_cryptogram => '111111111100cryptogram', - :source => :android_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') + response = @gateway.authorize(100, credit_card, test: true, order_id: '1') assert_equal 'transaction_id', response.authorization end @@ -686,12 +1171,44 @@ def test_supports_network_tokenization end def test_unsuccessful_transaction_returns_id_when_available - Braintree::TransactionGateway.any_instance.expects(:sale).returns(braintree_error_result(transaction: {id: 'transaction_id'})) + Braintree::TransactionGateway.any_instance.expects(:sale).returns(braintree_error_result(transaction: { id: 'transaction_id' })) assert response = @gateway.purchase(100, credit_card('41111111111111111111')) refute response.success? assert response.authorization.present? end + def test_unsuccessful_adding_bank_account_to_customer + bank_account = check({ account_number: '1000000002', routing_number: '011000015' }) + options = { + billing_address: { + address1: '1670', + address2: '1670 NW 82ND AVE', + city: 'Miami', + state: 'FL', + zip: '32191' + }, + ach_mandate: 'ACH Mandate', + merchant_account_id: 'merchant_account_id' + } + customer = stub( + credit_cards: [stub_everything], + email: 'email', + phone: '321-654-0987', + first_name: 'John', + last_name: 'Smith' + ) + customer.stubs(:id).returns('123') + + Braintree::CustomerGateway.any_instance.expects(:create).returns(Braintree::SuccessfulResult.new(customer:)) + Braintree::ClientTokenGateway.any_instance.expects(:generate).returns('IntcImNsaWVudF90b2tlblwiOlwiMTIzNFwifSI=') + ActiveMerchant::Billing::TokenNonce.any_instance.expects(:ssl_request).returns(JSON.generate(token_bank_response)) + Braintree::PaymentMethodGateway.any_instance.expects(:create).returns(braintree_error_result(message: 'US bank account is not accepted by merchant account.')) + + assert response = @gateway.store(bank_account, options) + refute response.success? + assert_equal response.message, 'US bank account is not accepted by merchant account.' + end + def test_unsuccessful_transaction_returns_message_when_available Braintree::TransactionGateway.any_instance. expects(:sale). @@ -727,17 +1244,426 @@ def test_refund_unsettled_payment_forces_void_on_full_refund assert response.success? end + def test_refund_unsettled_payment_other_error_does_not_void + Braintree::TransactionGateway.any_instance. + expects(:refund). + returns(braintree_error_result(message: 'Some error message')) + + Braintree::TransactionGateway.any_instance. + expects(:void). + never + + response = @gateway.refund(1.00, 'transaction_id', force_full_refund_if_unsettled: true) + refute response.success? + end + + def test_stored_credential_recurring_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_credential: stored_credential(:cardholder, :recurring, :initial) }) + end + + def test_stored_credential_recurring_cit_used + 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_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: 'recurring' + } + ) + ).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( + { + external_vault: { + status: 'will_vault' + }, + transaction_source: 'recurring_first' + } + ) + ).returns(braintree_result) + + @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', stored_credential: stored_credential(:merchant, :recurring, :initial) }) + end + + def test_stored_credential_recurring_mit_used + 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_credential: stored_credential(:merchant, :recurring, id: '123ABC') }) + end + + def test_stored_credential_installment_cit_initial + 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_credential: stored_credential(:cardholder, :installment, :initial) }) + end + + def test_stored_credential_installment_cit_used + 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_credential: stored_credential(:cardholder, :installment, id: '123ABC') }) + end + + def test_stored_credential_installment_mit_initial + 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_credential: stored_credential(:merchant, :installment, :initial) }) + end + + def test_stored_credential_installment_mit_used + 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_credential: stored_credential(:merchant, :installment, id: '123ABC') }) + end + + def test_stored_credential_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_credential: stored_credential(:cardholder, :unscheduled, :initial) }) + end + + def test_stored_credential_unscheduled_cit_used + Braintree::TransactionGateway.any_instance.expects(:sale).with( + standard_purchase_params.merge( + { + external_vault: { + status: 'vaulted', + previous_network_transaction_id: '123ABC' + }, + transaction_source: '' + } + ) + ).returns(braintree_result) + + @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', stored_credential: stored_credential(:cardholder, :unscheduled, id: '123ABC') }) + end + + def test_stored_credential_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_credential: stored_credential(:merchant, :unscheduled, :initial) }) + end + + def test_stored_credential_unscheduled_mit_used + Braintree::TransactionGateway.any_instance.expects(:sale).with( + standard_purchase_params.merge( + { + external_vault: { + status: 'vaulted', + previous_network_transaction_id: '123ABC' + }, + transaction_source: 'unscheduled' + } + ) + ).returns(braintree_result) + + @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', stored_credential: stored_credential(:merchant, :unscheduled, id: '123ABC') }) + end + + def test_stored_credential_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_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_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( + { + external_vault: { + status: 'will_vault' + }, + transaction_source: 'moto' + } + ) + ).returns(braintree_result) + + @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_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_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_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_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_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_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' }) + + err = @gateway.store(bank_account, { customer: 'abc123' }) + assert_equal 'billing_address is required parameter to store and verify Bank accounts.', err.message + end + + def test_returns_error_on_authorize_when_passing_a_bank_account + bank_account = check({ account_number: '1000000002', routing_number: '011000015' }) + response = @gateway.authorize(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_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: { + address1: '1670', + address2: '1670 NW 82ND AVE', + city: 'Miami', + state: 'FL', + zip: '32191' + } + } + bank_account = check({ account_number: '1000000002', routing_number: '011000015' }) + response = @gateway.store(bank_account, options) + + assert_failure response + assert_match(/ach_mandate is a required parameter to process/, response.message) + end + + def test_scrub_sensitive_data + assert_equal filtered_success_token_nonce, @gateway.scrub(success_create_token_nonce) + end + + def test_transcript_scrubbing_network_token + assert_equal @gateway.scrub(pre_scrub_network_token), post_scrub_network_token + 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 = {}) - Braintree::SuccessfulResult.new(:transaction => Braintree::Transaction._new(nil, {:id => 'transaction_id'}.merge(options))) + Braintree::SuccessfulResult.new(transaction: Braintree::Transaction._new(nil, { id: 'transaction_id' }.merge(options))) end def braintree_error_result(options = {}) - Braintree::ErrorResult.new(@internal_gateway, {errors: {}}.merge(options)) + Braintree::ErrorResult.new(@internal_gateway, { errors: {} }.merge(options)) end - def with_braintree_configuration_restoration(&block) + def with_braintree_configuration_restoration(&) # Remember the wiredump device since we may overwrite it existing_wiredump_device = ActiveMerchant::Billing::BraintreeBlueGateway.wiredump_device @@ -749,4 +1675,844 @@ def with_braintree_configuration_restoration(&block) # Reset the Braintree logger Braintree::Configuration.logger = nil end + + def standard_purchase_params + { + amount: '1.00', + order_id: '1', + customer: { id: nil, email: nil, phone: nil, + first_name: 'Longbob', last_name: 'Longsen' }, + options: { store_in_vault: false, submit_for_settlement: true, hold_in_escrow: nil }, + custom_fields: nil, + credit_card: { + number: '41111111111111111111', + cvv: '123', + expiration_month: '09', + expiration_year: (Time.now.year + 1).to_s, + cardholder_name: 'Longbob Longsen' + } + } + 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 + + def success_create_token_nonce + <<-RESPONSE + [Braintree] + [Braintree] 673970040 + [Braintree] tokenusbankacct_bc_tbf5zn_6xtcs8_wmknct_y3yfy5_sg6 + [Braintree] + [Braintree] network_check + [Braintree] + [Braintree] + [Braintree] + [Braintree] eyJ2ZXJzaW9uIjoyLCJhdXRob3JpemF0aW9uRmluZ2VycHJpbnQiOiJleUowZVhBaU9pSktWMVFpTENKaGJHY2lPaUpGVXpJMU5pSXNJbXRwWkNJNklqSXdNVGd3TkRJMk1UWXRjMkZ1WkdKdmVDSXNJbWx6Y3lJNkltaDBkSEJ6T2k4dllYQnBMbk5oYm1SaWIzZ3VZbkpoYVc1MGNtVmxaMkYwWlhkaGVTNWpiMjBpZlEuZXlKbGVIQWlPakUyTkRNeE5EazFNVEFzSW1wMGFTSTZJbVJpTkRJME1XRmpMVGMwTkdVdE5EWmpOQzFoTjJWakxUbGlNakpoWm1KaFl6QmxZU0lzSW5OMVlpSTZJbXRrTm5SaVkydGpaR1JtTm5sMlpHY2lMQ0pwYzNNaU9pSm9kSFJ3Y3pvdkwyRndhUzV6WVc1a1ltOTRMbUp5WVdsdWRISmxaV2RoZEdWM1lYa3VZMjl0SWl3aWJXVnlZMmhoYm5RaU9uc2ljSFZpYkdsalgybGtJam9pYTJRMmRHSmphMk5rWkdZMmVYWmtaeUlzSW5abGNtbG1lVjlqWVhKa1gySjVYMlJsWm1GMWJIUWlPblJ5ZFdWOUxDSnlhV2RvZEhNaU9sc2liV0Z1WVdkbFgzWmhkV3gwSWwwc0luTmpiM0JsSWpwYklrSnlZV2x1ZEhKbFpUcFdZWFZzZENKZExDSnZjSFJwYjI1eklqcDdmWDAuYnpKRUFZWWxSenhmOUJfNGJvN1JrUlZaMERtR1pEVFRieDVQWWxXdFNCSjhnc19pT3RTQ0MtWHRYcEM3NE5pV1A5a0g0MG9neWVKVzZaNkpTbnNOTGciLCJjb25maWdVcmwiOiJodHRwczovL2FwaS5zYW5kYm94LmJyYWludHJlZWdhdGV3YXkuY29tOjQ0My9tZXJjaGFudHMva2Q2dGJja2NkZGY2eXZkZy9jbGllbnRfYXBpL3YxL2NvbmZpZ3VyYXRpb24iLCJncmFwaFFMIjp7InVybCI6Imh0dHBzOi8vcGF5bWVudHMuc2FuZGJveC5icmFpbnRyZWUtYXBpLmNvbS9ncmFwaHFsIiwiZGF0ZSI6IjIwMTgtMDUtMDgiLCJmZWF0dXJlcyI6WyJ0b2tlbml6ZV9jcmVkaXRfY2FyZHMiXX0sImNsaWVudEFwaVVybCI6Imh0dHBzOi8vYXBpLnNhbmRib3guYnJhaW50cmVlZ2F0ZXdheS5jb206NDQzL21lcmNoYW50cy9rZDZ0YmNrY2RkZjZ5dmRnL2NsaWVudF9hcGkiLCJlbnZpcm9ubWVudCI6InNhbmRib3giLCJtZXJjaGFudElkIjoia2Q2dGJja2NkZGY2eXZkZyIsImFzc2V0c1VybCI6Imh0dHBzOi8vYXNzZXRzLmJyYWludHJlZWdhdGV3YXkuY29tIiwiYXV0aFVybCI6Imh0dHBzOi8vYXV0aC52ZW5tby5zYW5kYm94LmJyYWludHJlZWdhdGV3YXkuY29tIiwidmVubW8iOiJvZmYiLCJjaGFsbGVuZ2VzIjpbXSwidGhyZWVEU2VjdXJlRW5hYmxlZCI6dHJ1ZSwiYW5hbHl0aWNzIjp7InVybCI6Imh0dHBzOi8vb3JpZ2luLWFuYWx5dGljcy1zYW5kLnNhbmRib3guYnJhaW50cmVlLWFwaS5jb20va2Q2dGJja2NkZGY2eXZkZyJ9LCJwYXlwYWxFbmFibGVkIjp0cnVlLCJicmFpbnRyZWVfYXBpIjp7InVybCI6Imh0dHBzOi8vcGF5bWVudHMuc2FuZGJveC5icmFpbnRyZWUtYXBpLmNvbSIsImFjY2Vzc190b2tlbiI6ImV5SjBlWEFpT2lKS1YxUWlMQ0poYkdjaU9pSkZVekkxTmlJc0ltdHBaQ0k2SWpJd01UZ3dOREkyTVRZdGMyRnVaR0p2ZUNJc0ltbHpjeUk2SW1oMGRIQnpPaTh2WVhCcExuTmhibVJpYjNndVluSmhhVzUwY21WbFoyRjBaWGRoZVM1amIyMGlmUS5leUpsZUhBaU9qRTJORE14TkRrMU1UQXNJbXAwYVNJNklqRmhNMkpqTm1OaExUY3hNalV0TkdKaU5TMWlOMk5tTFdReU5HUTNNMlEyWWpJd01TSXNJbk4xWWlJNkltdGtOblJpWTJ0alpHUm1ObmwyWkdjaUxDSnBjM01pT2lKb2RIUndjem92TDJGd2FTNXpZVzVrWW05NExtSnlZV2x1ZEhKbFpXZGhkR1YzWVhrdVkyOXRJaXdpYldWeVkyaGhiblFpT25zaWNIVmliR2xqWDJsa0lqb2lhMlEyZEdKamEyTmtaR1kyZVhaa1p5SXNJblpsY21sbWVWOWpZWEprWDJKNVgyUmxabUYxYkhRaU9uUnlkV1Y5TENKeWFXZG9kSE1pT2xzaWRHOXJaVzVwZW1VaUxDSnRZVzVoWjJWZmRtRjFiSFFpWFN3aWMyTnZjR1VpT2xzaVFuSmhhVzUwY21WbE9sWmhkV3gwSWwwc0ltOXdkR2x2Ym5NaU9udDlmUS52ZGtCVFVpOGtPdm1lSUVvdjRYMFBtVmpuLVFER2JNSWhyQ3JmVkpRcUIxVG5GSVYySkx3U2RxYlFXXzN6R2RIcUl6WkVzVEtQdXNxRF9nWUhwR2xjdyJ9LCJwYXlwYWwiOnsiYmlsbGluZ0FncmVlbWVudHNFbmFibGVkIjp0cnVlLCJlbnZpcm9ubWVudE5vTmV0d29yayI6dHJ1ZSwidW52ZXR0ZWRNZXJjaGFudCI6ZmFsc2UsImFsbG93SHR0cCI6dHJ1ZSwiZGlzcGxheU5hbWUiOiJlbmRhdmEiLCJjbGllbnRJZCI6bnVsbCwicHJpdmFjeVVybCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9wcCIsInVzZXJBZ3JlZW1lbnRVcmwiOiJodHRwOi8vZXhhbXBsZS5jb20vdG9zIiwiYmFzZVVybCI6Imh0dHBzOi8vYXNzZXRzLmJyYWludHJlZWdhdGV3YXkuY29tIiwiYXNzZXRzVXJsIjoiaHR0cHM6Ly9jaGVja291dC5wYXlwYWwuY29tIiwiZGlyZWN0QmFzZVVybCI6bnVsbCwiZW52aXJvbm1lbnQiOiJvZmZsaW5lIiwiYnJhaW50cmVlQ2xpZW50SWQiOiJtYXN0ZXJjbGllbnQzIiwibWVyY2hhbnRBY2NvdW50SWQiOiJlbmRhdmEiLCJjdXJyZW5jeUlzb0NvZGUiOiJVU0QifX0= + [Braintree] + [Braintree] + [Braintree] 011000015 + [Braintree] 0000 + [Braintree] checking + [Braintree] Jon Doe + [Braintree] FEDERAL RESERVE BANK + [Braintree] + [Braintree] 2022-01-24T22:25:11Z + [Braintree] By clicking ["Checkout"], I authorize Braintree, a service of PayPal, on behalf of [your business name here] (i) to verify my bank account information using bank information and consumer reports and (ii) to debit my bank account. + [Braintree] + [Braintree] personal + [Braintree] true + [Braintree] 1000000000 + [Braintree] + [Braintree] true + [Braintree] + [Braintree] Jon + [Braintree] Doe + [Braintree] true + [Braintree] 9dkrvzg + [Braintree] 673970040 + [Braintree] Y3VzdG9tZXJfNjczOTcwMDQw + [Braintree] https://assets.braintreegateway.com/payment_method_logo/us_bank_account.png?environment=sandbox + [Braintree] + [Braintree] + [Braintree] verified + [Braintree] + [Braintree] endava + [Braintree] 1000 + [Braintree] Approved + [Braintree] d4gaqtek + [Braintree] network_check + [Braintree] 2022-01-24T22:25:12Z + [Braintree] + [Braintree] 9dkrvzg + [Braintree] 0000 + [Braintree] checking + [Braintree] Jon Doe + [Braintree] FEDERAL RESERVE BANK + [Braintree] 011000015 + [Braintree] true + [Braintree] personal + [Braintree] + [Braintree] 2022-01-24T22:25:12Z + [Braintree] 2022-01-24T22:25:12Z + [Braintree] dXNiYW5rYWNjb3VudHZlcmlmaWNhdGlvbl9kNGdhcXRlaw + [Braintree] + [Braintree] + [Braintree] cGF5bWVudG1ldGhvZF91c2JfOWRrcnZ6Zw + [Braintree] 2022-01-24T22:25:12Z + [Braintree] 2022-01-24T22:25:12Z + [Braintree] + RESPONSE + end + + def filtered_success_token_nonce + <<-RESPONSE + [Braintree] + [Braintree] 673970040 + [Braintree] [FILTERED] + [Braintree] + [Braintree] network_check + [Braintree] + [Braintree] + [Braintree] + [Braintree] [FILTERED] + [Braintree] + [Braintree] + [Braintree] 011000015 + [Braintree] 0000 + [Braintree] checking + [Braintree] Jon Doe + [Braintree] FEDERAL RESERVE BANK + [Braintree] + [Braintree] 2022-01-24T22:25:11Z + [Braintree] By clicking ["Checkout"], I authorize Braintree, a service of PayPal, on behalf of [your business name here] (i) to verify my bank account information using bank information and consumer reports and (ii) to debit my bank account. + [Braintree] + [Braintree] personal + [Braintree] true + [Braintree] [FILTERED] + [Braintree] + [Braintree] true + [Braintree] + [Braintree] Jon + [Braintree] Doe + [Braintree] true + [Braintree] [FILTERED] + [Braintree] 673970040 + [Braintree] Y3VzdG9tZXJfNjczOTcwMDQw + [Braintree] https://assets.braintreegateway.com/payment_method_logo/us_bank_account.png?environment=sandbox + [Braintree] + [Braintree] + [Braintree] verified + [Braintree] + [Braintree] endava + [Braintree] 1000 + [Braintree] Approved + [Braintree] d4gaqtek + [Braintree] network_check + [Braintree] 2022-01-24T22:25:12Z + [Braintree] + [Braintree] [FILTERED] + [Braintree] 0000 + [Braintree] checking + [Braintree] Jon Doe + [Braintree] FEDERAL RESERVE BANK + [Braintree] 011000015 + [Braintree] true + [Braintree] personal + [Braintree] + [Braintree] 2022-01-24T22:25:12Z + [Braintree] 2022-01-24T22:25:12Z + [Braintree] dXNiYW5rYWNjb3VudHZlcmlmaWNhdGlvbl9kNGdhcXRlaw + [Braintree] + [Braintree] + [Braintree] cGF5bWVudG1ldGhvZF91c2JfOWRrcnZ6Zw + [Braintree] 2022-01-24T22:25:12Z + [Braintree] 2022-01-24T22:25:12Z + [Braintree] + RESPONSE + end + + def pre_scrub_network_token + <<-RESPONSE + [Braintree] + [Braintree] 47.70 + [Braintree] 111111 + [Braintree] + [Braintree] + [Braintree] test_transaction@gmail.com + [Braintree] 123341 + [Braintree] John + [Braintree] Smith + [Braintree] + [Braintree] + [Braintree] false + [Braintree] true + [Braintree] + [Braintree] true + [Braintree] + [Braintree] + [Braintree] 111111 + [Braintree] 11111122233 + [Braintree] checkout-flow + [Braintree] 0 + [Braintree] + [Braintree] Account-12344 + [Braintree] + [Braintree] 41111111111111 + [Braintree] 02 + [Braintree] 2028 + [Braintree] John Smith + [Braintree] + [Braintree] /wBBBBBBBPZWYOv4AmbmrruuUDDDD= + [Braintree] 07 + [Braintree] + [Braintree] + [Braintree] + [Braintree] vaulted + [Braintree] 312343241232 + [Braintree] + [Braintree] recurring + [Braintree] + [Braintree] 251 Test STree + [Braintree] + [Braintree] + [Braintree] Los Angeles + [Braintree] CA + [Braintree] 57753 + [Braintree] US + [Braintree] USA + [Braintree] + [Braintree] + [Braintree] 251 Test Street + [Braintree] + [Braintree] + [Braintree] Los Angeles + [Braintree] CA + [Braintree] 57753 + [Braintree] US + [Braintree] USA + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] CHANNEL_BT + [Braintree] sale + [Braintree] + + I, [2024-08-16T16:36:13.440224 #2217917] INFO -- : [Braintree] [16/Aug/2024 16:36:13 UTC] POST /merchants/js7myvkvrjt5khpb/transactions 201 + D, [2024-08-16T16:36:13.440275 #2217917] DEBUG -- : [Braintree] [16/Aug/2024 16:36:13 UTC] 201 + D, [2024-08-16T16:36:13.440973 #2217917] DEBUG -- : [Braintree] + [Braintree] + [Braintree] ftq5rn1j + [Braintree] submitted_for_settlement + [Braintree] sale + [Braintree] USD + [Braintree] 47.70 + [Braintree] 47.70 + [Braintree] CHANNEL + [Braintree] + [Braintree] + [Braintree] 114475310 + [Braintree] 2024-08-16T16:36:12Z + [Braintree] 2024-08-16T16:36:13Z + [Braintree] + [Braintree] + [Braintree] John + [Braintree] Smith + [Braintree] + [Braintree] test_email@gmail.com + [Braintree] + [Braintree] 8765432432 + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] 251 Test Street + [Braintree] + [Braintree] Los Angeles + [Braintree] CA + [Braintree] 5773 + [Braintree] United States of America + [Braintree] US + [Braintree] USA + [Braintree] 840 + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] 251 Test Street + [Braintree] + [Braintree] Anna Smith + [Braintree] CA + [Braintree] 32343 + [Braintree] United States of America + [Braintree] US + [Braintree] USA + [Braintree] 840 + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] 1122334455 + [Braintree] 12356432 + [Braintree] tbyb-second + [Braintree] 0 + [Braintree] + [Braintree] false + [Braintree] + [Braintree] M + [Braintree] M + [Braintree] I + [Braintree] + [Braintree] 796973 + [Braintree] 1000 + [Braintree] Approved + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] false + [Braintree] + [Braintree] true + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] https://assets.braintreegateway.com/payment_method_logo/unknown.png?environment=production + [Braintree] false + [Braintree] Unknown + [Braintree] Unknown + [Braintree] Unknown + [Braintree] Unknown + [Braintree] Unknown + [Braintree] Unknown + [Braintree] Unknown + [Braintree] Unknown + [Braintree] Unknown + [Braintree] + [Braintree] + [Braintree] + [Braintree] false + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] 41111 + [Braintree] 111 + [Braintree] Visa + [Braintree] 02 + [Braintree] 2028 + [Braintree] US + [Braintree] John Smith + [Braintree] https://assets.braintreegateway.com/paymenn + [Braintree] true + [Braintree] No + [Braintree] No + [Braintree] Yes + [Braintree] Yes + [Braintree] Unknown + [Braintree] No + [Braintree] Test Bank Account + [Braintree] USA + [Braintree] F + [Braintree] + [Braintree] credit + [Braintree] + [Braintree] + [Braintree] + [Braintree] 2024-08-16T16:36:13Z + [Braintree] authorized + [Braintree] 47.70 + [Braintree] testemail@gmail.com + [Braintree] api + [Braintree] + [Braintree] + [Braintree] 2024-08-16T16:36:13Z + [Braintree] submitted_for_settlement + [Braintree] 47.70 + [Braintree] testemail@gmail.com + [Braintree] api + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] true + [Braintree] CHANNEL_BT + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] network_token + [Braintree] + [Braintree] + [Braintree] 00 + [Braintree] Successful approval/completion or V.I.P. PIN verification is successful + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] 1122334455667786 + [Braintree] approved + [Braintree] 2024-08-17T16:36:13Z + [Braintree] + [Braintree] + [Braintree] false + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] ddetwte3DG43GDR + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] 112233445566 + [Braintree] + [Braintree] CHANNEL_MERCHANT + [Braintree] + [Braintree] + [Braintree] New York + [Braintree] NY + [Braintree] 10012 + [Braintree] 551-453-46223 + [Braintree] + [Braintree] false + [Braintree] + [Braintree] + [Braintree] + [Braintree] fqq5tm1j + [Braintree] dHJhbnNhY3RpE3Gppse33o + [Braintree] 47.70 + [Braintree] USD + [Braintree] 1000 + [Braintree] Approved + [Braintree] 755332 + [Braintree] TEST-STORE + [Braintree] + [Braintree] + [Braintree] New York + [Braintree] NY + [Braintree] 10012 + [Braintree] 551-733-45235 + [Braintree] + [Braintree] 122334553 + [Braintree] + [Braintree] sale + [Braintree] false + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + RESPONSE + end + + def post_scrub_network_token + <<-RESPONSE + [Braintree] + [Braintree] 47.70 + [Braintree] 111111 + [Braintree] + [Braintree] + [Braintree] test_transaction@gmail.com + [Braintree] 123341 + [Braintree] John + [Braintree] Smith + [Braintree] + [Braintree] + [Braintree] false + [Braintree] true + [Braintree] + [Braintree] true + [Braintree] + [Braintree] + [Braintree] 111111 + [Braintree] 11111122233 + [Braintree] checkout-flow + [Braintree] 0 + [Braintree] + [Braintree] Account-12344 + [Braintree] + [Braintree] [FILTERED] + [Braintree] 02 + [Braintree] 2028 + [Braintree] John Smith + [Braintree] + [Braintree] [FILTERED] + [Braintree] 07 + [Braintree] + [Braintree] + [Braintree] + [Braintree] vaulted + [Braintree] 312343241232 + [Braintree] + [Braintree] recurring + [Braintree] + [Braintree] 251 Test STree + [Braintree] + [Braintree] + [Braintree] Los Angeles + [Braintree] CA + [Braintree] 57753 + [Braintree] US + [Braintree] USA + [Braintree] + [Braintree] + [Braintree] 251 Test Street + [Braintree] + [Braintree] + [Braintree] Los Angeles + [Braintree] CA + [Braintree] 57753 + [Braintree] US + [Braintree] USA + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] CHANNEL_BT + [Braintree] sale + [Braintree] + + I, [2024-08-16T16:36:13.440224 #2217917] INFO -- : [Braintree] [16/Aug/2024 16:36:13 UTC] POST /merchants/js7myvkvrjt5khpb/transactions 201 + D, [2024-08-16T16:36:13.440275 #2217917] DEBUG -- : [Braintree] [16/Aug/2024 16:36:13 UTC] 201 + D, [2024-08-16T16:36:13.440973 #2217917] DEBUG -- : [Braintree] + [Braintree] + [Braintree] ftq5rn1j + [Braintree] submitted_for_settlement + [Braintree] sale + [Braintree] USD + [Braintree] 47.70 + [Braintree] 47.70 + [Braintree] CHANNEL + [Braintree] + [Braintree] + [Braintree] 114475310 + [Braintree] 2024-08-16T16:36:12Z + [Braintree] 2024-08-16T16:36:13Z + [Braintree] + [Braintree] + [Braintree] John + [Braintree] Smith + [Braintree] + [Braintree] test_email@gmail.com + [Braintree] + [Braintree] 8765432432 + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] 251 Test Street + [Braintree] + [Braintree] Los Angeles + [Braintree] CA + [Braintree] 5773 + [Braintree] United States of America + [Braintree] US + [Braintree] USA + [Braintree] 840 + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] 251 Test Street + [Braintree] + [Braintree] Anna Smith + [Braintree] CA + [Braintree] 32343 + [Braintree] United States of America + [Braintree] US + [Braintree] USA + [Braintree] 840 + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] 1122334455 + [Braintree] 12356432 + [Braintree] tbyb-second + [Braintree] 0 + [Braintree] + [Braintree] false + [Braintree] + [Braintree] M + [Braintree] M + [Braintree] I + [Braintree] + [Braintree] 796973 + [Braintree] 1000 + [Braintree] Approved + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] false + [Braintree] + [Braintree] true + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] https://assets.braintreegateway.com/payment_method_logo/unknown.png?environment=production + [Braintree] false + [Braintree] Unknown + [Braintree] Unknown + [Braintree] Unknown + [Braintree] Unknown + [Braintree] Unknown + [Braintree] Unknown + [Braintree] Unknown + [Braintree] Unknown + [Braintree] Unknown + [Braintree] + [Braintree] + [Braintree] + [Braintree] false + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] 41111 + [Braintree] 111 + [Braintree] Visa + [Braintree] 02 + [Braintree] 2028 + [Braintree] US + [Braintree] John Smith + [Braintree] https://assets.braintreegateway.com/paymenn + [Braintree] true + [Braintree] No + [Braintree] No + [Braintree] Yes + [Braintree] Yes + [Braintree] Unknown + [Braintree] No + [Braintree] Test Bank Account + [Braintree] USA + [Braintree] F + [Braintree] + [Braintree] credit + [Braintree] + [Braintree] + [Braintree] + [Braintree] 2024-08-16T16:36:13Z + [Braintree] authorized + [Braintree] 47.70 + [Braintree] testemail@gmail.com + [Braintree] api + [Braintree] + [Braintree] + [Braintree] 2024-08-16T16:36:13Z + [Braintree] submitted_for_settlement + [Braintree] 47.70 + [Braintree] testemail@gmail.com + [Braintree] api + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] true + [Braintree] CHANNEL_BT + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] network_token + [Braintree] + [Braintree] + [Braintree] 00 + [Braintree] Successful approval/completion or V.I.P. PIN verification is successful + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] 1122334455667786 + [Braintree] approved + [Braintree] 2024-08-17T16:36:13Z + [Braintree] + [Braintree] + [Braintree] false + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] ddetwte3DG43GDR + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] 112233445566 + [Braintree] + [Braintree] CHANNEL_MERCHANT + [Braintree] + [Braintree] + [Braintree] New York + [Braintree] NY + [Braintree] 10012 + [Braintree] 551-453-46223 + [Braintree] + [Braintree] false + [Braintree] + [Braintree] + [Braintree] + [Braintree] fqq5tm1j + [Braintree] dHJhbnNhY3RpE3Gppse33o + [Braintree] 47.70 + [Braintree] USD + [Braintree] 1000 + [Braintree] Approved + [Braintree] 755332 + [Braintree] TEST-STORE + [Braintree] + [Braintree] + [Braintree] New York + [Braintree] NY + [Braintree] 10012 + [Braintree] 551-733-45235 + [Braintree] + [Braintree] 122334553 + [Braintree] + [Braintree] sale + [Braintree] false + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + RESPONSE + end end diff --git a/test/unit/gateways/braintree_orange_test.rb b/test/unit/gateways/braintree_orange_test.rb index 80eebb19dfc..49a15e4abe6 100644 --- a/test/unit/gateways/braintree_orange_test.rb +++ b/test/unit/gateways/braintree_orange_test.rb @@ -5,14 +5,14 @@ class BraintreeOrangeTest < Test::Unit::TestCase def setup @gateway = BraintreeOrangeGateway.new( - :login => 'LOGIN', - :password => 'PASSWORD' + login: 'LOGIN', + password: 'PASSWORD' ) @credit_card = credit_card @amount = 100 - @options = { :billing_address => address } + @options = { billing_address: address } end def test_successful_purchase @@ -27,7 +27,7 @@ def test_successful_purchase def test_fractional_amounts response = stub_comms do @gateway.purchase(100, @credit_card, @options.merge(currency: 'JPY')) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| refute_match(/amount=1.00/, data) end.respond_with(successful_purchase_response) @@ -47,7 +47,7 @@ def test_successful_store def test_add_processor result = {} - @gateway.send(:add_processor, result, {:processor => 'ccprocessorb'} ) + @gateway.send(:add_processor, result, { processor: 'ccprocessorb' }) assert_equal ['processor_id'], result.stringify_keys.keys.sort assert_equal 'ccprocessorb', result[:processor_id] end @@ -86,8 +86,8 @@ def test_unsuccessful_verify def test_add_address result = {} - @gateway.send(:add_address, result, {:address1 => '164 Waverley Street', :country => 'US', :state => 'CO'} ) - assert_equal ['address1', 'city', 'company', 'country', 'phone', 'state', 'zip'], result.stringify_keys.keys.sort + @gateway.send(:add_address, result, { address1: '164 Waverley Street', country: 'US', state: 'CO' }) + assert_equal %w[address1 city company country phone state zip], result.stringify_keys.keys.sort assert_equal 'CO', result['state'] assert_equal '164 Waverley Street', result['address1'] assert_equal 'US', result['country'] @@ -96,8 +96,8 @@ def test_add_address def test_add_shipping_address result = {} - @gateway.send(:add_address, result, {:address1 => '164 Waverley Street', :country => 'US', :state => 'CO'},'shipping' ) - assert_equal ['shipping_address1', 'shipping_city', 'shipping_company', 'shipping_country', 'shipping_phone', 'shipping_state', 'shipping_zip'], result.stringify_keys.keys.sort + @gateway.send(:add_address, result, { address1: '164 Waverley Street', country: 'US', state: 'CO' }, 'shipping') + assert_equal %w[shipping_address1 shipping_city shipping_company shipping_country shipping_phone shipping_state shipping_zip], result.stringify_keys.keys.sort assert_equal 'CO', result['shipping_state'] assert_equal '164 Waverley Street', result['shipping_address1'] assert_equal 'US', result['shipping_country'] @@ -106,26 +106,26 @@ def test_add_shipping_address def test_adding_store_adds_vault_id_flag result = {} - @gateway.send(:add_creditcard, result, @credit_card, :store => true) - assert_equal ['ccexp', 'ccnumber', 'customer_vault', 'cvv', 'firstname', 'lastname'], result.stringify_keys.keys.sort + @gateway.send(:add_creditcard, result, @credit_card, store: true) + assert_equal %w[ccexp ccnumber customer_vault cvv firstname lastname], result.stringify_keys.keys.sort assert_equal 'add_customer', result[:customer_vault] end def test_blank_store_doesnt_add_vault_flag result = {} - @gateway.send(:add_creditcard, result, @credit_card, {} ) - assert_equal ['ccexp', 'ccnumber', 'cvv', 'firstname', 'lastname'], result.stringify_keys.keys.sort + @gateway.send(:add_creditcard, result, @credit_card, {}) + assert_equal %w[ccexp ccnumber cvv firstname lastname], result.stringify_keys.keys.sort assert_nil result[:customer_vault] end def test_accept_check post = {} - check = Check.new(:name => 'Fred Bloggs', - :routing_number => '111000025', - :account_number => '123456789012', - :account_holder_type => 'personal', - :account_type => 'checking') + check = Check.new(name: 'Fred Bloggs', + routing_number: '111000025', + account_number: '123456789012', + account_holder_type: 'personal', + account_type: 'checking') @gateway.send(:add_check, post, check, {}) assert_equal %w[account_holder_type account_type checkaba checkaccount checkname payment], post.stringify_keys.keys.sort end @@ -155,7 +155,7 @@ def test_add_eci @gateway.purchase(@amount, @credit_card, {}) @gateway.expects(:commit).with { |_, _, parameters| parameters[:billing_method] == 'recurring' } - @gateway.purchase(@amount, @credit_card, {:eci => 'recurring'}) + @gateway.purchase(@amount, @credit_card, { eci: 'recurring' }) end def test_transcript_scrubbing diff --git a/test/unit/gateways/braintree_test.rb b/test/unit/gateways/braintree_test.rb index 7cff9f6512f..28fb1d0bc87 100644 --- a/test/unit/gateways/braintree_test.rb +++ b/test/unit/gateways/braintree_test.rb @@ -1,20 +1,19 @@ require 'test_helper' class BraintreeTest < Test::Unit::TestCase - def test_new_with_login_password_creates_braintree_orange gateway = BraintreeGateway.new( - :login => 'LOGIN', - :password => 'PASSWORD' + login: 'LOGIN', + password: 'PASSWORD' ) assert_instance_of BraintreeOrangeGateway, gateway end def test_new_with_merchant_id_creates_braintree_blue gateway = BraintreeGateway.new( - :merchant_id => 'MERCHANT_ID', - :public_key => 'PUBLIC_KEY', - :private_key => 'PRIVATE_KEY' + merchant_id: 'MERCHANT_ID', + public_key: 'PUBLIC_KEY', + private_key: 'PRIVATE_KEY' ) assert_instance_of BraintreeBlueGateway, gateway end @@ -28,7 +27,7 @@ def test_should_have_homepage_url end def test_should_have_supported_credit_card_types - assert_equal [:visa, :master, :american_express, :discover, :jcb, :diners_club, :maestro], BraintreeGateway.supported_cardtypes + assert_equal %i[visa master american_express discover jcb diners_club maestro], BraintreeGateway.supported_cardtypes end def test_should_have_default_currency 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/bridge_pay_test.rb b/test/unit/gateways/bridge_pay_test.rb index ce60e3e05f9..dde12fc7ee8 100644 --- a/test/unit/gateways/bridge_pay_test.rb +++ b/test/unit/gateways/bridge_pay_test.rb @@ -63,7 +63,7 @@ def test_authorize_and_capture capture = stub_comms do @gateway.capture(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/OK2657/, data) end.respond_with(successful_capture_response) @@ -80,7 +80,7 @@ def test_refund refund = stub_comms do @gateway.refund(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/OK9757/, data) end.respond_with(successful_refund_response) @@ -97,7 +97,7 @@ def test_void refund = stub_comms do @gateway.void(response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/OK9757/, data) end.respond_with(successful_refund_response) @@ -123,15 +123,15 @@ def test_store_and_purchase_with_token def test_passing_cvv stub_comms do @gateway.purchase(@amount, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/#{@credit_card.verification_value}/, data) end.respond_with(successful_purchase_response) end def test_passing_billing_address stub_comms do - @gateway.purchase(@amount, @credit_card, :billing_address => address) - end.check_request do |endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, billing_address: address) + end.check_request do |_endpoint, data, _headers| assert_match(/Street=456\+My\+Street/, data) assert_match(/Zip=K1C2N6/, data) end.respond_with(successful_purchase_response) diff --git a/test/unit/gateways/cams_test.rb b/test/unit/gateways/cams_test.rb index 23102e41c39..fcd0a53cc54 100644 --- a/test/unit/gateways/cams_test.rb +++ b/test/unit/gateways/cams_test.rb @@ -7,8 +7,8 @@ def setup password: 'password9' ) - @credit_card = credit_card('4111111111111111', :month => 5, :year => 10) - @bad_credit_card = credit_card('4242424245555555', :month => 5, :year => 10) + @credit_card = credit_card('4111111111111111', month: 5, year: 10) + @bad_credit_card = credit_card('4242424245555555', month: 5, year: 10) @amount = 100 @options = { @@ -116,46 +116,46 @@ def test_scrub private def pre_scrubbed - <<-PRE_SCRUBBED -opening connection to secure.centralams.com:443... -opened -starting SSL for secure.centralams.com:443... -SSL established -<- "POST /gw/api/transact.php 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: secure.centralams.com\r\nContent-Length: 249\r\n\r\n" -<- "amount=1.03¤cy=USD&ccnumber=4111111111111111&ccexp=0916&firstname=Longbob&lastname=Longsen&address1=1234 My Street&address2=Apt 1&city=Ottawa&state=ON&zip=K1C2N6&country=US&phone=(555)555-5555&type=&password=password9&username=testintegrationc" --> "HTTP/1.1 200 OK\r\n" --> "Date: Tue, 21 Apr 2015 23:27:05 GMT\r\n" --> "Server: Apache\r\n" --> "Content-Length: 132\r\n" --> "Connection: close\r\n" --> "Content-Type: text/html; charset=UTF-8\r\n" --> "\r\n" -reading 132 bytes... --> "response=1&responsetext=SUCCESS&authcode=123456&transactionid=2654605773&avsresponse=N&cvvresponse=&orderid=&type=&response_code=100" -read 132 bytes -Conn close + <<~PRE_SCRUBBED + opening connection to secure.centralams.com:443... + opened + starting SSL for secure.centralams.com:443... + SSL established + <- "POST /gw/api/transact.php 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: secure.centralams.com\r\nContent-Length: 249\r\n\r\n" + <- "amount=1.03¤cy=USD&ccnumber=4111111111111111&ccexp=0916&firstname=Longbob&lastname=Longsen&address1=1234 My Street&address2=Apt 1&city=Ottawa&state=ON&zip=K1C2N6&country=US&phone=(555)555-5555&type=&password=password9&username=testintegrationc" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Tue, 21 Apr 2015 23:27:05 GMT\r\n" + -> "Server: Apache\r\n" + -> "Content-Length: 132\r\n" + -> "Connection: close\r\n" + -> "Content-Type: text/html; charset=UTF-8\r\n" + -> "\r\n" + reading 132 bytes... + -> "response=1&responsetext=SUCCESS&authcode=123456&transactionid=2654605773&avsresponse=N&cvvresponse=&orderid=&type=&response_code=100" + read 132 bytes + Conn close PRE_SCRUBBED end def post_scrubbed - <<-POST_SCRUBBED -opening connection to secure.centralams.com:443... -opened -starting SSL for secure.centralams.com:443... -SSL established -<- "POST /gw/api/transact.php 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: secure.centralams.com\r\nContent-Length: 249\r\n\r\n" -<- "amount=1.03¤cy=USD&ccnumber=[FILTERED]&ccexp=0916&firstname=Longbob&lastname=Longsen&address1=1234 My Street&address2=Apt 1&city=Ottawa&state=ON&zip=K1C2N6&country=US&phone=(555)555-5555&type=&password=[FILTERED]&username=testintegrationc" --> "HTTP/1.1 200 OK\r\n" --> "Date: Tue, 21 Apr 2015 23:27:05 GMT\r\n" --> "Server: Apache\r\n" --> "Content-Length: 132\r\n" --> "Connection: close\r\n" --> "Content-Type: text/html; charset=UTF-8\r\n" --> "\r\n" -reading 132 bytes... --> "response=1&responsetext=SUCCESS&authcode=123456&transactionid=2654605773&avsresponse=N&cvvresponse=&orderid=&type=&response_code=100" -read 132 bytes -Conn close + <<~POST_SCRUBBED + opening connection to secure.centralams.com:443... + opened + starting SSL for secure.centralams.com:443... + SSL established + <- "POST /gw/api/transact.php 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: secure.centralams.com\r\nContent-Length: 249\r\n\r\n" + <- "amount=1.03¤cy=USD&ccnumber=[FILTERED]&ccexp=0916&firstname=Longbob&lastname=Longsen&address1=1234 My Street&address2=Apt 1&city=Ottawa&state=ON&zip=K1C2N6&country=US&phone=(555)555-5555&type=&password=[FILTERED]&username=testintegrationc" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Tue, 21 Apr 2015 23:27:05 GMT\r\n" + -> "Server: Apache\r\n" + -> "Content-Length: 132\r\n" + -> "Connection: close\r\n" + -> "Content-Type: text/html; charset=UTF-8\r\n" + -> "\r\n" + reading 132 bytes... + -> "response=1&responsetext=SUCCESS&authcode=123456&transactionid=2654605773&avsresponse=N&cvvresponse=&orderid=&type=&response_code=100" + read 132 bytes + Conn close POST_SCRUBBED end diff --git a/test/unit/gateways/card_connect_test.rb b/test/unit/gateways/card_connect_test.rb index 704a89ec6ef..7188dea17dc 100644 --- a/test/unit/gateways/card_connect_test.rb +++ b/test/unit/gateways/card_connect_test.rb @@ -14,11 +14,122 @@ def setup billing_address: address, description: 'Store Purchase' } + + @three_ds_secure = { + version: '2.0', + cavv: 'AJkBByEyYgAAAASwgmEodQAAAAA=', + eci: '05', + xid: '3875d372-d96d-412a-a806-5ac367d095b1' + } + end + + def test_three_ds_2_object_construction + post = {} + @three_ds_secure[:ds_transaction_id] = '3875d372-d96d-412a-a806-5ac367d095b1' + @options[:three_d_secure] = @three_ds_secure + + @gateway.send(:add_three_ds_mpi_data, post, @options) + three_ds_options = @options[:three_d_secure] + assert_equal three_ds_options[:cavv], post[:securevalue] + assert_equal three_ds_options[:eci], post[:secureflag] + assert_equal three_ds_options[:ds_transaction_id], post[:securedstid] + end + + def test_purchase_with_three_ds + @options[:three_d_secure] = @three_ds_secure + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + three_ds_params = JSON.parse(data)['three_dsecure'] + assert_equal 'AJkBByEyYgAAAASwgmEodQAAAAA=', three_ds_params['cavv'] + assert_equal '05', three_ds_params['eci'] + assert_equal '3875d372-d96d-412a-a806-5ac367d095b1', three_ds_params['xid'] + end.respond_with(successful_purchase_response) + end + + def test_initial_purchase_with_stored_credential + stored_credential_options = { + initial_transaction: true, + reason_type: 'recurring', + initiator: 'merchant' + } + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_options })) + end.check_request do |_verb, _url, data, _headers| + request = JSON.parse(data) + assert_equal request['cof'], 'M' + assert_equal request['cofscheduled'], 'Y' + assert_equal request['cofpermission'], 'Y' + end.respond_with(successful_purchase_response) + end + + def test_subsequent_purchase_with_stored_credential + stored_credential_options = { + initial_transaction: false, + reason_type: 'recurring', + initiator: 'cardholder' + } + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_options })) + end.check_request do |_verb, _url, data, _headers| + request = JSON.parse(data) + assert_equal request['cof'], 'C' + assert_equal request['cofscheduled'], 'Y' + assert_equal request['cofpermission'], 'N' + end.respond_with(successful_purchase_response) end - def test_incorrect_domain + def test_purchase_with_ecomind + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge({ ecomind: 't' })) + end.check_request do |_verb, _url, data, _headers| + request = JSON.parse(data) + assert_equal request['ecomind'], 'T' + end.respond_with(successful_purchase_response) + end + + def test_purchase_with_recurring + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge({ recurring: true })) + end.check_request do |_verb, _url, data, _headers| + request = JSON.parse(data) + assert_equal request['ecomind'], 'R' + end.respond_with(successful_purchase_response) + end + + def test_purchase_without_recurring + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge({ recurring: false })) + end.check_request do |_verb, _url, data, _headers| + request = JSON.parse(data) + assert_equal request['ecomind'], 'E' + end.respond_with(successful_purchase_response) + end + + def test_allow_domains_without_ports + assert CardConnectGateway.new(username: 'username', password: 'password', merchant_id: 'merchand_id', domain: 'https://vendor.cardconnect.com/test') + end + + def test_add_address + result = {} + + @gateway.send(:add_address, result, billing_address: { address1: '123 Test St.', address2: '5F', city: 'Testville', zip: '12345', state: 'AK' }) + assert_equal '123 Test St.', result[:address] + assert_equal '5F', result[:address2] + assert_equal 'Testville', result[:city] + assert_equal 'AK', result[:region] + assert_equal '12345', result[:postal] + end + + def test_reject_domains_without_card_connect assert_raise(ArgumentError) { - CardConnectGateway.new(username: 'username', password: 'password', merchant_id: 'merchand_id', domain: 'www.google.com') + CardConnectGateway.new(username: 'username', password: 'password', merchant_id: 'merchand_id', domain: 'https://www.google.com') + } + end + + def test_reject_domains_without_https + assert_raise(ArgumentError) { + CardConnectGateway.new(username: 'username', password: 'password', merchant_id: 'merchand_id', domain: 'ttps://cardconnect.com') } end @@ -79,7 +190,7 @@ def test_failed_authorize def test_successful_capture @gateway.expects(:ssl_request).returns(successful_capture_response) - response = @gateway.capture(@amount,'363168161558', @options) + response = @gateway.capture(@amount, '363168161558', @options) assert_success response assert_equal '363168161558', response.authorization @@ -185,14 +296,13 @@ def test_failed_store def test_successful_unstore stub_comms(@gateway, :ssl_request) do @gateway.unstore('1|16700875781344019340') - end.check_request do |verb, url, data, headers| + end.check_request do |verb, url, _data, _headers| assert_equal :delete, verb assert_match %r{16700875781344019340/1}, url end.respond_with(successful_unstore_response) end def test_failed_unstore - end def test_scrub @@ -200,6 +310,17 @@ def test_scrub assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed end + def test_frontendid_is_added_to_post_data_parameters + @gateway.class.application_id = 'my_app' + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_, _, body| + assert_equal 'my_app', JSON.parse(body)['frontendid'] + end.respond_with(successful_purchase_response) + ensure + @gateway.class.application_id = nil + end + private def pre_scrubbed diff --git a/test/unit/gateways/card_save_test.rb b/test/unit/gateways/card_save_test.rb index bb19495a1f1..6689ca22eef 100644 --- a/test/unit/gateways/card_save_test.rb +++ b/test/unit/gateways/card_save_test.rb @@ -3,10 +3,10 @@ class CardSaveTest < Test::Unit::TestCase def setup Base.mode = :test - @gateway = CardSaveGateway.new(:login => 'login', :password => 'password') + @gateway = CardSaveGateway.new(login: 'login', password: 'password') @credit_card = credit_card @amount = 100 - @options = {:order_id =>'1', :billing_address => address, :description =>'Store Purchase'} + @options = { order_id: '1', billing_address: address, description: 'Store Purchase' } end def test_successful_purchase @@ -273,5 +273,4 @@ def failed_refund ) end - end diff --git a/test/unit/gateways/card_stream_test.rb b/test/unit/gateways/card_stream_test.rb index 8eab0ad4030..f648a960a42 100644 --- a/test/unit/gateways/card_stream_test.rb +++ b/test/unit/gateways/card_stream_test.rb @@ -5,45 +5,63 @@ class CardStreamTest < Test::Unit::TestCase def setup @gateway = CardStreamGateway.new( - :login => 'login', - :shared_secret => 'secret' + login: 'login', + shared_secret: 'secret' ) - @visacreditcard = credit_card('4929421234600821', - :month => '12', - :year => '2014', - :verification_value => '356', - :brand => :visa + @visacreditcard = credit_card( + '4929421234600821', + month: '12', + year: '2014', + verification_value: '356', + brand: :visa ) @visacredit_options = { - :billing_address => { - :address1 => 'Flat 6, Primrose Rise', - :address2 => '347 Lavender Road', - :city => '', - :state => 'Northampton', - :zip => 'NN17 8YG ' + billing_address: { + address1: 'Flat 6, Primrose Rise', + address2: '347 Lavender Road', + city: '', + state: 'Northampton', + zip: 'NN17 8YG ' }, - :order_id => generate_unique_id, - :description => 'AM test purchase' + order_id: generate_unique_id, + description: 'AM test purchase' + } + + @visacredit_three_ds_options = { + threeds_required: true, + three_ds_version: '2.1.0', + three_d_secure: { + enrolled: 'true', + authentication_response_status: 'Y', + eci: '05', + cavv: 'Y2FyZGluYWxjb21tZXJjZWF1dGg', + xid: '362DF058-6061-47F1-A504-CACCBDF422B7' + } } @visacredit_descriptor_options = { - :billing_address => { - :address1 => 'Flat 6, Primrose Rise', - :address2 => '347 Lavender Road', - :city => '', - :state => 'Northampton', - :zip => 'NN17 8YG ' + billing_address: { + address1: 'Flat 6, Primrose Rise', + address2: '347 Lavender Road', + city: '', + state: 'Northampton', + zip: 'NN17 8YG ' }, - :merchant_name => 'merchant', - :dynamic_descriptor => 'product' + merchant_name: 'merchant', + dynamic_descriptor: 'product' } - @declined_card = credit_card('4000300011112220', - :month => '9', - :year => '2014' + @amex = credit_card( + '374245455400001', + month: '12', + year: 2014, + verification_value: '4887', + brand: :american_express ) + + @declined_card = credit_card('4000300011112220', month: '9', year: '2014') end def test_successful_visacreditcard_authorization @@ -130,7 +148,7 @@ def test_successful_visacreditcard_purchase def test_successful_visacreditcard_purchase_with_descriptors stub_comms do @gateway.purchase(284, @visacreditcard, @visacredit_descriptor_options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/statementNarrative1=merchant/, data) assert_match(/statementNarrative2=product/, data) end.respond_with(successful_purchase_response_with_descriptors) @@ -139,7 +157,7 @@ def test_successful_visacreditcard_purchase_with_descriptors def test_successful_visacreditcard_purchase_with_default_ip stub_comms do @gateway.purchase(284, @visacreditcard, @visacredit_options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/remoteAddress=1\.1\.1\.1/, data) end.respond_with(successful_purchase_response_with_descriptors) end @@ -147,7 +165,7 @@ def test_successful_visacreditcard_purchase_with_default_ip def test_successful_visacreditcard_purchase_with_default_country stub_comms do @gateway.purchase(284, @visacreditcard, @visacredit_options.delete(:billing_address)) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/customerCountryCode=GB/, data) end.respond_with(successful_purchase_response) end @@ -179,6 +197,14 @@ def test_declined_mastercard_purchase assert response.test? end + def test_successful_amex_purchase_with_localized_invoice_amount + stub_comms do + @gateway.purchase(28400, @amex, @visacredit_descriptor_options.merge(currency: 'JPY', order_id: '1234567890')) + end.check_request do |_endpoint, data, _headers| + assert_match(/item1GrossValue=284&/, data) + end.respond_with(successful_purchase_response) + end + def test_successful_verify response = stub_comms do @gateway.verify(@visacreditcard, @visacredit_options) @@ -196,11 +222,10 @@ def test_failed_verify end def test_purchase_options - # Default purchase = stub_comms do @gateway.purchase(142, @visacreditcard, @visacredit_options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/type=1/, data) end.respond_with(successful_purchase_response) @@ -208,7 +233,7 @@ def test_purchase_options purchase = stub_comms do @gateway.purchase(142, @visacreditcard, @visacredit_options.merge(type: 2)) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/type=2/, data) end.respond_with(successful_purchase_response) @@ -216,7 +241,7 @@ def test_purchase_options purchase = stub_comms do @gateway.purchase(142, @visacreditcard, @visacredit_options.merge(currency: 'PEN')) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/currencyCode=604/, data) end.respond_with(successful_purchase_response) @@ -225,7 +250,7 @@ def test_purchase_options def test_successful_purchase_without_street_address @gateway.expects(:ssl_post).returns(successful_purchase_response) - assert response = @gateway.purchase(142, @visacreditcard, billing_address: {state: 'Northampton'}) + assert response = @gateway.purchase(142, @visacreditcard, billing_address: { state: 'Northampton' }) assert_equal 'APPROVED', response.message end @@ -235,21 +260,39 @@ def test_successful_purchase_without_any_address assert_equal 'APPROVED', response.message end + def test_adding_country_code + %i[authorize purchase refund].each do |action| + stub_comms do + @gateway.send(action, 142, @visacreditcard, @visacredit_options.merge(country_code: 'US')) + end.check_request do |_endpoint, data, _headers| + assert_match(/&countryCode=US/, data) + end.respond_with(successful_purchase_response) + end + end + def test_hmac_signature_added_to_post post_params = "action=SALE&amount=10000&captureDelay=0&cardCVV=356&cardExpiryMonth=12&cardExpiryYear=14&cardNumber=4929421234600821&countryCode=GB¤cyCode=826&customerAddress=Flat+6%2C+Primrose+Rise+347+Lavender+Road&customerCountryCode=GB&customerName=Longbob+Longsen&customerPostCode=NN17+8YG+&merchantID=login&orderRef=AM+test+purchase&remoteAddress=1.1.1.1&threeDSRequired=N&transactionUnique=#{@visacredit_options[:order_id]}&type=1" expected_signature = Digest::SHA512.hexdigest("#{post_params}#{@gateway.options[:shared_secret]}") - @gateway.expects(:ssl_post).with do |url, data| + @gateway.expects(:ssl_post).with do |_url, data| data.include?("signature=#{expected_signature}") end.returns(successful_authorization_response) @gateway.purchase(10000, @visacreditcard, @visacredit_options) end + def test_nonfractional_currency_handling + stub_comms do + @gateway.authorize(200, @visacreditcard, @visacredit_options.merge(currency: 'JPY')) + end.check_request do |_endpoint, data, _headers| + assert_match(/amount=2¤cyCode=392/, data) + end.respond_with(successful_authorization_response) + end + def test_3ds_response purchase = stub_comms do @gateway.purchase(142, @visacreditcard, @visacredit_options.merge(threeds_required: true)) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/threeDSRequired=Y/, data) end.respond_with(successful_purchase_response_with_3dsecure) @@ -262,14 +305,14 @@ def test_3ds_response def test_deprecated_3ds_required assert_deprecation_warning(CardStreamGateway::THREEDSECURE_REQUIRED_DEPRECATION_MESSAGE) do @gateway = CardStreamGateway.new( - :login => 'login', - :shared_secret => 'secret', - :threeDSRequired => true + login: 'login', + shared_secret: 'secret', + threeDSRequired: true ) end stub_comms do @gateway.purchase(142, @visacreditcard, @visacredit_options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/threeDSRequired=Y/, data) end.respond_with(successful_purchase_response) end @@ -277,11 +320,45 @@ def test_deprecated_3ds_required def test_default_3dsecure_required stub_comms do @gateway.purchase(142, @visacreditcard, @visacredit_options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/threeDSRequired=N/, data) end.respond_with(successful_purchase_response) end + def test_3ds2_data + stub_comms do + @gateway.purchase(142, @visacreditcard, @visacredit_options.merge(@visacredit_three_ds_options)) + end.check_request(skip_response: true) do |_endpoint, data, _headers| + assert_match(/threeDSRequired=Y/, data) + assert_match(/threeDSEnrolled=Y/, data) + assert_match(/threeDSAuthenticated=Y/, data) + assert_match(/threeDSECI=05/, data) + assert_match(/threeDSCAVV=Y2FyZGluYWxjb21tZXJjZWF1dGg/, data) + assert_match(/threeDSXID=362DF058-6061-47F1-A504-CACCBDF422B7/, data) + end + end + + def test_3ds2_not_enrolled + stub_comms do + @visacredit_three_ds_options[:three_d_secure][:enrolled] = 'false' + @gateway.purchase(142, @visacreditcard, @visacredit_options.merge(@visacredit_three_ds_options)) + end.check_request(skip_response: true) do |_endpoint, data, _headers| + assert_match(/threeDSRequired=Y/, data) + assert_match(/threeDSEnrolled=N/, data) + end + end + + def test_3ds2_not_authenticated + stub_comms do + @visacredit_three_ds_options[:three_d_secure][:authentication_response_status] = 'N' + @gateway.purchase(142, @visacreditcard, @visacredit_options.merge(@visacredit_three_ds_options)) + end.check_request(skip_response: true) do |_endpoint, data, _headers| + assert_match(/threeDSRequired=Y/, data) + assert_match(/threeDSEnrolled=Y/, data) + assert_match(/threeDSAuthenticated=N/, data) + end + end + def test_transcript_scrubbing assert_equal scrubbed_transcript, @gateway.scrub(transcript) end @@ -341,16 +418,16 @@ def failed_reference_purchase_response end def transcript - <<-eos + <<-REQUEST POST /direct/ 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: gateway.cardstream.com\r\nContent-Length: 501\r\n\r\n" amount=¤cyCode=826&transactionUnique=a017ca2ac0569188517ad8368c36a06d&orderRef=AM+test+purchase&customerName=Longbob+Longsen&cardNumber=4929421234600821&cardExpiryMonth=12&cardExpiryYear=14&cardCVV=356&customerAddress=Flat+6%2C+Primrose+Rise+347+Lavender+Road&customerPostCode=NN17+8YG+&merchantID=102922&action=SALE&type=1&countryCode=GB&threeDSRequired=N&signature=970b3fe099a85c9922a79af46c2cb798616b9fbd044a921ac5eb46cd1907a5e89b8c720aae59c7eb1d81a59563f209d5db51aa3c270838199f2bfdcbe2c1149d - eos + REQUEST end def scrubbed_transcript - <<-eos + <<-REQUEST POST /direct/ 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: gateway.cardstream.com\r\nContent-Length: 501\r\n\r\n" amount=¤cyCode=826&transactionUnique=a017ca2ac0569188517ad8368c36a06d&orderRef=AM+test+purchase&customerName=Longbob+Longsen&cardNumber=[FILTERED]&cardExpiryMonth=12&cardExpiryYear=14&cardCVV=[FILTERED]&customerAddress=Flat+6%2C+Primrose+Rise+347+Lavender+Road&customerPostCode=NN17+8YG+&merchantID=102922&action=SALE&type=1&countryCode=GB&threeDSRequired=N&signature=970b3fe099a85c9922a79af46c2cb798616b9fbd044a921ac5eb46cd1907a5e89b8c720aae59c7eb1d81a59563f209d5db51aa3c270838199f2bfdcbe2c1149d - eos + REQUEST end end diff --git a/test/unit/gateways/cardknox_test.rb b/test/unit/gateways/cardknox_test.rb index e4270ba97b2..11e05970f77 100644 --- a/test/unit/gateways/cardknox_test.rb +++ b/test/unit/gateways/cardknox_test.rb @@ -16,7 +16,7 @@ def setup def test_successful_purchase_passing_extra_info response = stub_comms do @gateway.purchase(@amount, @credit_card, @options.merge(order_id: '1337', description: 'socool')) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/xOrderID=1337/, data) assert_match(/xDescription=socool/, data) end.respond_with(successful_purchase_response) @@ -66,18 +66,16 @@ def test_manual_entry_is_properly_indicated_on_purchase @credit_card.manual_entry = true response = stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| - + end.check_request do |_endpoint, data, _headers| assert_match %r{xCardNum=4242424242424242}, data assert_match %r{xCardPresent=true}, data - end.respond_with(successful_purchase_response) assert_success response end def test_ip_is_being_sent # failed - @gateway.expects(:ssl_post).with do |url, data| + @gateway.expects(:ssl_post).with do |_url, data| data =~ /xIP=123.123.123.123/ end.returns(successful_purchase_response) @@ -133,7 +131,7 @@ def test_successful_capture def test_failed_capture @gateway.expects(:ssl_post).returns(failed_capture_response) - assert capture = @gateway.capture(@amount-1, '') + assert capture = @gateway.capture(@amount - 1, '') assert_failure capture assert_equal 'Original transaction not specified', capture.message end @@ -203,7 +201,7 @@ def test_scrub private def purchase_request - 'xKey=api_key&xVersion=4.5.4&xSoftwareName=Active+Merchant&xSoftwareVersion=1.52.0&xCommand=cc%3Asale&xAmount=1.00&xCardNum=4242424242424242&xCVV=123&xExp=0916&xName=Longbob+Longsen&xBillFirstName=Longbob&xBillLastName=Longsen&xBillCompany=Widgets+Inc&xBillStreet=456+My+Street&xBillStreet2=Apt+1&xBillCity=Ottawa&xBillState=ON&xBillZip=K1C2N6&xBillCountry=CA&xBillPhone=%28555%29555-5555&xShipFirstName=Longbob&xShipLastName=Longsen&xShipCompany=Widgets+Inc&xShipStreet=456+My+Street&xShipStreet2=Apt+1&xShipCity=Ottawa&xShipState=ON&xShipZip=K1C2N6&xShipCountry=CA&xShipPhone=%28555%29555-5555&xStreet=456+My+Street&xZip=K1C2N6' + 'xKey=api_key&xVersion=4.5.4&xSoftwareName=Active+Merchant&xSoftwareVersion=1.52.0&xCommand=cc%3Asale&xAmount=1.00&xCardNum=4242424242424242&xCVV=123&xExp=0916&xName=Longbob+Longsen&xBillFirstName=Longbob&xBillLastName=Longsen&xBillCompany=Widgets+Inc&xBillStreet=456+My+Street&xBillStreet2=Apt+1&xBillCity=Ottawa&xBillState=ON&xBillZip=K1C2N6&xBillCountry=CA&xBillPhone=%28555%29555-5555&xShipFirstName=Longbob&xShipLastName=Longsen&xShipCompany=Widgets+Inc&xShipStreet=456+My+Street&xShipStreet2=Apt+1&xShipCity=Ottawa&xShipState=ON&xShipZip=K1C2N6&xShipCountry=CA&xShipPhone=%28555%29555-5555&xStreet=456+My+Street&xZip=K1C2N6' end def successful_purchase_response # passed avs and cvv @@ -258,7 +256,7 @@ def successful_check_refund_void end def successful_verify_response - 'xResult=A&xStatus=Approved&xError=&xRefNum=15314566&xAuthCode=608755&xBatch=&xAvsResultCode=NNN&xAvsResult=Address%3a+No+Match+%26+5+Digit+Zip%3a+No+Match&xCvvResultCode=N&xCvvResult=No+Match&xAuthAmount=1.00&xToken=09dc51aceb98440fbf0847cad2941d45&xMaskedCardNumber=4xxxxxxxxxxx2224&xName=Longbob+Longsen' + 'xResult=A&xStatus=Approved&xError=&xRefNum=15314566&xAuthCode=608755&xBatch=&xAvsResultCode=NNN&xAvsResult=Address%3a+No+Match+%26+5+Digit+Zip%3a+No+Match&xCvvResultCode=N&xCvvResult=No+Match&xAuthAmount=1.00&xToken=09dc51aceb98440fbf0847cad2941d45&xMaskedCardNumber=4xxxxxxxxxxx2224&xName=Longbob+Longsen' end def failed_verify_response @@ -266,10 +264,10 @@ def failed_verify_response end def pre_scrubbed - 'xKey=api_key&xVersion=4.5.4&xSoftwareName=Active+Merchant&xSoftwareVersion=1.52.0&xCommand=cc%3Asale&xAmount=1.00&xCardNum=4242424242424242&xCVV=123&xExp=0916&xName=Longbob+Longsen&xBillFirstName=Longbob&xBillLastName=Longsen&xBillCompany=Widgets+Inc&xBillStreet=456+My+Street&xBillStreet2=Apt+1&xBillCity=Ottawa&xBillState=ON&xBillZip=K1C2N6&xBillCountry=CA&xBillPhone=%28555%29555-5555&xShipFirstName=Longbob&xShipLastName=Longsen&xShipCompany=Widgets+Inc&xShipStreet=456+My+Street&xShipStreet2=Apt+1&xShipCity=Ottawa&xShipState=ON&xShipZip=K1C2N6&xShipCountry=CA&xShipPhone=%28555%29555-5555&xStreet=456+My+Street&xZip=K1C2N6' + 'xKey=api_key&xVersion=4.5.4&xSoftwareName=Active+Merchant&xSoftwareVersion=1.52.0&xCommand=cc%3Asale&xAmount=1.00&xCardNum=4242424242424242&xCVV=123&xExp=0916&xName=Longbob+Longsen&xBillFirstName=Longbob&xBillLastName=Longsen&xBillCompany=Widgets+Inc&xBillStreet=456+My+Street&xBillStreet2=Apt+1&xBillCity=Ottawa&xBillState=ON&xBillZip=K1C2N6&xBillCountry=CA&xBillPhone=%28555%29555-5555&xShipFirstName=Longbob&xShipLastName=Longsen&xShipCompany=Widgets+Inc&xShipStreet=456+My+Street&xShipStreet2=Apt+1&xShipCity=Ottawa&xShipState=ON&xShipZip=K1C2N6&xShipCountry=CA&xShipPhone=%28555%29555-5555&xStreet=456+My+Street&xZip=K1C2N6' end def post_scrubbed - 'xKey=[FILTERED]&xVersion=4.5.4&xSoftwareName=Active+Merchant&xSoftwareVersion=1.52.0&xCommand=cc%3Asale&xAmount=1.00&xCardNum=[FILTERED]&xCVV=[FILTERED]&xExp=0916&xName=Longbob+Longsen&xBillFirstName=Longbob&xBillLastName=Longsen&xBillCompany=Widgets+Inc&xBillStreet=456+My+Street&xBillStreet2=Apt+1&xBillCity=Ottawa&xBillState=ON&xBillZip=K1C2N6&xBillCountry=CA&xBillPhone=%28555%29555-5555&xShipFirstName=Longbob&xShipLastName=Longsen&xShipCompany=Widgets+Inc&xShipStreet=456+My+Street&xShipStreet2=Apt+1&xShipCity=Ottawa&xShipState=ON&xShipZip=K1C2N6&xShipCountry=CA&xShipPhone=%28555%29555-5555&xStreet=456+My+Street&xZip=K1C2N6' + 'xKey=[FILTERED]&xVersion=4.5.4&xSoftwareName=Active+Merchant&xSoftwareVersion=1.52.0&xCommand=cc%3Asale&xAmount=1.00&xCardNum=[FILTERED]&xCVV=[FILTERED]&xExp=0916&xName=Longbob+Longsen&xBillFirstName=Longbob&xBillLastName=Longsen&xBillCompany=Widgets+Inc&xBillStreet=456+My+Street&xBillStreet2=Apt+1&xBillCity=Ottawa&xBillState=ON&xBillZip=K1C2N6&xBillCountry=CA&xBillPhone=%28555%29555-5555&xShipFirstName=Longbob&xShipLastName=Longsen&xShipCompany=Widgets+Inc&xShipStreet=456+My+Street&xShipStreet2=Apt+1&xShipCity=Ottawa&xShipState=ON&xShipZip=K1C2N6&xShipCountry=CA&xShipPhone=%28555%29555-5555&xStreet=456+My+Street&xZip=K1C2N6' end end diff --git a/test/unit/gateways/cardprocess_test.rb b/test/unit/gateways/cardprocess_test.rb index 5377cd16c73..97e8061f2ff 100644 --- a/test/unit/gateways/cardprocess_test.rb +++ b/test/unit/gateways/cardprocess_test.rb @@ -167,7 +167,7 @@ def test_error_code_parsing '800.800.202' => :invalid_zip } codes.each_pair do |code, key| - response = {'result' => {'code' => code}} + response = { 'result' => { 'code' => code } } assert_equal Gateway::STANDARD_ERROR_CODE[key], @gateway.send(:error_code_from, response), "expecting #{code} => #{key}" end end diff --git a/test/unit/gateways/cashnet_test.rb b/test/unit/gateways/cashnet_test.rb index a3e00477a0e..c1706a51dd8 100644 --- a/test/unit/gateways/cashnet_test.rb +++ b/test/unit/gateways/cashnet_test.rb @@ -22,6 +22,35 @@ def test_successful_purchase assert_equal '1234', response.authorization end + def test_successful_purchase_with_multiple_items + options = { item_codes: { item_code: 'APPFEE', item_code2: 'LOBSTER', item_code3: 'CODES', amount: 100, amount2: 1234, amount3: 4321 } } + + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match('itemcode=APPFEE&itemcode2=LOBSTER&itemcode3=CODES&amount=1.00&amount2=12.34&amount3=43.21', data) + end.respond_with(successful_purchase_response) + + assert_instance_of Response, response + assert_success response + assert_equal '1234', response.authorization + end + + def test_successful_purchase_with_filtered_itemcodes + options = { item_codes: { item_code: 'APPFEE', item_code2: 'LOBSTER', item_code3: 'CODES', bad_key: 'BADVALUE', amount: 100, amount2: 1234, amount3: 4321 } } + + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match('itemcode=APPFEE&itemcode2=LOBSTER&itemcode3=CODES&amount=1.00&amount2=12.34&amount3=43.21', data) + refute_match('badkey=BADVALUE', data) + end.respond_with(successful_purchase_response) + + assert_instance_of Response, response + assert_success response + assert_equal '1234', response.authorization + end + def test_failed_purchase @gateway.expects(:ssl_post).returns(failed_purchase_response) @@ -55,12 +84,12 @@ def test_supported_countries end def test_supported_card_types - assert_equal [:visa, :master, :american_express, :discover, :diners_club, :jcb], CashnetGateway.supported_cardtypes + assert_equal %i[visa master american_express discover diners_club jcb], CashnetGateway.supported_cardtypes end def test_add_invoice result = {} - @gateway.send(:add_invoice, result, order_id: '#1001') + @gateway.send(:add_invoice, result, 1000, order_id: '#1001') assert_equal '#1001', result[:order_number] end @@ -76,9 +105,9 @@ def test_add_creditcard def test_add_address result = {} - @gateway.send(:add_address, result, billing_address: {address1: '123 Test St.', address2: '5F', city: 'Testville', zip: '12345', state: 'AK'} ) + @gateway.send(:add_address, result, billing_address: { address1: '123 Test St.', address2: '5F', city: 'Testville', zip: '12345', state: 'AK' }) - assert_equal ['addr_g', 'city_g', 'state_g', 'zip_g'], result.stringify_keys.keys.sort + assert_equal %w[addr_g city_g state_g zip_g], result.stringify_keys.keys.sort assert_equal '123 Test St.,5F', result[:addr_g] assert_equal 'Testville', result[:city_g] assert_equal 'AK', result[:state_g] @@ -93,11 +122,11 @@ def test_add_customer_data def test_action_meets_minimum_requirements params = { - amount: '1.01', + amount: '1.01' } @gateway.send(:add_creditcard, params, @credit_card) - @gateway.send(:add_invoice, params, {}) + @gateway.send(:add_invoice, params, 101, {}) assert data = @gateway.send(:post_data, 'SALE', params) minimum_requirements.each do |key| @@ -108,7 +137,7 @@ def test_action_meets_minimum_requirements def test_successful_purchase_with_fname_and_lname stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card, {}) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/fname=Longbob/, data) assert_match(/lname=Longsen/, data) end.respond_with(successful_purchase_response) @@ -127,7 +156,7 @@ def test_passes_custcode_from_credentials gateway = CashnetGateway.new(merchant: 'X', operator: 'X', password: 'test123', merchant_gateway_name: 'X', custcode: 'TheCustCode') stub_comms(gateway, :ssl_request) do gateway.purchase(@amount, @credit_card, {}) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/custcode=TheCustCode/, data) end.respond_with(successful_purchase_response) end @@ -136,7 +165,7 @@ def test_allows_custcode_override gateway = CashnetGateway.new(merchant: 'X', operator: 'X', password: 'test123', merchant_gateway_name: 'X', custcode: 'TheCustCode') stub_comms(gateway, :ssl_request) do gateway.purchase(@amount, @credit_card, custcode: 'OveriddenCustCode') - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/custcode=OveriddenCustCode/, data) end.respond_with(successful_purchase_response) end @@ -146,7 +175,17 @@ def test_scrub assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed end + def test_preventing_retries + Net::HTTP.any_instance. + expects(:request). + raises(Errno::ECONNREFUSED) + + error = assert_raises(ActiveMerchant::ConnectionError) { @gateway.purchase(@amount, @credit_card) } + assert_equal 'The remote server refused the connection', error.message + end + private + def expected_expiration_date '%02d%02d' % [@credit_card.month, @credit_card.year.to_s[2..4]] end @@ -176,128 +215,128 @@ def invalid_response end def pre_scrubbed - <<-TRANSCRIPT -opening connection to train.cashnet.com:443... -opened -starting SSL for train.cashnet.com:443... -SSL established -<- "POST /givecorpsgateway 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: train.cashnet.com\r\nContent-Length: 364\r\n\r\n" -<- "command=SALE&merchant=GiveCorpGW&operator=givecorp&password=14givecorps&station=WEB&custcode=ActiveMerchant%2F1.76.0&cardno=5454545454545454&cid=123&expdate=1215&card_name_g=Longbob+Longsen&fname=Longbob&lname=Longsen&order_number=c440ec8493f215d21c8a993ceae30129&itemcode=FEE&addr_g=456+My+Street%2CApt+1&city_g=Ottawa&state_g=ON&zip_g=K1C2N6&email_g=&amount=1.00" --> "HTTP/1.1 302 Found\r\n" --> "Date: Wed, 03 Jan 2018 17:03:35 GMT\r\n" --> "Content-Type: text/html; charset=utf-8\r\n" --> "Transfer-Encoding: chunked\r\n" --> "Connection: close\r\n" --> "Set-Cookie: AWSALB=5ISjTg8Mez7jS1kEnzY4j5NkQ5bdlwDDNmfzTyEMBmILpb0Tn3k58pUQTGHBj3NUpciP0uqQs7FaAb42YZvt35ndLERGJA0dPQ03iCfrqbneQ+Wm5BhDzMGo5GUT; Expires=Wed, 10 Jan 2018 17:03:35 GMT; Path=/\r\n" --> "Set-Cookie: AWSALB=bVhwwfJ2D6cI5zB3eapqNStEzF5yX1pXhaJGUBUCa+DZhEgn/TZGxznxIOYB9qKqzkPF4lq/zxWg/tuMBTiY4JGLRjayyhizvHnj2smrnNvr2DLQN7ZjLSh51BzM; Expires=Wed, 10 Jan 2018 17:03:35 GMT; Path=/\r\n" --> "Cache-Control: private\r\n" --> "Location: https://train.cashnet.com/cashneti/Gateway/htmlgw.aspx?client=EMARKETVENDOR_DEMO&command=SALE&merchant=GiveCorpGW&operator=givecorp&password=14givecorps&station=WEB&custcode=ActiveMerchant%2f1.76.0&cardno=5454545454545454&cid=123&expdate=1215&card_name_g=Longbob+Longsen&fname=Longbob&lname=Longsen&order_number=c440ec8493f215d21c8a993ceae30129&itemcode=FEE&addr_g=456+My+Street%2cApt+1&city_g=Ottawa&state_g=ON&zip_g=K1C2N6&email_g=&amount=1.00\r\n" --> "Set-Cookie: ASP.NET_SessionId=; path=/; HttpOnly\r\n" --> "P3P: CP=\"NOI DSP COR NID NOR\"\r\n" --> "Set-Cookie: BNI_persistence=0000000000000000000000004d79da0a00005000; Path=/\r\n" --> "Strict-Transport-Security: max-age=31536000\r\n" --> "\r\n" --> "282\r\n" -reading 642 bytes... --> "Object moved\r\n

Object moved to here.

\r\n\r\n" -read 642 bytes -reading 2 bytes... --> "\r\n" -read 2 bytes --> "0\r\n" --> "\r\n" -Conn close -opening connection to train.cashnet.com:443... -opened -starting SSL for train.cashnet.com:443... -SSL established -<- "GET /cashneti/Gateway/htmlgw.aspx?client=EMARKETVENDOR_DEMO&command=SALE&merchant=GiveCorpGW&operator=givecorp&password=14givecorps&station=WEB&custcode=ActiveMerchant%2f1.76.0&cardno=5454545454545454&cid=123&expdate=1215&card_name_g=Longbob+Longsen&fname=Longbob&lname=Longsen&order_number=c440ec8493f215d21c8a993ceae30129&itemcode=FEE&addr_g=456+My+Street%2cApt+1&city_g=Ottawa&state_g=ON&zip_g=K1C2N6&email_g=&amount=1.00 HTTP/1.1\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: train.cashnet.com\r\n\r\n" --> "HTTP/1.1 200 OK\r\n" --> "Date: Wed, 03 Jan 2018 17:03:35 GMT\r\n" --> "Content-Type: text/html; charset=utf-8\r\n" --> "Transfer-Encoding: chunked\r\n" --> "Connection: close\r\n" --> "Set-Cookie: AWSALB=lFPwFYRnXJHRNmE6NCRAIfHtQadwx4bYJoT5xeAL5AuAXPcm1vYWx5F/s5FBr3GcungifktpWlwIgAmWS29K7YRXTCjk4xmcAnhXS86fpVUVQt4ECwPH2xdv8tf2; Expires=Wed, 10 Jan 2018 17:03:35 GMT; Path=/\r\n" --> "Set-Cookie: AWSALB=mEfysFNBclo1/9+tTuI/XtHrmVkD89Fh6tAJ3Gl0u2EuLCYTW5VwEq+fVqYG1fEkN02dbhKSkIdM22QvyT6cRccDaUBsYAnOKjg2JlVShJlf+li5tfbrsUDk14jG; Expires=Wed, 10 Jan 2018 17:03:35 GMT; Path=/\r\n" --> "Cache-Control: private\r\n" --> "Set-Cookie: ASP.NET_SessionId=3ocslggtk4cdz54unbdnm25o; path=/; HttpOnly\r\n" --> "P3P: CP=\"NOI DSP COR NID NOR\"\r\n" --> "Set-Cookie: BNI_persistence=0000000000000000000000004d79da0a00005000; Path=/\r\n" --> "Strict-Transport-Security: max-age=31536000\r\n" --> "\r\n" --> "3a\r\n" -reading 58 bytes... --> "result=0&tx=77972&busdate=7/25/2017" -read 58 bytes -reading 2 bytes... --> "\r\n" -read 2 bytes --> "0\r\n" --> "\r\n" -Conn close -TRANSCRIPT + <<~TRANSCRIPT + opening connection to train.cashnet.com:443... + opened + starting SSL for train.cashnet.com:443... + SSL established + <- "POST /givecorpsgateway 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: train.cashnet.com\r\nContent-Length: 364\r\n\r\n" + <- "command=SALE&merchant=GiveCorpGW&operator=givecorp&password=14givecorps&station=WEB&custcode=ActiveMerchant%2F1.76.0&cardno=5454545454545454&cid=123&expdate=1215&card_name_g=Longbob+Longsen&fname=Longbob&lname=Longsen&order_number=c440ec8493f215d21c8a993ceae30129&itemcode=FEE&addr_g=456+My+Street%2CApt+1&city_g=Ottawa&state_g=ON&zip_g=K1C2N6&email_g=&amount=1.00" + -> "HTTP/1.1 302 Found\r\n" + -> "Date: Wed, 03 Jan 2018 17:03:35 GMT\r\n" + -> "Content-Type: text/html; charset=utf-8\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "Set-Cookie: AWSALB=5ISjTg8Mez7jS1kEnzY4j5NkQ5bdlwDDNmfzTyEMBmILpb0Tn3k58pUQTGHBj3NUpciP0uqQs7FaAb42YZvt35ndLERGJA0dPQ03iCfrqbneQ+Wm5BhDzMGo5GUT; Expires=Wed, 10 Jan 2018 17:03:35 GMT; Path=/\r\n" + -> "Set-Cookie: AWSALB=bVhwwfJ2D6cI5zB3eapqNStEzF5yX1pXhaJGUBUCa+DZhEgn/TZGxznxIOYB9qKqzkPF4lq/zxWg/tuMBTiY4JGLRjayyhizvHnj2smrnNvr2DLQN7ZjLSh51BzM; Expires=Wed, 10 Jan 2018 17:03:35 GMT; Path=/\r\n" + -> "Cache-Control: private\r\n" + -> "Location: https://train.cashnet.com/cashneti/Gateway/htmlgw.aspx?client=EMARKETVENDOR_DEMO&command=SALE&merchant=GiveCorpGW&operator=givecorp&password=14givecorps&station=WEB&custcode=ActiveMerchant%2f1.76.0&cardno=5454545454545454&cid=123&expdate=1215&card_name_g=Longbob+Longsen&fname=Longbob&lname=Longsen&order_number=c440ec8493f215d21c8a993ceae30129&itemcode=FEE&addr_g=456+My+Street%2cApt+1&city_g=Ottawa&state_g=ON&zip_g=K1C2N6&email_g=&amount=1.00\r\n" + -> "Set-Cookie: ASP.NET_SessionId=; path=/; HttpOnly\r\n" + -> "P3P: CP=\"NOI DSP COR NID NOR\"\r\n" + -> "Set-Cookie: BNI_persistence=0000000000000000000000004d79da0a00005000; Path=/\r\n" + -> "Strict-Transport-Security: max-age=31536000\r\n" + -> "\r\n" + -> "282\r\n" + reading 642 bytes... + -> "Object moved\r\n

Object moved to here.

\r\n\r\n" + read 642 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + opening connection to train.cashnet.com:443... + opened + starting SSL for train.cashnet.com:443... + SSL established + <- "GET /cashneti/Gateway/htmlgw.aspx?client=EMARKETVENDOR_DEMO&command=SALE&merchant=GiveCorpGW&operator=givecorp&password=14givecorps&station=WEB&custcode=ActiveMerchant%2f1.76.0&cardno=5454545454545454&cid=123&expdate=1215&card_name_g=Longbob+Longsen&fname=Longbob&lname=Longsen&order_number=c440ec8493f215d21c8a993ceae30129&itemcode=FEE&addr_g=456+My+Street%2cApt+1&city_g=Ottawa&state_g=ON&zip_g=K1C2N6&email_g=&amount=1.00 HTTP/1.1\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: train.cashnet.com\r\n\r\n" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Wed, 03 Jan 2018 17:03:35 GMT\r\n" + -> "Content-Type: text/html; charset=utf-8\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "Set-Cookie: AWSALB=lFPwFYRnXJHRNmE6NCRAIfHtQadwx4bYJoT5xeAL5AuAXPcm1vYWx5F/s5FBr3GcungifktpWlwIgAmWS29K7YRXTCjk4xmcAnhXS86fpVUVQt4ECwPH2xdv8tf2; Expires=Wed, 10 Jan 2018 17:03:35 GMT; Path=/\r\n" + -> "Set-Cookie: AWSALB=mEfysFNBclo1/9+tTuI/XtHrmVkD89Fh6tAJ3Gl0u2EuLCYTW5VwEq+fVqYG1fEkN02dbhKSkIdM22QvyT6cRccDaUBsYAnOKjg2JlVShJlf+li5tfbrsUDk14jG; Expires=Wed, 10 Jan 2018 17:03:35 GMT; Path=/\r\n" + -> "Cache-Control: private\r\n" + -> "Set-Cookie: ASP.NET_SessionId=3ocslggtk4cdz54unbdnm25o; path=/; HttpOnly\r\n" + -> "P3P: CP=\"NOI DSP COR NID NOR\"\r\n" + -> "Set-Cookie: BNI_persistence=0000000000000000000000004d79da0a00005000; Path=/\r\n" + -> "Strict-Transport-Security: max-age=31536000\r\n" + -> "\r\n" + -> "3a\r\n" + reading 58 bytes... + -> "result=0&tx=77972&busdate=7/25/2017" + read 58 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + TRANSCRIPT end def post_scrubbed - <<-SCRUBBED -opening connection to train.cashnet.com:443... -opened -starting SSL for train.cashnet.com:443... -SSL established -<- "POST /givecorpsgateway 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: train.cashnet.com\r\nContent-Length: 364\r\n\r\n" -<- "command=SALE&merchant=GiveCorpGW&operator=givecorp&password=[FILTERED]&station=WEB&custcode=ActiveMerchant%2F1.76.0&cardno=[FILTERED]&cid=[FILTERED]&expdate=1215&card_name_g=Longbob+Longsen&fname=Longbob&lname=Longsen&order_number=c440ec8493f215d21c8a993ceae30129&itemcode=FEE&addr_g=456+My+Street%2CApt+1&city_g=Ottawa&state_g=ON&zip_g=K1C2N6&email_g=&amount=1.00" --> "HTTP/1.1 302 Found\r\n" --> "Date: Wed, 03 Jan 2018 17:03:35 GMT\r\n" --> "Content-Type: text/html; charset=utf-8\r\n" --> "Transfer-Encoding: chunked\r\n" --> "Connection: close\r\n" --> "Set-Cookie: AWSALB=5ISjTg8Mez7jS1kEnzY4j5NkQ5bdlwDDNmfzTyEMBmILpb0Tn3k58pUQTGHBj3NUpciP0uqQs7FaAb42YZvt35ndLERGJA0dPQ03iCfrqbneQ+Wm5BhDzMGo5GUT; Expires=Wed, 10 Jan 2018 17:03:35 GMT; Path=/\r\n" --> "Set-Cookie: AWSALB=bVhwwfJ2D6cI5zB3eapqNStEzF5yX1pXhaJGUBUCa+DZhEgn/TZGxznxIOYB9qKqzkPF4lq/zxWg/tuMBTiY4JGLRjayyhizvHnj2smrnNvr2DLQN7ZjLSh51BzM; Expires=Wed, 10 Jan 2018 17:03:35 GMT; Path=/\r\n" --> "Cache-Control: private\r\n" --> "Location: https://train.cashnet.com/cashneti/Gateway/htmlgw.aspx?client=EMARKETVENDOR_DEMO&command=SALE&merchant=GiveCorpGW&operator=givecorp&password=[FILTERED]&station=WEB&custcode=ActiveMerchant%2f1.76.0&cardno=[FILTERED]&cid=[FILTERED]&expdate=1215&card_name_g=Longbob+Longsen&fname=Longbob&lname=Longsen&order_number=c440ec8493f215d21c8a993ceae30129&itemcode=FEE&addr_g=456+My+Street%2cApt+1&city_g=Ottawa&state_g=ON&zip_g=K1C2N6&email_g=&amount=1.00\r\n" --> "Set-Cookie: ASP.NET_SessionId=; path=/; HttpOnly\r\n" --> "P3P: CP=\"NOI DSP COR NID NOR\"\r\n" --> "Set-Cookie: BNI_persistence=0000000000000000000000004d79da0a00005000; Path=/\r\n" --> "Strict-Transport-Security: max-age=31536000\r\n" --> "\r\n" --> "282\r\n" -reading 642 bytes... --> "Object moved\r\n

Object moved to here.

\r\n\r\n" -read 642 bytes -reading 2 bytes... --> "\r\n" -read 2 bytes --> "0\r\n" --> "\r\n" -Conn close -opening connection to train.cashnet.com:443... -opened -starting SSL for train.cashnet.com:443... -SSL established -<- "GET /cashneti/Gateway/htmlgw.aspx?client=EMARKETVENDOR_DEMO&command=SALE&merchant=GiveCorpGW&operator=givecorp&password=[FILTERED]&station=WEB&custcode=ActiveMerchant%2f1.76.0&cardno=[FILTERED]&cid=[FILTERED]&expdate=1215&card_name_g=Longbob+Longsen&fname=Longbob&lname=Longsen&order_number=c440ec8493f215d21c8a993ceae30129&itemcode=FEE&addr_g=456+My+Street%2cApt+1&city_g=Ottawa&state_g=ON&zip_g=K1C2N6&email_g=&amount=1.00 HTTP/1.1\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: train.cashnet.com\r\n\r\n" --> "HTTP/1.1 200 OK\r\n" --> "Date: Wed, 03 Jan 2018 17:03:35 GMT\r\n" --> "Content-Type: text/html; charset=utf-8\r\n" --> "Transfer-Encoding: chunked\r\n" --> "Connection: close\r\n" --> "Set-Cookie: AWSALB=lFPwFYRnXJHRNmE6NCRAIfHtQadwx4bYJoT5xeAL5AuAXPcm1vYWx5F/s5FBr3GcungifktpWlwIgAmWS29K7YRXTCjk4xmcAnhXS86fpVUVQt4ECwPH2xdv8tf2; Expires=Wed, 10 Jan 2018 17:03:35 GMT; Path=/\r\n" --> "Set-Cookie: AWSALB=mEfysFNBclo1/9+tTuI/XtHrmVkD89Fh6tAJ3Gl0u2EuLCYTW5VwEq+fVqYG1fEkN02dbhKSkIdM22QvyT6cRccDaUBsYAnOKjg2JlVShJlf+li5tfbrsUDk14jG; Expires=Wed, 10 Jan 2018 17:03:35 GMT; Path=/\r\n" --> "Cache-Control: private\r\n" --> "Set-Cookie: ASP.NET_SessionId=3ocslggtk4cdz54unbdnm25o; path=/; HttpOnly\r\n" --> "P3P: CP=\"NOI DSP COR NID NOR\"\r\n" --> "Set-Cookie: BNI_persistence=0000000000000000000000004d79da0a00005000; Path=/\r\n" --> "Strict-Transport-Security: max-age=31536000\r\n" --> "\r\n" --> "3a\r\n" -reading 58 bytes... --> "result=0&tx=77972&busdate=7/25/2017" -read 58 bytes -reading 2 bytes... --> "\r\n" -read 2 bytes --> "0\r\n" --> "\r\n" -Conn close -SCRUBBED + <<~SCRUBBED + opening connection to train.cashnet.com:443... + opened + starting SSL for train.cashnet.com:443... + SSL established + <- "POST /givecorpsgateway 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: train.cashnet.com\r\nContent-Length: 364\r\n\r\n" + <- "command=SALE&merchant=GiveCorpGW&operator=givecorp&password=[FILTERED]&station=WEB&custcode=ActiveMerchant%2F1.76.0&cardno=[FILTERED]&cid=[FILTERED]&expdate=1215&card_name_g=Longbob+Longsen&fname=Longbob&lname=Longsen&order_number=c440ec8493f215d21c8a993ceae30129&itemcode=FEE&addr_g=456+My+Street%2CApt+1&city_g=Ottawa&state_g=ON&zip_g=K1C2N6&email_g=&amount=1.00" + -> "HTTP/1.1 302 Found\r\n" + -> "Date: Wed, 03 Jan 2018 17:03:35 GMT\r\n" + -> "Content-Type: text/html; charset=utf-8\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "Set-Cookie: AWSALB=5ISjTg8Mez7jS1kEnzY4j5NkQ5bdlwDDNmfzTyEMBmILpb0Tn3k58pUQTGHBj3NUpciP0uqQs7FaAb42YZvt35ndLERGJA0dPQ03iCfrqbneQ+Wm5BhDzMGo5GUT; Expires=Wed, 10 Jan 2018 17:03:35 GMT; Path=/\r\n" + -> "Set-Cookie: AWSALB=bVhwwfJ2D6cI5zB3eapqNStEzF5yX1pXhaJGUBUCa+DZhEgn/TZGxznxIOYB9qKqzkPF4lq/zxWg/tuMBTiY4JGLRjayyhizvHnj2smrnNvr2DLQN7ZjLSh51BzM; Expires=Wed, 10 Jan 2018 17:03:35 GMT; Path=/\r\n" + -> "Cache-Control: private\r\n" + -> "Location: https://train.cashnet.com/cashneti/Gateway/htmlgw.aspx?client=EMARKETVENDOR_DEMO&command=SALE&merchant=GiveCorpGW&operator=givecorp&password=[FILTERED]&station=WEB&custcode=ActiveMerchant%2f1.76.0&cardno=[FILTERED]&cid=[FILTERED]&expdate=1215&card_name_g=Longbob+Longsen&fname=Longbob&lname=Longsen&order_number=c440ec8493f215d21c8a993ceae30129&itemcode=FEE&addr_g=456+My+Street%2cApt+1&city_g=Ottawa&state_g=ON&zip_g=K1C2N6&email_g=&amount=1.00\r\n" + -> "Set-Cookie: ASP.NET_SessionId=; path=/; HttpOnly\r\n" + -> "P3P: CP=\"NOI DSP COR NID NOR\"\r\n" + -> "Set-Cookie: BNI_persistence=0000000000000000000000004d79da0a00005000; Path=/\r\n" + -> "Strict-Transport-Security: max-age=31536000\r\n" + -> "\r\n" + -> "282\r\n" + reading 642 bytes... + -> "Object moved\r\n

Object moved to here.

\r\n\r\n" + read 642 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + opening connection to train.cashnet.com:443... + opened + starting SSL for train.cashnet.com:443... + SSL established + <- "GET /cashneti/Gateway/htmlgw.aspx?client=EMARKETVENDOR_DEMO&command=SALE&merchant=GiveCorpGW&operator=givecorp&password=[FILTERED]&station=WEB&custcode=ActiveMerchant%2f1.76.0&cardno=[FILTERED]&cid=[FILTERED]&expdate=1215&card_name_g=Longbob+Longsen&fname=Longbob&lname=Longsen&order_number=c440ec8493f215d21c8a993ceae30129&itemcode=FEE&addr_g=456+My+Street%2cApt+1&city_g=Ottawa&state_g=ON&zip_g=K1C2N6&email_g=&amount=1.00 HTTP/1.1\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: train.cashnet.com\r\n\r\n" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Wed, 03 Jan 2018 17:03:35 GMT\r\n" + -> "Content-Type: text/html; charset=utf-8\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "Set-Cookie: AWSALB=lFPwFYRnXJHRNmE6NCRAIfHtQadwx4bYJoT5xeAL5AuAXPcm1vYWx5F/s5FBr3GcungifktpWlwIgAmWS29K7YRXTCjk4xmcAnhXS86fpVUVQt4ECwPH2xdv8tf2; Expires=Wed, 10 Jan 2018 17:03:35 GMT; Path=/\r\n" + -> "Set-Cookie: AWSALB=mEfysFNBclo1/9+tTuI/XtHrmVkD89Fh6tAJ3Gl0u2EuLCYTW5VwEq+fVqYG1fEkN02dbhKSkIdM22QvyT6cRccDaUBsYAnOKjg2JlVShJlf+li5tfbrsUDk14jG; Expires=Wed, 10 Jan 2018 17:03:35 GMT; Path=/\r\n" + -> "Cache-Control: private\r\n" + -> "Set-Cookie: ASP.NET_SessionId=3ocslggtk4cdz54unbdnm25o; path=/; HttpOnly\r\n" + -> "P3P: CP=\"NOI DSP COR NID NOR\"\r\n" + -> "Set-Cookie: BNI_persistence=0000000000000000000000004d79da0a00005000; Path=/\r\n" + -> "Strict-Transport-Security: max-age=31536000\r\n" + -> "\r\n" + -> "3a\r\n" + reading 58 bytes... + -> "result=0&tx=77972&busdate=7/25/2017" + read 58 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + SCRUBBED end end 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..9b9cd524819 --- /dev/null +++ b/test/unit/gateways/cecabank_rest_json_test.rb @@ -0,0 +1,409 @@ +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' + ) + + @no_encrypted_gateway = CecabankJsonGateway.new( + merchant_id: '12345678', + acquirer_bin: '12345678', + terminal_id: '00000003', + cypher_key: 'enc_key' + ) + + @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' + } + + @apple_pay_network_token = network_tokenization_credit_card( + '4507670001000009', + eci: '05', + payment_cryptogram: 'xgQAAAAAAAAAAAAAAAAAAAAAAAAA', + month: '12', + year: Time.now.year, + source: :apple_pay, + verification_value: '989' + ) + + @google_pay_network_token = network_tokenization_credit_card( + '4507670001000009', + eci: '05', + payment_cryptogram: 'xgQAAAAAAAAAAAAAAAAAAAAAAAAA', + month: '12', + year: Time.now.year, + source: :google_pay, + verification_value: '989' + ) + 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_successful_purchase_with_apple_pay + stub_comms(@no_encrypted_gateway, :ssl_post) do + @no_encrypted_gateway.purchase(@amount, @apple_pay_network_token, @options.merge(xid: 'some_xid')) + end.check_request do |_endpoint, data, _headers| + data = JSON.parse(data) + params = JSON.parse(Base64.decode64(data['parametros'])) + common_ap_gp_assertions(params, @apple_pay_network_token, wallet_type: 'A') + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_google_pay + stub_comms(@no_encrypted_gateway, :ssl_post) do + @no_encrypted_gateway.purchase(@amount, @google_pay_network_token, @options.merge(xid: 'some_xid')) + end.check_request do |_endpoint, data, _headers| + data = JSON.parse(data) + params = JSON.parse(Base64.decode64(data['parametros'])) + common_ap_gp_assertions(params, @google_pay_network_token, wallet_type: 'G') + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_apple_pay_encrypted_gateway + stub_comms do + @gateway.purchase(@amount, @apple_pay_network_token, @options.merge(xid: 'some_xid')) + end.check_request do |_endpoint, data, _headers| + data = JSON.parse(data) + encryoted_params = JSON.parse(Base64.decode64(data['parametros'])) + sensitive_json = decrypt_sensitive_fields(@gateway.options, encryoted_params['encryptedData']) + sensitive_params = JSON.parse(sensitive_json) + common_ap_gp_assertions(sensitive_params, @apple_pay_network_token, wallet_type: 'A') + 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 common_ap_gp_assertions(params, payment_method, wallet_type) + assert_include params, 'wallet' + assert_equal params['pan'], payment_method.number + wallet = JSON.parse(params['wallet']) + assert_equal wallet['authentication_value'], payment_method.payment_cryptogram + assert_equal wallet['xid'], 'some_xid' + assert_equal wallet['eci'], payment_method.eci + 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 e21446ea95e..b2218845379 100644 --- a/test/unit/gateways/cecabank_test.rb +++ b/test/unit/gateways/cecabank_test.rb @@ -4,19 +4,19 @@ class CecabankTest < Test::Unit::TestCase include CommStub def setup - @gateway = CecabankGateway.new( - :merchant_id => '12345678', - :acquirer_bin => '12345678', - :terminal_id => '00000003', - :key => 'enc_key' + @gateway = CecabankXmlGateway.new( + merchant_id: '12345678', + acquirer_bin: '12345678', + terminal_id: '00000003', + cypher_key: 'enc_key' ) @credit_card = credit_card @amount = 100 @options = { - :order_id => '1', - :description => 'Store Purchase' + order_id: '1', + description: 'Store Purchase' } end @@ -37,13 +37,13 @@ def test_invalid_xml_response_handling assert_instance_of Response, response assert_failure response assert_match(/Unable to parse the response/, response.message) - assert_match(/No close tag for/, response.params['error_message']) + # assert_match(/No close tag for/, response.params['error_message']) end def test_expiration_date_sent_correctly stub_comms do @gateway.purchase(@amount, credit_card('4242424242424242', month: 1, year: 2014), @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/Caducidad=201401&/, data, 'Expected expiration date format is yyyymm') end.respond_with(successful_purchase_response) end @@ -53,6 +53,7 @@ def test_unsuccessful_request assert response = @gateway.purchase(@amount, @credit_card, @options) assert_failure response + assert_match(/Formato CVV2\/CVC2 no valido/, response.message) assert response.test? end @@ -72,69 +73,68 @@ def test_unsuccessful_refund_request assert_failure response assert response.test? end - + def test_transcript_scrubbing assert_equal scrubbed_transcript, @gateway.scrub(transcript) end - private def successful_purchase_response - <<-RESPONSE - - - - 171.00 Euros - - 101000 - 12345678901234567890 - ##PAN## - - + <<~RESPONSE + + + + 171.00 Euros + + 101000 + 12345678901234567890 + ##PAN## + + RESPONSE end def failed_purchase_response - <<-RESPONSE - - - - 27 - - - + <<~RESPONSE + + + + 27 + + + RESPONSE end def invalid_xml_purchase_response - <<-RESPONSE -
- -Invalid unparsable xml in the response + <<~RESPONSE +
+ + Invalid unparsable xml in the response RESPONSE end def successful_refund_response - <<-RESPONSE - - - - 1.00 Euros - - + <<~RESPONSE + + + + 1.00 Euros + + RESPONSE end def failed_refund_response - <<-RESPONSE - - - - 15 - ]]> - - + <<~RESPONSE + + + + 15 + ]]> + + RESPONSE end diff --git a/test/unit/gateways/cenpos_test.rb b/test/unit/gateways/cenpos_test.rb index e43caa1a1a4..0fd8f452a1b 100644 --- a/test/unit/gateways/cenpos_test.rb +++ b/test/unit/gateways/cenpos_test.rb @@ -5,9 +5,9 @@ class CenposTest < Test::Unit::TestCase def setup @gateway = CenposGateway.new( - :merchant_id => 'merchant_id', - :password => 'password', - :user_id => 'user_id' + merchant_id: 'merchant_id', + password: 'password', + user_id: 'user_id' ) @credit_card = credit_card @@ -115,7 +115,7 @@ def test_successful_authorize_and_capture capture = stub_comms do @gateway.capture(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/1760035844/, data) end.respond_with(successful_capture_response) @@ -151,7 +151,7 @@ def test_successful_void void = stub_comms do @gateway.void(response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/1760035844/, data) end.respond_with(successful_void_response) @@ -161,7 +161,7 @@ def test_successful_void def test_failed_void response = stub_comms do @gateway.void('1758584451|4242|1.00') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/1758584451/, data) end.respond_with(failed_void_response) @@ -178,7 +178,7 @@ def test_successful_refund refund = stub_comms do @gateway.refund(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/1609995363/, data) end.respond_with(successful_refund_response) @@ -307,7 +307,6 @@ def failed_refund_response ) end - def successful_credit_response %( Approved091.13VISA0091.13160999621100 diff --git a/test/unit/gateways/checkout_test.rb b/test/unit/gateways/checkout_test.rb index bc12cd16476..d58e55ee55e 100644 --- a/test/unit/gateways/checkout_test.rb +++ b/test/unit/gateways/checkout_test.rb @@ -5,8 +5,8 @@ class CheckoutTest < Test::Unit::TestCase def setup @gateway = ActiveMerchant::Billing::CheckoutGateway.new( - :merchant_id => 'SBMTEST', # Merchant Code - :password => 'Password1!' # Processing Password + merchant_id: 'SBMTEST', # Merchant Code + password: 'Password1!' # Processing Password ) @options = { order_id: generate_unique_id @@ -55,7 +55,7 @@ def test_unsuccessful_authorize def test_unsuccessful_capture @gateway.expects(:ssl_post).returns(failed_capture_response) - assert response = @gateway.capture(100, '||||' , @options) + assert response = @gateway.capture(100, '||||', @options) assert_failure response assert_equal 'EGP00173', response.params['error_code_tag'] assert response.test? @@ -72,21 +72,20 @@ def test_unsuccessful_purchase def test_passes_correct_currency stub_comms do - @gateway.purchase(100, credit_card, @options.merge( - currency: 'EUR' - )) - end.check_request do |endpoint, data, headers| + @gateway.purchase(100, credit_card, @options.merge(currency: 'EUR')) + end.check_request do |_endpoint, data, _headers| assert_match(/EUR<\/bill_currencycode>/, data) end.respond_with(successful_purchase_response) end def test_passes_descriptors + options = @options.merge( + descriptor_name: 'ZahName', + descriptor_city: 'Oakland' + ) stub_comms do - @gateway.purchase(100, credit_card, @options.merge( - descriptor_name: 'ZahName', - descriptor_city: 'Oakland' - )) - end.check_request do |endpoint, data, headers| + @gateway.purchase(100, credit_card, options) + end.check_request do |_endpoint, data, _headers| assert_match(/ZahName<\/descriptor_name>/, data) assert_match(/Oakland<\/descriptor_city>/, data) end.respond_with(successful_purchase_response) @@ -96,7 +95,7 @@ def test_successful_void @options['orderid'] = '9c38d0506da258e216fa072197faaf37' void = stub_comms(@gateway, :ssl_request) do @gateway.void('36919371|9c38d0506da258e216fa072197faaf37|1|CAD|100', @options) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| # Should only be one pair of track id tags. assert_equal 2, data.scan(/trackid/).count end.respond_with(successful_void_response) diff --git a/test/unit/gateways/checkout_v2_test.rb b/test/unit/gateways/checkout_v2_test.rb index d6189454c5f..870c9bf3743 100644 --- a/test/unit/gateways/checkout_v2_test.rb +++ b/test/unit/gateways/checkout_v2_test.rb @@ -5,25 +5,83 @@ class CheckoutV2Test < Test::Unit::TestCase def setup @gateway = CheckoutV2Gateway.new( - secret_key: '1111111111111', + secret_key: '1111111111111' ) - + @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' + + @lvl_2_3_options = { + order_id: '1', + billing_address: address, + shipping_address: address, + description: 'Purchase', + email: 'longbob.longsen@example.com', + processing_channel_id: 'pc_lxgl7aqahkzubkundd2l546hdm', + invoice_id: 12462, + tax_number: 123456, + from_address_zip: 12345, + tax_amount: 30, + shipping_amount: 20, + discount_amount: 10, + duty_amount: 5, + line_items: [ + { # only for American Express in level 2 or any lvl 3 + commodity_code: 123, + name: 'glass', + quantity: 1, + unit_price: 200, + tax_amount: 12, + discount_amount: 12, + total_amount: 200, + reference: 'glass123', + unit_of_measure: 'Centimeters' + }, + { + commodity_code: 456, + name: 'water', + quantity: 2, + unit_price: 100, + tax_amount: 6, + discount_amount: 6, + total_amount: 100, + reference: 'water123', + unit_of_measure: 'Liters' + } + ] + } + end + + def test_supported_card_types + assert_equal CheckoutV2Gateway.supported_cardtypes, %i[visa master american_express diners_club maestro discover jcb mada bp_plus patagonia_365 tarjeta_sol] + 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) assert_success response - assert_equal 'charge_test_941CA9CE174U76BD29C8', response.authorization + assert_equal 'pay_bgv5tmah6fmuzcmcrcro6exe6m', response.authorization assert response.test? end def test_successful_purchase_includes_avs_result - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card) end.respond_with(successful_purchase_response) @@ -34,15 +92,248 @@ 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) assert_equal 'Y', response.cvv_result['code'] end + def test_successful_purchase_using_vts_network_token_without_eci + network_token = network_tokenization_credit_card( + '4242424242424242', + { source: :network_token, brand: 'visa' } + ) + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, network_token) + end.check_request do |_method, _endpoint, data, _headers| + request_data = JSON.parse(data) + + assert_equal(request_data['source']['type'], 'network_token') + assert_equal(request_data['source']['token'], network_token.number) + assert_equal(request_data['source']['token_type'], 'vts') + assert_equal(request_data['source']['eci'], '05') + assert_equal(request_data['source']['cryptogram'], network_token.payment_cryptogram) + end.respond_with(successful_purchase_with_network_token_response) + + assert_success response + assert_equal '2FCFE326D92D4C27EDD699560F484', response.params['source']['payment_account_reference'] + assert response.test? + end + + def test_successful_passing_processing_channel_id + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, { processing_channel_id: '123456abcde' }) + end.check_request do |_method, _endpoint, data, _headers| + request_data = JSON.parse(data) + assert_equal(request_data['processing_channel_id'], '123456abcde') + end.respond_with(successful_purchase_response) + end + + def test_successful_passing_risk_data + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, { + risk: { + enabled: 'true', + device_session_id: '12345-abcd' + } + }) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data)['risk'] + assert_equal request['enabled'], true + assert_equal request['device_session_id'], '12345-abcd' + end.respond_with(successful_purchase_response) + end + + def test_successful_passing_incremental_authorization + response = stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, { incremental_authorization: 'abcd1234' }) + end.check_request do |_method, endpoint, _data, _headers| + assert_include endpoint, 'abcd1234' + end.respond_with(successful_incremental_authorize_response) + + assert_success response + end + + def test_successful_passing_authorization_type + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, { authorization_type: 'Estimated' }) + end.check_request do |_method, _endpoint, data, _headers| + request_data = JSON.parse(data) + assert_equal(request_data['authorization_type'], 'Estimated') + end.respond_with(successful_purchase_response) + end + + def test_successful_passing_exemption_and_challenge_indicator + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, { execute_threed: true, exemption: 'no_preference', challenge_indicator: 'trusted_listing' }) + end.check_request do |_method, _endpoint, data, _headers| + request_data = JSON.parse(data) + assert_equal(request_data['3ds']['exemption'], 'no_preference') + assert_equal(request_data['3ds']['challenge_indicator'], 'trusted_listing') + end.respond_with(successful_purchase_response) + end + + def test_successful_passing_capture_type + stub_comms(@gateway, :ssl_request) do + @gateway.capture(@amount, 'abc', { capture_type: 'NonFinal' }) + end.check_request do |_method, _endpoint, data, _headers| + request_data = JSON.parse(data) + assert_equal(request_data['capture_type'], 'NonFinal') + end.respond_with(successful_capture_response) + end + + def test_successful_purchase_using_vts_network_token_with_eci + network_token = network_tokenization_credit_card( + '4242424242424242', + { source: :network_token, brand: 'visa', eci: '06' } + ) + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, network_token) + end.check_request do |_method, _endpoint, data, _headers| + request_data = JSON.parse(data) + + assert_equal(request_data['source']['type'], 'network_token') + assert_equal(request_data['source']['token'], network_token.number) + assert_equal(request_data['source']['token_type'], 'vts') + assert_equal(request_data['source']['eci'], '06') + assert_equal(request_data['source']['cryptogram'], network_token.payment_cryptogram) + end.respond_with(successful_purchase_with_network_token_response) + + assert_success response + assert_equal '2FCFE326D92D4C27EDD699560F484', response.params['source']['payment_account_reference'] + assert response.test? + end + + def test_successful_purchase_using_mdes_network_token + network_token = network_tokenization_credit_card( + '5436031030606378', + { source: :network_token, brand: 'master' } + ) + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, network_token) + end.check_request do |_method, _endpoint, data, _headers| + request_data = JSON.parse(data) + + assert_equal(request_data['source']['type'], 'network_token') + assert_equal(request_data['source']['token'], network_token.number) + assert_equal(request_data['source']['token_type'], 'mdes') + assert_equal(request_data['source']['eci'], nil) + assert_equal(request_data['source']['cryptogram'], network_token.payment_cryptogram) + end.respond_with(successful_purchase_with_network_token_response) + + assert_success response + assert_equal '2FCFE326D92D4C27EDD699560F484', response.params['source']['payment_account_reference'] + assert response.test? + end + + def test_successful_purchase_using_apple_pay_network_token + network_token = network_tokenization_credit_card( + '4242424242424242', + { source: :apple_pay, eci: '05', payment_cryptogram: 'AgAAAAAAAIR8CQrXcIhbQAAAAAA' } + ) + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, network_token) + end.check_request do |_method, _endpoint, data, _headers| + request_data = JSON.parse(data) + + assert_equal(request_data['source']['type'], 'network_token') + assert_equal(request_data['source']['token'], network_token.number) + assert_equal(request_data['source']['token_type'], 'applepay') + assert_equal(request_data['source']['eci'], '05') + assert_equal(request_data['source']['cryptogram'], network_token.payment_cryptogram) + end.respond_with(successful_purchase_with_network_token_response) + + assert_success response + assert_equal '2FCFE326D92D4C27EDD699560F484', response.params['source']['payment_account_reference'] + assert response.test? + end + + def test_successful_purchase_using_android_pay_network_token + network_token = network_tokenization_credit_card( + '4242424242424242', + { source: :android_pay, eci: '05', payment_cryptogram: 'AgAAAAAAAIR8CQrXcIhbQAAAAAA' } + ) + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, network_token) + end.check_request do |_method, _endpoint, data, _headers| + request_data = JSON.parse(data) + + assert_equal(request_data['source']['type'], 'network_token') + assert_equal(request_data['source']['token'], network_token.number) + assert_equal(request_data['source']['token_type'], 'googlepay') + assert_equal(request_data['source']['eci'], '05') + assert_equal(request_data['source']['cryptogram'], network_token.payment_cryptogram) + end.respond_with(successful_purchase_with_network_token_response) + + assert_success response + assert_equal '2FCFE326D92D4C27EDD699560F484', response.params['source']['payment_account_reference'] + assert response.test? + end + + def test_successful_purchase_using_google_pay_network_token + network_token = network_tokenization_credit_card( + '4242424242424242', + { source: :google_pay, eci: '05', payment_cryptogram: 'AgAAAAAAAIR8CQrXcIhbQAAAAAA' } + ) + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, network_token) + end.check_request do |_method, _endpoint, data, _headers| + request_data = JSON.parse(data) + + assert_equal(request_data['source']['type'], 'network_token') + assert_equal(request_data['source']['token'], network_token.number) + assert_equal(request_data['source']['token_type'], 'googlepay') + assert_equal(request_data['source']['eci'], '05') + assert_equal(request_data['source']['cryptogram'], network_token.payment_cryptogram) + end.respond_with(successful_purchase_with_network_token_response) + + assert_success response + assert_equal '2FCFE326D92D4C27EDD699560F484', response.params['source']['payment_account_reference'] + assert response.test? + end + + def test_successful_purchase_using_google_pay_pan_only_network_token + network_token = network_tokenization_credit_card( + '4242424242424242', + { source: :google_pay } + ) + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, network_token) + end.check_request do |_method, _endpoint, data, _headers| + request_data = JSON.parse(data) + + assert_equal(request_data['source']['type'], 'network_token') + assert_equal(request_data['source']['token'], network_token.number) + assert_equal(request_data['source']['token_type'], 'googlepay') + assert_equal(request_data['source']['eci'], nil) + assert_equal(request_data['source']['cryptogram'], nil) + end.respond_with(successful_purchase_with_network_token_response) + + assert_success response + assert_equal '2FCFE326D92D4C27EDD699560F484', response.params['source']['payment_account_reference'] + assert response.test? + end + + def test_successful_render_for_oauth + processing_channel_id = 'abcd123' + response = stub_comms(@gateway_oauth, :ssl_request) do + @gateway_oauth.purchase(@amount, @credit_card, { processing_channel_id: }) + 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) @@ -53,7 +344,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) @@ -61,32 +352,148 @@ def test_successful_authorize_includes_cvv_result end def test_purchase_with_additional_fields - response = stub_comms do - @gateway.purchase(@amount, @credit_card, {descriptor_city: 'london', descriptor_name: 'sherlock'}) - end.check_request do |endpoint, data, headers| - assert_match(/"descriptor\":{\"name\":\"sherlock\",\"city\":\"london\"}/, data) + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, { descriptor_city: 'london', descriptor_name: 'sherlock' }) + 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(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card) + end.check_request do |_method, _endpoint, data, _headers| + request_data = JSON.parse(data) + assert_equal(request_data['metadata']['udf1'], 'mada') + end.respond_with(successful_purchase_response) + end + def test_failed_purchase - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card) end.respond_with(failed_purchase_response) assert_failure response assert_equal Gateway::STANDARD_ERROR_CODE[:invalid_number], response.error_code end + def test_failed_purchase_3ds + 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 'Insufficient Funds', 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 'charge_test_AF1A29AD350Q748C7EA8', response.authorization + 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) @@ -94,23 +501,270 @@ 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: 'charge_123' + previous_charge_id: 'pay_123', + processing_channel_id: 'pc_123' + } + @gateway.authorize(@amount, @credit_card, options) + 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) + end.respond_with(successful_authorize_response) + + assert_success response + assert_equal 'pay_fj3xswqe3emuxckocjx6td73ni', response.authorization + + capture = stub_comms(@gateway, :ssl_request) do + @gateway.capture(@amount, response.authorization) + end.respond_with(successful_capture_response) + + assert_success capture + end + + def test_successful_purchase_with_stored_credentials + 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 |_method, _endpoint, data, _headers| + assert_match(%r{"payment_type":"Installment"}, data) + assert_match(%r{"merchant_initiated":false}, data) + end.respond_with(successful_purchase_initial_stored_credential_response) + + assert_success initial_response + assert_equal 'pay_7jcf4ovmwnqedhtldca3fjli2y', initial_response.params['id'] + network_transaction_id = initial_response.params['id'] + + response = stub_comms(@gateway, :ssl_request) do + options = { + stored_credential: { + initial_transaction: false, + reason_type: 'recurring', + network_transaction_id: + } + } + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['previous_payment_id'], 'pay_7jcf4ovmwnqedhtldca3fjli2y' + assert_equal request['source']['stored'], true + end.respond_with(successful_purchase_using_stored_credential_response) + + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_stored_credentials_merchant_initiated_transaction_id + response = stub_comms(@gateway, :ssl_request) do + options = { + stored_credential: { + initial_transaction: false + }, + merchant_initiated_transaction_id: 'pay_7jcf4ovmwnqedhtldca3fjli2y' + } + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['previous_payment_id'], 'pay_7jcf4ovmwnqedhtldca3fjli2y' + assert_equal request['source']['stored'], true + end.respond_with(successful_purchase_using_stored_credential_response) + + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_extra_customer_data + stub_comms(@gateway, :ssl_request) do + options = { + phone_country_code: '1', + billing_address: address + } + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['source']['phone']['number'], '(555)555-5555' + assert_equal request['source']['phone']['country_code'], '1' + assert_equal request['customer']['name'], 'Longbob Longsen' + end.respond_with(successful_purchase_response) + end + + def test_no_customer_name_included_in_token_purchase + stub_comms(@gateway, :ssl_request) do + options = { + phone_country_code: '1', + billing_address: address + } + @gateway.purchase(@amount, @token, options) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['source']['phone']['number'], '(555)555-5555' + assert_equal request['source']['phone']['country_code'], '1' + refute_includes data, 'name' + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_metadata + response = stub_comms(@gateway, :ssl_request) do + options = { + metadata: { + coupon_code: 'NY2018', + partner_id: '123989' + } + } + @gateway.purchase(@amount, @credit_card, options) + 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) + + 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(@gateway, :ssl_request) do + options = { + metadata: { + coupon_code: 'NY2018', + partner_id: '123989' + } + } + @gateway.authorize(@amount, @credit_card, options) + 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) + + assert_success response + assert_equal 'pay_fj3xswqe3emuxckocjx6td73ni', response.authorization + + capture = stub_comms(@gateway, :ssl_request) do + @gateway.capture(@amount, response.authorization) + end.respond_with(successful_capture_response) + + assert_success capture + end + + def test_moto_transaction_is_properly_set + response = stub_comms(@gateway, :ssl_request) do + options = { + metadata: { + manual_entry: true + } + } + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(%r{"payment_type":"MOTO"}, data) + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_3ds_passed + 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 |_method, _endpoint, data, _headers| + assert_match(%r{"success_url"}, data) + assert_match(%r{"failure_url"}, data) + end.respond_with(successful_authorize_response) + + assert_success response + end + + 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 + + def test_failed_verify_payment + response = stub_comms(@gateway, :ssl_request) do + @gateway.verify_payment('testValue') + end.respond_with(failed_verify_payment_response) + + assert_failure response + end + + def test_successful_authorize_and_capture_with_3ds + response = stub_comms(@gateway, :ssl_request) do + options = { + execute_threed: true, + attempt_n3d: true, + three_d_secure: { + version: '1.0.2', + eci: '05', + cryptogram: '1234', + xid: '1234', + authentication_response_status: 'Y' + } + } + @gateway.authorize(@amount, @credit_card, options) + end.respond_with(successful_authorize_response) + + assert_success response + assert_equal 'pay_fj3xswqe3emuxckocjx6td73ni', response.authorization + + capture = stub_comms(@gateway, :ssl_request) do + @gateway.capture(@amount, response.authorization) + end.respond_with(successful_capture_response) + + assert_success capture + end + + def test_successful_authorize_and_capture_with_3ds2 + response = stub_comms(@gateway, :ssl_request) do + options = { + execute_threed: true, + three_d_secure: { + version: '2.0.0', + eci: '05', + cryptogram: '1234', + ds_transaction_id: '1234', + authentication_response_status: 'Y' + } } @gateway.authorize(@amount, @credit_card, options) - end.check_request do |endpoint, data, headers| - assert_match(%r{"cardOnFile":true}, data) - assert_match(%r{"transactionIndicator":2}, data) - assert_match(%r{"previousChargeId":"charge_123"}, data) end.respond_with(successful_authorize_response) assert_success response - assert_equal 'charge_test_AF1A29AD350Q748C7EA8', response.authorization + 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) @@ -118,7 +772,7 @@ def test_successful_authorize_and_capture_with_additional_options 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) @@ -128,7 +782,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) @@ -136,14 +790,38 @@ 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 'charge_test_AF1A29AD350Q748C7EA8', response.authorization + 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) + + assert_success void + end + + def test_successful_void_with_metadata + response = stub_comms(@gateway, :ssl_request) do + options = { + metadata: { + coupon_code: 'NY2018', + partner_id: '123989' + } + } + @gateway.authorize(@amount, @credit_card, options) + 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) + + assert_success response + assert_equal 'pay_fj3xswqe3emuxckocjx6td73ni', response.authorization + + void = stub_comms(@gateway, :ssl_request) do @gateway.void(response.authorization) end.respond_with(successful_void_response) @@ -151,22 +829,175 @@ def test_successful_void end def test_failed_void - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.void('5d53a33d960c46d00f5dc061947d998c') end.respond_with(failed_void_response) - assert_failure response end + def test_successfully_passes_fund_type_and_fields + options = { + funds_transfer_type: 'FD', + source_type: 'currency_account', + source_id: 'ca_spwmped4qmqenai7hcghquqle4', + account_holder_type: 'individual', + metadata: { transaction_token: '123' } + } + response = stub_comms(@gateway, :ssl_request) do + @gateway.credit(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['instruction']['funds_transfer_type'], options[:funds_transfer_type] + assert_equal request['source']['type'], options[:source_type] + assert_equal request['source']['id'], options[:source_id] + assert_equal request['destination']['account_holder']['type'], options[:account_holder_type] + assert_equal request['destination']['account_holder']['first_name'], @credit_card.first_name + assert_equal request['destination']['account_holder']['last_name'], @credit_card.last_name + assert_equal request['metadata']['transaction_token'], '123' + end.respond_with(successful_credit_response) + assert_success response + end + + def test_successful_money_transfer_payout_via_credit + options = { + instruction_purpose: 'leisure', + account_holder_type: 'individual', + billing_address: address, + payout: true, + destination: { + account_holder: { + phone: { + number: '9108675309', + country_code: '1' + }, + identification: { + type: 'passport', + number: '1234567890' + }, + email: 'too_many_fields@checkout.com', + date_of_birth: '2004-10-27', + country_of_birth: 'US' + } + }, + sender: { + type: 'individual', + first_name: 'Jane', + middle_name: 'Middle', + last_name: 'Doe', + reference: '012345', + reference_type: 'other', + source_of_funds: 'debit', + identification: { + type: 'passport', + number: '0987654321', + issuing_country: 'US', + date_of_expiry: '2027-07-07' + }, + address: { + address1: '205 Main St', + address2: 'Apt G', + city: 'Winchestertonfieldville', + state: 'IA', + country: 'US', + zip: '12345' + }, + date_of_birth: '2004-10-27', + country_of_birth: 'US', + nationality: 'US' + } + } + response = stub_comms(@gateway, :ssl_request) do + @gateway.credit(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['instruction']['purpose'], 'leisure' + assert_equal request['destination']['account_holder']['phone']['number'], '9108675309' + assert_equal request['destination']['account_holder']['phone']['country_code'], '1' + assert_equal request['destination']['account_holder']['identification']['number'], '1234567890' + assert_equal request['destination']['account_holder']['identification']['type'], 'passport' + assert_equal request['destination']['account_holder']['email'], 'too_many_fields@checkout.com' + assert_equal request['destination']['account_holder']['date_of_birth'], '2004-10-27' + assert_equal request['destination']['account_holder']['country_of_birth'], 'US' + assert_equal request['sender']['type'], 'individual' + assert_equal request['sender']['first_name'], 'Jane' + assert_equal request['sender']['middle_name'], 'Middle' + assert_equal request['sender']['last_name'], 'Doe' + assert_equal request['sender']['reference'], '012345' + assert_equal request['sender']['reference_type'], 'other' + assert_equal request['sender']['source_of_funds'], 'debit' + assert_equal request['sender']['identification']['type'], 'passport' + assert_equal request['sender']['identification']['number'], '0987654321' + assert_equal request['sender']['identification']['issuing_country'], 'US' + assert_equal request['sender']['identification']['date_of_expiry'], '2027-07-07' + assert_equal request['sender']['address']['address_line1'], '205 Main St' + assert_equal request['sender']['address']['address_line2'], 'Apt G' + assert_equal request['sender']['address']['city'], 'Winchestertonfieldville' + assert_equal request['sender']['address']['state'], 'IA' + assert_equal request['sender']['address']['country'], 'US' + assert_equal request['sender']['address']['zip'], '12345' + assert_equal request['sender']['date_of_birth'], '2004-10-27' + assert_equal request['sender']['nationality'], 'US' + end.respond_with(successful_credit_response) + assert_success response + end + + def test_transaction_successfully_reverts_to_regular_credit_when_payout_is_nil + options = { + instruction_purpose: 'leisure', + account_holder_type: 'individual', + billing_address: address, + payout: nil, + destination: { + account_holder: { + email: 'too_many_fields@checkout.com' + } + }, + sender: { + type: 'individual' + } + } + response = stub_comms(@gateway, :ssl_request) do + @gateway.credit(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + refute_includes data, 'email' + refute_includes data, 'sender' + end.respond_with(successful_credit_response) + assert_success response + end + def test_successful_refund - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card) end.respond_with(successful_purchase_response) assert_success response - assert_equal 'charge_test_941CA9CE174U76BD29C8', response.authorization + 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) + + assert_success refund + end + + def test_successful_refund_with_metadata + response = stub_comms(@gateway, :ssl_request) do + options = { + metadata: { + coupon_code: 'NY2018', + partner_id: '123989' + } + } + @gateway.authorize(@amount, @credit_card, options) + 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) + + assert_success response + assert_equal 'pay_bgv5tmah6fmuzcmcrcro6exe6m', response.authorization + + refund = stub_comms(@gateway, :ssl_request) do @gateway.refund(@amount, response.authorization) end.respond_with(successful_refund_response) @@ -174,7 +1005,7 @@ def test_successful_refund 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) @@ -182,106 +1013,312 @@ 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_authorize_response, failed_void_response) + end.respond_with(successful_verify_response) assert_success response assert_equal 'Succeeded', response.message end def test_failed_verify - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.verify(@credit_card) - end.respond_with(failed_authorize_response, successful_void_response) + end.respond_with(failed_verify_response) assert_failure response - assert_equal 'Invalid Card Number', response.message + assert_equal 'request_invalid: card_number_invalid', response.message + end + + def test_successful_store + stub_comms(@gateway, :ssl_request) do + @gateway.store(@credit_card) + end.check_request do |_method, endpoint, data, _headers| + if /tokens/.match?(endpoint) + assert_match(%r{"type":"card"}, data) + assert_match(%r{"number":"4242424242424242"}, data) + assert_match(%r{"cvv":"123"}, data) + assert_match('/tokens', endpoint) + elsif /instruments/.match?(endpoint) + assert_match(%r{"type":"token"}, data) + assert_match(%r{"token":"tok_}, data) + end + end.respond_with(succesful_token_response, succesful_store_response) + end + + def test_successful_tokenize + stub_comms(@gateway, :ssl_request) do + @gateway.send(:tokenize, @credit_card) + end.check_request do |_action, endpoint, data, _headers| + assert_match(%r{"type":"card"}, data) + assert_match(%r{"number":"4242424242424242"}, data) + assert_match(%r{"cvv":"123"}, data) + assert_match('/tokens', endpoint) + end.respond_with(succesful_token_response) end def test_transcript_scrubbing assert_equal post_scrubbed, @gateway.scrub(pre_scrubbed) end + def test_network_transaction_scrubbing + assert_equal network_transaction_post_scrubbed, @gateway.scrub(network_transaction_pre_scrubbed) + end + def test_invalid_json - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card) end.respond_with(invalid_json_response) assert_failure response - assert_match %r{Invalid JSON response}, response.message + assert_match %r{Invalid JSON response received from Checkout.com Unified Payments Gateway. Please contact Checkout.com if you continue to receive this message.}, response.message 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) assert_failure response - assert_match(/70000: 70077/, response.error_code) + assert_match(/request_invalid: card_expired/, response.error_code) + end + + def test_error_type_without_error_code_returned + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card) + end.respond_with(error_type_without_error_codes_response) + + assert_failure response + assert_match(/request_invalid/, response.error_code) + end + + def test_4xx_error_message + @gateway.expects(:ssl_request).raises(error_4xx_response) + + assert response = @gateway.purchase(@amount, @credit_card) + + assert_failure response + assert_match(/401: Unauthorized/, response.message) end def test_supported_countries - assert_equal ['AD', 'AE', 'AT', 'BE', 'BG', 'CH', 'CY', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FO', 'FI', 'FR', 'GB', 'GI', 'GL', 'GR', 'HR', 'HU', 'IE', 'IS', 'IL', 'IT', 'LI', 'LT', 'LU', 'LV', 'MC', 'MT', 'NL', 'NO', 'PL', 'PT', 'RO', 'SE', 'SI', 'SM', 'SK', 'SJ', 'TR', 'VA'], @gateway.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 + + def test_authorize_with_level_2_3_data + response = stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, @lvl_2_3_options) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request.dig('customer', 'tax_number'), 123456 + assert_equal request.dig('processing', 'order_id'), 12462 + assert_equal request.dig('processing', 'tax_amount'), 30 + assert_equal request.dig('processing', 'discount_amount'), 10 + assert_equal request.dig('processing', 'shipping_amount'), 20 + assert_equal request.dig('processing', 'duty_amount'), 5 + assert_equal request.dig('shipping', 'from_address_zip'), 12345 + + item_one = request['items'][0] + item_two = request['items'][1] + + assert_equal item_one['reference'], 'glass123' + assert_equal item_one['name'], 'glass' + assert_equal item_one['quantity'], 1 + assert_equal item_one['unit_price'], 200 + assert_equal item_one['tax_amount'], 12 + assert_equal item_one['discount_amount'], 12 + assert_equal item_one['total_amount'], 200 + assert_equal item_one['commodity_code'], 123 + assert_equal item_one['unit_of_measure'], 'Centimeters' + + assert_equal item_two['reference'], 'water123' + assert_equal item_two['name'], 'water' + assert_equal item_two['quantity'], 2 + assert_equal item_two['unit_price'], 100 + assert_equal item_two['tax_amount'], 6 + assert_equal item_two['discount_amount'], 6 + assert_equal item_two['total_amount'], 100 + assert_equal item_two['commodity_code'], 456 + assert_equal item_two['unit_of_measure'], 'Liters' + end.respond_with(successful_authorize_response) + + assert_success response + assert_equal 'Succeeded', response.message + assert_equal 'pay_fj3xswqe3emuxckocjx6td73ni', response.authorization + end + + def test_capture_with_level_2_3_data + response = stub_comms(@gateway, :ssl_request) do + @gateway.capture(@amount, 'some_value', @lvl_2_3_options) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request.dig('customer', 'tax_number'), 123456 + assert_equal request.dig('processing', 'order_id'), 12462 + assert_equal request.dig('processing', 'tax_amount'), 30 + assert_equal request.dig('processing', 'discount_amount'), 10 + assert_equal request.dig('processing', 'duty_amount'), 5 + assert_equal request.dig('processing', 'shipping_amount'), 20 + assert_equal request.dig('shipping', 'from_address_zip'), 12345 + + item_one = request['items'][0] + item_two = request['items'][1] + + assert_equal item_one['name'], 'glass' + assert_equal item_one['quantity'], 1 + assert_equal item_one['unit_price'], 200 + assert_equal item_one['reference'], 'glass123' + assert_equal item_one['commodity_code'], 123 + assert_equal item_one['unit_of_measure'], 'Centimeters' + assert_equal item_one['total_amount'], 200 + assert_equal item_one['tax_amount'], 12 + assert_equal item_one['discount_amount'], 12 + + assert_equal item_two['reference'], 'water123' + assert_equal item_two['name'], 'water' + assert_equal item_two['quantity'], 2 + assert_equal item_two['unit_price'], 100 + assert_equal item_two['tax_amount'], 6 + assert_equal item_two['discount_amount'], 6 + assert_equal item_two['total_amount'], 100 + assert_equal item_two['commodity_code'], 456 + assert_equal item_two['unit_of_measure'], 'Liters' + end.respond_with(successful_capture_response) + + assert_success response + assert_equal 'Succeeded', response.message end private def pre_scrubbed %q( - <- "POST /v2/charges/card HTTP/1.1\r\nContent-Type: application/json;charset=UTF-8\r\nAuthorization: sk_test_ab12301d-e432-4ea7-97d1-569809518aaf\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: api2.checkout.com\r\nContent-Length: 346\r\n\r\n" - <- "{\"autoCapture\":\"n\",\"value\":\"200\",\"trackId\":\"1\",\"currency\":\"USD\",\"card\":{\"name\":\"Longbob Longsen\",\"number\":\"4242424242424242\",\"cvv\":\"100\",\"expiryYear\":\"2018\" + <- "POST /payments HTTP/1.1\r\nContent-Type: application/json;charset=UTF-8\r\nAuthorization: sk_test_ab12301d-e432-4ea7-97d1-569809518aaf\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: api.checkout.com\r\nContent-Length: 346\r\n\r\n" + <- "{\"capture\":false,\"amount\":\"200\",\"reference\":\"1\",\"currency\":\"USD\",\"source\":{\"type\":\"card\",\"name\":\"Longbob Longsen\",\"number\":\"4242424242424242\",\"cvv\":\"100\",\"expiry_year\":\"2025\" + ) + end + + def network_transaction_pre_scrubbed + %q( + <- "POST /payments HTTP/1.1\r\nContent-Type: application/json;charset=UTF-8\r\nAuthorization: sk_test_ab12301d-e432-4ea7-97d1-569809518aaf\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: api.checkout.com\r\nContent-Length: 346\r\n\r\n" + <- "{\"amount\":\"100\",\"reference\":\"1\",\"currency\":\"USD\",\"metadata\":{\"udf5\":\"ActiveMerchant\"},\"source\":{\"type\":\"network_token\",\"token\":\"4242424242424242\",\"token_type\":\"applepay\",\"cryptogram\":\"AgAAAAAAAIR8CQrXcIhbQAAAAAA\",\"eci\":\"05\",\"expiry_year\":\"2025\",\"expiry_month\":\"10\",\"billing_address\":{\"address_line1\":\"456 My Street\",\"address_line2\":\"Apt 1\",\"city\":\"Ottawa\",\"state\":\"ON\",\"country\":\"CA\",\"zip\":\"K1C2N6\"}},\"customer\":{\"email\":\"longbob.longsen@example.com\"}}" + ) + end + + def network_transaction_post_scrubbed + %q( + <- "POST /payments HTTP/1.1\r\nContent-Type: application/json;charset=UTF-8\r\nAuthorization: [FILTERED]\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: api.checkout.com\r\nContent-Length: 346\r\n\r\n" + <- "{\"amount\":\"100\",\"reference\":\"1\",\"currency\":\"USD\",\"metadata\":{\"udf5\":\"ActiveMerchant\"},\"source\":{\"type\":\"network_token\",\"token\":\"[FILTERED]\",\"token_type\":\"applepay\",\"cryptogram\":\"[FILTERED]\",\"eci\":\"05\",\"expiry_year\":\"2025\",\"expiry_month\":\"10\",\"billing_address\":{\"address_line1\":\"456 My Street\",\"address_line2\":\"Apt 1\",\"city\":\"Ottawa\",\"state\":\"ON\",\"country\":\"CA\",\"zip\":\"K1C2N6\"}},\"customer\":{\"email\":\"longbob.longsen@example.com\"}}" ) end def post_scrubbed %q( - <- "POST /v2/charges/card HTTP/1.1\r\nContent-Type: application/json;charset=UTF-8\r\nAuthorization: [FILTERED]\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: api2.checkout.com\r\nContent-Length: 346\r\n\r\n" - <- "{\"autoCapture\":\"n\",\"value\":\"200\",\"trackId\":\"1\",\"currency\":\"USD\",\"card\":{\"name\":\"Longbob Longsen\",\"number\":\"[FILTERED]\",\"cvv\":\"[FILTERED]\",\"expiryYear\":\"2018\" + <- "POST /payments HTTP/1.1\r\nContent-Type: application/json;charset=UTF-8\r\nAuthorization: [FILTERED]\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: api.checkout.com\r\nContent-Length: 346\r\n\r\n" + <- "{\"capture\":false,\"amount\":\"200\",\"reference\":\"1\",\"currency\":\"USD\",\"source\":{\"type\":\"card\",\"name\":\"Longbob Longsen\",\"number\":\"[FILTERED]\",\"cvv\":\"[FILTERED]\",\"expiry_year\":\"2025\" + ) + 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":"charge_test_941CA9CE174U76BD29C8", - "liveMode":false, - "created":"2015-05-27T20:45:58Z", - "value":200.0, - "currency":"USD", - "trackId":"1", - "description":null, - "email":"longbob.longsen@gmail.com", - "chargeMode":1, - "transactionIndicator":1, - "customerIp":null, - "responseMessage":"Approved", - "responseAdvancedInfo":"Approved", - "responseCode":"10000", - "card": { - "cvvCheck":"Y", - "avsCheck":"S" - } - } + {"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' + purchase_response.to_json + end + + def successful_purchase_initial_stored_credential_response + %( + {"id":"pay_7jcf4ovmwnqedhtldca3fjli2y","action_id":"act_7jcf4ovmwnqedhtldca3fjli2y","amount":200,"currency":"USD","approved":true,"status":"Authorized","auth_code":"587541","eci":"05","scheme_id":"776561034288791","response_code":"10000","response_summary":"Approved","risk":{"flagged":false},"source":{"id":"src_m2ooveyd2dxuzh277ft4obgkwm","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_tr53e5z2dlmetpo2ehbsuk76yu","email":"longbob.longsen@example.com","name":"Longbob Longsen"},"processed_on":"2021-03-29T20:22:48Z","reference":"1","processing":{"acquirer_transaction_id":"8266949399","retrieval_reference_number":"731420439000"},"_links":{"self":{"href":"https://api.sandbox.checkout.com/payments/pay_7jcf4ovmwnqedhtldca3fjli2y"},"actions":{"href":"https://api.sandbox.checkout.com/payments/pay_7jcf4ovmwnqedhtldca3fjli2y/actions"},"capture":{"href":"https://api.sandbox.checkout.com/payments/pay_7jcf4ovmwnqedhtldca3fjli2y/captures"},"void":{"href":"https://api.sandbox.checkout.com/payments/pay_7jcf4ovmwnqedhtldca3fjli2y/voids"}}} + ) + end + + def successful_purchase_using_stored_credential_response + %( + {"id":"pay_udodtu4ogljupp2jvy2cxf4jme","action_id":"act_udodtu4ogljupp2jvy2cxf4jme","amount":200,"currency":"USD","approved":true,"status":"Authorized","auth_code":"680745","eci":"05","scheme_id":"491049486700108","response_code":"10000","response_summary":"Approved","risk":{"flagged":false},"source":{"id":"src_m2ooveyd2dxuzh277ft4obgkwm","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_tr53e5z2dlmetpo2ehbsuk76yu","email":"longbob.longsen@example.com","name":"Longbob Longsen"},"processed_on":"2021-03-29T20:22:49Z","reference":"1","processing":{"acquirer_transaction_id":"4026777708","retrieval_reference_number":"633985559433"},"_links":{"self":{"href":"https://api.sandbox.checkout.com/payments/pay_udodtu4ogljupp2jvy2cxf4jme"},"actions":{"href":"https://api.sandbox.checkout.com/payments/pay_udodtu4ogljupp2jvy2cxf4jme/actions"},"capture":{"href":"https://api.sandbox.checkout.com/payments/pay_udodtu4ogljupp2jvy2cxf4jme/captures"},"void":{"href":"https://api.sandbox.checkout.com/payments/pay_udodtu4ogljupp2jvy2cxf4jme/voids"}}} ) end def failed_purchase_response %( { - "id":"charge_test_941CA9CE174U76BD29C8", - "liveMode":false, - "created":"2015-05-27T20:45:58Z", - "value":200.0, + "id":"pay_awjzhfj776gulbp2nuslj4agbu", + "amount":200, "currency":"USD", - "trackId":"1", - "description":null, - "email":"longbob.longsen@gmail.com", - "chargeMode":1, - "transactionIndicator":1, - "customerIp":null, - "responseMessage":"Invalid Card Number", - "responseAdvancedInfo":"If credit card number contains characters other digits, or bank does not recognize this number as a valid credit card number", - "responseCode":"20014", - "card": { + "reference":"1", + "response_summary": "Invalid Card Number", + "response_code":"20014", + "customer": { + "id": "cus_zvnv7gsblfjuxppycd7bx4erue", + "email": "longbob.longsen@example.com", + "name": "Sarah Mitchell" + }, + "source": { "cvvCheck":"Y", "avsCheck":"S" } @@ -289,68 +1326,136 @@ 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 %( - { - "id":"charge_test_AF1A29AD350Q748C7EA8", - "liveMode":false, - "created":"2017-11-13T14:05:27Z", - "value":200, - "currency":"USD", - "trackId":"1", - "description":null, - "email":"longbob.longsen@example.com", - "chargeMode":1, - "transactionIndicator":1, - "customerIp":null, - "responseMessage":"Approved", - "responseAdvancedInfo":"Approved", - "responseCode":"10000", - "status":"Authorised", - "authCode":"923189", - "isCascaded":false, - "autoCapture":"N", - "autoCapTime":0.0, - "card":{"customerId": - "cust_12DCEB24-ACEA-48AB-BEF2-35A3C09BE581", - "expiryMonth":"06", - "expiryYear":"2018", - "billingDetails":{ - "addressLine1":"456 My Street", - "addressLine2":"Apt 1", - "postcode":"K1C2N6", - "country":"CA", - "city":"Ottawa", - "state":"ON", - "phone":{"number":"(555)555-5555"} - }, - "id":"card_CFA314F4-388D-4CF4-BE6F-940D894C9E64", - "last4":"4242", - "bin":"424242", - "paymentMethod":"Visa", - "fingerprint":"F639CAB2745BEE4140BF86DF6B6D6E255C5945AAC3788D923FA047EA4C208622", - "name":"Longbob Longsen", - "cvvCheck":"Y", - "avsCheck":"S" + { + "id": "pay_fj3xswqe3emuxckocjx6td73ni", + "action_id": "act_fj3xswqe3emuxckocjx6td73ni", + "amount": 200, + "currency": "USD", + "approved": true, + "status": "Authorized", + "auth_code": "858188", + "eci": "05", + "scheme_id": "638284745624527", + "response_code": "10000", + "response_summary": "Approved", + "risk": { + "flagged": false + }, + "source": { + "id": "src_nq6m5dqvxmsunhtzf7adymbq3i", + "type": "card", + "expiry_month": 8, + "expiry_year": 2025, + "name": "Sarah Mitchell", + "scheme": "Visa", + "last4": "4242", + "fingerprint": "5CD3B9CB15338683110959D165562D23084E1FF564F420FE9A990DF0BCD093FC", + "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" }, - "riskCheck":true, - "customerPaymentPlans":null, - "metadata":{}, - "shippingDetails":{ - "addressLine1":null, - "addressLine2":null, - "postcode":null, - "country":null, - "city":null, - "state":null, - "phone":{} + "customer": { + "id": "cus_ssxcidkqvfde7lfn5n7xzmgv2a", + "email": "longbob.longsen@example.com", + "name": "Sarah Mitchell" }, - "products":[], - "udf1":null, - "udf2":null, - "udf3":null, - "udf4":null, - "udf5":null + "processed_on": "2019-03-24T10:14:32Z", + "reference": "ORD-5023-4E89", + "_links": { + "self": { + "href": "https://api.sandbox.checkout.com/payments/pay_fj3xswqe3emuxckocjx6td73ni" + }, + "actions": { + "href": "https://api.sandbox.checkout.com/payments/pay_fj3xswqe3emuxckocjx6td73ni/actions" + }, + "capture": { + "href": "https://api.sandbox.checkout.com/payments/pay_fj3xswqe3emuxckocjx6td73ni/captures" + }, + "void": { + "href": "https://api.sandbox.checkout.com/payments/pay_fj3xswqe3emuxckocjx6td73ni/voids" + } + } } ) end @@ -358,126 +1463,185 @@ def successful_authorize_response def failed_authorize_response %( { - "id":"charge_test_941CA9CE174U76BD29C8", - "liveMode":false, - "created":"2015-05-27T20:45:58Z", - "value":200.0, + "id":"pay_awjzhfj776gulbp2nuslj4agbu", + "amount":200, "currency":"USD", - "trackId":"1", - "description":null, - "email":"longbob.longsen@gmail.com", - "chargeMode":1, - "transactionIndicator":1, - "customerIp":null, - "responseMessage":"Invalid Card Number", - "responseAdvancedInfo":"If credit card number contains characters other digits, or bank does not recognize this number as a valid credit card number", - "responseCode":"20014" + "reference":"1", + "customer": { + "id": "cus_zvnv7gsblfjuxppycd7bx4erue", + "email": "longbob.longsen@example.com", + "name": "Sarah Mitchell" + }, + "response_summary": "Invalid Card Number", + "response_code":"20014" } ) end - def successful_capture_response + def successful_incremental_authorize_response %( - { - "id":"charge_test_941CA9CE174U76BD29C8", - "liveMode":false, - "created":"2015-05-27T20:45:58Z", - "value":200.0, - "currency":"USD", - "trackId":"1", - "description":null, - "email":"longbob.longsen@gmail.com", - "chargeMode":1, - "transactionIndicator":1, - "customerIp":null, - "responseMessage":"Captured", - "responseAdvancedInfo":"Captured", - "responseCode":"10000" + { + "action_id": "act_q4dbxom5jbgudnjzjpz7j2z6uq", + "amount": 50, + "currency": "USD", + "approved": true, + "status": "Authorized", + "auth_code": "503198", + "expires_on": "2020-04-20T10:11:12Z", + "eci": "05", + "scheme_id": "511129554406717", + "response_code": "10000", + "response_summary": "Approved", + "balances": { + "total_authorized": 150, + "total_voided": 0, + "available_to_void": 150, + "total_captured": 0, + "available_to_capture": 150, + "total_refunded": 0, + "available_to_refund": 0 + }, + "processed_on": "2020-03-16T22:11:24Z", + "reference": "ORD-752-814", + "processing": { + "acquirer_transaction_id": "8367314942", + "retrieval_reference_number": "162588399162" + }, + "_links": { + "self": { + "href": "https://api.sandbox.checkout.com/payments/pay_tqgk5c6k2nnexagtcuom5ktlua" + }, + "actions": { + "href": "https://api.sandbox.checkout.com/payments/pay_tqgk5c6k2nnexagtcuom5ktlua/actions" + }, + "authorize": { + "href": "https://api.sandbox.checkout.com/payments/pay_tqgk5c6k2nnexagtcuom5ktlua/authorizations" + }, + "capture": { + "href": "https://api.sandbox.checkout.com/payments/pay_tqgk5c6k2nnexagtcuom5ktlua/captures" + }, + "void": { + "href": "https://api.sandbox.checkout.com/payments/pay_tqgk5c6k2nnexagtcuom5ktlua/voids" + } + } } ) end - def failed_capture_response + def successful_capture_response %( { - "errorCode":"405", - "message":"You tried to access the endpoint with an invalid method", + "action_id": "act_2f56bhkau5dubequbv5aa6w4qi", + "reference": "1" } ) end + def failed_capture_response + %( + ) + end + def successful_refund_response %( - { - "id":"charge_test_941CA9CE174U76BD29C8", - "liveMode":false, - "created":"2015-05-27T20:45:58Z", - "value":200.0, - "currency":"USD", - "trackId":"1", - "description":null, - "email":"longbob.longsen@gmail.com", - "chargeMode":1, - "transactionIndicator":1, - "customerIp":null, - "responseMessage":"Refunded", - "responseAdvancedInfo":"Refunded", - "responseCode":"10000" - } + { + "action_id": "act_2f56bhkau5dubequbv5aa6w4qi", + "reference": "1" + } ) end def failed_refund_response + %( + ) + end + + def successful_void_response %( { - "errorCode":"405", - "message":"You tried to access the endpoint with an invalid method", + "action_id": "act_2f56bhkau5dubequbv5aa6w4qi", + "reference": "1" } ) end - def successful_void_response + def successful_credit_response %( - { - "id":"charge_test_941CA9CE174U76BD29C8", - "liveMode":false, - "created":"2015-05-27T20:45:58Z", - "value":200.0, - "currency":"USD", - "trackId":"1", - "description":null, - "email":"longbob.longsen@gmail.com", - "chargeMode":1, - "transactionIndicator":1, - "customerIp":null, - "responseMessage":"Voided", - "responseAdvancedInfo":"Voided", - "responseCode":"10000" + { + "id": "pay_jhzh3u7vxcgezlcek7ymzyy6be", + "status": "Pending", + "reference": "ORD-5023-4E89", + "instruction": { + "value_date": "2022-08-09T06:11:37.2306547+00:00" + }, + "_links": { + "self": { + "href": "https://api.sandbox.checkout.com/payments/pay_jhzh3u7vxcgezlcek7ymzyy6be" + }, + "actions": { + "href": "https://api.sandbox.checkout.com/payments/pay_jhzh3u7vxcgezlcek7ymzyy6be/actions" + } } + } ) end def failed_void_response %( - { - "errorCode":"405", - "message":"You tried to access the endpoint with an invalid method", - } ) end def invalid_json_response %( { - "id": "charge_test_123456", + "id": "pay_123", ) end def error_code_response %( { - "eventId":"1b206f69-b4db-4259-9713-b72dfe0f19da","errorCode":"70000","message":"Validation error","errorMessageCodes":["70077"],"errors":["Expired Card"] + "request_id": "e5a3ce6f-a4e9-4445-9ec7-e5975e9a6213","error_type": "request_invalid","error_codes": ["card_expired"] + } + ) + end + + def error_type_without_error_codes_response + %( + { + "request_id": "e5a3ce6f-a4e9-4445-9ec7-e5975e9a6213","error_type": "request_invalid" } ) end + + def error_4xx_response + mock_response = Net::HTTPUnauthorized.new('1.1', '401', 'Unauthorized') + mock_response.stubs(:body).returns('') + + ActiveMerchant::ResponseError.new(mock_response) + end + + def successful_verify_payment_response + %( + {"id":"pay_tkvif5mf54eerhd3ysuawfcnt4","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":"Authorized","approved":true,"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":"10000","response_summary":"Approved"}],"_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 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"}}} + ) + end + + def successful_verify_response + %({"id":"pay_ij6bctwxpzdulm53xyksio7gm4","action_id":"act_ij6bctwxpzdulm53xyksio7gm4","amount":0,"currency":"USD","approved":true,"status":"Card Verified","auth_code":"881790","eci":"05","scheme_id":"305756859646779","response_code":"10000","response_summary":"Approved","risk":{"flagged":false},"source":{"id":"src_nica37p5k7aufhs3rsv2te7xye","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_r2yb7f2upmsuhm6nbruoqn657y","email":"longbob.longsen@example.com","name":"Longbob Longsen"},"processed_on":"2020-09-18T18:17:45Z","reference":"1","processing":{"acquirer_transaction_id":"4932795322","retrieval_reference_number":"954188232380"},"_links":{"self":{"href":"https://api.sandbox.checkout.com/payments/pay_ij6bctwxpzdulm53xyksio7gm4"},"actions":{"href":"https://api.sandbox.checkout.com/payments/pay_ij6bctwxpzdulm53xyksio7gm4/actions"}}}) + end + + def failed_verify_response + %({"request_id":"911829c3-519a-47e8-bbc1-17337789fda0","error_type":"request_invalid","error_codes":["card_number_invalid"]}) + end end diff --git a/test/unit/gateways/citrus_pay_test.rb b/test/unit/gateways/citrus_pay_test.rb index 40b126e3782..bc39c2034bd 100644 --- a/test/unit/gateways/citrus_pay_test.rb +++ b/test/unit/gateways/citrus_pay_test.rb @@ -48,7 +48,7 @@ def test_authorize_and_capture capture = stub_comms(@gateway, :ssl_request) do @gateway.capture(@amount, response.authorization) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/f3d100a7-18d9-4609-aabc-8a710ad0e210/, data) end.respond_with(successful_capture_response) @@ -65,7 +65,7 @@ def test_refund refund = stub_comms(@gateway, :ssl_request) do @gateway.refund(@amount, response.authorization) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/ce61e06e-8c92-4a0f-a491-6eb473d883dd/, data) end.respond_with(successful_refund_response) @@ -82,7 +82,7 @@ def test_void void = stub_comms(@gateway, :ssl_request) do @gateway.void(response.authorization) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/ce61e06e-8c92-4a0f-a491-6eb473d883dd/, data) end.respond_with(successful_void_response) @@ -91,16 +91,16 @@ def test_void def test_passing_alpha3_country_code stub_comms(@gateway, :ssl_request) do - @gateway.authorize(@amount, @credit_card, :billing_address => {country: 'US'}) - end.check_request do |method, endpoint, data, headers| + @gateway.authorize(@amount, @credit_card, billing_address: { country: 'US' }) + end.check_request do |_method, _endpoint, data, _headers| assert_match(/USA/, data) end.respond_with(successful_authorize_response) end def test_non_existent_country stub_comms(@gateway, :ssl_request) do - @gateway.authorize(@amount, @credit_card, :billing_address => {country: 'Blah'}) - end.check_request do |method, endpoint, data, headers| + @gateway.authorize(@amount, @credit_card, billing_address: { country: 'Blah' }) + end.check_request do |_method, _endpoint, data, _headers| assert_match(/"country":null/, data) end.respond_with(successful_authorize_response) end @@ -108,15 +108,15 @@ def test_non_existent_country def test_passing_cvv stub_comms(@gateway, :ssl_request) do @gateway.authorize(@amount, @credit_card) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/#{@credit_card.verification_value}/, data) end.respond_with(successful_authorize_response) end def test_passing_billing_address stub_comms(@gateway, :ssl_request) do - @gateway.authorize(@amount, @credit_card, :billing_address => address) - end.check_request do |method, endpoint, data, headers| + @gateway.authorize(@amount, @credit_card, billing_address: address) + end.check_request do |_method, _endpoint, data, _headers| parsed = JSON.parse(data) assert_equal('456 My Street', parsed['billing']['address']['street']) assert_equal('K1C2N6', parsed['billing']['address']['postcodeZip']) @@ -125,8 +125,8 @@ def test_passing_billing_address def test_passing_shipping_name stub_comms(@gateway, :ssl_request) do - @gateway.authorize(@amount, @credit_card, :shipping_address => address) - end.check_request do |method, endpoint, data, headers| + @gateway.authorize(@amount, @credit_card, shipping_address: address) + end.check_request do |_method, _endpoint, data, _headers| parsed = JSON.parse(data) assert_equal('Jim', parsed['shipping']['firstName']) assert_equal('Smith', parsed['shipping']['lastName']) @@ -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', @@ -166,24 +166,8 @@ 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.check_request do |_method, endpoint, _data, _headers| + assert_match(/secure.uat.tnspayments.com/, endpoint) end.respond_with(successful_capture_response) assert_success response diff --git a/test/unit/gateways/clearhaus_test.rb b/test/unit/gateways/clearhaus_test.rb index a30a5b3894c..b5d74ff46ec 100644 --- a/test/unit/gateways/clearhaus_test.rb +++ b/test/unit/gateways/clearhaus_test.rb @@ -55,8 +55,8 @@ def test_successful_authorize_with_threed response = @gateway.authorize(@amount, @credit_card, @options.merge(pares: '123')) assert_success response assert response.test? - end.check_request do |endpoint, data, headers| - expr = { threed_secure: { pares: '123' } }.to_query + end.check_request do |_endpoint, data, _headers| + expr = { card: { pares: '123' } }.to_query assert_match expr, data end.respond_with(successful_authorize_response) end @@ -66,9 +66,9 @@ def test_additional_params response = @gateway.authorize(@amount, @credit_card, @options.merge(order_id: '123', text_on_statement: 'test')) assert_success response assert response.test? - end.check_request do |endpoint, data, headers| - order_expr = { reference: '123'}.to_query - tos_expr = { text_on_statement: 'test'}.to_query + end.check_request do |_endpoint, data, _headers| + order_expr = { reference: '123' }.to_query + tos_expr = { text_on_statement: 'test' }.to_query assert_match order_expr, data assert_match tos_expr, data @@ -82,7 +82,7 @@ def test_successful_authorize_with_card assert_equal '84412a34-fa29-4369-a098-0165a80e8fda', response.authorization assert response.test? - end.check_request do |endpoint, data, headers| + end.check_request do |endpoint, _data, _headers| assert_match %r{/cards/4110/authorizations}, endpoint end.respond_with(successful_authorize_response) end @@ -153,7 +153,7 @@ def test_successful_void def test_failed_void @gateway.expects(:ssl_post).returns(failed_void_response) - response = @gateway.void( @credit_card, @options) + response = @gateway.void(@credit_card, @options) assert_failure response assert_equal 40000, response.error_code @@ -222,10 +222,10 @@ def test_signing_request assert_equal '84412a34-fa29-4369-a098-0165a80e8fda', response.authorization assert response.test? - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, _data, headers| assert headers['Signature'] assert_match %r{7e51b92e-ca7e-48e3-8a96-7d66cf1f2da2 RS256-hex}, headers['Signature'] - assert_match %r{02f56ed1f6c60cdefd$}, headers['Signature'] + assert_match %r{25f8283c3cc43911d7$}, headers['Signature'] end.respond_with(successful_authorize_response) end @@ -241,15 +241,15 @@ def test_cleans_whitespace_from_private_key assert_equal '84412a34-fa29-4369-a098-0165a80e8fda', response.authorization assert response.test? - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, _data, headers| assert headers['Signature'] assert_match %r{7e51b92e-ca7e-48e3-8a96-7d66cf1f2da2 RS256-hex}, headers['Signature'] - assert_match %r{02f56ed1f6c60cdefd$}, headers['Signature'] + assert_match %r{25f8283c3cc43911d7$}, headers['Signature'] end.respond_with(successful_authorize_response) end def test_unsuccessful_signing_request_with_invalid_key - gateway = ClearhausGateway.new(api_key: 'test_key', signing_key: @test_signing_key, private_key: 'foo') + gateway = ClearhausGateway.new(api_key: 'test_key', signing_key: @test_signing_key, private_key: 'foo') # stub actual network access, but this shouldn't be reached gateway.stubs(:ssl_post).returns(nil) @@ -266,6 +266,14 @@ def test_scrub assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed end + def test_nonfractional_currency_handling + stub_comms do + @gateway.authorize(200, @credit_card, @options.merge(currency: 'JPY')) + end.check_request do |_endpoint, data, _headers| + assert_match(/amount=2&card/, data) + end.respond_with(successful_authorize_response) + end + private def pre_scrubbed @@ -275,7 +283,7 @@ def pre_scrubbed starting SSL for gateway.test.clearhaus.com:443... SSL established <- "POST /authorizations HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAuthorization: Basic NTI2Y2Y1NjQtMTE5Yy00YmI2LTljZjgtMDAxNWVhYzdlNGY2Og==\r\nUser-Agent: Clearhaus ActiveMerchantBindings/1.54.0\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nConnection: close\r\nHost: gateway.test.clearhaus.com\r\nContent-Length: 128\r\n\r\n" -<- "amount=100&card%5Bcsc%5D=123&card%5Bexpire_month%5D=09&card%5Bexpire_year%5D=2016&card%5Bnumber%5D=4111111111111111¤cy=EUR" +<- "amount=100&card%5Bcsc%5D=123&card%5Bexpire_month%5D=09&card%5Bexpire_year%5D=2016&card%5Bpan%5D=4111111111111111¤cy=EUR" -> "HTTP/1.1 201 Created\r\n" -> "Content-Type: application/vnd.clearhaus-gateway.hal+json; version=0.9.0; charset=utf-8\r\n" -> "Date: Wed, 28 Oct 2015 18:56:11 GMT\r\n" @@ -318,7 +326,7 @@ def post_scrubbed starting SSL for gateway.test.clearhaus.com:443... SSL established <- "POST /authorizations HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAuthorization: Basic [FILTERED]\r\nUser-Agent: Clearhaus ActiveMerchantBindings/1.54.0\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nConnection: close\r\nHost: gateway.test.clearhaus.com\r\nContent-Length: 128\r\n\r\n" -<- "amount=100&card%5Bcsc%5D=[FILTERED]&card%5Bexpire_month%5D=09&card%5Bexpire_year%5D=2016&card%5Bnumber%5D=[FILTERED]¤cy=EUR" +<- "amount=100&card%5Bcsc%5D=[FILTERED]&card%5Bexpire_month%5D=09&card%5Bexpire_year%5D=2016&card%5Bpan%5D=[FILTERED]¤cy=EUR" -> "HTTP/1.1 201 Created\r\n" -> "Content-Type: application/vnd.clearhaus-gateway.hal+json; version=0.9.0; charset=utf-8\r\n" -> "Date: Wed, 28 Oct 2015 18:56:11 GMT\r\n" @@ -355,7 +363,7 @@ def post_scrubbed end def test_private_key - %Q{-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBALYK0zmwuYkH3YWcFNLLddx5cwDxEY7Gi1xITuQqRrU4yD3uSw+J\nWYKknb4Tbndb6iEHY+e6gIGD+49TojnNeIUCAwEAAQJARyuYRRe4kcBHdPL+mSL+\nY0IAGkAlUyKAXYXPghidKD/v/oLrFaZWALGM2clv6UoYYpPnInSgbcud4sTcfeUm\nQQIhAN2JZ2qv0WGcbIopBpwpQ5jDxMGVkmkVVUEWWABGF8+pAiEA0lySxTELZm8b\nGx9UEDRghN+Qv/OuIKFldu1Ba4f8W30CIQCaQFIBtunTTVdF28r+cLzgYW9eWwbW\npEP4TdZ4WlW6AQIhAMDCTUdeUpjxlH/87BXROORozAXocBW8bvJUI486U5ctAiAd\nInviQqJd1KTGRDmWIGrE5YACVmW2JSszD9t5VKxkAA==\n-----END RSA PRIVATE KEY-----} + "-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBALYK0zmwuYkH3YWcFNLLddx5cwDxEY7Gi1xITuQqRrU4yD3uSw+J\nWYKknb4Tbndb6iEHY+e6gIGD+49TojnNeIUCAwEAAQJARyuYRRe4kcBHdPL+mSL+\nY0IAGkAlUyKAXYXPghidKD/v/oLrFaZWALGM2clv6UoYYpPnInSgbcud4sTcfeUm\nQQIhAN2JZ2qv0WGcbIopBpwpQ5jDxMGVkmkVVUEWWABGF8+pAiEA0lySxTELZm8b\nGx9UEDRghN+Qv/OuIKFldu1Ba4f8W30CIQCaQFIBtunTTVdF28r+cLzgYW9eWwbW\npEP4TdZ4WlW6AQIhAMDCTUdeUpjxlH/87BXROORozAXocBW8bvJUI486U5ctAiAd\nInviQqJd1KTGRDmWIGrE5YACVmW2JSszD9t5VKxkAA==\n-----END RSA PRIVATE KEY-----" end def failed_purchase_response @@ -366,11 +374,11 @@ def successful_authorize_response { 'id' => '84412a34-fa29-4369-a098-0165a80e8fda', 'status' => { - 'code' => 20000 + 'code' => 20000 }, 'processed_at' => '2014-07-09T09:53:41+00:00', '_links' => { - 'captures' => { 'href' => '/authorizations/84412a34-fa29-4369-a098-0165a80e8fda/captures' } + 'captures' => { 'href' => '/authorizations/84412a34-fa29-4369-a098-0165a80e8fda/captures' } } }.to_json end @@ -381,20 +389,20 @@ def failed_authorize_response def successful_capture_response { - 'id' => 'd8e92a70-3030-4d4d-8ad2-684b230c1bed', - 'status' => { - 'code' => 20000 + 'id' => 'd8e92a70-3030-4d4d-8ad2-684b230c1bed', + 'status' => { + 'code' => 20000 + }, + 'processed_at' => '2014-07-09T11:47:28+00:00', + 'amount' => 1000, + '_links' => { + 'authorization' => { + 'href' => '/authorizations/84412a34-fa29-4369-a098-0165a80e8fda' }, - 'processed_at' => '2014-07-09T11:47:28+00:00', - 'amount' => 1000, - '_links' => { - 'authorization' => { - 'href' => '/authorizations/84412a34-fa29-4369-a098-0165a80e8fda' - }, - 'refunds' => { - 'href' => '/authorizations/84412a34-fa29-4369-a098-0165a80e8fda/refunds' - } + 'refunds' => { + 'href' => '/authorizations/84412a34-fa29-4369-a098-0165a80e8fda/refunds' } + } }.to_json end @@ -406,12 +414,12 @@ def successful_refund_response { 'id' => 'f04c0872-47ce-4683-8d8c-e154221bba14', 'status' => { - 'code' => 20000 + 'code' => 20000 }, 'processed_at' => '2014-07-09T11:57:58+00:00', 'amount' => 500, '_links' => { - 'authorization' => { 'href' => '/authorizations/84412a34-fa29-4369-a098-0165a80e8fda' } + 'authorization' => { 'href' => '/authorizations/84412a34-fa29-4369-a098-0165a80e8fda' } } }.to_json end @@ -437,8 +445,8 @@ def successful_void_response 'captures' => { 'href' => '/authorizations/77d08c40-cfa9-42e3-993d-795f772b70a4/captures' }, - 'voids' => { 'href' => '/authorizations/77d08c40-cfa9-42e3-993d-795f772b70a4/voids'}, - 'refunds' => { 'href' => '/authorizations/77d08c40-cfa9-42e3-993d-795f772b70a4/refunds'} + 'voids' => { 'href' => '/authorizations/77d08c40-cfa9-42e3-993d-795f772b70a4/voids' }, + 'refunds' => { 'href' => '/authorizations/77d08c40-cfa9-42e3-993d-795f772b70a4/refunds' } } } end @@ -451,14 +459,14 @@ def successful_store_response { 'id' => '58dabba0-e9ea-4133-8c38-bfa1028c1ed2', 'status' => { - 'code'=> 20000 + 'code' => 20000 }, 'processed_at' => '2014-07-09T12:14:31+00:00', 'last4' => '0004', 'scheme' => 'mastercard', '_links' => { - 'authorizations' => { 'href' => '/cards/58dabba0-e9ea-4133-8c38-bfa1028c1ed2/authorizations' }, - 'credits'=> { 'href' => '/cards/58dabba0-e9ea-4133-8c38-bfa1028c1ed2/credits' } + 'authorizations' => { 'href' => '/cards/58dabba0-e9ea-4133-8c38-bfa1028c1ed2/authorizations' }, + 'credits' => { 'href' => '/cards/58dabba0-e9ea-4133-8c38-bfa1028c1ed2/credits' } } }.to_json end @@ -468,7 +476,6 @@ def failed_store_response end def failed_ch_response - { 'status' => { 'code' => 40000, 'message' => 'General input error' }}.to_json + { 'status' => { 'code' => 40000, 'message' => 'General input error' } }.to_json end - end diff --git a/test/unit/gateways/commerce_hub_test.rb b/test/unit/gateways/commerce_hub_test.rb new file mode 100644 index 00000000000..ea9fe3ec72f --- /dev/null +++ b/test/unit/gateways/commerce_hub_test.rb @@ -0,0 +1,987 @@ +require 'test_helper' + +class CommerceHubTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = CommerceHubGateway.new(api_key: 'login', api_secret: 'password', merchant_id: '12345', terminal_id: '0001') + + @amount = 1204 + @credit_card = credit_card('4005550000000019', month: '02', year: '2035', verification_value: '123') + @google_pay = network_tokenization_credit_card( + '4005550000000019', + brand: 'visa', + eci: '05', + month: '02', + year: '2035', + source: :google_pay, + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + transaction_id: '13456789' + ) + @apple_pay = network_tokenization_credit_card( + '4005550000000019', + brand: 'visa', + eci: '05', + month: '02', + year: '2035', + source: :apple_pay, + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + transaction_id: '13456789' + ) + @no_supported_source = network_tokenization_credit_card( + '4005550000000019', + brand: 'visa', + eci: '05', + month: '02', + year: '2035', + source: :no_source, + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + ) + @network_token = network_tokenization_credit_card( + '4005550000000019', + brand: 'visa', + eci: '05', + month: '02', + year: '2035', + source: :network_token, + 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) + + assert_success response + end + + def test_successful_purchase_with_google_pay + response = stub_comms do + @gateway.purchase(@amount, @google_pay, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['transactionDetails']['captureFlag'], true + assert_equal request['merchantDetails']['terminalId'], @gateway.options[:terminal_id] + assert_equal request['merchantDetails']['merchantId'], @gateway.options[:merchant_id] + assert_equal request['amount']['total'], (@amount / 100.0).to_f + assert_equal request['source']['card']['cardData'], @google_pay.number + assert_equal request['source']['cavv'], @google_pay.payment_cryptogram + assert_equal request['source']['walletType'], 'GOOGLE_PAY' + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_apple_pay + response = stub_comms do + @gateway.purchase(@amount, @apple_pay, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['transactionDetails']['captureFlag'], true + assert_equal request['merchantDetails']['terminalId'], @gateway.options[:terminal_id] + assert_equal request['merchantDetails']['merchantId'], @gateway.options[:merchant_id] + assert_equal request['amount']['total'], (@amount / 100.0).to_f + assert_equal request['source']['card']['cardData'], @apple_pay.number + assert_equal request['source']['cavv'], @apple_pay.payment_cryptogram + assert_equal request['source']['walletType'], 'APPLE_PAY' + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_no_supported_source_as_apple_pay + response = stub_comms do + @gateway.purchase(@amount, @no_supported_source, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['transactionDetails']['captureFlag'], true + assert_equal request['merchantDetails']['terminalId'], @gateway.options[:terminal_id] + assert_equal request['merchantDetails']['merchantId'], @gateway.options[:merchant_id] + assert_equal request['amount']['total'], (@amount / 100.0).to_f + assert_equal request['source']['card']['cardData'], @apple_pay.number + assert_equal request['source']['cavv'], @apple_pay.payment_cryptogram + assert_equal request['source']['walletType'], 'APPLE_PAY' + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_network_token + response = stub_comms do + @gateway.purchase(@amount, @network_token, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['transactionDetails']['captureFlag'], true + assert_equal request['merchantDetails']['terminalId'], @gateway.options[:terminal_id] + assert_equal request['merchantDetails']['merchantId'], @gateway.options[:merchant_id] + assert_equal request['amount']['total'], (@amount / 100.0).to_f + assert_equal request['source']['tokenData'], @network_token.number + assert_equal request['source']['cryptogram'], @network_token.payment_cryptogram + assert_equal request['source']['tokenSource'], 'NETWORK_TOKEN' + assert_equal request['source']['card']['expirationMonth'], "0#{@network_token.month}" + end.respond_with(successful_purchase_response) + + 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) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['transactionDetails']['captureFlag'], false + assert_equal request['merchantDetails']['terminalId'], @gateway.options[:terminal_id] + assert_equal request['merchantDetails']['merchantId'], @gateway.options[:merchant_id] + assert_equal request['amount']['total'], (@amount / 100.0).to_f + assert_equal request['source']['card']['cardData'], @credit_card.number + assert_equal request['source']['card']['securityCode'], @credit_card.verification_value + assert_equal request['source']['card']['securityCodeIndicator'], 'PROVIDED' + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_failed_purchase_and_authorize + @gateway.expects(:ssl_post).returns(failed_purchase_and_authorize_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert_equal 'string', response.error_code + end + + def test_successful_parsing_of_billing_and_shipping_addresses + address_with_phone = address.merge({ phone_number: '000-000-00-000' }) + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ billing_address: address_with_phone, shipping_address: address_with_phone })) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + %w(shipping billing).each do |key| + assert_equal request[key + 'Address']['address']['street'], address_with_phone[:address1] + assert_equal request[key + 'Address']['address']['houseNumberOrName'], address_with_phone[:address2] + assert_equal request[key + 'Address']['address']['recipientNameOrAddress'], address_with_phone[:name] + assert_equal request[key + 'Address']['address']['city'], address_with_phone[:city] + assert_equal request[key + 'Address']['address']['stateOrProvince'], address_with_phone[:state] + assert_equal request[key + 'Address']['address']['postalCode'], address_with_phone[:zip] + assert_equal request[key + 'Address']['address']['country'], address_with_phone[:country] + assert_equal request[key + 'Address']['phone']['phoneNumber'], address_with_phone[:phone_number] + end + end.respond_with(successful_authorize_response) + end + + def test_successful_void + response = stub_comms do + @gateway.void('abc123|authorization123', @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal 'authorization123', request['referenceTransactionDetails']['referenceTransactionId'] + assert_equal 'CHARGES', request['referenceTransactionDetails']['referenceTransactionType'] + assert_nil request['transactionDetails']['captureFlag'] + end.respond_with(successful_void_and_refund_response) + + assert_success response + end + + def test_successful_refund + response = stub_comms do + @gateway.refund(nil, 'abc123|authorization123', @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['referenceTransactionDetails']['referenceTransactionId'], 'authorization123' + assert_equal request['referenceTransactionDetails']['referenceTransactionType'], 'CHARGES' + assert_nil request['transactionDetails']['captureFlag'] + assert_nil request['amount'] + end.respond_with(successful_void_and_refund_response) + + assert_success response + end + + def test_successful_partial_refund + response = stub_comms do + @gateway.refund(@amount - 1, 'abc123|authorization123', @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['referenceTransactionDetails']['referenceTransactionId'], 'authorization123' + assert_equal request['referenceTransactionDetails']['referenceTransactionType'], 'CHARGES' + assert_nil request['transactionDetails']['captureFlag'] + assert_equal request['amount']['total'], ((@amount - 1) / 100.0).to_f + assert_equal request['amount']['currency'], 'USD' + end.respond_with(successful_void_and_refund_response) + + assert_success response + end + + def test_successful_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:) + } + end + + def test_successful_store + response = stub_comms do + @gateway.store(@credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['merchantDetails']['terminalId'], @gateway.options[:terminal_id] + assert_equal request['merchantDetails']['merchantId'], @gateway.options[:merchant_id] + assert_equal request['source']['card']['cardData'], @credit_card.number + assert_equal request['source']['card']['securityCode'], @credit_card.verification_value + assert_equal request['source']['card']['securityCodeIndicator'], 'PROVIDED' + end.respond_with(successful_store_response) + + assert_success response + assert_equal response.params['paymentTokens'].first['tokenData'], response.authorization + end + + def test_successful_verify + stub_comms do + @gateway.verify(@credit_card, @options) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_match %r{verification}, endpoint + assert_equal request['source']['sourceType'], 'PaymentCard' + end.respond_with(successful_authorize_response) + end + + def test_getting_avs_cvv_from_response + gateway_resp = { + 'paymentReceipt' => { + 'processorResponseDetails' => { + 'bankAssociationDetails' => { + 'associationResponseCode' => 'V000', + 'avsSecurityCodeResponse' => { + 'streetMatch' => 'NONE', + 'postalCodeMatch' => 'NONE', + 'securityCodeMatch' => 'NOT_CHECKED', + 'association' => { + 'securityCodeResponse' => 'X', + 'avsCode' => 'Y' + } + } + } + } + } + } + + assert_equal 'X', @gateway.send(:get_avs_cvv, gateway_resp, 'cvv') + assert_equal 'Y', @gateway.send(:get_avs_cvv, gateway_resp, 'avs') + end + + def test_successful_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + def test_uses_order_id_to_keep_transaction_references_when_provided + @options[:order_id] = 'abc123' + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.respond_with(successful_purchase_response) + + assert_success response + assert_equal 'abc123|6304d53be8d94312a620962afc9c012d', response.authorization + end + + def test_detect_success_state_for_verify_on_success_transaction + gateway_resp = { + 'gatewayResponse' => { + 'transactionState' => 'VERIFIED' + } + } + + assert @gateway.send :success_from, gateway_resp, 'verify' + end + + def test_detect_success_state_for_verify_on_failure_transaction + gateway_resp = { + 'gatewayResponse' => { + 'transactionState' => 'NOT_VERIFIED' + } + } + + refute @gateway.send :success_from, gateway_resp, 'verify' + end + + def test_add_reference_transaction_details_capture_reference_id + authorization = '|922e-59fc86a36c03' + + @gateway.send :add_reference_transaction_details, @post, authorization, {}, :capture + assert_equal '922e-59fc86a36c03', @post[:referenceTransactionDetails][:referenceTransactionId] + assert_nil @post[:referenceTransactionDetails][:referenceTransactionType] + end + + def test_add_reference_transaction_details_void_reference_id + authorization = '|922e-59fc86a36c03' + + @gateway.send :add_reference_transaction_details, @post, authorization, {}, :void + assert_equal '922e-59fc86a36c03', @post[:referenceTransactionDetails][:referenceTransactionId] + assert_equal 'CHARGES', @post[:referenceTransactionDetails][:referenceTransactionType] + end + + def test_add_reference_transaction_details_refund_reference_id + authorization = '|922e-59fc86a36c03' + + @gateway.send :add_reference_transaction_details, @post, authorization, {}, :refund + assert_equal '922e-59fc86a36c03', @post[:referenceTransactionDetails][:referenceTransactionId] + assert_equal 'CHARGES', @post[:referenceTransactionDetails][:referenceTransactionType] + end + + def test_successful_purchase_when_encrypted_credit_card_present + @options[:order_id] = 'abc123' + @options[:encryption_data] = { + keyId: SecureRandom.uuid, + encryptionType: 'RSA', + encryptionBlock: SecureRandom.alphanumeric(20), + encryptionBlockFields: 'card.cardData:16,card.nameOnCard:8,card.expirationMonth:2,card.expirationYear:4,card.securityCode:3', + encryptionTarget: 'MANUAL' + } + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + refute_nil request['source']['encryptionData'] + assert_equal request['source']['sourceType'], 'PaymentCard' + assert_equal request['source']['encryptionData']['keyId'], @options[:encryption_data][:keyId] + assert_equal request['source']['encryptionData']['encryptionType'], 'RSA' + assert_equal request['source']['encryptionData']['encryptionBlock'], @options[:encryption_data][:encryptionBlock] + assert_equal request['source']['encryptionData']['encryptionBlockFields'], @options[:encryption_data][:encryptionBlockFields] + assert_equal request['source']['encryptionData']['encryptionTarget'], 'MANUAL' + end.respond_with(successful_purchase_response) + + assert_success response + end + + private + + def successful_purchase_response + <<~RESPONSE + { + "gatewayResponse": { + "transactionType": "CHARGE", + "transactionState": "CAPTURED", + "transactionOrigin": "ECOM", + "transactionProcessingDetails": { + "orderId": "CHG018048a66aafc64d789cb018a53c30fd74", + "transactionTimestamp": "2022-10-06T11:27:45.593359Z", + "apiTraceId": "6304d53be8d94312a620962afc9c012d", + "clientRequestId": "5106241", + "transactionId": "6304d53be8d94312a620962afc9c012d" + } + }, + "source": { + "sourceType": "PaymentCard", + "card": { + "expirationMonth": "02", + "expirationYear": "2035", + "securityCodeIndicator": "PROVIDED", + "bin": "400555", + "last4": "0019", + "scheme": "VISA" + } + }, + "paymentReceipt": { + "approvedAmount": { + "total": 12.04, + "currency": "USD" + }, + "processorResponseDetails": { + "approvalStatus": "APPROVED", + "approvalCode": "000238", + "referenceNumber": "962afc9c012d", + "processor": "FISERV", + "host": "NASHVILLE", + "networkInternationalId": "0001", + "responseCode": "000", + "responseMessage": "Approved", + "hostResponseCode": "00", + "additionalInfo": [ + { + "name": "HOST_RAW_PROCESSOR_RESPONSE", + "value": "ARAyIAHvv70O77+9AAAAAAAAAAAAEgQQBhAnRQE1JAABWTk2MmFmYzljMDEyZDAwMDIzODAwMDk5OTk5OTk=" + } + ] + } + }, + "transactionDetails": { + "captureFlag": true, + "transactionCaptureType": "hcs", + "processingCode": "000000", + "merchantInvoiceNumber": "123456789012", + "createToken": true, + "retrievalReferenceNumber": "962afc9c012d" + }, + "transactionInteraction": { + "posEntryMode": "UNSPECIFIED", + "posConditionCode": "CARD_NOT_PRESENT_ECOM", + "additionalPosInformation": { + "dataEntrySource": "UNSPECIFIED", + "posFeatures": { + "pinAuthenticationCapability": "UNSPECIFIED", + "terminalEntryCapability": "UNSPECIFIED" + } + }, + "hostPosConditionCode": "59" + }, + "merchantDetails": { + "tokenType": "BBY0", + "terminalId": "10000001", + "merchantId": "100008000003683" + }, + "networkDetails": { + "network": { + "network": "Visa" + } + } + } + RESPONSE + end + + def successful_authorize_response + <<~RESPONSE + { + "gatewayResponse": { + "transactionType": "CHARGE", + "transactionState": "AUTHORIZED", + "transactionOrigin": "ECOM", + "transactionProcessingDetails": { + "orderId": "CHG01fb29348b9f8a48ed875e6bea3af41744", + "transactionTimestamp": "2022-10-06T11:28:27.131701Z", + "apiTraceId": "000bc22420f448288f1226d28dfdf275", + "clientRequestId": "9573527", + "transactionId": "000bc22420f448288f1226d28dfdf275" + } + }, + "source": { + "sourceType": "PaymentCard", + "card": { + "expirationMonth": "02", + "expirationYear": "2035", + "bin": "400555", + "last4": "0019", + "scheme": "VISA" + } + }, + "paymentReceipt": { + "approvedAmount": { + "total": 12.04, + "currency": "USD" + }, + "processorResponseDetails": { + "approvalStatus": "APPROVED", + "approvalCode": "000239", + "referenceNumber": "26d28dfdf275", + "processor": "FISERV", + "host": "NASHVILLE", + "networkInternationalId": "0001", + "responseCode": "000", + "responseMessage": "Approved", + "hostResponseCode": "00", + "additionalInfo": [ + { + "name": "HOST_RAW_PROCESSOR_RESPONSE", + "value": "ARAyIAHvv70O77+9AAAAAAAAAAAAEgQQBhAoJzQ2aAABWTI2ZDI4ZGZkZjI3NTAwMDIzOTAwMDk5OTk5OTk=" + } + ] + } + }, + "transactionDetails": { + "captureFlag": false, + "transactionCaptureType": "hcs", + "processingCode": "000000", + "merchantInvoiceNumber": "123456789012", + "createToken": true, + "retrievalReferenceNumber": "26d28dfdf275" + }, + "transactionInteraction": { + "posEntryMode": "UNSPECIFIED", + "posConditionCode": "CARD_NOT_PRESENT_ECOM", + "additionalPosInformation": { + "dataEntrySource": "UNSPECIFIED", + "posFeatures": { + "pinAuthenticationCapability": "UNSPECIFIED", + "terminalEntryCapability": "UNSPECIFIED" + } + }, + "hostPosConditionCode": "59" + }, + "merchantDetails": { + "tokenType": "BBY0", + "terminalId": "10000001", + "merchantId": "100008000003683" + }, + "networkDetails": { + "network": { + "network": "Visa" + } + } + } + RESPONSE + end + + def failed_purchase_and_authorize_response + <<~RESPONSE + { + "gatewayResponse": { + "transactionType": "CHARGE", + "transactionState": "AUTHORIZED", + "transactionOrigin": "ECOM", + "transactionProcessingDetails": { + "orderId": "R-3b83fca8-2f9c-4364-86ae-12c91f1fcf16", + "transactionTimestamp": "2016-04-16T16:06:05Z", + "apiTraceId": "1234567a1234567b1234567c1234567d", + "clientRequestId": "30dd879c-ee2f-11db-8314-0800200c9a66", + "transactionId": "838916029301" + } + }, + "error": [ + { + "type": "HOST", + "code": "string", + "field": "source.sourceType", + "message": "Missing type ID property.", + "additionalInfo": "The Reauthorization request was not successful and the Cancel of referenced authorization transaction was not processed, per Auth_before_Cancel configuration" + } + ] + } + RESPONSE + end + + def successful_void_and_refund_response + <<~RESPONSE + { + "gatewayResponse": { + "transactionType": "CANCEL", + "transactionState": "AUTHORIZED", + "transactionOrigin": "ECOM", + "transactionProcessingDetails": { + "transactionTimestamp": "2021-06-20T23:42:48Z", + "orderId": "RKOrdID-525133851837", + "apiTraceId": "362866ac81864d7c9d1ff8b5aa6e98db", + "clientRequestId": "4345791", + "transactionId": "84356531338" + } + }, + "source": { + "sourceType": "PaymentCard", + "card": { + "bin": "40055500", + "last4": "0019", + "scheme": "VISA", + "expirationMonth": "10", + "expirationYear": "2030" + } + }, + "paymentReceipt": { + "approvedAmount": { + "total": 12.04, + "currency": "USD" + }, + "merchantName": "Merchant Name", + "merchantAddress": "123 Peach Ave", + "merchantCity": "Atlanta", + "merchantStateOrProvince": "GA", + "merchantPostalCode": "12345", + "merchantCountry": "US", + "merchantURL": "https://www.somedomain.com", + "processorResponseDetails": { + "approvalStatus": "APPROVED", + "approvalCode": "OK5882", + "schemeTransactionId": "0225MCC625628", + "processor": "FISERV", + "host": "NASHVILLE", + "responseCode": "000", + "responseMessage": "APPROVAL", + "hostResponseCode": "00", + "hostResponseMessage": "APPROVAL", + "localTimestamp": "2021-06-20T23:42:48Z", + "bankAssociationDetails": { + "associationResponseCode": "000", + "transactionTimestamp": "2021-06-20T23:42:48Z" + } + } + }, + "transactionDetails": { + "merchantInvoiceNumber": "123456789012" + } + } + RESPONSE + end + + def successful_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 + { + "gatewayResponse": { + "transactionType": "TOKENIZE", + "transactionState": "AUTHORIZED", + "transactionOrigin": "ECOM", + "transactionProcessingDetails": { + "transactionTimestamp": "2021-06-20T23:42:48Z", + "orderId": "RKOrdID-525133851837", + "apiTraceId": "362866ac81864d7c9d1ff8b5aa6e98db", + "clientRequestId": "4345791", + "transactionId": "84356531338" + } + }, + "source": { + "sourceType": "PaymentCard", + "card": { + "bin": "40055500", + "last4": "0019", + "scheme": "VISA", + "expirationMonth": "10", + "expirationYear": "2030" + } + }, + "paymentTokens": [ + { + "tokenData": "8519371934460009", + "tokenSource": "TRANSARMOR", + "tokenResponseCode": "000", + "tokenResponseDescription": "SUCCESS" + }, + { + "tokenData": "8519371934460010", + "tokenSource": "CHASE", + "tokenResponseCode": "000", + "tokenResponseDescription": "SUCCESS" + } + ], + "processorResponseDetails": { + "approvalStatus": "APPROVED", + "approvalCode": "OK5882", + "schemeTransactionId": "0225MCC625628", + "processor": "FISERV", + "host": "NASHVILLE", + "responseCode": "000", + "responseMessage": "APPROVAL", + "hostResponseCode": "00", + "hostResponseMessage": "APPROVAL", + "localTimestamp": "2021-06-20T23:42:48Z", + "bankAssociationDetails": { + "associationResponseCode": "000", + "transactionTimestamp": "2021-06-20T23:42:48Z" + } + } + } + RESPONSE + end + + def pre_scrubbed + <<~PRE_SCRUBBED + opening connection to cert.api.fiservapps.com:443... + opened + starting SSL for cert.api.fiservapps.com:443... + SSL established + <- "POST /ch/payments/v1/charges HTTP/1.1\r\nContent-Type: application/json\r\nClient-Request-Id: 3473900\r\nApi-Key: nEcoHEQZjKtkKW9dN6yH7x4gO2EIARKe\r\nTimestamp: 1670258885014\r\nAccept-Language: application/json\r\nAuth-Token-Type: HMAC\r\nAccept: application/json\r\nAuthorization: TQh0nE38Mv7cbxbX3oSIUxZ4RzMkTmS2hpUSd6Rgi98=\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nHost: cert.api.fiservapps.com\r\nContent-Length: 500\r\n\r\n" + <- "{\"transactionDetails\":{\"captureFlag\":true,\"merchantInvoiceNumber\":\"995952121195\"},\"amount\":{\"total\":12.04,\"currency\":\"USD\"},\"source\":{\"sourceType\":\"PaymentCard\",\"card\":{\"cardData\":\"4005550000000019\",\"expirationMonth\":\"02\",\"expirationYear\":\"2035\",\"securityCode\":\"123\",\"securityCodeIndicator\":\"PROVIDED\"}},\"transactionInteraction\":{\"origin\":\"ECOM\",\"eciIndicator\":\"CHANNEL_ENCRYPTED\",\"posConditionCode\":\"CARD_NOT_PRESENT_ECOM\"},\"merchantDetails\":{\"terminalId\":\"10000001\",\"merchantId\":\"100008000003683\"}}" + -> "HTTP/1.1 201 Created\r\n" + -> "Date: Mon, 05 Dec 2022 16:48:06 GMT\r\n" + -> "Content-Type: application/json\r\n" + -> "Content-Length: 1709\r\n" + -> "Connection: close\r\n" + -> "Expires: 0\r\n" + -> "Referrer-Policy: no-referrer\r\n" + -> "X-Frame-Options: DENY\r\n" + -> "X-Vcap-Request-Id: 30397096-5cb9-46e1-7c63-3ac2494ca38e\r\n" + -> "targetServerReceivedEndTimestamp: 1670258886388\r\n" + -> "targetServerSentStartTimestamp: 1670258885212\r\n" + -> "X-Xss-Protection: 1; mode=block\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "Strict-Transport-Security: max-age=31536000; includeSubDomains\r\n" + -> "Cache-Control: no-store, no-cache, must-revalidate\r\n" + -> "Pragma: no-cache\r\n" + -> "Access-Control-Max-Age: 86400\r\n" + -> "ApiTraceId: 19d178570f274a2196540af6e2e0bf55\r\n" + -> "Via: 1.1 dca1-bit16021\r\n" + -> "\r\n" + reading 1709 bytes... + -> "{\"gatewayResponse\":{\"transactionType\":\"CHARGE\",\"transactionState\":\"CAPTURED\",\"transactionOrigin\":\"ECOM\",\"transactionProcessingDetails\":{\"orderId\":\"CHG0147086beb95194e808a3bf88e052285d7\",\"transactionTimestamp\":\"2022-12-05T16:48:05.358725Z\",\"apiTraceId\":\"19d178570f274a2196540af6e2e0bf55\",\"clientRequestId\":\"3473900\",\"transactionId\":\"19d178570f274a2196540af6e2e0bf55\"}},\"source\":{\"sourceType\":\"PaymentCard\",\"card\":{\"expirationMonth\":\"02\",\"expirationYear\":\"2035\",\"securityCodeIndicator\":\"PROVIDED\",\"bin\":\"400555\",\"last4\":\"0019\",\"scheme\":\"VISA\"}},\"paymentReceipt\":{\"approvedAmount\":{\"total\":12.04,\"currency\":\"USD\"},\"processorResponseDetails\":{\"approvalStatus\":\"APPROVED\",\"approvalCode\":\"000119\",\"referenceNumber\":\"0af6e2e0bf55\",\"processor\":\"FISERV\",\"host\":\"NASHVILLE\",\"networkInternationalId\":\"0001\",\"responseCode\":\"000\",\"responseMessage\":\"Approved\",\"hostResponseCode\":\"00\",\"additionalInfo\":[{\"name\":\"HOST_RAW_PROCESSOR_RESPONSE\",\"value\":\"ARAyIAHvv70O77+9AAIAAAAAAAAAEgQSBRZIBTNCVQABWTBhZjZlMmUwYmY1NTAwMDExOTAwMDk5OTk5OTkABAACMTQ=\"}]}},\"transactionDetails\":{\"captureFlag\":true,\"transactionCaptureType\":\"hcs\",\"processingCode\":\"000000\",\"merchantInvoiceNumber\":\"995952121195\",\"createToken\":true,\"retrievalReferenceNumber\":\"0af6e2e0bf55\",\"cavvInPrimary\":false},\"transactionInteraction\":{\"posEntryMode\":\"UNSPECIFIED\",\"posConditionCode\":\"CARD_NOT_PRESENT_ECOM\",\"additionalPosInformation\":{\"dataEntrySource\":\"UNSPECIFIED\",\"posFeatures\":{\"pinAuthenticationCapability\":\"UNSPECIFIED\",\"terminalEntryCapability\":\"UNSPECIFIED\"}},\"hostPosEntryMode\":\"000\",\"hostPosConditionCode\":\"59\"},\"merchantDetails\":{\"tokenType\":\"BBY0\",\"terminalId\":\"10000001\",\"merchantId\":\"100008000003683\"},\"networkDetails\":{\"network\":{\"network\":\"Visa\"}}}" + read 1709 bytes + Conn close + PRE_SCRUBBED + end + + def post_scrubbed + <<~POST_SCRUBBED + opening connection to cert.api.fiservapps.com:443... + opened + starting SSL for cert.api.fiservapps.com:443... + SSL established + <- "POST /ch/payments/v1/charges HTTP/1.1\r\nContent-Type: application/json\r\nClient-Request-Id: 3473900\r\nApi-Key: [FILTERED]\r\nTimestamp: 1670258885014\r\nAccept-Language: application/json\r\nAuth-Token-Type: HMAC\r\nAccept: application/json\r\nAuthorization: [FILTERED]\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nHost: cert.api.fiservapps.com\r\nContent-Length: 500\r\n\r\n" + <- "{\"transactionDetails\":{\"captureFlag\":true,\"merchantInvoiceNumber\":\"995952121195\"},\"amount\":{\"total\":12.04,\"currency\":\"USD\"},\"source\":{\"sourceType\":\"PaymentCard\",\"card\":{\"cardData\":\"[FILTERED]\",\"expirationMonth\":\"02\",\"expirationYear\":\"2035\",\"securityCode\":\"[FILTERED]\",\"securityCodeIndicator\":\"PROVIDED\"}},\"transactionInteraction\":{\"origin\":\"ECOM\",\"eciIndicator\":\"CHANNEL_ENCRYPTED\",\"posConditionCode\":\"CARD_NOT_PRESENT_ECOM\"},\"merchantDetails\":{\"terminalId\":\"10000001\",\"merchantId\":\"100008000003683\"}}" + -> "HTTP/1.1 201 Created\r\n" + -> "Date: Mon, 05 Dec 2022 16:48:06 GMT\r\n" + -> "Content-Type: application/json\r\n" + -> "Content-Length: 1709\r\n" + -> "Connection: close\r\n" + -> "Expires: 0\r\n" + -> "Referrer-Policy: no-referrer\r\n" + -> "X-Frame-Options: DENY\r\n" + -> "X-Vcap-Request-Id: 30397096-5cb9-46e1-7c63-3ac2494ca38e\r\n" + -> "targetServerReceivedEndTimestamp: 1670258886388\r\n" + -> "targetServerSentStartTimestamp: 1670258885212\r\n" + -> "X-Xss-Protection: 1; mode=block\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "Strict-Transport-Security: max-age=31536000; includeSubDomains\r\n" + -> "Cache-Control: no-store, no-cache, must-revalidate\r\n" + -> "Pragma: no-cache\r\n" + -> "Access-Control-Max-Age: 86400\r\n" + -> "ApiTraceId: 19d178570f274a2196540af6e2e0bf55\r\n" + -> "Via: 1.1 dca1-bit16021\r\n" + -> "\r\n" + reading 1709 bytes... + -> "{\"gatewayResponse\":{\"transactionType\":\"CHARGE\",\"transactionState\":\"CAPTURED\",\"transactionOrigin\":\"ECOM\",\"transactionProcessingDetails\":{\"orderId\":\"CHG0147086beb95194e808a3bf88e052285d7\",\"transactionTimestamp\":\"2022-12-05T16:48:05.358725Z\",\"apiTraceId\":\"19d178570f274a2196540af6e2e0bf55\",\"clientRequestId\":\"3473900\",\"transactionId\":\"19d178570f274a2196540af6e2e0bf55\"}},\"source\":{\"sourceType\":\"PaymentCard\",\"card\":{\"expirationMonth\":\"02\",\"expirationYear\":\"2035\",\"securityCodeIndicator\":\"PROVIDED\",\"bin\":\"400555\",\"last4\":\"0019\",\"scheme\":\"VISA\"}},\"paymentReceipt\":{\"approvedAmount\":{\"total\":12.04,\"currency\":\"USD\"},\"processorResponseDetails\":{\"approvalStatus\":\"APPROVED\",\"approvalCode\":\"000119\",\"referenceNumber\":\"0af6e2e0bf55\",\"processor\":\"FISERV\",\"host\":\"NASHVILLE\",\"networkInternationalId\":\"0001\",\"responseCode\":\"000\",\"responseMessage\":\"Approved\",\"hostResponseCode\":\"00\",\"additionalInfo\":[{\"name\":\"HOST_RAW_PROCESSOR_RESPONSE\",\"value\":\"ARAyIAHvv70O77+9AAIAAAAAAAAAEgQSBRZIBTNCVQABWTBhZjZlMmUwYmY1NTAwMDExOTAwMDk5OTk5OTkABAACMTQ=\"}]}},\"transactionDetails\":{\"captureFlag\":true,\"transactionCaptureType\":\"hcs\",\"processingCode\":\"000000\",\"merchantInvoiceNumber\":\"995952121195\",\"createToken\":true,\"retrievalReferenceNumber\":\"0af6e2e0bf55\",\"cavvInPrimary\":false},\"transactionInteraction\":{\"posEntryMode\":\"UNSPECIFIED\",\"posConditionCode\":\"CARD_NOT_PRESENT_ECOM\",\"additionalPosInformation\":{\"dataEntrySource\":\"UNSPECIFIED\",\"posFeatures\":{\"pinAuthenticationCapability\":\"UNSPECIFIED\",\"terminalEntryCapability\":\"UNSPECIFIED\"}},\"hostPosEntryMode\":\"000\",\"hostPosConditionCode\":\"59\"},\"merchantDetails\":{\"tokenType\":\"BBY0\",\"terminalId\":\"10000001\",\"merchantId\":\"100008000003683\"},\"networkDetails\":{\"network\":{\"network\":\"Visa\"}}}" + read 1709 bytes + Conn close + POST_SCRUBBED + end +end diff --git a/test/unit/gateways/commercegate_test.rb b/test/unit/gateways/commercegate_test.rb index 5fe0e065ba0..43bebb23e42 100644 --- a/test/unit/gateways/commercegate_test.rb +++ b/test/unit/gateways/commercegate_test.rb @@ -14,7 +14,7 @@ def setup @amount = 1000 @options = { - address: address + address: } end @@ -59,7 +59,6 @@ def test_successful_refund assert_equal 'EUR', response.params['currencyCode'] end - def test_successful_void @gateway.expects(:ssl_post).returns(successful_void_response) assert response = @gateway.void('100130291412', @options) diff --git a/test/unit/gateways/conekta_test.rb b/test/unit/gateways/conekta_test.rb index 215a50b1ba3..3330ae58b06 100644 --- a/test/unit/gateways/conekta_test.rb +++ b/test/unit/gateways/conekta_test.rb @@ -4,41 +4,41 @@ class ConektaTest < Test::Unit::TestCase include CommStub def setup - @gateway = ConektaGateway.new(:key => 'key_eYvWV7gSDkNYXsmr') + @gateway = ConektaGateway.new(key: 'key_eYvWV7gSDkNYXsmr') @amount = 300 @credit_card = ActiveMerchant::Billing::CreditCard.new( - :number => '4242424242424242', - :verification_value => '183', - :month => '01', - :year => '2018', - :first_name => 'Mario F.', - :last_name => 'Moreno Reyes' + number: '4242424242424242', + verification_value: '183', + month: '01', + year: '2018', + first_name: 'Mario F.', + last_name: 'Moreno Reyes' ) @declined_card = ActiveMerchant::Billing::CreditCard.new( - :number => '4000000000000002', - :verification_value => '183', - :month => '01', - :year => '2018', - :first_name => 'Mario F.', - :last_name => 'Moreno Reyes' + number: '4000000000000002', + verification_value: '183', + month: '01', + year: '2018', + first_name: 'Mario F.', + last_name: 'Moreno Reyes' ) @options = { - :device_fingerprint => '41l9l92hjco6cuekf0c7dq68v4', - :description => 'Blue clip', - :success_url => 'https://www.example.com/success', - :failure_url => 'https://www.example.com/failure', - :address1 => 'Rio Missisipi #123', - :address2 => 'Paris', - :city => 'Guerrero', - :country => 'Mexico', - :zip => '5555', - :customer => 'Mario Reyes', - :phone => '12345678', - :carrier => 'Estafeta' + device_fingerprint: '41l9l92hjco6cuekf0c7dq68v4', + description: 'Blue clip', + success_url: 'https://www.example.com/success', + failure_url: 'https://www.example.com/failure', + address1: 'Rio Missisipi #123', + address2: 'Paris', + city: 'Guerrero', + country: 'Mexico', + zip: '5555', + customer: 'Mario Reyes', + phone: '12345678', + carrier: 'Estafeta' } end @@ -69,8 +69,8 @@ def test_unsuccessful_purchase def test_successful_purchase_with_installments response = stub_comms(@gateway, :ssl_request) do - @gateway.purchase(@amount, @credit_card, @options.merge({monthly_installments: '3'})) - end.check_request do |method, endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, @options.merge({ monthly_installments: '3' })) + end.check_request do |_method, _endpoint, data, _headers| assert_match %r{monthly_installments=3}, data end.respond_with(successful_purchase_response) @@ -139,7 +139,7 @@ def test_unsuccessful_capture end def test_invalid_key - gateway = ConektaGateway.new(:key => 'invalid_token') + gateway = ConektaGateway.new(key: 'invalid_token') gateway.expects(:ssl_request).returns(failed_login_response) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response @@ -154,8 +154,8 @@ def test_adds_application_and_meta_headers } response = stub_comms(@gateway, :ssl_request) do - @gateway.purchase(@amount, 'tok_xxxxxxxxxxxxxxxx', @options.merge(application: application, meta: {its_so_meta: 'even this acronym'})) - end.check_request do |method, endpoint, data, headers| + @gateway.purchase(@amount, 'tok_xxxxxxxxxxxxxxxx', @options.merge(application:, meta: { its_so_meta: 'even this acronym' })) + end.check_request do |_method, _endpoint, _data, headers| assert_match(/\"application\"/, headers['X-Conekta-Client-User-Agent']) assert_match(/\"name\":\"app\"/, headers['X-Conekta-Client-User-Agent']) assert_match(/\"version\":\"1.0\"/, headers['X-Conekta-Client-User-Agent']) diff --git a/test/unit/gateways/creditcall_test.rb b/test/unit/gateways/creditcall_test.rb index 146606ff742..9103162f6cf 100644 --- a/test/unit/gateways/creditcall_test.rb +++ b/test/unit/gateways/creditcall_test.rb @@ -112,7 +112,7 @@ def test_failed_verify def test_verification_value_sent stub_comms do @gateway.authorize(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(%r(123)m, data) end.respond_with(successful_authorize_response) end @@ -121,7 +121,7 @@ def test_verification_value_not_sent @credit_card.verification_value = ' ' stub_comms do @gateway.authorize(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_no_match(/CSC/, data) end.respond_with(successful_authorize_response) end @@ -129,25 +129,25 @@ def test_verification_value_not_sent def test_options_add_avs_additional_verification_fields stub_comms do @gateway.authorize(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_no_match(/AdditionalVerification/, data) end.respond_with(successful_authorize_response) stub_comms do @gateway.authorize(@amount, @credit_card, @options.merge(verify_zip: 'false', verify_address: 'false')) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_no_match(/AdditionalVerification/, data) end.respond_with(successful_authorize_response) stub_comms do @gateway.authorize(@amount, @credit_card, @options.merge(verify_zip: 'true', verify_address: 'true')) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/\n K1C2N6<\/Zip>\n
/, data) end.respond_with(successful_authorize_response) stub_comms do @gateway.authorize(@amount, @credit_card, @options.merge(verify_zip: 'true', verify_address: 'false')) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/ \n K1C2N6<\/Zip>\n <\/AdditionalVerification>\n/, data) end.respond_with(successful_authorize_response) end diff --git a/test/unit/gateways/credorax_test.rb b/test/unit/gateways/credorax_test.rb index 8c588c3f108..55364906738 100644 --- a/test/unit/gateways/credorax_test.rb +++ b/test/unit/gateways/credorax_test.rb @@ -12,18 +12,75 @@ def setup billing_address: address, description: 'Store Purchase' } + + @normalized_3ds_2_options = { + reference: '345123', + shopper_email: 'john.smith@test.com', + shopper_ip: '77.110.174.153', + shopper_reference: 'John Smith', + billing_address: address, + shipping_address: address, + order_id: '123', + execute_threed: true, + three_ds_initiate: '03', + f23: '1', + three_ds_reqchallengeind: '04', + three_ds_challenge_window_size: '01', + stored_credential: { reason_type: 'unscheduled' }, + three_ds_2: { + channel: 'browser', + notification_url: 'www.example.com', + browser_info: { + accept_header: 'unknown', + depth: 100, + java: false, + language: 'US', + height: 1000, + width: 500, + timezone: '-120', + user_agent: 'unknown' + } + } + } + + @nt_credit_card = network_tokenization_credit_card( + '4176661000001015', + brand: 'visa', + eci: '07', + source: :network_token, + payment_cryptogram: 'AgAAAAAAosVKVV7FplLgQRYAAAA=' + ) + + @apple_pay_card = network_tokenization_credit_card( + '4176661000001015', + month: 10, + year: Time.new.year + 2, + first_name: 'John', + last_name: 'Smith', + verification_value: '737', + payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', + eci: '07', + transaction_id: 'abc123', + source: :apple_pay + ) + end + + def test_supported_card_types + klass = @gateway.class + assert_equal %i[visa master maestro american_express jcb discover diners_club], klass.supported_cardtypes end def test_successful_purchase response = stub_comms do @gateway.purchase(@amount, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_no_match(/i8=sample-eci%3Asample-cavv%3Asample-xid/, data) end.respond_with(successful_purchase_response) assert_success response assert_equal '8a82944a5351570601535955efeb513c;006596;02617cf5f02ccaed239b6521748298c5;purchase', response.authorization + assert_equal 'Succeeded', response.message assert response.test? end @@ -47,11 +104,12 @@ def test_successful_authorize_and_capture capture = stub_comms do @gateway.capture(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/8a829449535154bc0153595952a2517a/, data) end.respond_with(successful_capture_response) assert_success capture + assert_equal 'Succeeded', response.message end def test_failed_authorize @@ -70,6 +128,7 @@ def test_failed_capture end.respond_with(failed_capture_response) assert_failure response + assert_equal '2. At least one of input parameters is malformed.: Parameter [g4] cannot be empty.', response.message end def test_successful_void @@ -82,21 +141,23 @@ def test_successful_void void = stub_comms do @gateway.void(response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/8a829449535154bc0153595952a2517a/, data) end.respond_with(successful_void_response) assert_success void + assert_equal 'Succeeded', void.message end def test_failed_void response = stub_comms do @gateway.void('5d53a33d960c46d00f5dc061947d998c') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/5d53a33d960c46d00f5dc061947d998c/, data) end.respond_with(failed_void_response) assert_failure response + assert_equal '2. At least one of input parameters is malformed.: Parameter [g4] cannot be empty.', response.message end def test_successful_refund @@ -109,10 +170,30 @@ def test_successful_refund refund = stub_comms do @gateway.refund(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/8a82944a5351570601535955efeb513c/, data) end.respond_with(successful_refund_response) + assert_success refund + assert_equal 'Succeeded', refund.message + end + + def test_successful_refund_with_recipient_fields + refund_options = { + recipient_street_address: 'street', + recipient_city: 'chicago', + recipient_province_code: '312', + recipient_country_code: 'US' + } + refund = stub_comms do + @gateway.refund(@amount, '123', refund_options) + end.check_request do |_endpoint, data, _headers| + assert_match(/j6=street/, data) + assert_match(/j7=chicago/, data) + assert_match(/j8=312/, data) + assert_match(/j9=USA/, data) + end.respond_with(successful_refund_response) + assert_success refund end @@ -122,6 +203,43 @@ def test_failed_refund end.respond_with(failed_refund_response) assert_failure response + assert_equal '2. At least one of input parameters is malformed.: Parameter [g4] cannot be empty.', response.message + end + + def test_successful_referral_cft + response = stub_comms do + @gateway.purchase(@amount, @credit_card) + end.respond_with(successful_purchase_response) + + assert_success response + assert_equal '8a82944a5351570601535955efeb513c;006596;02617cf5f02ccaed239b6521748298c5;purchase', response.authorization + + referral_cft = stub_comms do + @gateway.refund(@amount, response.authorization, { referral_cft: true, first_name: 'John', last_name: 'Smith' }) + end.check_request do |_endpoint, data, _headers| + assert_match(/8a82944a5351570601535955efeb513c/, data) + # Confirm that `j5` (first name) and `j13` (surname) parameters are present + # These fields are required for CFT payouts as of Sept 1, 2020 + assert_match(/j5=John/, data) + assert_match(/j13=Smith/, data) + # Confirm that the transaction type is `referral_cft` + assert_match(/O=34/, data) + end.respond_with(successful_referral_cft_response) + + assert_success referral_cft + assert_equal 'Succeeded', referral_cft.message + end + + def test_failed_referral_cft + response = stub_comms do + @gateway.refund(nil, '', referral_cft: true) + end.check_request do |_endpoint, data, _headers| + # Confirm that the transaction type is `referral_cft` + assert_match(/O=34/, data) + end.respond_with(failed_referral_cft_response) + + assert_failure response + assert_equal 'Referred to transaction has not been found.', response.message end def test_successful_credit @@ -132,9 +250,27 @@ def test_successful_credit assert_success response assert_equal '8a82944a53515706015359604c135301;;868f8b942fae639d28e27e8933d575d4;credit', response.authorization + assert_equal 'Succeeded', response.message assert response.test? end + def test_credit_sends_correct_action_code + stub_comms do + @gateway.credit(@amount, @credit_card) + end.check_request do |_endpoint, data, _headers| + assert_match(/O=35/, data) + end.respond_with(successful_credit_response) + end + + def test_credit_sends_customer_name + stub_comms do + @gateway.credit(@amount, @credit_card, { first_name: 'Test', last_name: 'McTest' }) + end.check_request do |_endpoint, data, _headers| + assert_match(/j5=Test/, data) + assert_match(/j13=McTest/, data) + end.respond_with(successful_credit_response) + end + def test_failed_credit response = stub_comms do @gateway.credit(@amount, @credit_card) @@ -174,13 +310,144 @@ def test_transcript_scrubbing assert_equal scrubbed_transcript, @gateway.scrub(transcript) end - def test_adds_3d_secure_fields - options_with_3ds = @options.merge({eci: 'sample-eci', cavv: 'sample-cavv', xid: 'sample-xid'}) + def test_adds_3d2_secure_fields + options_with_3ds = @normalized_3ds_2_options response = stub_comms do @gateway.purchase(@amount, @credit_card, options_with_3ds) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| + assert_match(/3ds_channel=02/, data) + assert_match(/3ds_transtype=01/, data) + assert_match(/3ds_initiate=03/, data) + assert_match(/f23=1/, data) + assert_match(/3ds_reqchallengeind=04/, data) + assert_match(/3ds_redirect_url=www.example.com/, data) + assert_match(/3ds_challengewindowsize=01/, data) + assert_match(/d5=unknown/, data) + assert_match(/3ds_browsertz=-120/, data) + assert_match(/3ds_browserscreenwidth=500/, data) + assert_match(/3ds_browserscreenheight=1000/, data) + assert_match(/3ds_browsercolordepth=100/, data) + assert_match(/d6=US/, data) + assert_match(/3ds_browserjavaenabled=false/, data) + assert_match(/3ds_browseracceptheader=unknown/, data) + assert_match(/3ds_shipaddrstate=ON/, data) + assert_match(/3ds_shipaddrpostcode=K1C2N6/, data) + assert_match(/3ds_shipaddrline2=Apt\+1/, data) + assert_match(/3ds_shipaddrline1=456\+My\+Street/, data) + assert_match(/3ds_shipaddrcountry=CA/, data) + assert_match(/3ds_shipaddrcity=Ottawa/, data) + refute_match(/3ds_version/, data) + end.respond_with(successful_purchase_response) + + assert_success response + + assert_equal '8a82944a5351570601535955efeb513c;006596;02617cf5f02ccaed239b6521748298c5;purchase', response.authorization + assert response.test? + end + + def test_does_not_add_incomplete_3d2_shipping_address + incomplete_shipping_address = { + state: 'ON', + zip: 'K1C2N6', + address1: '456 My Street', + address2: '', + country: 'CA', + city: 'Ottawa' + } + options_with_3ds = @normalized_3ds_2_options.merge(shipping_address: incomplete_shipping_address) + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options_with_3ds) + end.check_request do |_endpoint, data, _headers| + assert_match(/3ds_initiate=03/, data) + assert_not_match(/3ds_shipaddrstate=/, data) + assert_not_match(/3ds_shipaddrpostcode=/, data) + assert_not_match(/3ds_shipaddrline1=/, data) + assert_not_match(/3ds_shipaddrline2=/, data) + assert_not_match(/3ds_shipaddrcountry=/, data) + assert_not_match(/3ds_shipaddrcity=/, data) + end.respond_with(successful_purchase_response) + assert_success response + assert response.test? + end + + def test_adds_correct_3ds_browsercolordepth_when_color_depth_is_30 + @normalized_3ds_2_options[:three_ds_2][:browser_info][:depth] = 30 + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @normalized_3ds_2_options) + end.check_request do |_endpoint, data, _headers| + assert_match(/3ds_browsercolordepth=32/, data) + end.respond_with(successful_purchase_response) + + assert_success response + + assert_equal '8a82944a5351570601535955efeb513c;006596;02617cf5f02ccaed239b6521748298c5;purchase', response.authorization + assert response.test? + end + + def test_adds_3d2_secure_fields_with_3ds_transtype_specified + options_with_3ds = @normalized_3ds_2_options.merge(three_ds_transtype: '03') + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options_with_3ds) + end.check_request do |_endpoint, data, _headers| + assert_match(/3ds_channel=02/, data) + assert_match(/3ds_transtype=03/, data) + end.respond_with(successful_purchase_response) + + assert_success response + + assert_equal '8a82944a5351570601535955efeb513c;006596;02617cf5f02ccaed239b6521748298c5;purchase', response.authorization + assert response.test? + end + + def test_purchase_adds_3d_secure_fields + options_with_3ds = @options.merge({ eci: 'sample-eci', cavv: 'sample-cavv', xid: 'sample-xid', three_ds_version: '1.0.2' }) + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options_with_3ds) + end.check_request do |_endpoint, data, _headers| assert_match(/i8=sample-eci%3Asample-cavv%3Asample-xid/, data) + assert_match(/3ds_version=1.0&/, data) + end.respond_with(successful_purchase_response) + + assert_success response + + assert_equal '8a82944a5351570601535955efeb513c;006596;02617cf5f02ccaed239b6521748298c5;purchase', response.authorization + assert response.test? + end + + def test_purchase_adds_3d_secure_fields_via_normalized_hash + version = '1.0.2' + eci = 'sample-eci' + cavv = 'sample-cavv' + xid = 'sample-xid' + options_with_normalized_3ds = @options.merge( + three_d_secure: { + version:, + eci:, + cavv:, + xid: + } + ) + + stub_comms do + @gateway.purchase(@amount, @credit_card, options_with_normalized_3ds) + end.check_request do |_endpoint, data, _headers| + assert_match(/i8=#{eci}%3A#{cavv}%3A#{xid}/, data) + assert_match(/3ds_version=1.0&/, data) + end.respond_with(successful_purchase_response) + end + + def test_3ds_channel_field_set_by_stored_credential_initiator + options_with_3ds = @normalized_3ds_2_options.merge(stored_credential_options(:merchant, :unscheduled, id: 'abc123')) + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options_with_3ds) + end.check_request do |_endpoint, data, _headers| + assert_match(/3ds_channel=03/, data) end.respond_with(successful_purchase_response) assert_success response @@ -189,12 +456,28 @@ def test_adds_3d_secure_fields assert response.test? end + def test_authorize_adds_3d_secure_fields + options_with_3ds = @options.merge({ eci: 'sample-eci', cavv: 'sample-cavv', xid: 'sample-xid' }) + + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options_with_3ds) + end.check_request do |_endpoint, data, _headers| + assert_match(/i8=sample-eci%3Asample-cavv%3Asample-xid/, data) + assert_match(/3ds_version=1.0/, data) + end.respond_with(successful_purchase_response) + + assert_success response + + assert_equal '8a82944a5351570601535955efeb513c;006596;02617cf5f02ccaed239b6521748298c5;authorize', response.authorization + assert response.test? + end + def test_defaults_3d_secure_cavv_field_to_none_if_not_present - options_with_3ds = @options.merge({eci: 'sample-eci', xid: 'sample-xid'}) + options_with_3ds = @options.merge({ eci: 'sample-eci', xid: 'sample-xid' }) response = stub_comms do @gateway.purchase(@amount, @credit_card, options_with_3ds) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/i8=sample-eci%3Anone%3Asample-xid/, data) end.respond_with(successful_purchase_response) @@ -204,27 +487,679 @@ def test_defaults_3d_secure_cavv_field_to_none_if_not_present assert response.test? end - def test_adds_a9_field - options_with_3ds = @options.merge({transaction_type: '8'}) + def test_adds_3ds2_fields_via_normalized_hash + version = '2' + eci = '05' + cavv = '637574652070757070792026206b697474656e73' + ds_transaction_id = '97267598-FAE6-48F2-8083-C23433990FBC' + options_with_normalized_3ds = @options.merge( + three_d_secure: { + version:, + eci:, + cavv:, + ds_transaction_id: + } + ) + + stub_comms do + @gateway.purchase(@amount, @credit_card, options_with_normalized_3ds) + end.check_request do |_endpoint, data, _headers| + assert_match(/i8=#{eci}%3A#{cavv}%3Anone/, data) + assert_match(/3ds_version=2/, data) + assert_match(/3ds_dstrxid=#{ds_transaction_id}/, data) + end.respond_with(successful_purchase_response) + end + + def test_adds_default_cavv_when_omitted_from_normalized_hash + version = '2.2.0' + eci = '05' + ds_transaction_id = '97267598-FAE6-48F2-8083-C23433990FBC' + options_with_normalized_3ds = @options.merge( + three_d_secure: { + version:, + eci:, + ds_transaction_id: + } + ) + stub_comms do + @gateway.purchase(@amount, @credit_card, options_with_normalized_3ds) + end.check_request do |_endpoint, data, _headers| + assert_match(/i8=#{eci}%3Anone%3Anone/, data) + assert_match(/3ds_version=2.2.0/, data) + assert_match(/3ds_dstrxid=#{ds_transaction_id}/, data) + end.respond_with(successful_purchase_response) + end + + def test_purchase_adds_a9_field + options_with_3ds = @options.merge({ transaction_type: '8' }) stub_comms do @gateway.purchase(@amount, @credit_card, options_with_3ds) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| + assert_match(/a9=8/, data) + end.respond_with(successful_purchase_response) + end + + def test_purchase_adds_aft_fields + aft_options = @options.merge( + aft: true, + sender_ref_number: 'test', + sender_fund_source: '01', + sender_country_code: 'USA', + sender_street_address: 'sender street', + sender_city: 'city', + sender_state: 'NY', + sender_first_name: 'george', + sender_last_name: 'smith', + recipient_street_address: 'street', + recipient_city: 'chicago', + recipient_province_code: '312', + recipient_postal_code: '12345', + recipient_country_code: 'USA', + recipient_first_name: 'logan', + recipient_last_name: 'bill' + ) + + stub_comms do + @gateway.purchase(@amount, @credit_card, aft_options) + end.check_request do |_endpoint, data, _headers| + # recipient fields + assert_match(/j5=logan/, data) + assert_match(/j6=street/, data) + assert_match(/j7=chicago/, data) + assert_match(/j8=312/, data) + assert_match(/j9=USA/, data) + assert_match(/j13=bill/, data) + assert_match(/j12=12345/, data) + # sender fields + assert_match(/s10=george/, data) + assert_match(/s11=smith/, data) + assert_match(/s12=sender\+street/, data) + assert_match(/s13=city/, data) + assert_match(/s14=NY/, data) + assert_match(/s15=USA/, data) + assert_match(/s17=test/, data) + assert_match(/s18=01/, data) + end.respond_with(successful_purchase_response) + end + + def test_authorize_adds_a9_field + options_with_3ds = @options.merge({ transaction_type: '8' }) + stub_comms do + @gateway.authorize(@amount, @credit_card, options_with_3ds) + end.check_request do |_endpoint, data, _headers| + assert_match(/a9=8/, data) + end.respond_with(successful_authorize_response) + end + + def test_credit_adds_a9_field + options_with_3ds = @options.merge({ transaction_type: '8' }) + stub_comms do + @gateway.credit(@amount, @credit_card, options_with_3ds) + end.check_request do |_endpoint, data, _headers| assert_match(/a9=8/, data) + end.respond_with(successful_credit_response) + end + + def test_authorize_adds_authorization_details + options_with_auth_details = @options.merge({ authorization_type: '2', multiple_capture_count: '5' }) + stub_comms do + @gateway.authorize(@amount, @credit_card, options_with_auth_details) + end.check_request do |_endpoint, data, _headers| + assert_match(/a10=2/, data) + assert_match(/a11=5/, data) + end.respond_with(successful_authorize_response) + end + + def test_purchase_adds_submerchant_id + @options[:submerchant_id] = '12345' + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/h3=12345/, data) end.respond_with(successful_purchase_response) end - def test_supports_billing_descriptor - @options.merge!({ billing_descriptor: 'abcdefghijkl'}) + def test_adds_moto_a2_field + @options[:metadata] = { manual_entry: true } stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| + assert_match(/a2=3/, data) + end.respond_with(successful_purchase_response) + end + + def test_authorize_adds_submerchant_id + @options[:submerchant_id] = '12345' + stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/h3=12345/, data) + end.respond_with(successful_authorize_response) + end + + def test_capture_adds_submerchant_id + response = stub_comms do + @gateway.authorize(@amount, @credit_card) + end.respond_with(successful_authorize_response) + + @options[:submerchant_id] = '12345' + stub_comms do + @gateway.capture(@amount, response.authorization, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/h3=12345/, data) + end.respond_with(successful_capture_response) + end + + def test_void_adds_submerchant_id + response = stub_comms do + @gateway.purchase(@amount, @credit_card) + end.respond_with(successful_authorize_response) + + @options[:submerchant_id] = '12345' + stub_comms do + @gateway.void(response.authorization, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/h3=12345/, data) + end.respond_with(successful_void_response) + end + + def test_refund_adds_submerchant_id + response = stub_comms do + @gateway.purchase(@amount, @credit_card) + end.respond_with(successful_purchase_response) + + @options[:submerchant_id] = '12345' + stub_comms do + @gateway.refund(@amount, response.authorization, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/h3=12345/, data) + end.respond_with(successful_refund_response) + end + + def test_credit_adds_submerchant_id + @options[:submerchant_id] = '12345' + stub_comms do + @gateway.credit(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/h3=12345/, data) + end.respond_with(successful_credit_response) + end + + def test_purchase_adds_billing_descriptor + @options[:billing_descriptor] = 'abcdefghijkl' + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| assert_match(/i2=abcdefghijkl/, data) end.respond_with(successful_purchase_response) end + def test_authorize_adds_billing_descriptor + @options[:billing_descriptor] = 'abcdefghijkl' + stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/i2=abcdefghijkl/, data) + end.respond_with(successful_authorize_response) + end + + def test_capture_adds_billing_descriptor + response = stub_comms do + @gateway.authorize(@amount, @credit_card) + end.respond_with(successful_authorize_response) + + @options[:billing_descriptor] = 'abcdefghijkl' + stub_comms do + @gateway.capture(@amount, response.authorization, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/i2=abcdefghijkl/, data) + end.respond_with(successful_capture_response) + end + + def test_refund_adds_billing_descriptor + response = stub_comms do + @gateway.purchase(@amount, @credit_card) + end.respond_with(successful_purchase_response) + + @options[:billing_descriptor] = 'abcdefghijkl' + stub_comms do + @gateway.refund(@amount, response.authorization, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/i2=abcdefghijkl/, data) + end.respond_with(successful_refund_response) + end + + def test_credit_adds_billing_descriptor + @options[:billing_descriptor] = 'abcdefghijkl' + stub_comms do + @gateway.credit(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/i2=abcdefghijkl/, data) + end.respond_with(successful_credit_response) + end + + def test_purchase_adds_processor_fields + @options[:processor] = 'TEST' + @options[:processor_merchant_id] = '123' + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/r1=TEST/, data) + assert_match(/r2=123/, data) + end.respond_with(successful_purchase_response) + end + + def test_authorize_adds_processor_fields + @options[:processor] = 'TEST' + @options[:processor_merchant_id] = '123' + stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/r1=TEST/, data) + assert_match(/r2=123/, data) + end.respond_with(successful_authorize_response) + end + + def test_capture_adds_processor_fields + response = stub_comms do + @gateway.authorize(@amount, @credit_card) + end.respond_with(successful_authorize_response) + + @options[:processor] = 'TEST' + @options[:processor_merchant_id] = '123' + stub_comms do + @gateway.capture(@amount, response.authorization, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/r1=TEST/, data) + assert_match(/r2=123/, data) + end.respond_with(successful_capture_response) + end + + def test_void_adds_processor_fields + response = stub_comms do + @gateway.purchase(@amount, @credit_card) + end.respond_with(successful_authorize_response) + + @options[:processor] = 'TEST' + @options[:processor_merchant_id] = '123' + stub_comms do + @gateway.void(response.authorization, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/r1=TEST/, data) + assert_match(/r2=123/, data) + end.respond_with(successful_void_response) + end + + def test_refund_adds_processor_fields + response = stub_comms do + @gateway.purchase(@amount, @credit_card) + end.respond_with(successful_purchase_response) + + @options[:processor] = 'TEST' + @options[:processor_merchant_id] = '123' + stub_comms do + @gateway.refund(@amount, response.authorization, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/r1=TEST/, data) + assert_match(/r2=123/, data) + end.respond_with(successful_refund_response) + end + + def test_credit_adds_processor_fields + @options[:processor] = 'TEST' + @options[:processor_merchant_id] = '123' + stub_comms do + @gateway.credit(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/r1=TEST/, data) + assert_match(/r2=123/, data) + end.respond_with(successful_credit_response) + end + + def test_purchase_adds_echo_field + @options[:echo] = 'Echo Parameter' + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/d2=Echo\+Parameter/, data) + end.respond_with(successful_purchase_response) + end + + def test_authorize_adds_echo_field + @options[:echo] = 'Echo Parameter' + stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/d2=Echo\+Parameter/, data) + end.respond_with(successful_authorize_response) + end + + def test_capture_adds_echo_field + response = stub_comms do + @gateway.authorize(@amount, @credit_card) + end.respond_with(successful_authorize_response) + + @options[:echo] = 'Echo Parameter' + stub_comms do + @gateway.capture(@amount, response.authorization, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/d2=Echo\+Parameter/, data) + end.respond_with(successful_capture_response) + end + + def test_void_adds_echo_field + response = stub_comms do + @gateway.purchase(@amount, @credit_card) + end.respond_with(successful_authorize_response) + + @options[:echo] = 'Echo Parameter' + stub_comms do + @gateway.void(response.authorization, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/d2=Echo\+Parameter/, data) + end.respond_with(successful_void_response) + end + + def test_refund_adds_echo_field + response = stub_comms do + @gateway.purchase(@amount, @credit_card) + end.respond_with(successful_purchase_response) + + @options[:echo] = 'Echo Parameter' + stub_comms do + @gateway.refund(@amount, response.authorization, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/d2=Echo\+Parameter/, data) + end.respond_with(successful_refund_response) + end + + def test_credit_adds_echo_field + @options[:echo] = 'Echo Parameter' + stub_comms do + @gateway.credit(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/d2=Echo\+Parameter/, data) + end.respond_with(successful_credit_response) + end + + def test_authorize_adds_cardholder_name_inquiry + @options[:account_name_inquiry] = true + @options[:first_name] = 'Art' + @options[:last_name] = 'Vandelay' + stub_comms do + @gateway.authorize(0, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/c22=Art/, data) + assert_match(/c23=Vandelay/, data) + assert_match(/a9=5/, data) + assert_not_match(/c1=/, data) + end.respond_with(successful_credit_response) + end + + def test_purchase_omits_phone_when_nil + # purchase passes the phone number when provided + @options[:billing_address][:phone] = '555-444-3333' + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/c2=555-444-3333/, data) + end.respond_with(successful_purchase_response) + + # purchase doesn't pass the phone number when nil + @options[:billing_address][:phone] = nil + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_not_match(/c2=/, data) + end.respond_with(successful_purchase_response) + end + + def test_purchase_omits_3ds_homephonecountry_when_phone_is_nil + # purchase passes 3ds_homephonecountry when it and phone number are provided + @options[:billing_address][:phone] = '555-444-3333' + @options[:three_ds_2] = { optional: { '3ds_homephonecountry': 'US' } } + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/c2=555-444-3333/, data) + assert_match(/3ds_homephonecountry=US/, data) + end.respond_with(successful_purchase_response) + + # purchase doesn't pass 3ds_homephonecountry when phone number is nil + @options[:billing_address][:phone] = nil + @options[:three_ds_2] = { optional: { '3ds_homephonecountry': 'US' } } + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_not_match(/c2=/, data) + assert_not_match(/3ds_homephonecountry=/, data) + end.respond_with(successful_purchase_response) + end + + def test_stored_credential_recurring_cit_initial + options = stored_credential_options(:cardholder, :recurring, :initial) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/a9=9/, data) + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_stored_credential_recurring_cit_used + options = stored_credential_options(:cardholder, :recurring, id: 'abc123') + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/a9=9/, data) + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_stored_credential_recurring_mit_initial + options = stored_credential_options(:merchant, :recurring, :initial) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/a9=1/, data) + end.respond_with(successful_authorize_response) + assert_match(/z50=abc123/, successful_authorize_response) + assert_success response + end + + def test_stored_credential_recurring_mit_used + options = stored_credential_options(:merchant, :recurring, id: 'abc123') + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/a9=2/, data) + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_stored_credential_installment_cit_initial + options = stored_credential_options(:cardholder, :installment, :initial) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/a9=9/, data) + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_stored_credential_installment_cit_used + options = stored_credential_options(:cardholder, :installment, id: 'abc123') + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/a9=9/, data) + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_stored_credential_installment_mit_initial + options = stored_credential_options(:merchant, :installment, :initial) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/a9=8/, data) + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_stored_credential_installment_mit_used + options = stored_credential_options(:merchant, :installment, id: 'abc123') + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/a9=8/, data) + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_stored_credential_unscheduled_cit_initial + options = stored_credential_options(:cardholder, :unscheduled, :initial) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/a9=9/, data) + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_stored_credential_unscheduled_cit_used + options = stored_credential_options(:cardholder, :unscheduled, id: 'abc123') + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/a9=9/, data) + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_stored_credential_unscheduled_mit_initial + options = stored_credential_options(:merchant, :unscheduled, :initial) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/a9=8/, data) + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_stored_credential_unscheduled_mit_used + options = stored_credential_options(:merchant, :unscheduled, id: 'abc123') + response = stub_comms do + @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 + end + + def test_purchase_with_stored_credential + options = stored_credential_options(:merchant, :recurring, id: 'abc123') + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/a9=2/, data) + assert_match(/g6=abc123/, data) + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_add_transaction_type_overrides_stored_credential_option + options = stored_credential_options(:merchant, :unscheduled, id: 'abc123').merge(transaction_type: '6') + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/a9=6/, data) + 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: 'ISK')) + end.check_request do |_endpoint, data, _headers| + assert_match(/a4=2&a1=/, data) + end.respond_with(successful_authorize_response) + end + + def test_3ds_2_optional_fields_adds_fields_to_the_root_of_the_post + post = {} + options = { three_ds_2: { optional: { '3ds_optional_field_1': :a, '3ds_optional_field_2': :b } } } + + @gateway.add_3ds_2_optional_fields(post, options) + + assert_equal post, { '3ds_optional_field_1': :a, '3ds_optional_field_2': :b } + end + + def test_3ds_2_optional_fields_does_not_overwrite_fields + post = { '3ds_optional_field_1': :existing_value } + options = { three_ds_2: { optional: { '3ds_optional_field_1': :a, '3ds_optional_field_2': :b } } } + + @gateway.add_3ds_2_optional_fields(post, options) + + assert_equal post, { '3ds_optional_field_1': :existing_value, '3ds_optional_field_2': :b } + end + + def test_3ds_2_optional_fields_does_not_empty_fields + post = {} + options = { three_ds_2: { optional: { '3ds_optional_field_1': '', '3ds_optional_field_2': 'null', '3ds_optional_field_3': nil } } } + + @gateway.add_3ds_2_optional_fields(post, options) + + 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) + { + order_id: '#1001', + description: 'AM test', + currency: 'GBP', + customer: '123', + stored_credential: stored_credential(*args, id:) + } + end + def successful_purchase_response 'M=SPREE978&O=1&T=03%2F09%2F2016+03%3A05%3A16&V=413&a1=02617cf5f02ccaed239b6521748298c5&a2=2&a4=100&a9=6&z1=8a82944a5351570601535955efeb513c&z13=606944188282&z14=U&z15=100&z2=0&z3=Transaction+has+been+executed+successfully.&z4=006596&z5=0&z6=00&z9=X&K=057e123af2fba5a37b4df76a7cb5cfb6' end @@ -234,7 +1169,7 @@ def failed_purchase_response end def successful_authorize_response - 'M=SPREE978&O=2&T=03%2F09%2F2016+03%3A08%3A58&V=413&a1=90f7449d555f7bed0a2c5d780475f0bf&a2=2&a4=100&a9=6&z1=8a829449535154bc0153595952a2517a&z13=606944188284&z14=U&z15=100&z2=0&z3=Transaction+has+been+executed+successfully.&z4=006597&z5=0&z6=00&z9=X&K=00effd2c80ab7ecd45b499c0bbea3d20' + 'M=SPREE978&O=2&T=03%2F09%2F2016+03%3A08%3A58&V=413&a1=90f7449d555f7bed0a2c5d780475f0bf&a2=2&a4=100&a9=6&z1=8a829449535154bc0153595952a2517a&z13=606944188284&z14=U&z15=100&z2=0&z3=Transaction+has+been+executed+successfully.&z4=006597&z5=0&z6=00&z9=X&K=00effd2c80ab7ecd45b499c0bbea3d20z50=abc123' end def failed_authorize_response @@ -265,12 +1200,20 @@ def failed_refund_response 'M=SPREE978&O=5&T=03%2F09%2F2016+03%3A16%3A06&V=413&a1=c2b481deffe0e27bdef1439655260092&a2=2&a4=-&a5=EUR&b1=-&z1=1A-1&z2=-9&z3=2.+At+least+one+of+input+parameters+is+malformed.%3A+Parameter+%5Bg4%5D+cannot+be+empty.&K=c2f6112b40c61859d03684ac8e422766' end + def successful_referral_cft_response + 'M=SPREE978&O=34&T=11%2F15%2F2019+15%3A56%3A08&V=413&a1=e852c517da0ffb0cde45671b39165449&a2=2&a4=100&a9=9&b2=2&g2=XZZ72c3228fc3b58525STV56T7YMFAJB&z1=XZZ72e64209459e8C2BAMTBS65MCNGIF&z13=931924132623&z2=0&z3=Transaction+has+been+executed+successfully.&z33=CREDORAX&z34=59990010&z39=XZZ72e64209459e8C2BAMTBS65MCNGIF&z4=HOSTOK&z6=00&K=76f8a35c3357a7613d63438bd86c06d9' + end + + def failed_referral_cft_response + 'T=11%2F15%2F2019+17%3A17%3A45&a1=896ffaf13766fff647d863e8ab0a707c&z1=XZZ7246087744e7993DRONGBWN4RNFWJ&z2=-9&z3=Referred+to+transaction+has+not+been+found.' + end + def successful_credit_response - 'M=SPREE978&O=6&T=03%2F09%2F2016+03%3A16%3A35&V=413&a1=868f8b942fae639d28e27e8933d575d4&a2=2&a4=100&z1=8a82944a53515706015359604c135301&z13=606944188289&z15=100&z2=0&z3=Transaction+has+been+executed+successfully.&z5=0&z6=00&K=51ba24f6ef3aa161f86e53c34c9616ac' + 'M=SPREE978&O=35&T=03%2F09%2F2016+03%3A16%3A35&V=413&a1=868f8b942fae639d28e27e8933d575d4&a2=2&a4=100&z1=8a82944a53515706015359604c135301&z13=606944188289&z15=100&z2=0&z3=Transaction+has+been+executed+successfully.&z5=0&z6=00&K=51ba24f6ef3aa161f86e53c34c9616ac' end def failed_credit_response - 'M=SPREE978&O=6&T=03%2F09%2F2016+03%3A16%3A59&V=413&a1=ff28246cfc811b1c686a52d08d075d9c&a2=2&a4=100&z1=8a829449535154bc01535960a962524f&z13=606944188290&z15=100&z2=05&z3=Transaction+has+been+declined.&z5=0&z6=57&K=cf34816d5c25dc007ef3525505c4c610' + 'M=SPREE978&O=35&T=03%2F09%2F2016+03%3A16%3A59&V=413&a1=ff28246cfc811b1c686a52d08d075d9c&a2=2&a4=100&z1=8a829449535154bc01535960a962524f&z13=606944188290&z15=100&z2=05&z3=Transaction+has+been+declined.&z5=0&z6=57&K=cf34816d5c25dc007ef3525505c4c610' end def empty_purchase_response diff --git a/test/unit/gateways/ct_payment_test.rb b/test/unit/gateways/ct_payment_test.rb new file mode 100644 index 00000000000..98da844e37f --- /dev/null +++ b/test/unit/gateways/ct_payment_test.rb @@ -0,0 +1,302 @@ +require 'test_helper' + +class CtPaymentTest < Test::Unit::TestCase + def setup + @gateway = CtPaymentGateway.new(api_key: 'api_key', company_number: 'company number', merchant_number: 'merchant_number') + @credit_card = credit_card + @amount = 100 + + @options = { + order_id: '1', + billing_address: address, + description: 'Store Purchase' + } + end + + def test_successful_purchase + @gateway.expects(:ssl_post).twice.returns(successful_purchase_response, successful_ack_response) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + + assert_equal '000007708972;443752 ;021efc336262;;', 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 + assert_equal Gateway::STANDARD_ERROR_CODE[:card_declined], response.error_code + end + + def test_successful_authorize + @gateway.expects(:ssl_post).twice.returns(successful_authorize_response, successful_ack_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + + assert_equal '000007708990;448572 ;0e7ebe0a804f;;', response.authorization + assert response.test? + 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 Gateway::STANDARD_ERROR_CODE[:card_declined], response.error_code + end + + def test_successful_capture + @gateway.expects(:ssl_post).twice.returns(successful_capture_response, successful_ack_response) + + response = @gateway.capture(@amount, '000007708990;448572 ;0e7ebe0a804f;', @options) + assert_success response + + assert_equal '000007708991; ;0636aca3dd8e;;', response.authorization + assert response.test? + end + + def test_failed_capture + @gateway.expects(:ssl_post).returns(failed_capture_response) + + response = @gateway.capture(@amount, '0123456789asd;0123456789asdf;12345678', @options) + assert_failure response + assert_equal Gateway::STANDARD_ERROR_CODE[:incorrect_number], response.error_code + end + + def test_successful_refund + @gateway.expects(:ssl_post).twice.returns(successful_refund_response, successful_ack_response) + + response = @gateway.refund(@amount, '000007708990;448572 ;0e7ebe0a804f;', @options) + assert_success response + + assert_equal '000007709004; ;0a08f144b6ea;;', response.authorization + assert response.test? + end + + def test_failed_refund + @gateway.expects(:ssl_post).returns(failed_refund_response) + + response = @gateway.capture(@amount, '0123456789asd;0123456789asdf;12345678', @options) + assert_failure response + assert_equal Gateway::STANDARD_ERROR_CODE[:incorrect_number], response.error_code + end + + def test_successful_void + @gateway.expects(:ssl_post).returns(successful_void_response) + + response = @gateway.void('000007708990;448572 ;0e7ebe0a804f;', @options) + assert_success response + + assert_equal '000007709013; ;0de38871ce96;;', response.authorization + assert response.test? + end + + def test_failed_void + @gateway.expects(:ssl_post).returns(failed_void_response) + + response = @gateway.void('0123456789asd;0123456789asdf;12345678') + assert_failure response + assert_equal Gateway::STANDARD_ERROR_CODE[:incorrect_number], response.error_code + end + + def test_successful_verify + @gateway.expects(:ssl_post).twice.returns(successful_verify_response, successful_ack_response) + + response = @gateway.verify(@credit_card, @options) + assert_success response + + assert_equal '000007709025; ;0b882fe35f69;;', response.authorization + assert response.test? + end + + def test_failed_verify + @gateway.expects(:ssl_post).returns(failed_verify_response) + + response = @gateway.verify(@credit_card, @options) + assert_failure response + assert_equal Gateway::STANDARD_ERROR_CODE[:card_declined], response.error_code + end + + def test_successful_credit + @gateway.expects(:ssl_post).twice.returns(successful_credit_response, successful_ack_response) + + response = @gateway.credit(@amount, @credit_card, @options) + assert_success response + + assert_equal '000007709063; ;054902f2ded0;;', response.authorization + assert response.test? + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + private + + def pre_scrubbed + %q( + opening connection to test.ctpaiement.ca:443... + opened + starting SSL for test.ctpaiement.ca:443... + SSL established + <- "POST /v1/purchase HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: test.ctpaiement.ca\r\nContent-Length: 528\r\n\r\n" + <- "auth-api-key=R46SNTJ42UCJ3264182Y0T087YHBA50RTK&payload=TWVyY2hhbnRUZXJtaW5hbE51bWJlcj0gICAgICZBbW91bnQ9MDAwMDAwMDAxMDAmT3BlcmF0b3JJRD0wMDAwMDAwMCZDdXJyZW5jeUNvZGU9VVNEJkludm9pY2VOdW1iZXI9MDYzZmI1MmMyOTc2JklucHV0VHlwZT1JJkxhbmd1YWdlQ29kZT1FJkNhcmRUeXBlPVYmQ2FyZE51bWJlcj00NTAxMTYxMTA3MjE3MjE0JkV4cGlyYXRpb25EYXRlPTA3MjAmQ2FyZEhvbGRlckFkZHJlc3M9NDU2IE15IFN0cmVldE90dGF3YSAgICAgICAgICAgICAgICAgIE9OJkNhcmRIb2xkZXJQb3N0YWxDb2RlPSAgIEsxQzJONiZDdXN0b21lck51bWJlcj0wMDAwMDAwMCZDb21wYW55TnVtYmVyPTAwNTg5Jk1lcmNoYW50TnVtYmVyPTUzNDAwMDMw" + -> "HTTP/1.1 200 200\r\n" + -> "Date: Fri, 29 Jun 2018 18:21:07 GMT\r\n" + -> "Server: Apache\r\n" + -> "Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\r\n" + -> "Connection: close\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Content-Type: application/json;charset=ISO-8859-1\r\n" + -> "\r\n" + -> "480\r\n" + reading 1152 bytes... + -> "{\"returnCode\":\" 00\",\"errorDescription\":null,\"authorizationNumber\":\"448186 \",\"referenceNumber\":\" \",\"transactionNumber\":\"000007709037\",\"batchNumber\":\"0001\",\"terminalNumber\":\"13366\",\"serverNumber\":\"0001\",\"timeStamp\":\"20180629-14210749\",\"trxCode\":\"00\",\"merchantNumber\":\"53400030\",\"amount\":\"00000000100\",\"invoiceNumber\":\"063fb52c2976\",\"trxType\":\"C\",\"cardType\":\"V\",\"cardNumber\":\"450116XXXXXX7214 \",\"expirationDate\":\"0720\",\"bankTerminalNumber\":\"53400188\",\"trxDate\":\"06292018\",\"trxTime\":\"142107\",\"accountType\":\"0\",\"trxMethod\":\"T@1\",\"languageCode\":\"E\",\"sequenceNumber\":\"000000000028\",\"receiptDisp\":\" APPROVED-THANK YOU \",\"terminalDisp\":\"APPROVED \",\"operatorId\":\"00000000\",\"surchargeAmount\":\"\",\"companyNumber\":\"00589\",\"secureID\":\"\",\"cvv2Cvc2Status\":\" \",\"iopIssuerConfirmationNumber\":null,\"iopIssuerName\":null,\"avsStatus\":null,\"holderName\":null,\"threeDSStatus\":null,\"emvLabel\":null,\"emvAID\":null,\"emvTVR\":null,\"emvTSI\":null,\"emvTC\":null,\"demoMode\":null,\"terminalInvoiceNumber\":null,\"cashbackAmount\":null,\"tipAmount\":null,\"taxAmount\":null,\"cvmResults\":null,\"token\":null,\"customerNumber\":null,\"email\":\"\"}" + read 1152 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + opening connection to test.ctpaiement.ca:443... + opened + starting SSL for test.ctpaiement.ca:443... + SSL established + <- "POST /v1/ack HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: test.ctpaiement.ca\r\nContent-Length: 156\r\n\r\n" + <- "auth-api-key=R46SNTJ42UCJ3264182Y0T087YHBA50RTK&payload=VHJhbnNhY3Rpb25OdW1iZXI9MDAwMDA3NzA5MDM3JkNvbXBhbnlOdW1iZXI9MDA1ODkmTWVyY2hhbnROdW1iZXI9NTM0MDAwMzA=" + -> "HTTP/1.1 200 200\r\n" + -> "Date: Fri, 29 Jun 2018 18:21:08 GMT\r\n" + -> "Server: Apache\r\n" + -> "Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\r\n" + -> "Connection: close\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Content-Type: application/json;charset=ISO-8859-1\r\n" + -> "\r\n" + -> "15\r\n" + reading 21 bytes... + -> "{\"returnCode\":\"true\"}" + read 21 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + ) + end + + def post_scrubbed + %q( + opening connection to test.ctpaiement.ca:443... + opened + starting SSL for test.ctpaiement.ca:443... + SSL established + <- "POST /v1/purchase HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: test.ctpaiement.ca\r\nContent-Length: 528\r\n\r\n" + <- "auth-api-key=[FILTERED]&payload=[FILTERED]" + -> "HTTP/1.1 200 200\r\n" + -> "Date: Fri, 29 Jun 2018 18:21:07 GMT\r\n" + -> "Server: Apache\r\n" + -> "Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\r\n" + -> "Connection: close\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Content-Type: application/json;charset=ISO-8859-1\r\n" + -> "\r\n" + -> "480\r\n" + reading 1152 bytes... + -> "{\"returnCode\":\" 00\",\"errorDescription\":null,\"authorizationNumber\":\"448186 \",\"referenceNumber\":\" \",\"transactionNumber\":\"000007709037\",\"batchNumber\":\"0001\",\"terminalNumber\":\"13366\",\"serverNumber\":\"0001\",\"timeStamp\":\"20180629-14210749\",\"trxCode\":\"00\",\"merchantNumber\":\"53400030\",\"amount\":\"00000000100\",\"invoiceNumber\":\"063fb52c2976\",\"trxType\":\"C\",\"cardType\":\"V\",\"cardNumber\":\"450116XXXXXX7214 \",\"expirationDate\":\"0720\",\"bankTerminalNumber\":\"53400188\",\"trxDate\":\"06292018\",\"trxTime\":\"142107\",\"accountType\":\"0\",\"trxMethod\":\"T@1\",\"languageCode\":\"E\",\"sequenceNumber\":\"000000000028\",\"receiptDisp\":\" APPROVED-THANK YOU \",\"terminalDisp\":\"APPROVED \",\"operatorId\":\"00000000\",\"surchargeAmount\":\"\",\"companyNumber\":\"00589\",\"secureID\":\"\",\"cvv2Cvc2Status\":\" \",\"iopIssuerConfirmationNumber\":null,\"iopIssuerName\":null,\"avsStatus\":null,\"holderName\":null,\"threeDSStatus\":null,\"emvLabel\":null,\"emvAID\":null,\"emvTVR\":null,\"emvTSI\":null,\"emvTC\":null,\"demoMode\":null,\"terminalInvoiceNumber\":null,\"cashbackAmount\":null,\"tipAmount\":null,\"taxAmount\":null,\"cvmResults\":null,\"token\":null,\"customerNumber\":null,\"email\":\"\"}" + read 1152 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + opening connection to test.ctpaiement.ca:443... + opened + starting SSL for test.ctpaiement.ca:443... + SSL established + <- "POST /v1/ack HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: test.ctpaiement.ca\r\nContent-Length: 156\r\n\r\n" + <- "auth-api-key=[FILTERED]&payload=[FILTERED]" + -> "HTTP/1.1 200 200\r\n" + -> "Date: Fri, 29 Jun 2018 18:21:08 GMT\r\n" + -> "Server: Apache\r\n" + -> "Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\r\n" + -> "Connection: close\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Content-Type: application/json;charset=ISO-8859-1\r\n" + -> "\r\n" + -> "15\r\n" + reading 21 bytes... + -> "{\"returnCode\":\"true\"}" + read 21 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + ) + end + + def successful_purchase_response + '{"returnCode":" 00","errorDescription":null,"authorizationNumber":"443752 ","referenceNumber":" ","transactionNumber":"000007708972","batchNumber":"0001","terminalNumber":"13366","serverNumber":"0001","timeStamp":"20180629-12110905","trxCode":"00","merchantNumber":"53400030","amount":"00000000100","invoiceNumber":"021efc336262","trxType":"C","cardType":"V","cardNumber":"450116XXXXXX7214 ","expirationDate":"0720","bankTerminalNumber":"53400188","trxDate":"06292018","trxTime":"121109","accountType":"0","trxMethod":"T@1","languageCode":"E","sequenceNumber":"000000000008","receiptDisp":" APPROVED-THANK YOU ","terminalDisp":"APPROVED ","operatorId":"00000000","surchargeAmount":"","companyNumber":"00589","secureID":"","cvv2Cvc2Status":" ","iopIssuerConfirmationNumber":null,"iopIssuerName":null,"avsStatus":null,"holderName":null,"threeDSStatus":null,"emvLabel":null,"emvAID":null,"emvTVR":null,"emvTSI":null,"emvTC":null,"demoMode":null,"terminalInvoiceNumber":null,"cashbackAmount":null,"tipAmount":null,"taxAmount":null,"cvmResults":null,"token":null,"customerNumber":null,"email":""}' + end + + def successful_ack_response + '{"returnCode":"true"}' + end + + def failed_purchase_response + '{"returnCode":" 05","errorDescription":"Transaction declined","authorizationNumber":" ","referenceNumber":" ","transactionNumber":"000007708975","batchNumber":"0000","terminalNumber":"13366","serverNumber":"0001","timeStamp":"20180629-12272768","trxCode":"00","merchantNumber":"53400030","amount":"00000000100","invoiceNumber":"098096b31937","trxType":"C","cardType":"V","cardNumber":"450224XXXXXX1718 ","expirationDate":"0919","bankTerminalNumber":"53400188","trxDate":"06292018","trxTime":"122727","accountType":"0","trxMethod":"T@1","languageCode":"E","sequenceNumber":"000000000009","receiptDisp":" TRANSACTION NOT APPROVED ","terminalDisp":"05-DECLINE ","operatorId":"00000000","surchargeAmount":"","companyNumber":"00589","secureID":"","cvv2Cvc2Status":" ","iopIssuerConfirmationNumber":null,"iopIssuerName":null,"avsStatus":null,"holderName":null,"threeDSStatus":null,"emvLabel":null,"emvAID":null,"emvTVR":null,"emvTSI":null,"emvTC":null,"demoMode":null,"terminalInvoiceNumber":null,"cashbackAmount":null,"tipAmount":null,"taxAmount":null,"cvmResults":null,"token":null,"customerNumber":null,"email":""}' + end + + def successful_authorize_response + '{"returnCode":" 00","errorDescription":null,"authorizationNumber":"448572 ","referenceNumber":" ","transactionNumber":"000007708990","batchNumber":"0001","terminalNumber":"13367","serverNumber":"0001","timeStamp":"20180629-12501747","trxCode":"01","merchantNumber":"53400030","amount":"00000000100","invoiceNumber":"0e7ebe0a804f","trxType":"C","cardType":"V","cardNumber":"450116XXXXXX7214 ","expirationDate":"0720","bankTerminalNumber":"53400189","trxDate":"06292018","trxTime":"125017","accountType":"0","trxMethod":"T@1","languageCode":"E","sequenceNumber":"000000000014","receiptDisp":" APPROVED-THANK YOU ","terminalDisp":"APPROVED ","operatorId":"00000000","surchargeAmount":"","companyNumber":"00589","secureID":"","cvv2Cvc2Status":" ","iopIssuerConfirmationNumber":null,"iopIssuerName":null,"avsStatus":null,"holderName":null,"threeDSStatus":null,"emvLabel":null,"emvAID":null,"emvTVR":null,"emvTSI":null,"emvTC":null,"demoMode":null,"terminalInvoiceNumber":null,"cashbackAmount":null,"tipAmount":null,"taxAmount":null,"cvmResults":null,"token":null,"customerNumber":null,"email":""}' + end + + def failed_authorize_response + '{"returnCode":" 05","errorDescription":"Transaction declined","authorizationNumber":" ","referenceNumber":" ","transactionNumber":"000007708993","batchNumber":"0000","terminalNumber":"13367","serverNumber":"0001","timeStamp":"20180629-13072751","trxCode":"01","merchantNumber":"53400030","amount":"00000000100","invoiceNumber":"02ec22cbb5db","trxType":"C","cardType":"V","cardNumber":"450224XXXXXX1718 ","expirationDate":"0919","bankTerminalNumber":"53400189","trxDate":"06292018","trxTime":"130727","accountType":"0","trxMethod":"T@1","languageCode":"E","sequenceNumber":"000000000015","receiptDisp":" TRANSACTION NOT APPROVED ","terminalDisp":"05-DECLINE ","operatorId":"00000000","surchargeAmount":"","companyNumber":"00589","secureID":"","cvv2Cvc2Status":" ","iopIssuerConfirmationNumber":null,"iopIssuerName":null,"avsStatus":null,"holderName":null,"threeDSStatus":null,"emvLabel":null,"emvAID":null,"emvTVR":null,"emvTSI":null,"emvTC":null,"demoMode":null,"terminalInvoiceNumber":null,"cashbackAmount":null,"tipAmount":null,"taxAmount":null,"cvmResults":null,"token":null,"customerNumber":null,"email":""}' + end + + def successful_capture_response + '{"returnCode":" 00","errorDescription":null,"authorizationNumber":" ","referenceNumber":" ","transactionNumber":"000007708991","batchNumber":"0001","terminalNumber":"13366","serverNumber":"0001","timeStamp":"20180629-12501869","trxCode":"02","merchantNumber":"53400030","amount":"00000000100","invoiceNumber":"0636aca3dd8e","trxType":"C","cardType":"V","cardNumber":"450116XXXXXX7214 ","expirationDate":"0720","bankTerminalNumber":"53400188","trxDate":"06292018","trxTime":"125018","accountType":"0","trxMethod":"T@1","languageCode":"E","sequenceNumber":"000000000015","receiptDisp":" APPROVED-THANK YOU ","terminalDisp":"APPROVED ","operatorId":"00000000","surchargeAmount":"","companyNumber":"00589","secureID":"","cvv2Cvc2Status":" ","iopIssuerConfirmationNumber":null,"iopIssuerName":null,"avsStatus":null,"holderName":null,"threeDSStatus":null,"emvLabel":null,"emvAID":null,"emvTVR":null,"emvTSI":null,"emvTC":null,"demoMode":null,"terminalInvoiceNumber":null,"cashbackAmount":null,"tipAmount":null,"taxAmount":null,"cvmResults":null,"token":null,"customerNumber":null,"email":""}' + end + + def failed_capture_response + '{"returnCode":"9068","errorDescription":"The original transaction number does not match any actual transaction","authorizationNumber":" ","referenceNumber":" ","transactionNumber":"000007708999","batchNumber":" ","terminalNumber":" ","serverNumber":" ","timeStamp":"20180629-13224441","trxCode":" ","merchantNumber":" ","amount":"00000000000","invoiceNumber":" ","trxType":" ","cardType":" ","cardNumber":" ","expirationDate":" ","bankTerminalNumber":" ","trxDate":" ","trxTime":" ","accountType":" ","trxMethod":" ","languageCode":" ","sequenceNumber":" ","receiptDisp":" OPERATION NON COMPLETEE ","terminalDisp":"9068: Contactez support.","operatorId":" ","surchargeAmount":" ","companyNumber":" ","secureID":"","cvv2Cvc2Status":" ","iopIssuerConfirmationNumber":null,"iopIssuerName":null,"avsStatus":null,"holderName":null,"threeDSStatus":null,"emvLabel":null,"emvAID":null,"emvTVR":null,"emvTSI":null,"emvTC":null,"demoMode":null,"terminalInvoiceNumber":null,"cashbackAmount":null,"tipAmount":null,"taxAmount":null,"cvmResults":null,"token":null,"customerNumber":null,"email":""}' + end + + def successful_refund_response + ' {"returnCode":" 00","errorDescription":null,"authorizationNumber":" ","referenceNumber":" ","transactionNumber":"000007709004","batchNumber":"0001","terminalNumber":"13367","serverNumber":"0001","timeStamp":"20180629-13294388","trxCode":"03","merchantNumber":"53400030","amount":"00000000100","invoiceNumber":"0a08f144b6ea","trxType":"C","cardType":"V","cardNumber":"450116XXXXXX7214 ","expirationDate":"0720","bankTerminalNumber":"53400189","trxDate":"06292018","trxTime":"132944","accountType":"0","trxMethod":"T@1","languageCode":"E","sequenceNumber":"000000000019","receiptDisp":" APPROVED-THANK YOU ","terminalDisp":"APPROVED ","operatorId":"00000000","surchargeAmount":"","companyNumber":"00589","secureID":"","cvv2Cvc2Status":" ","iopIssuerConfirmationNumber":null,"iopIssuerName":null,"avsStatus":null,"holderName":null,"threeDSStatus":null,"emvLabel":null,"emvAID":null,"emvTVR":null,"emvTSI":null,"emvTC":null,"demoMode":null,"terminalInvoiceNumber":null,"cashbackAmount":null,"tipAmount":null,"taxAmount":null,"cvmResults":null,"token":null,"customerNumber":null,"email":""}' + end + + def failed_refund_response + '{"returnCode":"9068","errorDescription":"The original transaction number does not match any actual transaction","authorizationNumber":" ","referenceNumber":" ","transactionNumber":"000007709009","batchNumber":" ","terminalNumber":" ","serverNumber":" ","timeStamp":"20180629-13402119","trxCode":" ","merchantNumber":" ","amount":"00000000000","invoiceNumber":" ","trxType":" ","cardType":" ","cardNumber":" ","expirationDate":" ","bankTerminalNumber":" ","trxDate":" ","trxTime":" ","accountType":" ","trxMethod":" ","languageCode":" ","sequenceNumber":" ","receiptDisp":" OPERATION NON COMPLETEE ","terminalDisp":"9068: Contactez support.","operatorId":" ","surchargeAmount":" ","companyNumber":" ","secureID":"","cvv2Cvc2Status":" ","iopIssuerConfirmationNumber":null,"iopIssuerName":null,"avsStatus":null,"holderName":null,"threeDSStatus":null,"emvLabel":null,"emvAID":null,"emvTVR":null,"emvTSI":null,"emvTC":null,"demoMode":null,"terminalInvoiceNumber":null,"cashbackAmount":null,"tipAmount":null,"taxAmount":null,"cvmResults":null,"token":null,"customerNumber":null,"email":""}' + end + + def successful_void_response + '{"returnCode":" 00","errorDescription":null,"authorizationNumber":" ","referenceNumber":" ","transactionNumber":"000007709013","batchNumber":"0001","terminalNumber":"13367","serverNumber":"0001","timeStamp":"20180629-13451840","trxCode":"04","merchantNumber":"53400030","amount":"00000000100","invoiceNumber":"0de38871ce96","trxType":"C","cardType":"V","cardNumber":"450116XXXXXX7214","expirationDate":"0720","bankTerminalNumber":"53400189","trxDate":"06292018","trxTime":"134518","accountType":"0","trxMethod":"T@1","languageCode":"E","sequenceNumber":"000000000023","receiptDisp":" APPROVED-THANK YOU ","terminalDisp":"APPROVED ","operatorId":"00000000","surchargeAmount":" ","companyNumber":" ","secureID":"","cvv2Cvc2Status":" ","iopIssuerConfirmationNumber":null,"iopIssuerName":null,"avsStatus":null,"holderName":null,"threeDSStatus":null,"emvLabel":null,"emvAID":null,"emvTVR":null,"emvTSI":null,"emvTC":null,"demoMode":null,"terminalInvoiceNumber":null,"cashbackAmount":null,"tipAmount":null,"taxAmount":null,"cvmResults":null,"token":null,"customerNumber":null,"email":""}' + end + + def failed_void_response + '{"returnCode":"9068","errorDescription":"The original transaction number does not match any actual transaction","authorizationNumber":" ","referenceNumber":" ","transactionNumber":"0000000000-1","batchNumber":" ","terminalNumber":" ","serverNumber":" ","timeStamp":"20180629-13520693","trxCode":" ","merchantNumber":" ","amount":"00000000000","invoiceNumber":" ","trxType":" ","cardType":" ","cardNumber":" ","expirationDate":" ","bankTerminalNumber":" ","trxDate":" ","trxTime":" ","accountType":" ","trxMethod":" ","languageCode":" ","sequenceNumber":" ","receiptDisp":" OPERATION NOT COMPLETED ","terminalDisp":"9068: Contact support. ","operatorId":" ","surchargeAmount":" ","companyNumber":" ","secureID":"","cvv2Cvc2Status":" ","iopIssuerConfirmationNumber":null,"iopIssuerName":null,"avsStatus":null,"holderName":null,"threeDSStatus":null,"emvLabel":null,"emvAID":null,"emvTVR":null,"emvTSI":null,"emvTC":null,"demoMode":null,"terminalInvoiceNumber":null,"cashbackAmount":null,"tipAmount":null,"taxAmount":null,"cvmResults":null,"token":null,"customerNumber":null,"email":""}' + end + + def successful_verify_response + '{"returnCode":" 00","errorDescription":null,"authorizationNumber":" ","referenceNumber":" ","transactionNumber":"000007709025","batchNumber":"0001","terminalNumber":"13367","serverNumber":"0001","timeStamp":"20180629-14023575","trxCode":"08","merchantNumber":"53400030","amount":"00000000000","invoiceNumber":"0b882fe35f69","trxType":"C","cardType":"V","cardNumber":"450116XXXXXX7214 ","expirationDate":"0720","bankTerminalNumber":"53400189","trxDate":"06292018","trxTime":"140236","accountType":"0","trxMethod":"T@1","languageCode":"E","sequenceNumber":"000000000025","receiptDisp":" APPROVED-THANK YOU ","terminalDisp":"APPROVED ","operatorId":"00000000","surchargeAmount":"","companyNumber":"00589","secureID":"","cvv2Cvc2Status":" ","iopIssuerConfirmationNumber":null,"iopIssuerName":null,"avsStatus":null,"holderName":null,"threeDSStatus":null,"emvLabel":null,"emvAID":null,"emvTVR":null,"emvTSI":null,"emvTC":null,"demoMode":null,"terminalInvoiceNumber":null,"cashbackAmount":null,"tipAmount":null,"taxAmount":null,"cvmResults":null,"token":null,"customerNumber":null,"email":""}' + end + + def failed_verify_response + '{"returnCode":" 05","errorDescription":"Transaction declined","authorizationNumber":" ","referenceNumber":" ","transactionNumber":"000007709029","batchNumber":"0000","terminalNumber":"13367","serverNumber":"0001","timeStamp":"20180629-14104707","trxCode":"08","merchantNumber":"53400030","amount":"00000000000","invoiceNumber":"0c0054d2bb7a","trxType":"C","cardType":"V","cardNumber":"450224XXXXXX1718 ","expirationDate":"0919","bankTerminalNumber":"53400189","trxDate":"06292018","trxTime":"141047","accountType":"0","trxMethod":"T@1","languageCode":"E","sequenceNumber":"000000000026","receiptDisp":" TRANSACTION NOT APPROVED ","terminalDisp":"05-DECLINE ","operatorId":"00000000","surchargeAmount":"","companyNumber":"00589","secureID":"","cvv2Cvc2Status":" ","iopIssuerConfirmationNumber":null,"iopIssuerName":null,"avsStatus":null,"holderName":null,"threeDSStatus":null,"emvLabel":null,"emvAID":null,"emvTVR":null,"emvTSI":null,"emvTC":null,"demoMode":null,"terminalInvoiceNumber":null,"cashbackAmount":null,"tipAmount":null,"taxAmount":null,"cvmResults":null,"token":null,"customerNumber":null,"email":""}' + end + + def successful_credit_response + '{"returnCode":" 00","errorDescription":null,"authorizationNumber":" ","referenceNumber":" ","transactionNumber":"000007709063","batchNumber":"0001","terminalNumber":"13366","serverNumber":"0001","timeStamp":"20180629-14420931","trxCode":"03","merchantNumber":"53400030","amount":"00000000100","invoiceNumber":"054902f2ded0","trxType":"C","cardType":"V","cardNumber":"450116XXXXXX7214 ","expirationDate":"0720","bankTerminalNumber":"53400188","trxDate":"06292018","trxTime":"144209","accountType":"0","trxMethod":"T@1","languageCode":"E","sequenceNumber":"000000000032","receiptDisp":" APPROVED-THANK YOU ","terminalDisp":"APPROVED ","operatorId":"00000000","surchargeAmount":"","companyNumber":"00589","secureID":"","cvv2Cvc2Status":" ","iopIssuerConfirmationNumber":null,"iopIssuerName":null,"avsStatus":null,"holderName":null,"threeDSStatus":null,"emvLabel":null,"emvAID":null,"emvTVR":null,"emvTSI":null,"emvTC":null,"demoMode":null,"terminalInvoiceNumber":null,"cashbackAmount":null,"tipAmount":null,"taxAmount":null,"cvmResults":null,"token":null,"customerNumber":null,"email":""}' + end +end 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..7a1fe1884ae --- /dev/null +++ b/test/unit/gateways/cyber_source_rest_test.rb @@ -0,0 +1,967 @@ +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') + @carnet_card = credit_card('5062280000000000', brand: 'carnet') + + @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 patagonia_365 tarjeta_sol] + 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_apple_pay_recurring + auth = @gateway.authorize(@amount, @apple_pay, @options) + @options[:stored_credential] = stored_credential(:merchant, :recurring, ntid: auth.network_transaction_id) + response = stub_comms do + @gateway.authorize(@amount, @apple_pay, @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') + assert_nil request['paymentInformation']['tokenizedCard']['cryptogram'] + end.respond_with(successful_purchase_response) + + assert_success 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_successful_credit_with_merchant_category_code + stub_comms do + @gateway.credit(@amount, @credit_card, @options.merge(merchant_category_code: '1111')) + end.check_request do |_endpoint, data, _headers| + json_data = JSON.parse(data) + assert_equal json_data['merchantInformation']['categoryCode'], '1111' + 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_successful_purchase_with_merchant_category_code + stub_comms do + @gateway.purchase(100, @credit_card, @options.merge(merchant_category_code: '1111')) + end.check_request do |_endpoint, data, _headers| + json_data = JSON.parse(data) + assert_equal json_data['merchantInformation']['categoryCode'], '1111' + 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 + + def test_accurate_card_type_and_code_for_carnet + stub_comms do + @gateway.purchase(100, @carnet_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal '002', request['paymentInformation']['card']['type'] + end.respond_with(successful_purchase_response) + end + + def test_failed_void + purchase = '1000|1842651133440156177166|AP4JY+Or4xRonEAOERAyMzQzOTEzMEM0MFZaNUZCBgDH3fgJ8AEGAMfd+AnwAwzRpAAA7RT/|purchase|100|USD|' + + response = stub_comms do + @gateway.void(purchase, @options) + end.respond_with(successful_void_response) + + assert_failure response + assert_equal nil, response.message + assert_equal nil, response.error_code + 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 + + def successful_void_response + <<-RESPONSE + { + "_links":{ + "void":{ + "method": "POST", + "href": "/pts/v2/payments/123/voids" + }, + "self":{ + "method": "GET", + "href": "/pts/v2/payments/123" + } + }, + "clientReferenceInformation":{ + "code": "abcdefg", + "partner": { + "solutionId": "HIJKLMN" + } + }, + "consumerAuthenticationInformation": { + "token": "token123" + }, + "id": "64234623421", + "orderInformation": { + "amountDetails": { + "totalAmount": "6.400", + "authorizedAmount": "6.400", + "currency": "JOD" + } + }, + "paymentAccountInformation":{ + "card": { + "type": "001" + } + }, + "paymentInformation": { + "accountFeatures": { + "group": "0" + }, + "tokenizedCard":{ + "type": "001" + }, + "scheme": "VISA DEBIT", + "bin": "411111", + "accountType": "Visa Classic", + "issuer": "CONOTOXIA SP. Z O.O", + "card": { + "type": "001" + }, + "binCountry": "PL" + }, + "processorInformation": { + "systemTraceAuditNumber": "77788844", + "approvalCode": "7883243", + "networkTransactionId": "0972342342342353", + "retrievalReferenceNumber":"785652341", + "transactionId": "00012321324232", + "responseCode": "00", + "avs": { + "code": "Z", + "codeRaw": "Z" + } + }, + "promotionInformation":{ + "code": "ABC12345", + "description": "percent discount", + "receiptData": "You have received a Visa Offer and saved 20% off your total (max discount for this offer is $20.00).", + "type": "V1" + }, + "reconciliationId": "987552323786342334", + "riskInformation": { + "localTime": "11:24:09", + "score": { + "result":"55", + "factorCodes":["B"], + "modelUsed":"default_uk" + }, + "infoCodes": { + "address":["TOR-TA","TM-TTN"], + "velocity":["TTEL-T6","TTEL-T7"] + }, + "profile": { + "earlyDecision": "ACCEPT" + }, + "casePriority":"3" + }, + "status": "ACCEPTED", + "submitTimeUtc": "2024-09-10T10:24:10Z" + } + RESPONSE + end +end diff --git a/test/unit/gateways/cyber_source_test.rb b/test/unit/gateways/cyber_source_test.rb index 318fc9931f9..565dfc0857d 100644 --- a/test/unit/gateways/cyber_source_test.rb +++ b/test/unit/gateways/cyber_source_test.rb @@ -8,49 +8,92 @@ def setup Base.mode = :test @gateway = CyberSourceGateway.new( - :login => 'l', - :password => 'p' + login: 'l', + password: 'p' ) @amount = 100 @customer_ip = '127.0.0.1' - @credit_card = credit_card('4111111111111111', :brand => 'visa') - @declined_card = credit_card('801111111111111', :brand => 'visa') + @credit_card = credit_card('4111111111111111', brand: 'visa') + @master_credit_card = credit_card('4111111111111111', brand: 'master') + @elo_credit_card = credit_card('5067310000000010', brand: 'elo') + @declined_card = credit_card('801111111111111', brand: 'visa') + @carnet_card = credit_card('5062280000000000', brand: 'carnet') + @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) + @apple_pay_discover = network_tokenization_credit_card('6011111111111117', + brand: 'discover', + transaction_id: '123', + eci: '05', + payment_cryptogram: '111111111100cryptogram', + source: :apple_pay) + @apple_pay_master = network_tokenization_credit_card('6011111111111117', + brand: 'master', + transaction_id: '123', + eci: '05', + payment_cryptogram: '111111111100cryptogram', + source: :apple_pay) + @google_pay = network_tokenization_credit_card('4242424242424242', source: :google_pay) @check = check() @options = { - :ip => @customer_ip, - :order_id => '1000', - :line_items => [ - { - :declared_value => @amount, - :quantity => 2, - :code => 'default', - :description => 'Giant Walrus', - :sku => 'WA323232323232323' - }, - { - :declared_value => @amount, - :quantity => 2, - :description => 'Marble Snowcone', - :sku => 'FAKE1232132113123' - } - ], - :currency => 'USD' + ip: @customer_ip, + order_id: '1000', + line_items: [ + { + declared_value: @amount, + quantity: 2, + code: 'default', + description: 'Giant Walrus', + sku: 'WA323232323232323', + tax_amount: '10', + national_tax: '5' + } + ], + currency: 'USD', + reconciliation_id: '181537' } @subscription_options = { - :order_id => generate_unique_id, - :credit_card => @credit_card, - :setup_fee => 100, - :subscription => { - :frequency => 'weekly', - :start_date => Date.today.next_week, - :occurrences => 4, - :automatic_renew => true, - :amount => 100 + order_id: generate_unique_id, + credit_card: @credit_card, + setup_fee: 100, + subscription: { + frequency: 'weekly', + start_date: Date.today.next_week, + occurrences: 4, + automatic_renew: true, + amount: 100 } } + + @issuer_additional_data = 'PR25000000000011111111111112222222sk111111111111111111111111111' + + '1111111115555555222233101abcdefghijkl7777777777777777777777777promotionCde' + end + + def test_supported_card_types + assert_equal CyberSourceGateway.supported_cardtypes, %i[visa master american_express discover diners_club jcb dankort maestro elo patagonia_365 tarjeta_sol] end def test_successful_credit_card_purchase @@ -59,7 +102,83 @@ def test_successful_credit_card_purchase assert response = @gateway.purchase(@amount, @credit_card, @options) assert_equal 'Successful transaction', response.message assert_success response - assert_equal "#{@options[:order_id]};#{response.params['requestID']};#{response.params['requestToken']};purchase;100;USD;", response.authorization + assert_equal "#{@options[:order_id]};#{response.params['requestID']};#{response.params['requestToken']};purchase;100;USD;;credit_card", response.authorization + assert response.test? + end + + def test_successful_purchase_with_other_tax_fields + stub_comms do + @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 + + def test_successful_purchase_with_merchant_category_code + stub_comms do + @gateway.purchase(100, @credit_card, @options.merge!(merchant_category_code: '1111')) + end.check_request do |_endpoint, data, _headers| + assert_match(/1111<\/merchantCategoryCode>/, data) + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_purchase_totals_data + stub_comms do + @gateway.purchase(100, @credit_card, @options.merge(discount_management_indicator: 'T', purchase_tax_amount: 7.89, original_amount: 1.23, invoice_amount: 1.23)) + end.check_request do |_endpoint, data, _headers| + assert_match(/\s+USD<\/currency>\s+T<\/discountManagementIndicator>\s+7.89<\/taxAmount>\s+1.00<\/grandTotalAmount>\s+1.23<\/originalAmount>\s+1.23<\/invoiceAmount>\s+<\/purchaseTotals>/m, data) + end.respond_with(successful_purchase_response) + end + + def test_successful_authorize_with_national_tax_indicator + national_tax_indicator = 1 + stub_comms do + @gateway.authorize(100, @credit_card, @options.merge(national_tax_indicator:)) + end.check_request do |_endpoint, data, _headers| + assert_match(/\s+#{national_tax_indicator}<\/nationalTaxIndicator>\s+<\/otherTax>/m, data) + end.respond_with(successful_authorization_response) + end + + def test_successful_authorize_with_merchant_category_code + stub_comms do + @gateway.authorize(100, @credit_card, @options.merge(merchant_category_code: '1111')) + end.check_request do |_endpoint, data, _headers| + assert_match(/1111<\/merchantCategoryCode>/, data) + end.respond_with(successful_authorization_response) + end + + def test_successful_authorize_with_cc_auth_service_fields + stub_comms do + @gateway.authorize(100, @credit_card, @options.merge(mobile_remote_payment_type: 'T')) + end.check_request do |_endpoint, data, _headers| + assert_match(/T<\/mobileRemotePaymentType>/, data) + end.respond_with(successful_authorization_response) + end + + def test_successful_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) + + assert response = @gateway.purchase(@amount, @elo_credit_card, @options) + assert_equal 'Successful transaction', response.message + assert_success response + assert_equal "#{@options[:order_id]};#{response.params['requestID']};#{response.params['requestToken']};purchase;100;USD;;credit_card", response.authorization assert response.test? end @@ -72,22 +191,254 @@ def test_purchase_includes_customer_ip @gateway.purchase(@amount, @credit_card, @options) end + def test_purchase_includes_issuer_additional_data + stub_comms do + @gateway.purchase(100, @credit_card, order_id: '1', issuer_additional_data: @issuer_additional_data) + end.check_request do |_endpoint, data, _headers| + assert_match(/\s+#{@issuer_additional_data}<\/additionalData>\s+<\/issuer>/m, data) + end.respond_with(successful_purchase_response) + end + def test_purchase_includes_mdd_fields stub_comms do @gateway.purchase(100, @credit_card, order_id: '1', mdd_field_2: 'CustomValue2', mdd_field_3: 'CustomValue3') - end.check_request do |endpoint, data, headers| - assert_match(/field2>CustomValue2.*field3>CustomValue3CustomValue2181537<\/reconciliationID>/, data) + end.respond_with(successful_purchase_response) + end + + def test_merchant_description + stub_comms do + @gateway.authorize(100, @credit_card, merchant_descriptor_name: 'Test Name', merchant_descriptor_address1: '123 Main Dr', merchant_descriptor_locality: 'Durham') + end.check_request do |_endpoint, data, _headers| + assert_match(%r(.*Test Name.*)m, data) + assert_match(%r(.*123 Main Dr.*)m, data) + assert_match(%r(.*Durham.*)m, data) + end.respond_with(successful_purchase_response) + end + + def test_allows_nil_values_in_billing_address + billing_address = { + address1: '123 Fourth St', + city: 'Fiveton', + state: '', + country: 'CA' + } + + stub_comms do + @gateway.authorize(100, @credit_card, billing_address:) + end.check_request do |_endpoint, data, _headers| + assert_nil billing_address[:zip] + assert_nil billing_address[:phone] + assert_match(%r(.*123 Fourth St.*)m, data) + assert_match(%r(.*Fiveton.*)m, data) + assert_match(%r(.*NC.*)m, data) + assert_match(%r(.*00000.*)m, data) + assert_match(%r(.*CA.*)m, data) + end.respond_with(successful_purchase_response) + end + + def test_uses_names_from_billing_address_if_present + name = 'Wesley Crusher' + + stub_comms do + @gateway.authorize(100, @credit_card, billing_address: { name: }) + end.check_request do |_endpoint, data, _headers| + assert_match(%r(.*Wesley.*)m, data) + assert_match(%r(.*Crusher.*)m, data) + end.respond_with(successful_purchase_response) + end + + def test_uses_names_from_shipping_address_if_present + name = 'Wesley Crusher' + + stub_comms do + @gateway.authorize(100, @credit_card, shipping_address: { name: }) + end.check_request do |_endpoint, data, _headers| + assert_match(%r(.*Wesley.*)m, data) + assert_match(%r(.*Crusher.*)m, data) + end.respond_with(successful_purchase_response) + end + + def test_uses_names_from_the_payment_method + stub_comms do + @gateway.authorize(100, @credit_card) + end.check_request do |_endpoint, data, _headers| + assert_match(%r(.*#{@credit_card.first_name}.*)m, data) + assert_match(%r(.*#{@credit_card.last_name}.*)m, data) + assert_match(%r(.*#{@credit_card.first_name}.*)m, data) + assert_match(%r(.*#{@credit_card.last_name}.*)m, data) + end.respond_with(successful_purchase_response) + end + + def test_purchase_includes_invoice_header + stub_comms do + @gateway.purchase(100, @credit_card, merchant_descriptor: 'Spreedly', reference_data_code: '3A', invoice_number: '1234567', merchant_descriptor_city: 'test123', submerchant_id: 'AVSBSGDHJMNGFR', merchant_descriptor_country: 'US', merchant_descriptor_state: 'NY') + end.check_request do |_endpoint, data, _headers| + assert_match(/Spreedly<\/merchantDescriptor>/, data) + assert_match(/3A<\/referenceDataCode>/, data) + assert_match(/1234567<\/invoiceNumber>/, data) + assert_match(/test123<\/merchantDescriptorCity>/, data) + assert_match(/AVSBSGDHJMNGFR<\/submerchantID>/, data) + assert_match(/US<\/merchantDescriptorCountry>/, data) + assert_match(/NY<\/merchantDescriptorState>/, data) + end.respond_with(successful_purchase_response) + end + + def test_purchase_with_apple_pay_includes_payment_solution_001 + stub_comms do + @gateway.purchase(100, @apple_pay) + end.check_request do |_endpoint, data, _headers| + assert_match(/001<\/paymentSolution>/, data) + end.respond_with(successful_purchase_response) + end + + def test_purchase_with_google_pay_includes_payment_solution_012 + stub_comms do + @gateway.purchase(100, @google_pay) + end.check_request do |_endpoint, data, _headers| + assert_match(/012<\/paymentSolution>/, data) + end.respond_with(successful_purchase_response) + end + + def test_purchase_includes_tax_management_indicator + stub_comms do + @gateway.purchase(100, @credit_card, tax_management_indicator: 3) + end.check_request do |_endpoint, data, _headers| + assert_match(/3<\/taxManagementIndicator>/, data) + end.respond_with(successful_purchase_response) + end + + 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) + end.check_request do |_endpoint, data, _headers| + assert_match(/\s+#{@issuer_additional_data}<\/additionalData>\s+<\/issuer>/m, data) + end.respond_with(successful_authorization_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| - assert_match(/field2>CustomValue2.*field3>CustomValue3CustomValue2181537<\/reconciliationID>/, data) + end.respond_with(successful_authorization_response) + end + + def test_authorize_includes_commerce_indicator + stub_comms do + @gateway.authorize(100, @credit_card, commerce_indicator: 'internet') + end.check_request do |_endpoint, data, _headers| + assert_match(/internet<\/commerceIndicator>/m, data) + end.respond_with(successful_authorization_response) + end + + def test_authorize_includes_installment_data + stub_comms do + @gateway.authorize(100, @credit_card, order_id: '1', installment_total_count: 5, installment_plan_type: 1, first_installment_date: '300101', installment_total_amount: 5.05, installment_annual_interest_rate: 1.09, installment_grace_period_duration: 3) + end.check_request do |_endpoint, data, _headers| + assert_xml_valid_to_xsd(data) + assert_match(/\s+5<\/totalCount>\s+5.05<\/totalAmount>\s+1<\/planType>\s+300101<\/firstInstallmentDate>\s+1.09<\/annualInterestRate>\s+3<\/gracePeriodDuration>\s+<\/installment>/, data) + end.respond_with(successful_authorization_response) + end + + def test_authorize_includes_less_installment_data + stub_comms do + @gateway.authorize(100, @credit_card, order_id: '1', installment_grace_period_duration: 3) + end.check_request do |_endpoint, data, _headers| + assert_xml_valid_to_xsd(data) + assert_match(/\s+3<\/gracePeriodDuration>\s+<\/installment>/, data) + end.respond_with(successful_authorization_response) + end + + def test_authorize_includes_customer_id + stub_comms do + @gateway.authorize(100, @credit_card, customer_id: '5afefb801188d70023b7debb') + end.check_request do |_endpoint, data, _headers| + assert_match(/5afefb801188d70023b7debb<\/customerID>/, data) + 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') + end.check_request do |_endpoint, data, _headers| + assert_match(%r(.*123.*)m, data) + assert_not_match(%r(.*123.*)m, data) + end.respond_with(successful_authorization_response) + end + + def test_authorize_includes_sales_slip_number + stub_comms do + @gateway.authorize(100, @credit_card, order_id: '1', sales_slip_number: '123') + end.check_request do |_endpoint, data, _headers| + assert_match(/123<\/salesSlipNumber>/, data) + end.respond_with(successful_authorization_response) + end + + def test_authorize_includes_airline_agent_code + stub_comms do + @gateway.authorize(100, @credit_card, order_id: '1', airline_agent_code: '7Q') + end.check_request do |_endpoint, data, _headers| + assert_match(/\s+7Q<\/agentCode>\s+<\/airlineData>/, data) end.respond_with(successful_authorization_response) end + def test_bank_account_purchase_includes_sec_code + stub_comms do + @gateway.purchase(@amount, @check, order_id: '1', sec_code: 'WEB') + end.check_request do |_endpoint, data, _headers| + assert_match(%r(.*WEB.*)m, data) + end.respond_with(successful_authorization_response) + end def test_successful_check_purchase @gateway.expects(:ssl_post).returns(successful_purchase_response) @@ -95,35 +446,46 @@ def test_successful_check_purchase assert response = @gateway.purchase(@amount, @check, @options) assert_equal 'Successful transaction', response.message assert_success response - assert_equal "#{@options[:order_id]};#{response.params['requestID']};#{response.params['requestToken']};purchase;100;USD;", response.authorization + assert_equal "#{@options[:order_id]};#{response.params['requestID']};#{response.params['requestToken']};purchase;100;USD;;check", response.authorization assert response.test? end def test_successful_pinless_debit_card_purchase @gateway.expects(:ssl_post).returns(successful_purchase_response) - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(:pinless_debit_card => true)) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(pinless_debit_card: true)) assert_equal 'Successful transaction', response.message assert_success response - assert_equal "#{@options[:order_id]};#{response.params['requestID']};#{response.params['requestToken']};purchase;100;USD;", response.authorization + assert_equal "#{@options[:order_id]};#{response.params['requestID']};#{response.params['requestToken']};purchase;100;USD;;credit_card", response.authorization assert response.test? end def test_successful_credit_cart_purchase_single_request_ignore_avs - @gateway.expects(:ssl_post).with do |host, request_body| + @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, @credit_card, options) + 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) - assert response = @gateway.purchase(@amount, @credit_card, @options.merge( - ignore_avs: true - )) + 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| + @gateway.expects(:ssl_post).with do |_host, request_body| assert_not_match %r'', request_body assert_not_match %r'', request_body true @@ -132,35 +494,129 @@ def test_successful_credit_cart_purchase_single_request_without_ignore_avs # globally ignored AVS for gateway instance: @gateway.options[:ignore_avs] = true - assert response = @gateway.purchase(@amount, @credit_card, @options.merge( - ignore_avs: false - )) + options = @options.merge(ignore_avs: false) + assert response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + + @gateway.expects(:ssl_post).with do |_host, request_body| + assert_not_match %r'', request_body + assert_not_match %r'', request_body + true + end.returns(successful_purchase_response) + + options = @options.merge(ignore_avs: 'false') + assert response = @gateway.purchase(@amount, @credit_card, options) assert_success response end def test_successful_credit_cart_purchase_single_request_ignore_ccv - @gateway.expects(:ssl_post).with do |host, request_body| + @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) - 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 def test_successful_credit_cart_purchase_single_request_without_ignore_ccv - @gateway.expects(:ssl_post).with do |host, request_body| + @gateway.expects(:ssl_post).with do |_host, request_body| assert_not_match %r'', request_body assert_not_match %r'', request_body 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| + assert_not_match %r'', request_body + assert_not_match %r'', request_body + true + end.returns(successful_purchase_response) + + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(ignore_cvv: 'false')) + assert_success response + end + + def test_successful_apple_pay_purchase_subsequent_auth_visa + @gateway.expects(:ssl_post).with do |_host, request_body| + assert_not_match %r'', request_body + assert_not_match %r'', request_body + assert_match %r'internet', request_body + true + end.returns(successful_purchase_response) + + options = @options.merge({ + stored_credential: { + initiator: 'merchant', + reason_type: 'unscheduled', + network_transaction_id: '016150703802094' + } + }) + assert response = @gateway.purchase(@amount, @apple_pay, options) + assert_success response + end + + def test_successful_apple_pay_purchase_subsequent_auth_mastercard + @gateway.expects(:ssl_post).with do |_host, request_body| + assert_not_match %r'', request_body + assert_match %r'internet', request_body + true + end.returns(successful_purchase_response) + + credit_card = network_tokenization_credit_card('5555555555554444', + brand: 'master', + transaction_id: '123', + eci: '05', + payment_cryptogram: '111111111100cryptogram', + source: :apple_pay) + options = @options.merge({ + stored_credential: { + initiator: 'merchant', + reason_type: 'unscheduled', + network_transaction_id: '016150703802094' + } + }) + assert response = @gateway.purchase(@amount, credit_card, options) + assert_success response + end + + def test_successful_apple_pay_purchase_subsequent_auth_discover + @gateway.expects(:ssl_post).with do |_host, request_body| + assert_match %r'', request_body + assert_match %r'dipb', request_body + true + end.returns(successful_purchase_response) + + options = @options.merge(enable_cybs_discover_apple_pay: true) + + assert response = @gateway.purchase(@amount, @apple_pay_discover, options) + assert_success response + end + + def test_successful_apple_pay_purchase_with_master + @gateway.expects(:ssl_post).with do |_host, request_body| + assert_not_match %r'', request_body + assert_not_match %r'', request_body + assert_match %r'spa', request_body + true + end.returns(successful_purchase_response) + + assert response = @gateway.purchase(@amount, @apple_pay_master, @options) assert_success response end @@ -199,6 +655,32 @@ 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) + assert_equal Response, response.class + assert response.success? + assert response.test? + end + def test_successful_credit_card_tax_request @gateway.stubs(:ssl_post).returns(successful_tax_response) assert response = @gateway.calculate_tax(@credit_card, @options) @@ -207,6 +689,32 @@ def test_successful_credit_card_tax_request assert response.test? end + def test_successful_credit_card_tax_request_with_amounts + stub_comms do + @gateway.calculate_tax(@credit_card, @options) + end.check_request do |_endpoint, data, _headers| + doc = REXML::Document.new(data) + REXML::XPath.each(doc, '//item') do |item| + request_item = @options[:line_items][item.attributes['id'].to_i] + assert_match(request_item[:tax_amount], item.get_elements('taxAmount')[0].text) + assert_match(request_item[:national_tax], item.get_elements('nationalTax')[0].text) + end + end.respond_with(successful_tax_response) + end + + def test_successful_credit_card_authorize_request_with_line_items + stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + doc = REXML::Document.new(data) + REXML::XPath.each(doc, '//item') do |item| + request_item = @options[:line_items][item.attributes['id'].to_i] + assert_match(request_item[:tax_amount], item.get_elements('taxAmount')[0].text) + assert_match(request_item[:national_tax], item.get_elements('nationalTax')[0].text) + end + end.respond_with(successful_tax_response) + end + def test_successful_credit_card_capture_request @gateway.stubs(:ssl_post).returns(successful_authorization_response, successful_capture_response) assert response = @gateway.authorize(@amount, @credit_card, @options) @@ -217,43 +725,123 @@ def test_successful_credit_card_capture_request assert response_capture.test? end - def test_successful_credit_card_purchase_request - @gateway.stubs(:ssl_post).returns(successful_capture_response) - assert response = @gateway.purchase(@amount, @credit_card, @options) - assert response.success? - assert response.test? + def test_capture_includes_local_tax_amount + stub_comms do + @gateway.capture(100, '1842651133440156177166', local_tax_amount: '0.17') + end.check_request do |_endpoint, data, _headers| + assert_match(/\s+0.17<\/localTaxAmount>\s+<\/otherTax>/, data) + end.respond_with(successful_capture_response) end - def test_successful_check_purchase_request - @gateway.stubs(:ssl_post).returns(successful_capture_response) - assert response = @gateway.purchase(@amount, @check, @options) - assert response.success? - assert response.test? + def test_capture_includes_national_tax_amount + stub_comms do + @gateway.capture(100, '1842651133440156177166', national_tax_amount: '0.05') + end.check_request do |_endpoint, data, _headers| + assert_match(/\s+0.05<\/nationalTaxAmount>\s+<\/otherTax>/, data) + end.respond_with(successful_capture_response) end - def test_requires_error_on_tax_calculation_without_line_items - assert_raise(ArgumentError){ @gateway.calculate_tax(@credit_card, @options.delete_if{|key, val| key == :line_items})} + def test_capture_with_additional_tax_fields + stub_comms do + @gateway.capture(100, '1842651133440156177166', user_po: 'ABC123', taxable: true, national_tax_indicator: 1) + end.check_request do |_endpoint, data, _headers| + assert_match(/ABC123<\/userPO>/, data) + assert_match(/true<\/taxable>/, data) + assert_match(/1<\/nationalTaxIndicator>/, data) + end.respond_with(successful_capture_response) end - def test_default_currency - assert_equal 'USD', CyberSourceGateway.default_currency + def test_capture_includes_gratuity_amount + stub_comms do + @gateway.capture(100, '1842651133440156177166', gratuity_amount: '3.05') + end.check_request do |_endpoint, data, _headers| + assert_match(/3.05<\/gratuityAmount>/, data) + end.respond_with(successful_capture_response) end - def test_successful_credit_card_store_request - @gateway.stubs(:ssl_post).returns(successful_create_subscription_response) - assert response = @gateway.store(@credit_card, @subscription_options) + def test_successful_credit_card_capture_with_elo_request + @gateway.stubs(:ssl_post).returns(successful_authorization_response, successful_capture_response) + assert response = @gateway.authorize(@amount, @elo_credit_card, @options) assert response.success? assert response.test? + assert response_capture = @gateway.capture(@amount, response.authorization) + assert response_capture.success? + assert response_capture.test? end - def test_successful_credit_card_update_request - @gateway.stubs(:ssl_post).returns(successful_create_subscription_response, successful_update_subscription_response) - assert response = @gateway.store(@credit_card, @subscription_options) - assert response.success? - assert response.test? - assert response = @gateway.update(response.authorization, @credit_card, @subscription_options) - assert response.success? - assert response.test? + 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| + assert_match(/CustomValue21111<\/merchantCategoryCode>/, data) + end.respond_with(successful_capture_response) + end + + def test_successful_credit_card_purchase_request + @gateway.stubs(:ssl_post).returns(successful_capture_response) + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert response.success? + assert response.test? + end + + def test_successful_credit_card_purchase_request_with_line_items + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + doc = REXML::Document.new(data) + REXML::XPath.each(doc, '//item') do |item| + request_item = @options[:line_items][item.attributes['id'].to_i] + assert_match(request_item[:tax_amount], item.get_elements('taxAmount')[0].text) + assert_match(request_item[:national_tax], item.get_elements('nationalTax')[0].text) + end + end.respond_with(successful_purchase_response) + end + + def test_successful_credit_card_purchase_with_elo_request + @gateway.stubs(:ssl_post).returns(successful_capture_response) + assert response = @gateway.purchase(@amount, @elo_credit_card, @options) + assert response.success? + assert response.test? + end + + def test_successful_check_purchase_request + @gateway.stubs(:ssl_post).returns(successful_capture_response) + assert response = @gateway.purchase(@amount, @check, @options) + assert response.success? + assert response.test? + end + + def test_requires_error_on_tax_calculation_without_line_items + assert_raise(ArgumentError) { @gateway.calculate_tax(@credit_card, @options.delete_if { |key, _val| key == :line_items }) } + end + + def test_default_currency + assert_equal 'USD', CyberSourceGateway.default_currency + end + + def test_successful_credit_card_store_request + @gateway.stubs(:ssl_post).returns(successful_create_subscription_response) + assert response = @gateway.store(@credit_card, @subscription_options) + assert response.success? + assert response.test? + end + + def test_successful_credit_card_update_request + @gateway.stubs(:ssl_post).returns(successful_create_subscription_response, successful_update_subscription_response) + assert response = @gateway.store(@credit_card, @subscription_options) + assert response.success? + assert response.test? + assert response = @gateway.update(response.authorization, @credit_card, @subscription_options) + assert response.success? + assert response.test? end def test_successful_credit_card_unstore_request @@ -261,7 +849,7 @@ def test_successful_credit_card_unstore_request assert response = @gateway.store(@credit_card, @subscription_options) assert response.success? assert response.test? - assert response = @gateway.unstore(response.authorization, :order_id => generate_unique_id) + assert response = @gateway.unstore(response.authorization, order_id: generate_unique_id) assert response.success? assert response.test? end @@ -271,7 +859,7 @@ def test_successful_credit_card_retrieve_request assert response = @gateway.store(@credit_card, @subscription_options) assert response.success? assert response.test? - assert response = @gateway.retrieve(response.authorization, :order_id => generate_unique_id) + assert response = @gateway.retrieve(response.authorization, order_id: generate_unique_id) assert response.success? assert response.test? end @@ -283,207 +871,1266 @@ def test_avs_result assert_equal 'Y', response.avs_result['code'] end - def test_cvv_result - @gateway.expects(:ssl_post).returns(successful_purchase_response) + def test_cvv_result + @gateway.expects(:ssl_post).returns(successful_purchase_response) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_equal 'M', response.cvv_result['code'] + end + + def test_successful_refund_request + @gateway.stubs(:ssl_post).returns(successful_capture_response, successful_refund_response) + assert_success(response = @gateway.purchase(@amount, @credit_card, @options)) + + assert_success(@gateway.refund(@amount, response.authorization)) + end + + def test_successful_refund_with_elo_request + @gateway.stubs(:ssl_post).returns(successful_capture_response, successful_refund_response) + assert_success(response = @gateway.purchase(@amount, @elo_credit_card, @options)) + + assert_success(@gateway.refund(@amount, response.authorization)) + end + + def test_successful_refund_with_merchant_category_code + stub_comms do + @gateway.refund(100, 'test;12345', @options.merge!(merchant_category_code: '1111')) + end.check_request do |_endpoint, data, _headers| + assert_match(/1111<\/merchantCategoryCode>/, data) + end.respond_with(successful_refund_response) + end + + def test_successful_credit_to_card_request + @gateway.stubs(:ssl_post).returns(successful_card_credit_response) + + assert_success(@gateway.credit(@amount, @credit_card, @options)) + end + + def test_successful_adjust_auth_request + @gateway.stubs(:ssl_post).returns(successful_incremental_auth_response) + assert_success(response = @gateway.authorize(@amount, @credit_card, @options)) + + assert_success(@gateway.adjust(@amount, response.authorization, @options)) + end + + def test_authorization_under_review_request + @gateway.stubs(:ssl_post).returns(authorization_review_response) + + assert_failure(response = @gateway.authorize(@amount, @credit_card, @options)) + assert response.fraud_review? + assert_equal(response.authorization, "#{@options[:order_id]};#{response.params['requestID']};#{response.params['requestToken']};authorize;100;USD;;") + end + + def test_successful_credit_to_subscription_request + @gateway.stubs(:ssl_post).returns(successful_create_subscription_response, successful_subscription_credit_response) + + assert response = @gateway.store(@credit_card, @subscription_options) + assert response.success? + assert response.test? + assert_success(@gateway.credit(@amount, response.authorization, @options)) + end + + def test_credit_includes_merchant_descriptor + stub_comms do + @gateway.credit(@amount, @credit_card, merchant_descriptor: 'Spreedly') + end.check_request do |_endpoint, data, _headers| + assert_match(/Spreedly<\/merchantDescriptor>/, data) + end.respond_with(successful_card_credit_response) + end + + def test_credit_includes_issuer_additional_data + stub_comms do + @gateway.credit(@amount, @credit_card, issuer_additional_data: @issuer_additional_data) + end.check_request do |_endpoint, data, _headers| + assert_match(/\s+#{@issuer_additional_data}<\/additionalData>\s+<\/issuer>/m, data) + end.respond_with(successful_card_credit_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| + assert_match(/CustomValue2\s+#{@issuer_additional_data}<\/additionalData>\s+<\/issuer>/m, data) + end.respond_with(successful_void_response) + end + + def test_void_includes_mdd_fields + authorization = '1000;1842651133440156177166;AP4JY+Or4xRonEAOERAyMzQzOTEzMEM0MFZaNUZCBgDH3fgJ8AEGAMfd+AnwAwzRpAAA7RT/;authorize;100;USD;' + + stub_comms do + @gateway.void(authorization, mdd_field_2: 'CustomValue2', mdd_field_3: 'CustomValue3') + end.check_request do |_endpoint, data, _headers| + assert_match(/CustomValue21.00<\/amount>), data + end.respond_with(successful_update_subscription_response) + end + + def test_successful_verify + response = stub_comms(@gateway, :ssl_request) do + @gateway.verify(@credit_card, @options) + end.respond_with(successful_authorization_response) + assert_success response + end + + def test_successful_verify_zero_amount_request + @options[:zero_amount_auth] = true + stub_comms(@gateway, :ssl_post) do + @gateway.verify(@credit_card, @options) + end.check_request(skip_response: true) do |_endpoint, data| + assert_match %r(0.00<\/grandTotalAmount>), data + end + end + + def test_successful_verify_request + stub_comms(@gateway, :ssl_post) do + @gateway.verify(@credit_card, @options) + end.check_request(skip_response: true) do |_endpoint, data| + assert_match %r(1.00<\/grandTotalAmount>), data + end + end + + def test_successful_verify_with_elo + response = stub_comms(@gateway, :ssl_request) do + @gateway.verify(@elo_credit_card, @options) + end.respond_with(successful_authorization_response) + assert_success response + end + + def test_unsuccessful_verify + response = stub_comms(@gateway, :ssl_request) do + @gateway.verify(@credit_card, @options) + end.respond_with(unsuccessful_authorization_response) + assert_failure response + assert_equal 'Invalid account number', response.message + end + + def test_successful_auth_with_network_tokenization_for_visa + response = stub_comms do + @gateway.authorize(@amount, @network_token, @options) + end.check_request do |_endpoint, body, _headers| + assert_xml_valid_to_xsd(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 + response = stub_comms do + @gateway.purchase(@amount, @network_token, @options) + end.check_request do |_endpoint, body, _headers| + assert_xml_valid_to_xsd(body) + assert_match %r'.+?'m, body + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_auth_with_network_tokenization_for_mastercard + @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', + brand: 'master', + transaction_id: '123', + eci: '05', + payment_cryptogram: '111111111100cryptogram', + source: :network_token + ) + + 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'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', + brand: 'american_express', + transaction_id: '123', + eci: '05', + payment_cryptogram: Base64.encode64('111111111100cryptogram'), + source: :network_token + ) + + assert response = @gateway.authorize(@amount, credit_card, @options) + assert_success response + end + + def test_cof_first + @options[:stored_credential] = { + initiator: 'cardholder', + reason_type: '', + initial_transaction: true, + network_transaction_id: '' + } + @options[:commerce_indicator] = 'internet' + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/\true/, data) + assert_not_match(/\true/, data) + assert_not_match(/\/, data) + assert_not_match(/\/, data) + assert_match(/\internet/, data) + end.respond_with(successful_authorization_response) + assert response.success? + end + + def test_cof_cit_auth + @options[:stored_credential] = { + initiator: 'cardholder', + reason_type: 'unscheduled', + initial_transaction: false, + network_transaction_id: '' + } + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/\/, data) + assert_match(/\/, data) + assert_not_match(/\/, data) + assert_not_match(/\/, data) + end.respond_with(successful_authorization_response) + assert response.success? + end + + def test_cof_unscheduled_mit_auth + @options[:stored_credential] = { + initiator: 'merchant', + reason_type: 'unscheduled', + initial_transaction: false, + network_transaction_id: '016150703802094' + } + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_not_match(/\/, data) + assert_match(/\true/, data) + assert_match(/\true/, data) + assert_match(/\016150703802094/, data) + end.respond_with(successful_authorization_response) + assert response.success? + end + + def test_cof_installment_mit_auth + @options[:stored_credential] = { + initiator: 'merchant', + reason_type: 'installment', + initial_transaction: false, + network_transaction_id: '016150703802094' + } + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_not_match(/\/, data) + assert_not_match(/\/, data) + assert_match(/\true/, data) + assert_match(/\016150703802094/, data) + assert_match(/\install/, data) + end.respond_with(successful_authorization_response) + assert response.success? + end + + def test_cof_recurring_mit_auth + @options[:stored_credential] = { + initiator: 'merchant', + reason_type: 'recurring', + initial_transaction: false, + network_transaction_id: '016150703802094' + } + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_not_match(/\/, data) + assert_not_match(/\/, data) + assert_match(/\true/, data) + assert_match(/\016150703802094/, data) + assert_match(/\recurring/, data) + end.respond_with(successful_authorization_response) + assert response.success? + end + + def test_cof_recurring_mit_purchase + @options[:stored_credential] = { + initiator: 'merchant', + reason_type: 'recurring', + initial_transaction: false, + network_transaction_id: '016150703802094' + } + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_not_match(/\/, data) + assert_not_match(/\/, data) + assert_match(/\true/, data) + assert_match(/\016150703802094/, data) + assert_match(/\recurring/, data) + end.respond_with(successful_purchase_response) + assert response.success? + end + + def test_cof_first_with_overrides + @options[:stored_credential] = { + initiator: 'cardholder', + reason_type: '', + initial_transaction: true, + network_transaction_id: '' + } + @options[:stored_credential_overrides] = { + subsequent_auth: 'true', + subsequent_auth_first: 'false', + subsequent_auth_stored_credential: 'true', + subsequent_auth_transaction_id: '54321' + } + @options[:commerce_indicator] = 'internet' + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/\false/, data) + assert_match(/\true/, data) + assert_match(/\true/, data) + assert_match(/\54321/, data) + assert_match(/\internet/, data) + end.respond_with(successful_authorization_response) + assert response.success? + end + + def test_nonfractional_currency_handling + @gateway.expects(:ssl_post).with do |_host, request_body| + assert_match %r(1), request_body + assert_match %r(JPY), request_body + true + end.returns(successful_nonfractional_authorization_response) + + assert response = @gateway.authorize(100, @credit_card, @options.merge(currency: 'JPY')) + 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_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(/\install/, 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(/\install/, 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_match(/\/, data) + assert_match(/\true/, data) + assert_not_match(/\true/, data) + assert_not_match(/\016150703802094/, data) + assert_match(/\install/, 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(/\recurring/, 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(/\recurring/, 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_match(/\/, data) + assert_match(/\true/, data) + assert_not_match(/\true/, data) + assert_not_match(/\016150703802094/, data) + assert_match(/\recurring/, 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) + + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_match %r(Missing end tag for), response.message + assert response.test? + end + + def test_3ds_enroll_response + three_ds_options = { + three_ds_2: { + browser_info: { + accept_header: 'unknown', + depth: 100, + java: false, + language: 'US', + height: 1000, + width: 500, + timezone: '-120', + user_agent: 'unknown' + } + }, + return_url: 'return_url.com', + payer_auth_enroll_service: true + } + purchase = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge!(three_ds_options)) + end.check_request do |_endpoint, data, _headers| + browser_info = three_ds_options.dig(:three_ds_2, :browser_info) + assert_match(/\/, data) + assert_match(/\#{browser_info[:depth]}\<\/httpBrowserColorDepth\>/, data) + assert_match(/\#{browser_info[:java]}\<\/httpBrowserJavaEnabled\>/, data) + assert_match(/\#{browser_info[:user_agent]}\<\/httpUserAgent\>/, data) + assert_match(/\#{three_ds_options[:return_url]}\<\/returnURL\>/, data) + end.respond_with(threedeesecure_purchase_response) + + assert_failure purchase + assert_equal 'YTJycDdLR3RIVnpmMXNFejJyazA=', purchase.params['xid'] + assert_equal 'eNpVUe9PwjAQ/d6/ghA/r2tBYMvRBEUFFEKQEP1Yu1Om7gfdJoy/3nZsgk2a3Lveu757B+utRhw/oyo0CphjlskPbIXBsC25TvuPD/lkc3xn2d2R6y+3LWA5WuFOwA/qLExiwRzX4UAbSEwLrbYyzgVItbuZLkS353HWA1pDAhHq6Vgw3ule9/pAT5BALCMUqnwznZJCKwRaZQiopIhzXYpB1wXaAAKF/hbbPE8zn9L9fu9cUB2VREBtAQF6FrQsbJSZOQ9hIF7Xs1KNg6dVZzXdxGk0f1nc4+eslMfREKitIBDIHAV3WZ+Z2+Ku3/F8bjRXeQIysmrEFeOOa0yoIYHUfjQ6Icbt02XGTFRojbFqRmoQATykSYymxlD+YjPDWfntxBqrcusg8wbmWGcrXNFD4w3z2IkfVkZRy6H13mi9YhP9W/0vhyyqPw==', purchase.params['paReq'] + assert_equal 'https://0eafstag.cardinalcommerce.com/EAFService/jsp/v1/redirect', purchase.params['acsURL'] + end + + def test_3ds_validate_response + validation = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(payer_auth_validate_service: true, authentication_transaction_id: 'ABC123')) + end.check_request do |_endpoint, data, _headers| + assert_match(/\/, data) + assert_match(/\ABC123\<\/authenticationTransactionID\>/, data) + end.respond_with(successful_threedeesecure_validate_response) + + assert_success validation + end + + def test_adds_3ds_brand_based_commerce_indicator + %w(visa maestro master american_express jcb discover diners_club).each do |brand| + @credit_card.brand = brand + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(three_d_secure: { cavv: 'anything but empty' })) + end.check_request do |_endpoint, data, _headers| + assert_match(/commerceIndicator\>#{CyberSourceGateway::ECI_BRAND_MAPPING[brand.to_sym]}#{eci}/, data) + assert_match(/#{cavv}/, data) + assert_match(/#{version}/, data) + assert_match(/#{ds_transaction_id}/, data) + assert_match(/#{authentication_response_status}/, data) + assert_match(/#{cavv_algorithm}/, data) + assert_match(/#{commerce_indicator}/, data) + assert_match(/#{enrolled}/, data) + end.respond_with(successful_purchase_response) + end + + def test_does_not_add_3ds2_fields_via_normalized_hash_when_cavv_and_commerce_indicator_absent + options = options_with_normalized_3ds(cavv: nil, commerce_indicator: nil) + stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_, data, _| + assert_not_match(/#{options[:three_d_secure][:eci]}#{options[:three_d_secure][:cavv]}#{options[:three_d_secure][:version]}#{options[:three_d_secure][:ds_transaction_id]}#{options[:three_d_secure][:authentication_response_status]}#{options[:three_d_secure][:cavv_algorithm]}#{options[:three_d_secure][:enrolled]}#{options[:commerce_indicator]}#{options[:three_d_secure][:eci]}#{options[:three_d_secure][:version]}#{options[:three_d_secure][:ds_transaction_id]}#{options[:three_d_secure][:authentication_response_status]}#{options[:three_d_secure][:cavv_algorithm]}#{options[:three_d_secure][:enrolled]}#{options[:three_d_secure][:eci]}#{options[:three_d_secure][:version]}#{options[:three_d_secure][:ds_transaction_id]}#{options[:three_d_secure][:authentication_response_status]}#{options[:three_d_secure][:cavv_algorithm]}#{options[:three_d_secure][:enrolled]}#{options[:commerce_indicator]}#{options[:three_d_secure][:eci]}#{options[:three_d_secure][:cavv]}#{options[:three_d_secure][:version]}#{options[:three_d_secure][:ds_transaction_id]}#{options[:three_d_secure][:authentication_response_status]}#{options[:three_d_secure][:cavv_algorithm]}#{options[:three_d_secure][:enrolled]}#{eci}/, data) + assert_match(/#{cavv}/, data) + assert_match(/#{version}/, data) + assert_match(/#{ds_transaction_id}/, data) + assert_match(/#{cavv_algorithm}/, data) + assert_match(/#{commerce_indicator}/, data) + assert_match(/#{collection_indicator}/, data) + end.respond_with(successful_purchase_response) + end + + def test_adds_mastercard_3ds2_default_collection_indicator + options_with_normalized_3ds = @options.merge( + three_d_secure: { + version: '2.0', + eci: '05', + cavv: '637574652070757070792026206b697474656e73', + ds_transaction_id: '97267598-FAE6-48F2-8083-C23433990FBC', + cavv_algorithm: 'vbv' + } + ) + + stub_comms do + @gateway.purchase(@amount, @master_credit_card, options_with_normalized_3ds) + end.check_request do |_endpoint, data, _headers| + assert_match(/2/, data) + end.respond_with(successful_purchase_response) + end + + def test_send_xid_for_3ds_1_regardless_of_cc_brand + options_with_normalized_3ds = @options.merge( + three_d_secure: { + eci: '05', + cavv: '637574652070757070792026206b697474656e73', + xid: 'this-is-an-xid', + ds_transaction_id: '97267598-FAE6-48F2-8083-C23433990FBC', + cavv_algorithm: 'vbv' + } + ) + + stub_comms do + @gateway.purchase(@amount, @elo_credit_card, options_with_normalized_3ds) + end.check_request do |_endpoint, data, _headers| + assert_match(/this-is-an-xid/, data) + end.respond_with(successful_purchase_response) + end + + def test_dont_send_cavv_as_xid_in_3ds2_for_mastercard + options_with_normalized_3ds = @options.merge( + three_d_secure: { + version: '2.0', + eci: '05', + cavv: '637574652070757070792026206b697474656e73', + xid: 'this-is-an-xid', + ds_transaction_id: '97267598-FAE6-48F2-8083-C23433990FBC', + cavv_algorithm: 'vbv' + } + ) + + stub_comms do + @gateway.purchase(@amount, @master_credit_card, options_with_normalized_3ds) + end.check_request do |_endpoint, data, _headers| + assert_match(/this-is-an-xid/, data) + end.respond_with(successful_purchase_response) + end + + def test_adds_cavv_as_xid_for_3ds2 + cavv = '637574652070757070792026206b697474656e73' + + options_with_normalized_3ds = @options.merge( + three_d_secure: { + version: '2.0', + eci: '05', + cavv:, + ds_transaction_id: '97267598-FAE6-48F2-8083-C23433990FBC', + cavv_algorithm: 'vbv' + } + ) + + stub_comms do + @gateway.purchase(@amount, @credit_card, options_with_normalized_3ds) + end.check_request do |_endpoint, data, _headers| + assert_match(/#{cavv}/, data) + end.respond_with(successful_purchase_response) + end + + def test_does_not_add_cavv_as_xid_if_xid_is_present + options_with_normalized_3ds = @options.merge( + three_d_secure: { + version: '2.0', + eci: '05', + cavv: '637574652070757070792026206b697474656e73', + xid: 'this-is-an-xid', + ds_transaction_id: '97267598-FAE6-48F2-8083-C23433990FBC', + cavv_algorithm: 'vbv' + } + ) + + stub_comms do + @gateway.purchase(@amount, @credit_card, options_with_normalized_3ds) + end.check_request do |_endpoint, data, _headers| + assert_match(/this-is-an-xid/, data) + 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: '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 - response = @gateway.purchase(@amount, @credit_card, @options) - assert_equal 'M', response.cvv_result['code'] + def test_scrub_network_token + assert_equal @gateway.scrub(pre_scrubbed_network_token), post_scrubbed_network_token end - def test_successful_refund_request - @gateway.stubs(:ssl_post).returns(successful_capture_response, successful_refund_response) - assert_success(response = @gateway.purchase(@amount, @credit_card, @options)) + def test_supports_scrubbing? + assert @gateway.supports_scrubbing? + end - assert_success(@gateway.refund(@amount, response.authorization)) + def test_supports_network_tokenization + assert_instance_of TrueClass, @gateway.supports_network_tokenization? end - def test_successful_credit_request - @gateway.stubs(:ssl_post).returns(successful_create_subscription_response, successful_credit_response) + def test_does_not_throw_on_invalid_xml + raw_response = mock + raw_response.expects(:body).returns(invalid_xml_response) + exception = ActiveMerchant::ResponseError.new(raw_response) + @gateway.expects(:ssl_post).raises(exception) - assert response = @gateway.store(@credit_card, @subscription_options) - assert response.success? - assert response.test? - assert_success(@gateway.credit(@amount, response.authorization, @options)) + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_failure response end - def test_successful_void_capture_request - @gateway.stubs(:ssl_post).returns(successful_capture_response, successful_auth_reversal_response) - assert response_capture = @gateway.capture(@amount, '1846925324700976124593') - assert response_capture.success? - assert response_capture.test? - assert response_auth_reversal = @gateway.void(response_capture.authorization, @options) - assert response_auth_reversal.success? + def test_address_email_has_a_default_when_email_option_is_empty + stub_comms do + @gateway.authorize(100, @credit_card, email: '') + end.check_request do |_endpoint, data, _headers| + assert_match('null@cybersource.com', data) + end.respond_with(successful_capture_response) end - def test_successful_void_authorization_request - @gateway.stubs(:ssl_post).returns(successful_authorization_response, successful_void_response) - assert response = @gateway.authorize(@amount, @credit_card, @options) - assert response.success? - assert response.test? - assert response_void = @gateway.void(response.authorization, @options) - assert response_void.success? + def test_country_code_sent_as_default_when_submitted_as_empty_string + stub_comms do + @gateway.authorize(100, @credit_card, billing_address: { country: '' }) + end.check_request do |_endpoint, data, _headers| + assert_match('US', data) + end.respond_with(successful_capture_response) end - def test_validate_pinless_debit_card_request - @gateway.stubs(:ssl_post).returns(successful_validate_pinless_debit_card) - assert response = @gateway.validate_pinless_debit_card(@credit_card, @options) - assert response.success? - assert_success(@gateway.void(response.authorization, @options)) - end + def test_default_address_does_not_override_when_hash_keys_are_strings + stub_comms do + @gateway.authorize(100, @credit_card, billing_address: { + 'address1' => '221B Baker Street', + 'city' => 'London', + 'zip' => 'NW16XE', + 'country' => 'GB' + }) + end.check_request do |_endpoint, data, _headers| + assert_match('221B Baker Street', data) + assert_match('London', data) + assert_match('NW16XE', data) + assert_match('GB', data) + end.respond_with(successful_capture_response) + end + + def test_adds_application_id_as_partner_solution_id + partner_id = 'partner_id' + CyberSourceGateway.application_id = partner_id - def test_validate_add_subscription_amount stub_comms do - @gateway.store(@credit_card, @subscription_options) - end.check_request do |endpoint, data, headers| - assert_match %r(1.00<\/amount>), data - end.respond_with(successful_update_subscription_response) - end + @gateway.authorize(100, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match("#{partner_id}", data) + end.respond_with(successful_capture_response) + ensure + CyberSourceGateway.application_id = nil + end + + def test_partner_solution_id_position_follows_schema + partner_id = 'partner_id' + CyberSourceGateway.application_id = partner_id + + @options[:stored_credential] = { + initiator: 'cardholder', + reason_type: '', + initial_transaction: true, + network_transaction_id: '' + } + @options[:commerce_indicator] = 'internet' - def test_successful_verify - response = stub_comms(@gateway, :ssl_request) do - @gateway.verify(@credit_card, @options) - end.respond_with(successful_authorization_response) - assert_success response + stub_comms do + @gateway.authorize(100, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match("\n#{partner_id}\ntrue\n\n", data) + end.respond_with(successful_capture_response) + ensure + CyberSourceGateway.application_id = nil end - def test_unsuccessful_verify - response = stub_comms(@gateway, :ssl_request) do - @gateway.verify(@credit_card, @options) - end.respond_with(unsuccessful_authorization_response) + def test_missing_field + @gateway.expects(:ssl_post).returns(missing_field_response) + + response = @gateway.purchase(@amount, credit_card, @options) + assert_failure response - assert_equal 'Invalid account number', response.message + assert_equal 'c:billTo/c:country', response.params['missingField'] 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' - ) + def test_invalid_field + @gateway.expects(:ssl_post).returns(invalid_field_response) - response = stub_comms do - @gateway.authorize(@amount, credit_card, @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 - end.respond_with(successful_purchase_response) + response = @gateway.purchase(@amount, credit_card, @options) - assert_success response + assert_failure response + assert_equal 'c:billTo/c:postalCode', response.params['invalidField'] 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' - ) + 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 = stub_comms do - @gateway.purchase(@amount, credit_card, @options) - end.check_request do |_endpoint, body, _headers| - assert_xml_valid_to_xsd(body) - assert_match %r'.+?'m, body - end.respond_with(successful_purchase_response) + response = @gateway.authorize(@amount, credit_card, @options.merge!(auto_void_230: true)) - assert_success response + 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_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 - true - end.returns(successful_purchase_response) + def test_cvv_mismatch + @gateway.expects(:ssl_post).returns(cvv_mismatch_response) + @gateway.expects(:void).never - credit_card = network_tokenization_credit_card('5555555555554444', - :brand => 'mastercard', - :transaction_id => '123', - :eci => '05', - :payment_cryptogram => '111111111100cryptogram' - ) + response = @gateway.purchase(@amount, credit_card, @options) - assert response = @gateway.authorize(@amount, credit_card, @options) - assert_success response + 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_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 - true - end.returns(successful_purchase_response) + 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)) - credit_card = network_tokenization_credit_card('378282246310005', - :brand => 'american_express', - :transaction_id => '123', - :eci => '05', - :payment_cryptogram => Base64.encode64('111111111100cryptogram') - ) + 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 - assert response = @gateway.authorize(@amount, credit_card, @options) - assert_success response + def test_able_to_properly_handle_40bytes_cryptogram + long_cryptogram = "NZwc40C4eTDWHVDXPekFaKkNYGk26w+GYDZmU50cATbjqOpNxR/eYA==\n" + credit_card = network_tokenization_credit_card('4111111111111111', brand: 'american_express', payment_cryptogram: long_cryptogram) + + stub_comms do + @gateway.authorize(@amount, credit_card, @options) + end.check_request(skip_response: true) do |_endpoint, body, _headers| + assert_xml_valid_to_xsd(body) + first_half = Base64.encode64(Base64.decode64(long_cryptogram)[0...20]) + second_half = Base64.encode64(Base64.decode64(long_cryptogram)[20...40]) + assert_match %r{#{first_half}}, body + assert_match %r{#{second_half}}, body + end end - def test_nonfractional_currency_handling - @gateway.expects(:ssl_post).with do |host, request_body| - assert_match %r(1), request_body - assert_match %r(JPY), request_body - true - end.returns(successful_nonfractional_authorization_response) + def test_able_to_properly_handle_20bytes_cryptogram + credit_card = network_tokenization_credit_card('4111111111111111', brand: 'american_express', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=') - assert response = @gateway.authorize(100, @credit_card, @options.merge(currency: 'JPY')) - assert_success response + stub_comms do + @gateway.authorize(@amount, credit_card, @options) + end.check_request(skip_response: true) do |_endpoint, body, _headers| + assert_xml_valid_to_xsd(body) + assert_match %r{#{credit_card.payment_cryptogram}\n}, body + assert_not_match %r{}, body + end end - def test_malformed_xml_handling - @gateway.expects(:ssl_post).returns(malformed_xml_response) + 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) - assert response = @gateway.purchase(@amount, @credit_card, @options) - assert_failure response - assert_match %r(Missing end tag for), response.message - assert response.test? + 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_3ds_enroll_response - purchase = stub_comms do - @gateway.purchase(@amount, @credit_card, @options.merge(payer_auth_enroll_service: true)) - end.check_request do |endpoint, data, headers| - assert_match(/\/, data) - end.respond_with(threedeesecure_purchase_response) + 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) - assert_failure purchase - assert_equal 'YTJycDdLR3RIVnpmMXNFejJyazA=', purchase.params['xid'] - assert_equal 'eNpVUe9PwjAQ/d6/ghA/r2tBYMvRBEUFFEKQEP1Yu1Om7gfdJoy/3nZsgk2a3Lveu757B+utRhw/oyo0CphjlskPbIXBsC25TvuPD/lkc3xn2d2R6y+3LWA5WuFOwA/qLExiwRzX4UAbSEwLrbYyzgVItbuZLkS353HWA1pDAhHq6Vgw3ule9/pAT5BALCMUqnwznZJCKwRaZQiopIhzXYpB1wXaAAKF/hbbPE8zn9L9fu9cUB2VREBtAQF6FrQsbJSZOQ9hIF7Xs1KNg6dVZzXdxGk0f1nc4+eslMfREKitIBDIHAV3WZ+Z2+Ku3/F8bjRXeQIysmrEFeOOa0yoIYHUfjQ6Icbt02XGTFRojbFqRmoQATykSYymxlD+YjPDWfntxBqrcusg8wbmWGcrXNFD4w3z2IkfVkZRy6H13mi9YhP9W/0vhyyqPw==', purchase.params['paReq'] - assert_equal 'https://0eafstag.cardinalcommerce.com/EAFService/jsp/v1/redirect', purchase.params['acsURL'] + 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_3ds_validate_response - validation = stub_comms do - @gateway.purchase(@amount, @credit_card, @options.merge(payer_auth_validate_service: true, pares: 'ABC123')) - end.check_request do |endpoint, data, headers| - assert_match(/\/, data) - assert_match(/\ABC123\<\/signedPARes\>/, data) - end.respond_with(successful_threedeesecure_validate_response) + 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_success validation + 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_scrub - assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + def test_routing_number_formatting_with_regular_routing_number + assert_equal @gateway.send(:format_routing_number, '012345678', { currency: 'USD' }), '012345678' end - def test_supports_scrubbing? - assert @gateway.supports_scrubbing? + def test_routing_number_formatting_with_canadian_routing_number + assert_equal @gateway.send(:format_routing_number, '12345678', { currency: 'USD' }), '12345678' end - def test_supports_network_tokenization - assert_instance_of TrueClass, @gateway.supports_network_tokenization? + def test_routing_number_formatting_with_canadian_routing_number_and_padding + assert_equal @gateway.send(:format_routing_number, '012345678', { currency: 'CAD' }), '12345678' + end + + def test_accurate_card_type_and_code_for_carnet + stub_comms do + @gateway.purchase(100, @carnet_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/002<\/cardType>/, data) + end.respond_with(successful_purchase_response) end private + def options_with_normalized_3ds( + cavv: '637574652070757070792026206b697474656e73', + commerce_indicator: 'commerce_indicator' + ) + xid = 'Y2FyZGluYWxjb21tZXJjZWF1dGg=' + authentication_response_status = 'Y' + cavv_algorithm = 2 + collection_indicator = 2 + ds_transaction_id = '97267598-FAE6-48F2-8083-C23433990FBC' + eci = '05' + enrolled = 'Y' + version = '2.0' + @options.merge( + three_d_secure: { + version:, + eci:, + xid:, + cavv:, + ds_transaction_id:, + cavv_algorithm:, + enrolled:, + authentication_response_status: + }, + commerce_indicator:, + collection_indicator: + ).compact + end + + def supported_cc_brand_without_inferred_commerce_indicator + (ActiveMerchant::Billing::CyberSourceGateway.supported_cardtypes - + ActiveMerchant::Billing::CyberSourceGateway::ECI_BRAND_MAPPING.keys).first + end + def pre_scrubbed <<-PRE_SCRUBBED opening connection to ics2wstest.ic3.com:443... @@ -508,6 +2155,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... @@ -533,26 +2228,34 @@ def post_scrubbed end def successful_purchase_response - <<-XML - - -2008-01-15T21:42:03.343Zb0a6cf9aa07f1a8495f89c364bbd6a9a2004333231260008401927ACCEPT100Afvvj7Ke2Fmsbq0wHFE2sM6R4GAptYZ0jwPSA+R9PhkyhFTb0KRjoE4+ynthZrG6tMBwjAtTUSD1001.00123456YYMM2008-01-15T21:42:03Z00U + <<~XML + + + 2008-01-15T21:42:03.343Zb0a6cf9aa07f1a8495f89c364bbd6a9a2004333231260008401927ACCEPT100Afvvj7Ke2Fmsbq0wHFE2sM6R4GAptYZ0jwPSA+R9PhkyhFTb0KRjoE4+ynthZrG6tMBwjAtTUSD1001.00123456YYMM2008-01-15T21:42:03Z00U + 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 - - -2007-07-12T18:31:53.838ZTEST111111111111842651133440156177166ACCEPT100AP4JY+Or4xRonEAOERAyMzQzOTEzMEM0MFZaNUZCBgDH3fgJ8AEGAMfd+AnwAwzRpAAA7RT/USD1001.00004542AI72007-07-12T18:31:53Z10023439130C40VZ2FB + <<~XML + + + 2007-07-12T18:31:53.838ZTEST111111111111842651133440156177166ACCEPT100AP4JY+Or4xRonEAOERAyMzQzOTEzMEM0MFZaNUZCBgDH3fgJ8AEGAMfd+AnwAwzRpAAA7RT/USD1001.00004542AI72007-07-12T18:31:53Z10023439130C40VZ2FB XML end def unsuccessful_authorization_response - <<-XML - - -2008-01-15T21:50:41.580Za1efca956703a2a5037178a8a28f73572004338415330008402434REJECT231Afvvj7KfIgU12gooCFE2/DanQIApt+G1OgTSA+R9PTnyhFTb0KRjgFY+ynyIFNdoKKAghwgx231 + <<~XML + + + 2008-01-15T21:50:41.580Za1efca956703a2a5037178a8a28f73572004338415330008402434REJECT231Afvvj7KfIgU12gooCFE2/DanQIApt+G1OgTSA+R9PTnyhFTb0KRjgFY+ynyIFNdoKKAghwgx231 XML end @@ -615,10 +2318,10 @@ def unsuccessful_authorization_response_with_reply end def successful_tax_response - <<-XML - - -2007-07-11T18:27:56.314ZTEST111111111111841784762620176127166ACCEPT100AMYJY9fl62i+vx2OEQYAx9zv/9UBZAAA5h5D1001.000Madison000WI0537170 + <<~XML + + + 2007-07-11T18:27:56.314ZTEST111111111111841784762620176127166ACCEPT100AMYJY9fl62i+vx2OEQYAx9zv/9UBZAAA5h5D1001.000Madison000WI0537170 XML end @@ -647,93 +2350,137 @@ def successful_delete_subscription_response end def successful_capture_response - <<-XML - 2007-07-17T17:15:32.642Ztest11111111111111111846925324700976124593ACCEPT100AP4JZB883WKS/34BEZAzMTE1OTI5MVQzWE0wQjEzBTUt3wbOAQUy3D7oDgMMmvQAnQglGBP1002007-07-17T17:15:32Z1.0031159291T3XM2B13 + <<~XML + 2007-07-17T17:15:32.642Ztest11111111111111111846925324700976124593ACCEPT100AP4JZB883WKS/34BEZAzMTE1OTI5MVQzWE0wQjEzBTUt3wbOAQUy3D7oDgMMmvQAnQglGBP1002007-07-17T17:15:32Z1.0031159291T3XM2B13 XML end def successful_refund_response - <<-XML - - -2008-01-21T16:00:38.927ZTEST111111111112009312387810008401927ACCEPT100Af/vj7OzPmut/eogHFCrBiwYsWTJy1r127CpCn0KdOgyTZnzKwVYCmzPmVgr9ID5H1WGTSTKuj0i30IE4+zsz2d/QNzwBwAACCPAUSD1002008-01-21T16:00:38Z1.00010112295WW70TBOPSSP2 + <<~XML + + + 2008-01-21T16:00:38.927ZTEST111111111112009312387810008401927ACCEPT100Af/vj7OzPmut/eogHFCrBiwYsWTJy1r127CpCn0KdOgyTZnzKwVYCmzPmVgr9ID5H1WGTSTKuj0i30IE4+zsz2d/QNzwBwAACCPAUSD1002008-01-21T16:00:38Z1.00010112295WW70TBOPSSP2 XML end - def successful_credit_response - <<-XML - - -2008-01-21T16:00:38.927ZTEST111111111112009312387810008401927ACCEPT100Af/vj7OzPmut/eogHFCrBiwYsWTJy1r127CpCn0KdOgyTZnzKwVYCmzPmVgr9ID5H1WGTSTKuj0i30IE4+zsz2d/QNzwBwAACCPAUSD1002012-09-28T16:59:25Z1.00010112295WW70TBOPSSP2 + def successful_incremental_auth_response + <<~XML + + + 2021-06-28T14:24:21.043Z26be3073f9e9d3ae20a1219085d66a316248902608336713204007ACCEPT120Axj37wSTUsDxxi1huMEn/6As2ZNHDlgybMGrJs2bOHLZg0YMWACojiSd78TSAAk3DxpJl6MV4IPICcmpYHjhsUsHHNVA1F6vUSD1200.00831000002021-06-28T14:24:21Z6248902605266689604010016153570198200A 118962!0100.00V1001BPDiscount: 0.10 ; Max Units: 20.002.00BP VISA Discount XML end - def successful_retrieve_subscription_response - <<-XML + def successful_card_credit_response + <<~XML + \n\n2019-05-16T20:25:05.234Z329b25a4540e05c731a4fb16112e4c725580383051126990804008ACCEPT100Ahj/7wSTLoNfMt0KyZQoGxDdm1ctGjlmo0/RdCA4BUafouhAdpAfJHYQyaSZbpAdvSeAnJl0GvmW6FZMoUAA/SE0USD1002019-05-16T20:25:05Z1.007359449300012345678901201234567 + XML + end + + def successful_subscription_credit_response + <<~XML - 2012-05-15T14:29:52.833Z0da9f4799515bfbfb85cbf6ab8839cde3370921927710176056428ACCEPT100AhjzbwSRbXng4q9oFCjYIAKb7zXE/n0gAQsQyaSZV0ekrf+AaAAA+Q2H100falsefalse411111XXXXXX1111092013001OttawaWidgets IncCAUSDsomeguy1232@fakeemail.net99991231JIMon-demandSMITHcredit card0K1C2N620120521ONCURRENT1234 My StreetApt 133709219062501760564280 + 2008-01-21T16:00:38.927ZTEST111111111112009312387810008401927ACCEPT100Af/vj7OzPmut/eogHFCrBiwYsWTJy1r127CpCn0KdOgyTZnzKwVYCmzPmVgr9ID5H1WGTSTKuj0i30IE4+zsz2d/QNzwBwAACCPAUSD1002012-09-28T16:59:25Z1.00010112295WW70TBOPSSP2 XML end - def successful_validate_pinless_debit_card + def successful_retrieve_subscription_response <<-XML - - -2013-05-13T13:52:57.159Z64270133684531771310176056442ACCEPT100AhijbwSRj3pM2QqPs2j0Ip+xoJXIsAMPYZNJMq6PSbs5ATAA6z421002013-05-13T13:52:57ZY + + + 2012-05-15T14:29:52.833Z0da9f4799515bfbfb85cbf6ab8839cde3370921927710176056428ACCEPT100AhjzbwSRbXng4q9oFCjYIAKb7zXE/n0gAQsQyaSZV0ekrf+AaAAA+Q2H100falsefalse411111XXXXXX1111092013001OttawaWidgets IncCAUSDsomeguy1232@fakeemail.net99991231JIMon-demandSMITHcredit card0K1C2N620120521ONCURRENT1234 My StreetApt 133709219062501760564280 XML end def successful_auth_reversal_response - <<-XML - - -2016-07-25T21:10:31.506Z296805293329eea14917a8d04c63a0c44694810311256262804010ACCEPT100Ahj//wSR/QMpn9U9RwRUIkG7Nm4cMm7KVRrS4tppCS5TonESgFLhgHRTp0gPkYP4ZNJMt0gO3pPFAnI/oGUyy27D1uIA+xVKUSD1001.001002016-07-25T21:10:31Z + <<~XML + + + 2016-07-25T21:10:31.506Z296805293329eea14917a8d04c63a0c44694810311256262804010ACCEPT100Ahj//wSR/QMpn9U9RwRUIkG7Nm4cMm7KVRrS4tppCS5TonESgFLhgHRTp0gPkYP4ZNJMt0gO3pPFAnI/oGUyy27D1uIA+xVKUSD1001.001002016-07-25T21:10:31Z XML end def successful_void_response - <<-XML - - -2016-07-25T20:50:50.583Zbb3b1bb530192c9dd20f121686c91c404694798504476543904007ACCEPT100Ahj//wSR/QLVu2z/GtIOIkG7Nm4bNW7KPRrRY0mvYS4YB0I7QFLgkgkAA0gAwfwyaSZbpAdvSeeBOR/QLVqII/qE+QAA3yVtUSD1002016-07-25T20:50:50Z1.00usd + <<~XML + + + 2016-07-25T20:50:50.583Zbb3b1bb530192c9dd20f121686c91c404694798504476543904007ACCEPT100Ahj//wSR/QLVu2z/GtIOIkG7Nm4bNW7KPRrRY0mvYS4YB0I7QFLgkgkAA0gAwfwyaSZbpAdvSeeBOR/QLVqII/qE+QAA3yVtUSD1002016-07-25T20:50:50Z1.00usd XML end def successful_nonfractional_authorization_response - <<-XML - - -2007-07-12T18:31:53.838ZTEST111111111111842651133440156177166ACCEPT100AP4JY+Or4xRonEAOERAyMzQzOTEzMEM0MFZaNUZCBgDH3fgJ8AEGAMfd+AnwAwzRpAAA7RT/JPY1001004542AI72007-07-12T18:31:53Z10023439130C40VZ2FB + <<~XML + + + 2007-07-12T18:31:53.838ZTEST111111111111842651133440156177166ACCEPT100AP4JY+Or4xRonEAOERAyMzQzOTEzMEM0MFZaNUZCBgDH3fgJ8AEGAMfd+AnwAwzRpAAA7RT/JPY1001004542AI72007-07-12T18:31:53Z10023439130C40VZ2FB + XML + end + + def authorization_review_response + <<~XML + + + 2007-07-12T18:31:53.838ZTEST111111111111842651133440156177166REVIEW480AP4JY+Or4xRonEAOERAyMzQzOTEzMEM0MFZaNUZCBgDH3fgJ8AEGAMfd+AnwAwzRpAAA7RT/USD1001.00004542AI72007-07-12T18:31:53Z10023439130C40VZ2FB XML end def malformed_xml_response - <<-XML - - -2008-01-15T21:42:03.343Zb0a6cf9aa07f1a8495f89c364bbd6a9a2004333231260008401927ACCEPT100Afvvj7Ke2Fmsbq0wHFE2sM6R4GAptYZ0jwPSA+R9PhkyhFTb0KRjoE4+ynthZrG6tMBwjAtTUSD1001.00123456YYMM2008-01-15T21:42:03Z00U

+ <<~XML + + + 2008-01-15T21:42:03.343Zb0a6cf9aa07f1a8495f89c364bbd6a9a2004333231260008401927ACCEPT100Afvvj7Ke2Fmsbq0wHFE2sM6R4GAptYZ0jwPSA+R9PhkyhFTb0KRjoE4+ynthZrG6tMBwjAtTUSD1001.00123456YYMM2008-01-15T21:42:03Z00U

XML end def threedeesecure_purchase_response - <<-XML - - -2017-10-17T20:39:27.392Z1a5ba4804da54b384c6e8a2d8057ea995082727663166909004012REJECT475AhjzbwSTE4kEGDR65zjsGwFLjtwzsJ0gXLJx6Xb0ky3SA7ek8AYA/A17475https://0eafstag.cardinalcommerce.com/EAFService/jsp/v1/redirecteNpVUe9PwjAQ/d6/ghA/r2tBYMvRBEUFFEKQEP1Yu1Om7gfdJoy/3nZsgk2a3Lveu757B+utRhw/oyo0CphjlskPbIXBsC25TvuPD/lkc3xn2d2R6y+3LWA5WuFOwA/qLExiwRzX4UAbSEwLrbYyzgVItbuZLkS353HWA1pDAhHq6Vgw3ule9/pAT5BALCMUqnwznZJCKwRaZQiopIhzXYpB1wXaAAKF/hbbPE8zn9L9fu9cUB2VREBtAQF6FrQsbJSZOQ9hIF7Xs1KNg6dVZzXdxGk0f1nc4+eslMfREKitIBDIHAV3WZ+Z2+Ku3/F8bjRXeQIysmrEFeOOa0yoIYHUfjQ6Icbt02XGTFRojbFqRmoQATykSYymxlD+YjPDWfntxBqrcusg8wbmWGcrXNFD4w3z2IkfVkZRy6H13mi9YhP9W/0vhyyqPw==1198888YTJycDdLR3RIVnpmMXNFejJyazA=<AuthProof><Time>2017 Oct 17 20:39:27</Time><DSUrl>https://csrtestcustomer34.cardinalcommerce.com/merchantacsfrontend/vereq.jsp?acqid=CYBS</DSUrl><VEReqProof><Message id="a2rp7KGtHVzf1sEz2rk0"><VEReq><version>1.0.2</version><pan>XXXXXXXXXXXX0002</pan><Merchant><acqBIN>469216</acqBIN><merID>1234567</merID></Merchant><Browser><deviceCategory>0</deviceCategory></Browser></VEReq></Message></VEReqProof><VEResProof><Message id="a2rp7KGtHVzf1sEz2rk0"><VERes><version>1.0.2</version><CH><enrolled>Y</enrolled><acctID>1198888</acctID></CH><url>https://testcustomer34.cardinalcommerce.com/merchantacsfrontend/pareq.jsp?vaa=b&amp;gold=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA</url><protocol>ThreeDSecure</protocol></VERes></Message></VEResProof></AuthProof>YENROLLED - XML + <<~XML + + + 2017-10-17T20:39:27.392Z1a5ba4804da54b384c6e8a2d8057ea995082727663166909004012REJECT475AhjzbwSTE4kEGDR65zjsGwFLjtwzsJ0gXLJx6Xb0ky3SA7ek8AYA/A17475https://0eafstag.cardinalcommerce.com/EAFService/jsp/v1/redirecteNpVUe9PwjAQ/d6/ghA/r2tBYMvRBEUFFEKQEP1Yu1Om7gfdJoy/3nZsgk2a3Lveu757B+utRhw/oyo0CphjlskPbIXBsC25TvuPD/lkc3xn2d2R6y+3LWA5WuFOwA/qLExiwRzX4UAbSEwLrbYyzgVItbuZLkS353HWA1pDAhHq6Vgw3ule9/pAT5BALCMUqnwznZJCKwRaZQiopIhzXYpB1wXaAAKF/hbbPE8zn9L9fu9cUB2VREBtAQF6FrQsbJSZOQ9hIF7Xs1KNg6dVZzXdxGk0f1nc4+eslMfREKitIBDIHAV3WZ+Z2+Ku3/F8bjRXeQIysmrEFeOOa0yoIYHUfjQ6Icbt02XGTFRojbFqRmoQATykSYymxlD+YjPDWfntxBqrcusg8wbmWGcrXNFD4w3z2IkfVkZRy6H13mi9YhP9W/0vhyyqPw==1198888YTJycDdLR3RIVnpmMXNFejJyazA=<AuthProof><Time>2017 Oct 17 20:39:27</Time><DSUrl>https://csrtestcustomer34.cardinalcommerce.com/merchantacsfrontend/vereq.jsp?acqid=CYBS</DSUrl><VEReqProof><Message id="a2rp7KGtHVzf1sEz2rk0"><VEReq><version>1.0.2</version><pan>XXXXXXXXXXXX0002</pan><Merchant><acqBIN>469216</acqBIN><merID>1234567</merID></Merchant><Browser><deviceCategory>0</deviceCategory></Browser></VEReq></Message></VEReqProof><VEResProof><Message id="a2rp7KGtHVzf1sEz2rk0"><VERes><version>1.0.2</version><CH><enrolled>Y</enrolled><acctID>1198888</acctID></CH><url>https://testcustomer34.cardinalcommerce.com/merchantacsfrontend/pareq.jsp?vaa=b&amp;gold=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA</url><protocol>ThreeDSecure</protocol></VERes></Message></VEResProof></AuthProof>YENROLLED + XML end def successful_threedeesecure_validate_response - <<-XML - - -2018-05-01T14:28:36.773Z23751b5aeb076ea5940c5b656284bf6a5251849164756591904009ACCEPT100Ahj//wSTHLQMXdtQnQUJGxDds0bNnDRoo0+VcdXMBUafKuOrnpAuWT9zDJpJlukB29J4YBpMctAxd21CdBQkwQ3gUSD10012.02831000YY2018-05-01T14:28:36Z00ZLIU5GM27GBP0110322000000E10000200000000000000120205011428360272225A4C495535474D32374742503833313030303030000159004400103232415050524F56414C00223134573031363135303730333830323039344730363400065649435241201002018-05-01T14:28:36Z12.02764668441000SuccessAAABAWFlmQAAAABjRWWZEEFgFz+=2vbv0505S2R4eGtHbEZqbnozeGhBRHJ6QzA=Y + <<~XML + + + 2018-05-01T14:28:36.773Z23751b5aeb076ea5940c5b656284bf6a5251849164756591904009ACCEPT100Ahj//wSTHLQMXdtQnQUJGxDds0bNnDRoo0+VcdXMBUafKuOrnpAuWT9zDJpJlukB29J4YBpMctAxd21CdBQkwQ3gUSD10012.02831000YY2018-05-01T14:28:36Z00ZLIU5GM27GBP0110322000000E10000200000000000000120205011428360272225A4C495535474D32374742503833313030303030000159004400103232415050524F56414C00223134573031363135303730333830323039344730363400065649435241201002018-05-01T14:28:36Z12.02764668441000SuccessAAABAWFlmQAAAABjRWWZEEFgFz+=2vbv0505S2R4eGtHbEZqbnozeGhBRHJ6QzA=Y + XML + end + + def missing_field_response + <<~XML + + + 2019-09-05T01:02:20.132Z9y2A7XGxMSOUqppiEXkiN8T38Jj5676453399086696204061REJECT101c:billTo/c:countryAhjz7wSTM7ido1SNM4cdGwFRfPELvH+kE/QkEg+jLpJlXR6RuUgJMmZ3E7RqkaZw46AAniPV101 + XML + end + + def invalid_field_response + <<~XML + + + 2019-09-05T14:10:46.665Z5676926465076767004068REJECT102c:billTo/c:postalCodeAhjzbwSTM78uTleCsJWkEAJRqivRidukDssiQgRm0ky3SA7oegDUiwLm + 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 + def assert_xml_valid_to_xsd(data, root_element = '//s:Body/*') - schema_file = File.open("#{File.dirname(__FILE__)}/../../schema/cyber_source/CyberSourceTransaction_#{CyberSourceGateway::XSD_VERSION}.xsd") + schema_file = File.open("#{File.dirname(__FILE__)}/../../schema/cyber_source/CyberSourceTransaction_#{CyberSourceGateway::TEST_XSD_VERSION}.xsd") doc = Nokogiri::XML(data) root = Nokogiri::XML(doc.xpath(root_element).to_s) xsd = Nokogiri::XML::Schema(schema_file) diff --git a/test/unit/gateways/d_local_test.rb b/test/unit/gateways/d_local_test.rb new file mode 100644 index 00000000000..7c5a5a12858 --- /dev/null +++ b/test/unit/gateways/d_local_test.rb @@ -0,0 +1,638 @@ +require 'test_helper' + +class DLocalTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = DLocalGateway.new(login: 'login', trans_key: 'password', secret_key: 'shhhhh_key') + @credit_card = credit_card + @amount = 100 + + @options = { + order_id: '1', + billing_address: address + } + @three_ds_secure = { + version: '1.0', + cavv: '3q2+78r+ur7erb7vyv66vv\/\/\/\/8=', + eci: '05', + xid: 'ODUzNTYzOTcwODU5NzY3Qw==', + enrolled: 'true', + authentication_response_status: 'Y' + } + end + + def test_supported_countries + assert_equal %w[AR BD BO BR CL CM CN CO CR DO EC EG GH GT IN ID JP KE MY MX MA NG PA PY PE PH SN SV TH TR TZ UG UY VN ZA], DLocalGateway.supported_countries + end + + def test_supported_card_types + assert_equal DLocalGateway.supported_cardtypes, %i[visa master american_express discover jcb diners_club maestro naranja cabal elo alia carnet patagonia_365 tarjeta_sol] + 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 'D-15104-05b0ec0c-5a1e-470a-b342-eb5f20758ef7', response.authorization + assert response.test? + end + + def test_purchase_with_save + DLocalGateway.application_id = 'ActiveMerchant' + 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'] + assert_equal 'ActiveMerchant', headers['X-Dlocal-Payment-Source'] + end.respond_with(successful_purchase_response) + end + + def test_purchase_with_ip_and_phone + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(ip: '127.0.0.1')) + end.check_request do |_endpoint, data, _headers| + assert_equal '127.0.0.1', JSON.parse(data)['payer']['ip'] + assert_equal '(555)555-5555', JSON.parse(data)['payer']['phone'] + end.respond_with(successful_purchase_response) + end + + def test_failed_purchase + @gateway.expects(:ssl_post).returns(failed_purchase_response) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_equal '300', response.error_code + end + + def test_purchase_with_installments + installments = '6' + installments_id = 'INS54434' + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(installments:, installments_id:)) + end.check_request do |_endpoint, data, _headers| + assert_equal installments, JSON.parse(data)['card']['installments'] + assert_equal installments_id, JSON.parse(data)['card']['installments_id'] + end.respond_with(successful_purchase_response) + end + + def test_purchase_with_network_tokens + credit_card = network_tokenization_credit_card('4242424242424242', payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=') + stub_comms do + @gateway.purchase(@amount, credit_card) + end.check_request do |_endpoint, data, _headers| + assert_equal 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', JSON.parse(data)['card']['cryptogram'] + assert_equal '4242424242424242', JSON.parse(data)['card']['network_token'] + end.respond_with(successful_purchase_response) + end + + def test_purchase_with_network_tokens_and_store_credential_type_subscription + options = @options.merge!(stored_credential: stored_credential(:merchant, :recurring, 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| + assert_equal 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', JSON.parse(data)['card']['cryptogram'] + assert_equal '4242424242424242', JSON.parse(data)['card']['network_token'] + assert_equal 'SUBSCRIPTION', JSON.parse(data)['card']['stored_credential_type'] + end.respond_with(successful_purchase_response) + end + + def test_purchase_with_network_tokens_and_store_credential_type_uneschedule + options = @options.merge!(stored_credential: stored_credential(:merchant, :unscheduled, 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| + assert_equal 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', JSON.parse(data)['card']['cryptogram'] + assert_equal '4242424242424242', JSON.parse(data)['card']['network_token'] + assert_equal 'UNSCHEDULED_CARD_ON_FILE', JSON.parse(data)['card']['stored_credential_type'] + end.respond_with(successful_purchase_response) + end + + def test_purchase_with_network_tokens_and_store_credential_usage_first + options = @options.merge!(stored_credential: stored_credential(:cardholder, :initial)) + credit_card = network_tokenization_credit_card('4242424242424242', payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=') + stub_comms do + @gateway.purchase(@amount, credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_equal 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', JSON.parse(data)['card']['cryptogram'] + assert_equal '4242424242424242', JSON.parse(data)['card']['network_token'] + assert_equal 'FIRST', JSON.parse(data)['card']['stored_credential_usage'] + end.respond_with(successful_purchase_response) + end + + def test_purchase_with_network_tokens_and_store_credential_type_card_on_file_and_credential_usage_used + options = @options.merge!(stored_credential: stored_credential(:cardholder, :unscheduled, 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| + assert_equal 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', JSON.parse(data)['card']['cryptogram'] + assert_equal '4242424242424242', JSON.parse(data)['card']['network_token'] + assert_equal 'CARD_ON_FILE', JSON.parse(data)['card']['stored_credential_type'] + assert_equal 'USED', JSON.parse(data)['card']['stored_credential_usage'] + end.respond_with(successful_purchase_response) + end + + def test_purchase_with_network_tokens_and_store_credential_usage + options = @options.merge!(stored_credential: stored_credential(:cardholder, :recurring, 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| + assert_equal 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', JSON.parse(data)['card']['cryptogram'] + assert_equal '4242424242424242', JSON.parse(data)['card']['network_token'] + assert_equal 'USED', JSON.parse(data)['card']['stored_credential_usage'] + end.respond_with(successful_purchase_response) + end + + def test_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' } } + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(additional_data:)) + end.check_request do |_endpoint, data, _headers| + assert_equal additional_data, JSON.parse(data)['additional_risk_data'] + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_country_overrride + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(country: 'Brazil')) + end.check_request do |_endpoint, data, _headers| + assert_equal 'BR', JSON.parse(data)['country'] + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_force_type + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(force_type: 'debit')) + end.check_request do |_endpoint, data, _headers| + assert_equal 'DEBIT', JSON.parse(data)['card']['force_type'] + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_original_order_id + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(original_order_id: '123ABC')) + end.check_request do |_endpoint, data, _headers| + assert_equal '123ABC', JSON.parse(data)['original_order_id'] + end.respond_with(successful_purchase_response) + end + + def test_successful_authorize + @gateway.expects(:ssl_post).returns(successful_authorize_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + + assert_equal 'D-15104-be03e883-3e6b-497d-840e-54c8b6209bc3', response.authorization + end + + def test_successful_authorize_without_address + @gateway.expects(:ssl_post).returns(successful_authorize_response) + + response = @gateway.authorize(@amount, @credit_card, @options.delete(:billing_address)) + assert_success response + + assert_equal 'D-15104-be03e883-3e6b-497d-840e-54c8b6209bc3', response.authorization + end + + def test_passing_billing_address + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"state\":\"ON\"/, data) + assert_match(/"city\":\"Ottawa\"/, data) + assert_match(/"zip_code\":\"K1C2N6\"/, data) + assert_match(/"street\":\"My Street\"/, data) + assert_match(/"number\":\"456\"/, data) + end.respond_with(successful_authorize_response) + end + + def test_passing_incomplete_billing_address + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, @options.merge(billing_address: address(address1: 'Just a Street'))) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"state\":\"ON\"/, data) + assert_match(/"city\":\"Ottawa\"/, data) + assert_match(/"zip_code\":\"K1C2N6\"/, data) + assert_match(/"street\":\"Just a Street\"/, data) + end.respond_with(successful_authorize_response) + end + + def test_passing_nil_address_1 + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, @options.merge(billing_address: address(address1: nil))) + end.check_request do |_method, _endpoint, data, _headers| + refute_match(/"street\"/, data) + end.respond_with(successful_authorize_response) + end + + def test_successful_inquire_with_payment_id + stub_comms(@gateway, :ssl_request) do + @gateway.inquire('D-15104-f9e16b85-5fc8-40f0-a4d8-4e73a892594f', {}) + end.check_request do |_method, endpoint, data, _headers| + refute_match(/"https:\/\/sandbox.dlocal.com\/payments\/D-15104-f9e16b85-5fc8-40f0-a4d8-4e73a892594f\/status\/"/, endpoint) + refute_match(nil, data) + end.respond_with(successful_payment_status_response) + end + + def test_successful_inquire_with_order_id + stub_comms(@gateway, :ssl_request) do + @gateway.inquire(nil, { order_id: '62595c5db10fdf7b5d5bb3a16d130992' }) + end.check_request do |_method, endpoint, data, _headers| + refute_match(/"https:\/\/sandbox.dlocal.com\/orders\/62595c5db10fdf7b5d5bb3a16d130992\/"/, endpoint) + refute_match(nil, data) + end.respond_with(successful_orders_response) + end + + def test_passing_country_as_string + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"country\":\"CA\"/, data) + end.respond_with(successful_authorize_response) + end + + def test_invalid_country + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, @options.merge(billing_address: address(country: 'INVALID'))) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/\"country\":null/, data) + end.respond_with(successful_authorize_response) + end + + def test_failed_authorize + @gateway.expects(:ssl_post).returns(failed_authorize_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert_equal '309', response.error_code + end + + def test_successful_capture + @gateway.expects(:ssl_post).returns(successful_capture_response) + + response = @gateway.capture(@amount, 'D-15104-be03e883-3e6b-497d-840e-54c8b6209bc3', @options) + assert_success response + + assert_equal 'D-15104-5a914b68-afb8-44f8-a849-8cf09ab6c246', response.authorization + end + + def test_failed_capture + @gateway.expects(:ssl_post).returns(failed_capture_response) + + response = @gateway.capture(@amount, 'D-15104-be03e883-3e6b-497d-840e-54c8b6209bc3', @options) + assert_failure response + assert_equal '4000', response.error_code + end + + def test_successful_refund + @gateway.expects(:ssl_post).returns(successful_refund_response) + + response = @gateway.refund(@amount, 'D-15104-be03e883-3e6b-497d-840e-54c8b6209bc3', @options) + assert_success response + + assert_equal 'REF-15104-a9cc29e5-1895-4cec-94bd-aa16c3b92570', response.authorization + end + + def test_pending_refund + @gateway.expects(:ssl_post).returns(pending_refund_response) + + response = @gateway.refund(@amount, 'D-15104-be03e883-3e6b-497d-840e-54c8b6209bc3', @options) + assert_success response + + assert_equal 'REF-15104-a9cc29e5-1895-4cec-94bd-aa16c3b92570', response.authorization + end + + def test_failed_refund + @gateway.expects(:ssl_post).returns(failed_refund_response) + + response = @gateway.refund(@amount, 'D-15104-be03e883-3e6b-497d-840e-54c8b6209bc3', @options) + assert_failure response + assert_equal '5007', response.error_code + end + + def test_successful_refund_with_description + stub_comms(@gateway, :ssl_request) do + @gateway.refund(@amount, @credit_card, @options.merge(description: 'test')) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"description\":\"test\"/, data) + end.respond_with(successful_refund_response) + end + + def test_successful_void + @gateway.expects(:ssl_post).returns(successful_void_response) + + response = @gateway.void('D-15104-be03e883-3e6b-497d-840e-54c8b6209bc3', @options) + assert_success response + + assert_equal 'The payment was cancelled', response.message + assert_equal 'D-15104-c147279d-14ab-4537-8ba6-e3e1cde0f8d2', response.authorization + end + + def test_faild_void_with_status_paid + @gateway.expects(:ssl_post).returns(failed_void_response_with_status_paid) + + response = @gateway.void('D-15104-be03e883-3e6b-497d-840e-54c8b6209bc3', @options) + assert_failure response + + assert_equal 'D-15104-c147279d-14ab-4537-8ba6-e3e1cde0f8d2', response.authorization + assert_equal '200', response.error_code + assert_equal 'The payment was paid', response.message + end + + def test_failed_void + @gateway.expects(:ssl_post).returns(failed_void_response) + + response = @gateway.void('D-15104-be03e883-3e6b-497d-840e-54c8b6209bc3', @options) + assert_failure response + + assert_equal '5002', response.error_code + end + + def test_successful_verify + @gateway.expects(:ssl_post).returns(successful_verify_response) + + response = @gateway.verify(@credit_card, @options) + assert_success response + + assert_equal 'T-15104-bb204de6-2708-4398-955f-2b16cf633687', response.authorization + end + + def test_failed_verify + @gateway.expects(:ssl_post).returns(failed_verify_response) + + response = @gateway.verify(@credit_card, @options) + assert_failure response + assert_equal '315', response.error_code + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + def test_api_version_param_header + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, _data, headers| + assert_equal '2.1', headers['X-Version'] + end.respond_with(successful_purchase_response) + end + + def test_idempotency_header + options = @options.merge(idempotency_key: '12345') + stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, _data, headers| + assert_equal '12345', headers['X-Idempotency-Key'] + end.respond_with(successful_purchase_response) + end + + def test_three_ds_v1_object_construction + post = {} + @options[:three_d_secure] = @three_ds_secure + + @gateway.send(:add_three_ds, post, @options) + + assert post[:three_dsecure] + ds_data = post[:three_dsecure] + ds_options = @options[:three_d_secure] + + assert_equal ds_options[:version], ds_data[:three_dsecure_version] + assert_equal ds_options[:cavv], ds_data[:cavv] + assert_equal ds_options[:eci], ds_data[:eci] + assert_equal ds_options[:xid], ds_data[:xid] + assert_equal nil, ds_data[:ds_transaction_id] + assert_equal 'Y', ds_data[:enrollment_response] + assert_equal ds_options[:authentication_response_status], ds_data[:authentication_response] + end + + def test_three_ds_v2_object_construction + post = {} + @three_ds_secure[:ds_transaction_id] = 'ODUzNTYzOTcwODU5NzY3Qw==' + @three_ds_secure[:version] = '2.2.0' + @options[:three_d_secure] = @three_ds_secure + + @gateway.send(:add_three_ds, post, @options) + + assert post[:three_dsecure] + ds_data = post[:three_dsecure] + ds_options = @options[:three_d_secure] + + assert_equal ds_options[:version], ds_data[:three_dsecure_version] + assert_equal ds_options[:cavv], ds_data[:cavv] + assert_equal ds_options[:eci], ds_data[:eci] + assert_equal nil, ds_data[:xid] + assert_equal ds_options[:ds_transaction_id], ds_data[:ds_transaction_id] + assert_equal 'Y', ds_data[:enrollment_response] + assert_equal ds_options[:authentication_response_status], ds_data[:authentication_response] + end + + def test_three_ds_version_validation + post = {} + @options[:three_d_secure] = @three_ds_secure + @gateway.send(:add_three_ds, post, @options) + resp = @gateway.send(:validate_three_ds_params, post[:three_dsecure]) + + assert_equal nil, resp + post[:three_dsecure][:three_dsecure_version] = '4.0' + resp = @gateway.send(:validate_three_ds_params, post[:three_dsecure]) + + assert_equal 'ThreeDs data is invalid', resp.message + assert_equal 'ThreeDs version not supported', resp.params['three_ds_version'] + end + + def test_three_ds_enrollment_validation + post = {} + @options[:three_d_secure] = @three_ds_secure + @gateway.send(:add_three_ds, post, @options) + post[:three_dsecure][:enrollment_response] = 'P' + resp = @gateway.send(:validate_three_ds_params, post[:three_dsecure]) + + assert_equal 'ThreeDs data is invalid', resp.message + assert_equal 'Enrollment value not supported', resp.params['enrollment'] + end + + def test_three_ds_auth_response_validation + post = {} + @options[:three_d_secure] = @three_ds_secure + @gateway.send(:add_three_ds, post, @options) + post[:three_dsecure][:authentication_response] = 'P' + resp = @gateway.send(:validate_three_ds_params, post[:three_dsecure]) + + assert_equal 'ThreeDs data is invalid', resp.message + assert_equal 'Authentication response value not supported', resp.params['auth_response'] + end + + def test_purchase_with_three_ds + @options[:three_d_secure] = @three_ds_secure + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + three_ds_params = JSON.parse(data)['three_dsecure'] + assert_equal '1.0', three_ds_params['three_dsecure_version'] + assert_equal '3q2+78r+ur7erb7vyv66vv\/\/\/\/8=', three_ds_params['cavv'] + assert_equal '05', three_ds_params['eci'] + assert_equal 'ODUzNTYzOTcwODU5NzY3Qw==', three_ds_params['xid'] + assert_equal 'Y', three_ds_params['enrollment_response'] + assert_equal 'Y', three_ds_params['authentication_response'] + end.respond_with(successful_purchase_response) + end + + def test_unsuccessfully_purchase_with_wrong_three_ds_data + @three_ds_secure.delete(:version) + @options[:three_d_secure] = @three_ds_secure + resp = @gateway.purchase(@amount, @credit_card, @options) + + assert_equal 'ThreeDs data is invalid', resp.message + assert_equal 'ThreeDs version not supported', resp.params['three_ds_version'] + 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 + %q( + <- "POST /secure_payments/ HTTP/1.1\r\nContent-Type: application/json\r\nX-Date: 2018-12-04T18:24:21Z\r\nX-Login: aeaf9bbfa1\r\nX-Trans-Key: 9de3769b7e\r\nAuthorization: V2-HMAC-SHA256, Signature: d58d0e87a59af50ff974dfeea176c067354682aa74a8ac115912576d4214a776\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: sandbox.dlocal.com\r\nContent-Length: 441\r\n\r\n" + <- "{\"amount\":\"1.00\",\"currency\":\"BRL\",\"payment_method_id\":\"CARD\",\"payment_method_type\":\"CARD\",\"payment_method_flow\":\"DIRECT\",\"country\":\"BR\",\"payer\":{\"name\":\"Longbob Longsen\",\"phone\":\"(555)555-5555\",\"document\":\"42243309114\",\"address\":null},\"card\":{\"holder_name\":\"Longbob Longsen\",\"expiration_month\":9,\"expiration_year\":2019,\"number\":\"4111111111111111\",\"cvv\":\"123\",\"capture\":true},\"order_id\":\"62595c5db10fdf7b5d5bb3a16d130992\",\"description\":\"200\"}" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: Reblaze Secure Web Gateway\r\n" + -> "Date: Tue, 04 Dec 2018 18:24:22 GMT\r\n" + -> "Content-Type: application/json;charset=utf-8\r\n" + -> "Content-Length: 565\r\n" + -> "Strict-Transport-Security: max-age=31536000; includeSubDomains\r\n" + -> "Via: 1.1 google\r\n" + -> "Alt-Svc: clear\r\n" + -> "Connection: close\r\n" + -> "\r\n" + reading 565 bytes... + -> "{\"id\":\"D-15104-9f5246d5-34e2-4f63-9d29-380ab1567ec9\",\"amount\":1.00,\"currency\":\"BRL\",\"payment_method_id\":\"CARD\",\"payment_method_type\":\"CARD\",\"payment_method_flow\":\"DIRECT\",\"country\":\"BR\",\"card\":{\"holder_name\":\"Longbob Longsen\",\"expiration_month\":9,\"expiration_year\":2019,\"brand\":\"VI\",\"last4\":\"1111\",\"card_id\":\"CV-434cb5d1-aece-4878-8ce2-24f887fc7ff5\"},\"created_date\":\"2018-12-04T18:24:21.000+0000\",\"approved_date\":\"2018-12-04T18:24:22.000+0000\",\"status\":\"PAID\",\"status_detail\":\"The payment was paid\",\"status_code\":\"200\",\"order_id\":\"62595c5db10fdf7b5d5bb3a16d130992\"}" + ) + end + + def post_scrubbed + %q( + <- "POST /secure_payments/ HTTP/1.1\r\nContent-Type: application/json\r\nX-Date: 2018-12-04T18:24:21Z\r\nX-Login: aeaf9bbfa1\r\nX-Trans-Key: [FILTERED]\r\nAuthorization: V2-HMAC-SHA256, Signature: d58d0e87a59af50ff974dfeea176c067354682aa74a8ac115912576d4214a776\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: sandbox.dlocal.com\r\nContent-Length: 441\r\n\r\n" + <- "{\"amount\":\"1.00\",\"currency\":\"BRL\",\"payment_method_id\":\"CARD\",\"payment_method_type\":\"CARD\",\"payment_method_flow\":\"DIRECT\",\"country\":\"BR\",\"payer\":{\"name\":\"Longbob Longsen\",\"phone\":\"(555)555-5555\",\"document\":\"42243309114\",\"address\":null},\"card\":{\"holder_name\":\"Longbob Longsen\",\"expiration_month\":9,\"expiration_year\":2019,\"number\":\"[FILTERED]\",\"cvv\":\"[FILTERED]\",\"capture\":true},\"order_id\":\"62595c5db10fdf7b5d5bb3a16d130992\",\"description\":\"200\"}" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: Reblaze Secure Web Gateway\r\n" + -> "Date: Tue, 04 Dec 2018 18:24:22 GMT\r\n" + -> "Content-Type: application/json;charset=utf-8\r\n" + -> "Content-Length: 565\r\n" + -> "Strict-Transport-Security: max-age=31536000; includeSubDomains\r\n" + -> "Via: 1.1 google\r\n" + -> "Alt-Svc: clear\r\n" + -> "Connection: close\r\n" + -> "\r\n" + reading 565 bytes... + -> "{\"id\":\"D-15104-9f5246d5-34e2-4f63-9d29-380ab1567ec9\",\"amount\":1.00,\"currency\":\"BRL\",\"payment_method_id\":\"CARD\",\"payment_method_type\":\"CARD\",\"payment_method_flow\":\"DIRECT\",\"country\":\"BR\",\"card\":{\"holder_name\":\"Longbob Longsen\",\"expiration_month\":9,\"expiration_year\":2019,\"brand\":\"VI\",\"last4\":\"1111\",\"card_id\":\"CV-434cb5d1-aece-4878-8ce2-24f887fc7ff5\"},\"created_date\":\"2018-12-04T18:24:21.000+0000\",\"approved_date\":\"2018-12-04T18:24:22.000+0000\",\"status\":\"PAID\",\"status_detail\":\"The payment was paid\",\"status_code\":\"200\",\"order_id\":\"62595c5db10fdf7b5d5bb3a16d130992\"}" + ) + end + + def successful_purchase_response + '{"id":"D-15104-05b0ec0c-5a1e-470a-b342-eb5f20758ef7","amount":1.00,"currency":"BRL","payment_method_id":"CARD","payment_method_type":"CARD","payment_method_flow":"DIRECT","country":"BR","card":{"holder_name":"Longbob Longsen","expiration_month":9,"expiration_year":2019,"brand":"VI","last4":"1111","card_id":"CV-993903e4-0b33-48fd-8d9b-99fd6c3f0d1a"},"created_date":"2018-12-06T20:20:41.000+0000","approved_date":"2018-12-06T20:20:42.000+0000","status":"PAID","status_detail":"The payment was paid","status_code":"200","order_id":"15940ef43d39331bc64f31341f8ccd93"}' + end + + def successful_purchase_with_installments_response + '{"id":"D-4-e2227981-8ec8-48fd-8e9a-19fedb08d73a","amount":1000,"currency":"BRL","payment_method_id":"CARD","payment_method_type":"CARD","payment_method_flow":"DIRECT","country":"BR","card":{"holder_name":"Thiago Gabriel","expiration_month":10,"expiration_year":2040,"brand":"VI","last4":"1111"},"created_date":"2019-02-06T21:04:43.000+0000","approved_date":"2019-02-06T21:04:44.000+0000","status":"PAID","status_detail":"The payment was paid.","status_code":"200","order_id":"657434343","notification_url":"http://merchant.com/notifications"}' + end + + def failed_purchase_response + '{"id":"D-15104-c3027e67-21f8-4308-8c94-06c44ffcea67","amount":1.00,"currency":"BRL","payment_method_id":"CARD","payment_method_type":"CARD","payment_method_flow":"DIRECT","country":"BR","card":{"holder_name":"Longbob Longsen","expiration_month":9,"expiration_year":2019,"brand":"VI","last4":"1111","card_id":"CV-529b0bb1-8b8a-42f4-b5e4-d358ffb2c978"},"created_date":"2018-12-06T20:22:40.000+0000","status":"REJECTED","status_detail":"The payment was rejected.","status_code":"300","order_id":"7aa5cd3200f287fbac51dcee32184260"}' + end + + def successful_authorize_response + '{"id":"D-15104-be03e883-3e6b-497d-840e-54c8b6209bc3","amount":1.00,"currency":"BRL","payment_method_id":"CARD","payment_method_type":"CARD","payment_method_flow":"DIRECT","country":"BR","card":{"holder_name":"Longbob Longsen","expiration_month":9,"expiration_year":2019,"brand":"VI","last4":"1111","card_id":"CV-ecd897ac-5361-45a1-a407-aaab044ce87e"},"created_date":"2018-12-06T20:24:46.000+0000","approved_date":"2018-12-06T20:24:46.000+0000","status":"AUTHORIZED","status_detail":"The payment was authorized","status_code":"600","order_id":"5694b51b79df484578158d7790b4aacf"}' + end + + def failed_authorize_response + '{"id":"D-15104-e6ed3df3-1380-46c6-92d4-29f0f567f799","amount":1.00,"currency":"BRL","payment_method_id":"CARD","payment_method_type":"CARD","payment_method_flow":"DIRECT","country":"BR","card":{"holder_name":"Longbob Longsen","expiration_month":9,"expiration_year":2019,"brand":"VI","last4":"1111","card_id":"CV-a6326a1d-b706-4e89-9dff-091d73d85b26"},"created_date":"2018-12-06T20:26:57.000+0000","status":"REJECTED","status_detail":"Card expired.","status_code":"309","order_id":"8ecd3101ba7a9a2d6ccb6465d33ff10d"}' + end + + def successful_capture_response + '{"id":"D-15104-5a914b68-afb8-44f8-a849-8cf09ab6c246","amount":1.00,"currency":"BRL","payment_method_id":"VI","payment_method_type":"CARD","payment_method_flow":"DIRECT","country":"BR","created_date":"2018-12-06T20:26:17.000+0000","approved_date":"2018-12-06T20:26:18.000+0000","status":"PAID","status_detail":"The payment was paid","status_code":"200","order_id":"f8276e468120faf3e7252e33ac5f9a73"}' + end + + def failed_capture_response + '{"code":4000,"message":"Payment not found"}' + end + + def successful_verify_response + '{"id":"T-15104-bb204de6-2708-4398-955f-2b16cf633687","amount":0,"currency":"BRL","payment_method_id":"CARD","payment_method_type":"CARD","payment_method_flow":"DIRECT","country":"BR","card":{"holder_name":"Longbob Longsen", "expiration_month":9, "expiration_year":2022, "brand":"VI", "last4":"1111", "verify":true},"three_dsecure":{},"created_date":"2021-11-05T19:54:34.000+0000","approved_date":"2021-11-05T19:54:35.000+0000","status":"VERIFIED","status_detail":"The payment was verified.","status_code":"700","order_id":"e3ec1f40e9cb06b2d9c61f35bd5115e9"}' + end + + def failed_verify_response + '{"id":"T-15104-585b4fb0-8fc5-4ae2-bb87-41218b744ca0","amount":0,"currency":"BRL","payment_method_id":"CARD","payment_method_type":"CARD","payment_method_flow":"DIRECT","country":"BR","card":{"holder_name":"Longbob Longsen", "expiration_month":9, "expiration_year":2022, "brand":"VI", "last4":"1111", "verify":true},"three_dsecure":{},"created_date":"2021-11-05T19:54:34.000+0000","status":"REJECTED","status_detail":"Invalid security code.","status_code":"315","order_id":"e013030bd5a7330a5d490247a9ca2bf47","description":"315"}' + end + + def successful_refund_response + '{"id":"REF-15104-a9cc29e5-1895-4cec-94bd-aa16c3b92570","payment_id":"D-15104-f9e16b85-5fc8-40f0-a4d8-4e73a892594f","status":"SUCCESS","currency":"BRL","created_date":"2018-12-06T20:28:37.000+0000","amount":1.00,"status_code":200,"status_detail":"The refund was paid","notification_url":"http://example.com","amount_refunded":1.00,"id_payment":"D-15104-f9e16b85-5fc8-40f0-a4d8-4e73a892594f"}' + end + + def successful_payment_status_response + '{"code":100,"message":"The payment is pending."}' + end + + def successful_orders_response + '{"order_id":"b809a1aa481b88aaa858144798da656d","payment_id":"T-15104-15f4044d-c4b1-4a38-9b47-bb8be126491d","currency":"BRL","amount":2.0,"created_date":"2022-09-19T13:16:22.000+0000","approved_date":"2022-09-19T13:16:22.000+0000","status":"PAID","status_detail":"The payment was paid.","status_code":"200"}' + end + + # I can't invoke a pending response and there is no example in docs, so this response is speculative + def pending_refund_response + '{"id":"REF-15104-a9cc29e5-1895-4cec-94bd-aa16c3b92570","payment_id":"D-15104-f9e16b85-5fc8-40f0-a4d8-4e73a892594f","status":"PENDING","currency":"BRL","created_date":"2018-12-06T20:28:37.000+0000","amount":1.00,"status_code":100,"status_detail":"The refund is pending","notification_url":"http://example.com","amount_refunded":1.00,"id_payment":"D-15104-f9e16b85-5fc8-40f0-a4d8-4e73a892594f"}' + end + + def failed_refund_response + '{"code":5007,"message":"Amount exceeded"}' + end + + def successful_void_response + '{"id":"D-15104-c147279d-14ab-4537-8ba6-e3e1cde0f8d2","amount":1.00,"currency":"BRL","payment_method_id":"VI","payment_method_type":"CARD","payment_method_flow":"DIRECT","country":"BR","created_date":"2018-12-06T20:38:01.000+0000","approved_date":"2018-12-06T20:38:01.000+0000","status":"CANCELLED","status_detail":"The payment was cancelled","status_code":"400","order_id":"46d8978863be935d892cfa3e992f65f3"}' + end + + def failed_void_response + '{"code":5002,"message":"Invalid transaction status"}' + end + + def failed_void_response_with_status_paid + '{"id":"D-15104-c147279d-14ab-4537-8ba6-e3e1cde0f8d2","amount":1.00,"currency":"BRL","payment_method_id":"VI","payment_method_type":"CARD","payment_method_flow":"DIRECT","country":"BR","created_date":"2018-12-06T20:38:01.000+0000","approved_date":"2018-12-06T20:38:01.000+0000","status":"PAID","status_detail":"The payment was paid","status_code":"200","order_id":"46d8978863be935d892cfa3e992f65f3"}' + 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/data_cash_test.rb b/test/unit/gateways/data_cash_test.rb index 58049ea6724..313f5ea71ea 100644 --- a/test/unit/gateways/data_cash_test.rb +++ b/test/unit/gateways/data_cash_test.rb @@ -3,8 +3,8 @@ class DataCashTest < Test::Unit::TestCase def setup @gateway = DataCashGateway.new( - :login => 'LOGIN', - :password => 'PASSWORD' + login: 'LOGIN', + password: 'PASSWORD' ) @credit_card = credit_card('4242424242424242') @@ -12,19 +12,19 @@ def setup @amount = 100 @address = { - :name => 'Mark McBride', - :address1 => 'Flat 12/3', - :address2 => '45 Main Road', - :city => 'London', - :state => 'None', - :country => 'GBR', - :zip => 'A987AA', - :phone => '(555)555-5555' + name: 'Mark McBride', + address1: 'Flat 12/3', + address2: '45 Main Road', + city: 'London', + state: 'None', + country: 'GBR', + zip: 'A987AA', + phone: '(555)555-5555' } @options = { - :order_id => generate_unique_id, - :billing_address => @address + order_id: generate_unique_id, + billing_address: @address } end @@ -83,20 +83,20 @@ def test_supported_countries end def test_supported_card_types - assert_equal [ :visa, :master, :american_express, :discover, :diners_club, :jcb, :maestro, :switch, :solo, :laser ], DataCashGateway.supported_cardtypes + assert_equal %i[visa master american_express discover diners_club jcb maestro], DataCashGateway.supported_cardtypes end def test_purchase_with_missing_order_id_option - assert_raise(ArgumentError){ @gateway.purchase(100, @credit_card, {}) } + assert_raise(ArgumentError) { @gateway.purchase(100, @credit_card, {}) } end def test_authorize_with_missing_order_id_option - assert_raise(ArgumentError){ @gateway.authorize(100, @credit_card, {}) } + assert_raise(ArgumentError) { @gateway.authorize(100, @credit_card, {}) } end def test_purchase_does_not_raise_exception_with_missing_billing_address @gateway.expects(:ssl_post).returns(successful_purchase_response) - assert @gateway.authorize(100, @credit_card, {:order_id => generate_unique_id }).is_a?(ActiveMerchant::Billing::Response) + assert @gateway.authorize(100, @credit_card, { order_id: generate_unique_id }).is_a?(ActiveMerchant::Billing::Response) end def test_continuous_authority_purchase_with_missing_continuous_authority_reference @@ -122,70 +122,71 @@ def test_capture_method_is_ecomm end private + def failed_purchase_response - <<-XML - - - NOT AUTHORISED - Mastercard - Japan - - 4500203037300784 - 85613a50952067796b1c6ab61c2cac - TEST - DECLINED - 7 - - + <<~XML + + + NOT AUTHORISED + Mastercard + Japan + + 4500203037300784 + 85613a50952067796b1c6ab61c2cac + TEST + DECLINED + 7 + + XML end def successful_purchase_response - <<-XML - - - - - notprovided - - matched - ACCEPTED - - notprovided - - 123456789 - Visa - United Kingdom - - 4400200050664928 - 2d24cc91284c1ed5c65d8821f1e752c7 - TEST - ACCEPTED - 1 - - + <<~XML + + + + + notprovided + + matched + ACCEPTED + + notprovided + + 123456789 + Visa + United Kingdom + + 4400200050664928 + 2d24cc91284c1ed5c65d8821f1e752c7 + TEST + ACCEPTED + 1 + + XML end def successful_purchase_using_continuous_authority_response - <<-XML - - - 123456789 - VISA Debit - United Kingdom - Barclays Bank PLC - - - Using account ref 4500203037301241. CONT_AUTH transaction complete - - 4400200050664928 - 3fc2b05ab38b70f0eb3a6b6d35c0de - TEST - ACCEPTED - 1 - - + <<~XML + + + 123456789 + VISA Debit + United Kingdom + Barclays Bank PLC + + + Using account ref 4500203037301241. CONT_AUTH transaction complete + + 4400200050664928 + 3fc2b05ab38b70f0eb3a6b6d35c0de + TEST + ACCEPTED + 1 + + XML end @@ -195,62 +196,62 @@ def test_transcript_scrubbing end def pre_scrub - <<-RAW -opening connection to testserver.datacash.com:443... -opened -starting SSL for testserver.datacash.com:443... -SSL established -<- "POST /Transaction 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: testserver.datacash.com\r\nContent-Length: 1067\r\n\r\n" -<- "\n\n \n 99626800\n 9YM3DjUa6\n \n \n \n auth\n \n 4539792100000003\n 03/20\n \n 444\n \n \n \n \n \n \n \n \n \n d36e05ce3604313963854fca858d11\n 1.98\n ecomm\n \n \n\n" --> "HTTP/1.1 200 OK\r\n" --> "Date: Wed, 03 Jan 2018 21:24:38 GMT\r\n" --> "Server: Apache\r\n" --> "Connection: close\r\n" --> "Transfer-Encoding: chunked\r\n" --> "Content-Type: text/plain; charset=iso-8859-1\r\n" --> "\r\n" --> "559\r\n" -reading 1369 bytes... --> "" --> "\n\n \n \n \n notchecked\n \n matched\n ACCEPTED\n \n notchecked\n \n 698899\n VISA Debit\n United Kingdom\n Barclays Bank PLC\n 00\n Approved or completed successfully\n D4B1B0558173CAE56E87293F9E9E899C8002F7B6\n \n RBS\n 4700204504678897\n d36e05ce3604313963854fca858d11\n 99626800\n TEST\n ACCEPTED\n 1\n \n\n\n" -read 1369 bytes -reading 2 bytes... --> "" --> "\r\n" -read 2 bytes --> "0\r\n" --> "\r\n" -Conn close + <<~RAW + opening connection to testserver.datacash.com:443... + opened + starting SSL for testserver.datacash.com:443... + SSL established + <- "POST /Transaction 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: testserver.datacash.com\r\nContent-Length: 1067\r\n\r\n" + <- "\n\n \n 99626800\n 9YM3DjUa6\n \n \n \n auth\n \n 4539792100000003\n 03/20\n \n 444\n \n \n \n \n \n \n \n \n \n d36e05ce3604313963854fca858d11\n 1.98\n ecomm\n \n \n\n" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Wed, 03 Jan 2018 21:24:38 GMT\r\n" + -> "Server: Apache\r\n" + -> "Connection: close\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Content-Type: text/plain; charset=iso-8859-1\r\n" + -> "\r\n" + -> "559\r\n" + reading 1369 bytes... + -> "" + -> "\n\n \n \n \n notchecked\n \n matched\n ACCEPTED\n \n notchecked\n \n 698899\n VISA Debit\n United Kingdom\n Barclays Bank PLC\n 00\n Approved or completed successfully\n D4B1B0558173CAE56E87293F9E9E899C8002F7B6\n \n RBS\n 4700204504678897\n d36e05ce3604313963854fca858d11\n 99626800\n TEST\n ACCEPTED\n 1\n \n\n\n" + read 1369 bytes + reading 2 bytes... + -> "" + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close RAW end def post_scrub - <<-SCRUBBED -opening connection to testserver.datacash.com:443... -opened -starting SSL for testserver.datacash.com:443... -SSL established -<- "POST /Transaction 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: testserver.datacash.com\r\nContent-Length: 1067\r\n\r\n" -<- "\n\n \n 99626800\n [FILTERED]\n \n \n \n auth\n \n [FILTERED]\n 03/20\n \n [FILTERED]\n \n \n \n \n \n \n \n \n \n d36e05ce3604313963854fca858d11\n 1.98\n ecomm\n \n \n\n" --> "HTTP/1.1 200 OK\r\n" --> "Date: Wed, 03 Jan 2018 21:24:38 GMT\r\n" --> "Server: Apache\r\n" --> "Connection: close\r\n" --> "Transfer-Encoding: chunked\r\n" --> "Content-Type: text/plain; charset=iso-8859-1\r\n" --> "\r\n" --> "559\r\n" -reading 1369 bytes... --> "" --> "\n\n \n \n \n notchecked\n \n matched\n ACCEPTED\n \n notchecked\n \n 698899\n VISA Debit\n United Kingdom\n Barclays Bank PLC\n 00\n Approved or completed successfully\n D4B1B0558173CAE56E87293F9E9E899C8002F7B6\n \n RBS\n 4700204504678897\n d36e05ce3604313963854fca858d11\n 99626800\n TEST\n ACCEPTED\n 1\n \n\n\n" -read 1369 bytes -reading 2 bytes... --> "" --> "\r\n" -read 2 bytes --> "0\r\n" --> "\r\n" -Conn close -SCRUBBED + <<~SCRUBBED + opening connection to testserver.datacash.com:443... + opened + starting SSL for testserver.datacash.com:443... + SSL established + <- "POST /Transaction 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: testserver.datacash.com\r\nContent-Length: 1067\r\n\r\n" + <- "\n\n \n 99626800\n [FILTERED]\n \n \n \n auth\n \n [FILTERED]\n 03/20\n \n [FILTERED]\n \n \n \n \n \n \n \n \n \n d36e05ce3604313963854fca858d11\n 1.98\n ecomm\n \n \n\n" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Wed, 03 Jan 2018 21:24:38 GMT\r\n" + -> "Server: Apache\r\n" + -> "Connection: close\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Content-Type: text/plain; charset=iso-8859-1\r\n" + -> "\r\n" + -> "559\r\n" + reading 1369 bytes... + -> "" + -> "\n\n \n \n \n notchecked\n \n matched\n ACCEPTED\n \n notchecked\n \n 698899\n VISA Debit\n United Kingdom\n Barclays Bank PLC\n 00\n Approved or completed successfully\n D4B1B0558173CAE56E87293F9E9E899C8002F7B6\n \n RBS\n 4700204504678897\n d36e05ce3604313963854fca858d11\n 99626800\n TEST\n ACCEPTED\n 1\n \n\n\n" + read 1369 bytes + reading 2 bytes... + -> "" + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + SCRUBBED end end diff --git a/test/unit/gateways/datatrans_test.rb b/test/unit/gateways/datatrans_test.rb new file mode 100644 index 00000000000..0c249bf6777 --- /dev/null +++ b/test/unit/gateways/datatrans_test.rb @@ -0,0 +1,454 @@ +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|123alias_token_id123|05|25' + + @billing_address = address + @no_country_billing_address = address(country: nil) + + @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_authorize_with_credit_card_and_no_country_billing_address + response = stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, @options.merge({ billing_address: @no_country_billing_address })) + end.check_request do |_action, endpoint, data, _headers| + parsed_data = JSON.parse(data) + common_assertions_authorize_purchase(endpoint, parsed_data) + billing = parsed_data['billing'] + assert_nil billing['country'] + 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_verify_with_credit_card + response = stub_comms(@gateway, :ssl_request) do + @gateway.verify(@credit_card, @options) + end.check_request do |_action, endpoint, data, _headers| + parsed_data = JSON.parse(data) + common_assertions_authorize_purchase(endpoint, parsed_data) unless parsed_data.empty? + end.respond_with(successful_authorize_response, successful_void_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_store + response = stub_comms(@gateway, :ssl_request) do + @gateway.store(@credit_card, @options) + end.check_request do |_action, endpoint, data, _headers| + assert_match('aliases/tokenize', endpoint) + parsed_data = JSON.parse(data) + request = parsed_data['requests'][0] + assert_equal('CARD', request['type']) + assert_equal(@credit_card.number, request['pan']) + end.respond_with(successful_store_response) + + assert_success response + end + + def test_unstore + response = stub_comms(@gateway, :ssl_request) do + @gateway.unstore(@transaction_reference, @options) + end.check_request do |_action, endpoint, data, _headers| + assert_match('aliases/123alias_token_id123', endpoint) + assert_equal data, '{}' + end.respond_with(successful_unstore_response) + + assert_success response + end + + def test_purchase_with_tpv + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @transaction_reference, @options) + end.check_request do |_action, endpoint, data, _headers| + parsed_data = JSON.parse(data) + common_assertions_authorize_purchase(endpoint, parsed_data) + assert_equal(@transaction_reference.split('|')[2], parsed_data['card']['alias']) + end.respond_with(successful_purchase_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}transactions/#{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, {}, '', {}) + # tes for store + assert_equal '||any_alias|any_month|any_year', @gateway.send(:authorization_from, { 'responses' => [{ 'alias' => 'any_alias' }] }, 'tokenize', { expiry_month: 'any_month', expiry_year: 'any_year' }) + # handle nil responses or missing keys + assert_equal '|||any_month|any_year', @gateway.send(:authorization_from, {}, 'tokenize', { expiry_month: 'any_month', expiry_year: 'any_year' }) + 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 successful_store_response + '{ + "overview":{"total":1, "successful":1, "failed":0}, + "responses": + [{ + "type":"CARD", + "alias":"7LHXscqwAAEAAAGQvYQBwc5zIs52AGRs", + "maskedCC":"424242xxxxxx4242", + "fingerprint":"F-dSjBoCMOYxomP49vzhdOYE" + }] + }' + 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 + alias successful_unstore_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_plus_test.rb b/test/unit/gateways/decidir_plus_test.rb new file mode 100644 index 00000000000..0b24917c2c2 --- /dev/null +++ b/test/unit/gateways/decidir_plus_test.rb @@ -0,0 +1,400 @@ +require 'test_helper' + +class DecidirPlusTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = DecidirPlusGateway.new(public_key: 'public_key', private_key: 'private_key') + @credit_card = credit_card + @payment_reference = '2bf7bffb-1257-4b45-8d42-42d090409b8a|448459' + @amount = 100 + + @options = { + billing_address: address, + description: 'Store Purchase' + } + + @sub_payments = [ + { + site_id: '04052018', + installments: 1, + amount: 1500 + }, + { + site_id: '04052018', + installments: 1, + amount: 1500 + } + ] + @fraud_detection = { + send_to_cs: false, + channel: 'Web', + dispatch_method: 'Store Pick Up', + csmdds: [ + { + code: 17, + description: 'Campo MDD17' + } + ] + } + end + + def test_supported_card_types + assert_equal DecidirPlusGateway.supported_cardtypes, %i[visa master american_express discover diners_club naranja cabal patagonia_365 tarjeta_sol] + end + + def test_successful_purchase + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @payment_reference, @options) + end.check_request do |_action, _endpoint, data, _headers| + assert_match(/2bf7bffb-1257-4b45-8d42-42d090409b8a/, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_failed_purchase + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.respond_with(failed_purchase_response) + + assert_failure response + end + + def test_failed_purchase_response + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.respond_with(failed_purchase_message_response) + + assert_failure response + assert_equal response.message, 'invalid_card: TRANS. NO PERMITIDA' + end + + def test_successful_capture + authorization = '12420186' + stub_comms(@gateway, :ssl_request) do + @gateway.capture(@amount, authorization) + end.check_request do |_action, endpoint, data, _headers| + request = JSON.parse(data) + assert_includes endpoint, "payments/#{authorization}" + assert_equal @amount, request['amount'] + end.respond_with(successful_purchase_response) + end + + def test_failed_refund_for_validation_errors + response = stub_comms(@gateway, :ssl_request) do + @gateway.refund(@amount, '12420186') + end.respond_with(failed_credit_message_response) + + assert_failure response + assert_equal('invalid_status_error - status: refunded', response.message) + end + + def test_failed_authorize + response = stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, @options) + end.respond_with(failed_authorize_response) + + assert_failure response + assert_equal response.error_code, response.params['status_details']['error']['reason']['id'] + end + + def test_successful_refund + response = stub_comms(@gateway, :ssl_request) do + @gateway.refund(@amount, @payment_reference) + end.respond_with(successful_refund_response) + + assert_success response + end + + def test_failed_refund + response = stub_comms(@gateway, :ssl_request) do + @gateway.refund(@amount, @payment_reference) + end.respond_with(failed_purchase_response) + + assert_failure response + end + + def test_successful_store + response = stub_comms(@gateway, :ssl_request) do + @gateway.store(@credit_card, @options) + end.check_request do |_action, _endpoint, data, _headers| + assert_match(/#{@credit_card.number}/, data) + assert_match(/#{@credit_card.name}/, data) + end.respond_with(successful_store_response) + + assert_success response + end + + def test_successful_store_with_additional_data_validation + options = { + card_holder_identification_type: 'dni', + card_holder_identification_number: '44567890', + card_holder_door_number: '348', + card_holder_birthday: '01012017' + } + response = stub_comms(@gateway, :ssl_request) do + @gateway.store(@credit_card, options) + end.check_request do |_action, _endpoint, data, _headers| + request = JSON.parse(data) + assert_equal(options[:card_holder_identification_type], request['card_holder_identification']['type']) + assert_equal(options[:card_holder_identification_number], request['card_holder_identification']['number']) + assert_equal(options[:card_holder_door_number].to_i, request['card_holder_door_number']) + assert_equal(options[:card_holder_birthday], request['card_holder_birthday']) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_unstore + token_id = '3d5992f9-90f8-4ac4-94dd-6baa7306941f' + response = stub_comms(@gateway, :ssl_request) do + @gateway.unstore(token_id) + end.check_request do |_action, endpoint, data, _headers| + assert_includes endpoint, "cardtokens/#{token_id}" + assert_empty JSON.parse(data) + end.respond_with(successful_unstore_response) + + assert_success response + end + + def test_successful_purchase_with_options + options = @options.merge(sub_payments: @sub_payments) + options[:installments] = 4 + options[:payment_type] = 'distributed' + options[:debit] = 'true' + options[:card_brand] = 'visa_debit' + + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @payment_reference, options) + end.check_request do |_action, _endpoint, data, _headers| + assert_equal(@sub_payments, JSON.parse(data, symbolize_names: true)[:sub_payments]) + assert_match(/#{options[:installments]}/, data) + assert_match(/#{options[:payment_type]}/, data) + assert_match(/\"payment_method_id\":31/, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_fraud_detection + options = @options.merge(fraud_detection: @fraud_detection) + + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @payment_reference, options) + end.check_request do |_action, _endpoint, data, _headers| + assert_equal(@fraud_detection, JSON.parse(data, symbolize_names: true)[:fraud_detection]) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_establishment_name + establishment_name = 'Heavenly Buffaloes' + options = @options.merge(establishment_name:) + + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @payment_reference, options) + end.check_request do |_action, _endpoint, data, _headers| + assert_equal(establishment_name, JSON.parse(data)['establishment_name']) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_wallet_id + wallet_id = 'moto' + options = @options.merge(wallet_id:) + + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @payment_reference, options) + end.check_request do |_action, _endpoint, data, _headers| + assert_equal(wallet_id, JSON.parse(data)['wallet_id']) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_aggregate_data + aggregate_data = { + indicator: '1', + identification_number: '308103480', + bill_to_pay: 'test1', + bill_to_refund: 'test2', + merchant_name: 'Heavenly Buffaloes', + street: 'Sesame', + number: '123', + postal_code: '22001', + category: 'yum', + channel: '005', + geographic_code: 'C1234', + city: 'Ciudad de Buenos Aires', + merchant_id: 'dec_agg', + province: 'Buenos Aires', + country: 'Argentina', + merchant_email: 'merchant@mail.com', + merchant_phone: '2678433111' + } + options = @options.merge(aggregate_data:) + + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @payment_reference, options) + end.check_request do |_action, _endpoint, data, _headers| + assert_equal(aggregate_data, JSON.parse(data, symbolize_names: true)[:aggregate_data]) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_fraud_detection_without_csmdds + @fraud_detection.delete(:csmdds) + options = @options.merge(fraud_detection: @fraud_detection) + + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @payment_reference, options) + end.check_request do |_action, _endpoint, data, _headers| + fraud_detection_fields = JSON.parse(data, symbolize_names: true)[:fraud_detection] + assert_equal(@fraud_detection, fraud_detection_fields) + assert_nil fraud_detection_fields[:csmdds] + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_void + authorization = '418943' + response = stub_comms(@gateway, :ssl_request) do + @gateway.void(authorization) + end.check_request do |_action, endpoint, data, _headers| + assert_includes endpoint, "payments/#{authorization}/refunds" + assert_equal '{}', data + end.respond_with(successful_void_response) + + assert_success response + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + private + + def pre_scrubbed + %q( + opening connection to developers.decidir.com:443... + opened + starting SSL for developers.decidir.com:443... + SSL established + <- "POST /api/v2/tokens HTTP/1.1\r\nContent-Type: application/json\r\nApikey: 96e7f0d36a0648fb9a8dcb50ac06d260\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: developers.decidir.com\r\nContent-Length: 207\r\n\r\n" + <- "{\"card_number\":\"4484590159923090\",\"card_expiration_month\":\"09\",\"card_expiration_year\":\"22\",\"security_code\":\"123\",\"card_holder_name\":\"Longbob Longsen\",\"card_holder_identification\":{\"type\":null,\"number\":null}}" + -> "HTTP/1.1 201 Created\r\n" + -> "Content-Type: application/json\r\n" + -> "Content-Length: 342\r\n" + -> "Connection: close\r\n" + -> "Date: Wed, 15 Dec 2021 15:04:23 GMT\r\n" + -> "vary: Origin\r\n" + -> "Access-Control-Allow-Origin: *\r\n" + -> "X-Kong-Upstream-Latency: 42\r\n" + -> "X-Kong-Proxy-Latency: 2\r\n" + -> "Via: kong/2.0.5\r\n" + -> "Strict-Transport-Security: max-age=16070400; includeSubDomains\r\n" + -> "Set-Cookie: TS017a11a6=012e46d8ee3b62f63065925e2c71ee113cba96e0166c66ac2397184d6961bbe2cd1b41d64f6ee14cb9d440cf66a097465e0a31a786; Path=/; Domain=.developers.decidir.com\r\n" + -> "\r\n" + reading 342 bytes... + -> "{\"id\":\"2e416527-b757-47e1-80e1-51b2cb77092f\",\"status\":\"active\",\"card_number_length\":16,\"date_created\":\"2021-12-14T16:20Z\",\"bin\":\"448459\",\"last_four_digits\":\"3090\",\"security_code_length\":3,\"expiration_month\":9,\"expiration_year\":22,\"date_due\":\"2021-12-14T16:35Z\",\"cardholder\":{\"identification\":{\"type\":\"\",\"number\":\"\"},\"name\":\"Longbob Longsen\"}}" + read 342 bytes + Conn close + ) + end + + def post_scrubbed + %q( + opening connection to developers.decidir.com:443... + opened + starting SSL for developers.decidir.com:443... + SSL established + <- "POST /api/v2/tokens HTTP/1.1\r\nContent-Type: application/json\r\nApikey: [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: developers.decidir.com\r\nContent-Length: 207\r\n\r\n" + <- "{\"card_number\":\"[FILTERED]",\"card_expiration_month\":\"09\",\"card_expiration_year\":\"22\",\"security_code\":\"[FILTERED]",\"card_holder_name\":\"Longbob Longsen\",\"card_holder_identification\":{\"type\":null,\"number\":null}}" + -> "HTTP/1.1 201 Created\r\n" + -> "Content-Type: application/json\r\n" + -> "Content-Length: 342\r\n" + -> "Connection: close\r\n" + -> "Date: Wed, 15 Dec 2021 15:04:23 GMT\r\n" + -> "vary: Origin\r\n" + -> "Access-Control-Allow-Origin: *\r\n" + -> "X-Kong-Upstream-Latency: 42\r\n" + -> "X-Kong-Proxy-Latency: 2\r\n" + -> "Via: kong/2.0.5\r\n" + -> "Strict-Transport-Security: max-age=16070400; includeSubDomains\r\n" + -> "Set-Cookie: TS017a11a6=012e46d8ee3b62f63065925e2c71ee113cba96e0166c66ac2397184d6961bbe2cd1b41d64f6ee14cb9d440cf66a097465e0a31a786; Path=/; Domain=.developers.decidir.com\r\n" + -> "\r\n" + reading 342 bytes... + -> "{\"id\":\"2e416527-b757-47e1-80e1-51b2cb77092f\",\"status\":\"active\",\"card_number_length\":16,\"date_created\":\"2021-12-14T16:20Z\",\"bin\":\"448459\",\"last_four_digits\":\"3090\",\"security_code_length\":3,\"expiration_month\":9,\"expiration_year\":22,\"date_due\":\"2021-12-14T16:35Z\",\"cardholder\":{\"identification\":{\"type\":\"\",\"number\":\"\"},\"name\":\"Longbob Longsen\"}}" + read 342 bytes + Conn close + ) + end + + def successful_store_response + %{ + {\"id\":\"cd4ba1c0-4b41-4c5c-8530-d0c757df8603\",\"status\":\"active\",\"card_number_length\":16,\"date_created\":\"2022-01-07T17:37Z\",\"bin\":\"448459\",\"last_four_digits\":\"3090\",\"security_code_length\":3,\"expiration_month\":9,\"expiration_year\":23,\"date_due\":\"2022-01-07T17:52Z\",\"cardholder\":{\"identification\":{\"type\":\"\",\"number\":\"\"},\"name\":\"Longbob Longsen\"}} + } + end + + def successful_unstore_response; end + + def successful_purchase_response + %{ + {\"id\":12232003,\"site_transaction_id\":\"d80cb4c7430b558cb9362b7bb89d2d38\",\"payment_method_id\":1,\"card_brand\":\"Visa\",\"amount\":100,\"currency\":\"ars\",\"status\":\"approved\",\"status_details\":{\"ticket\":\"4588\",\"card_authorization_code\":\"173710\",\"address_validation_code\":\"VTE0011\",\"error\":null},\"date\":\"2022-01-07T17:37Z\",\"customer\":null,\"bin\":\"448459\",\"installments\":1,\"first_installment_expiration_date\":null,\"payment_type\":\"single\",\"sub_payments\":[],\"site_id\":\"99999999\",\"fraud_detection\":null,\"aggregate_data\":null,\"establishment_name\":null,\"spv\":null,\"confirmed\":null,\"pan\":\"48d2eeca7a9041dc4b2008cf495bc5a8c4\",\"customer_token\":null,\"card_data\":\"/tokens/12232003\",\"token\":\"cd4ba1c0-4b41-4c5c-8530-d0c757df8603\"} + } + end + + def failed_purchase_response + %{ + {\"error_type\":\"invalid_request_error\",\"validation_errors\":[{\"code\":\"invalid_param\",\"param\":\"site_transaction_id\"}]} + } + end + + def failed_purchase_message_response + %{ + {\"id\":552537664,\"site_transaction_id\":\"9a59630fd51de97fbd8390adf796c683\",\"payment_method_id\":1,\"card_brand\":\"Visa\",\"amount\":74416,\"currency\":\"ars\",\"status\":\"rejected\",\"status_details\":{\"ticket\":\"2\",\"card_authorization_code\":\"\",\"address_validation_code\":\"VTE0000\",\"error\":{\"type\":\"invalid_card\",\"reason\":{\"id\":57,\"description\":\"TRANS. NO PERMITIDA\",\"additional_description\":\"\"}}},\"date\":\"2022-02-07T10:16Z\",\"customer\":null,\"bin\":\"466057\",\"installments\":1,\"first_installment_expiration_date\":null,\"payment_type\":\"single\",\"sub_payments\":[],\"site_id\":\"92003011\",\"fraud_detection\":{\"status\":null},\"aggregate_data\":null,\"establishment_name\":null,\"spv\":null,\"confirmed\":null,\"pan\":null,\"customer_token\":null,\"card_data\":\"/tokens/552537664\",\"token\":\"ea1bde57-5bdf-4f58-8586-df45c4359664\"} + } + end + + def failed_credit_message_response + %{ + {\"error_type\":\"invalid_status_error\",\"validation_errors\":{\"status\":\"refunded\"}} + } + end + + def successful_refund_response + %{ + {\"id\":417921,\"amount\":100,\"sub_payments\":null,\"error\":null,\"status\":\"approved\",\"status_details\":{\"ticket\":\"4589\",\"card_authorization_code\":\"173711\",\"address_validation_code\":\"VTE0011\",\"error\":null}} + } + end + + def failed_refund_response + %{ + {\"error_type\":\"not_found_error\",\"entity_name\":\"\",\"id\":\"\"} + } + end + + def failed_authorize_response + %{ + {\"id\": 12516088, \"site_transaction_id\": \"e77f40284fd5e0fba8e8ef7d1b784c5e\", \"payment_method_id\": 1, \"card_brand\": \"Visa\", \"amount\": 100, \"currency\": \"ars\", \"status\": \"rejected\", \"status_details\": { \"ticket\": \"393\", \"card_authorization_code\": \"\", \"address_validation_code\": \"VTE0000\", \"error\": { \"type\": \"invalid_card\", \"reason\": { \"id\": 3, \"description\": \"COMERCIO INVALIDO\", \"additional_description\": \"\" } } }, \"date\": \"2022-03-21T13:08Z\", \"customer\": null, \"bin\": \"400030\", \"installments\": 1, \"first_installment_expiration_date\": null, \"payment_type\": \"single\", \"sub_payments\": [], \"site_id\": \"92002480\", \"fraud_detection\": { \"status\": null }, \"aggregate_data\": null, \"establishment_name\": null, \"spv\": null, \"confirmed\": null, \"pan\": null, \"customer_token\": null, \"card_data\": \"/tokens/12516088\", \"token\": \"058972ba-4235-4452-bfdf-fc9f61e2c0f9\"} + } + end + + def successful_void_response + %{ + {"id":418966,"amount":100,"sub_payments":null,"error":null,"status":"approved","status_details":{"ticket":"4630","card_authorization_code":"074206","address_validation_code":"VTE0011","error":null}} + } + end + + def successful_verify_response + %{ + {"id":12421487,"site_transaction_id":"e6936a3fbc65cfa1fded1e84d4bbeaf9","payment_method_id":1,"card_brand":"Visa","amount":100,"currency":"ars","status":"approved","status_details":{"ticket":"4747","card_authorization_code":"094329","address_validation_code":"VTE0011","error":null},"date":"2022-01-20T09:43Z","customer":null,"bin":"448459","installments":1,"first_installment_expiration_date":null,"payment_type":"single","sub_payments":[],"site_id":"99999999","fraud_detection":null,"aggregate_data":null,"establishment_name":null,"spv":null,"confirmed":null,"pan":"48d2eeca7a9041dc4b2008cf495bc5a8c4","customer_token":null,"card_data":"/tokens/12421487","token":"a36cadd5-5b06-41f5-972d-fffd524e2a35"} + } + end +end diff --git a/test/unit/gateways/decidir_test.rb b/test/unit/gateways/decidir_test.rb new file mode 100644 index 00000000000..be5b630daac --- /dev/null +++ b/test/unit/gateways/decidir_test.rb @@ -0,0 +1,836 @@ +require 'test_helper' + +class DecidirTest < Test::Unit::TestCase + include CommStub + include ActiveMerchant::Billing::CreditCardFormatting + + def setup + @gateway_for_purchase = DecidirGateway.new(api_key: 'api_key') + @gateway_for_auth = DecidirGateway.new(api_key: 'api_key', preauth_mode: true) + @credit_card = credit_card + @amount = 100 + + @options = { + order_id: '1', + billing_address: address, + description: 'Store Purchase' + } + @fraud_detection = { + send_to_cs: false, + channel: 'Web', + dispatch_method: 'Store Pick Up', + csmdds: [ + { + code: 17, + description: 'Campo MDD17' + } + ], + device_unique_id: '111' + } + @sub_payments = [ + { + site_id: '04052018', + installments: 1, + amount: 1500 + }, + { + site_id: '04052018', + installments: 1, + amount: 1500 + } + ] + + @network_token = network_tokenization_credit_card( + '4012001037141112', + brand: 'visa', + eci: '05', + payment_cryptogram: '000203016912340000000FA08400317500000000', + verification_value: '123' + ) + end + + def test_supported_card_types + assert_equal DecidirGateway.supported_cardtypes, %i[visa master american_express diners_club naranja cabal tuya patagonia_365 tarjeta_sol] + end + + def test_successful_purchase + @gateway_for_purchase.expects(:ssl_request).returns(successful_purchase_response) + + response = @gateway_for_purchase.purchase(@amount, @credit_card, @options) + assert_success response + + assert_equal 7719132, response.authorization + assert_equal 'approved', response.message + assert response.test? + end + + def test_successful_purchase_with_options + options = { + ip: '127.0.0.1', + email: 'joe@example.com', + card_holder_door_number: '1234', + card_holder_birthday: '01011980', + card_holder_identification_type: 'dni', + card_holder_identification_number: '123456', + establishment_name: 'Heavenly Buffaloes', + installments: 12, + site_id: '99999999' + } + + response = stub_comms(@gateway_for_purchase, :ssl_request) do + @gateway_for_purchase.purchase(@amount, @credit_card, @options.merge(options)) + end.check_request do |_method, _endpoint, data, _headers| + assert data =~ /"card_holder_door_number":1234/ + assert data =~ /"card_holder_birthday":"01011980"/ + assert data =~ /"type":"dni"/ + assert data =~ /"number":"123456"/ + assert data =~ /"establishment_name":"Heavenly Buffaloes"/ + assert data =~ /"site_id":"99999999"/ + end.respond_with(successful_purchase_response) + + assert_equal 7719132, response.authorization + assert_equal 'approved', response.message + assert response.test? + end + + def test_successful_purchase_with_aggregate_data + options = { + aggregate_data: { + indicator: 1, + identification_number: '308103480', + bill_to_pay: 'test1', + bill_to_refund: 'test2', + merchant_name: 'Heavenly Buffaloes', + street: 'Sesame', + number: '123', + postal_code: '22001', + category: 'yum', + channel: '005', + geographic_code: 'C1234', + city: 'Ciudad de Buenos Aires', + merchant_id: 'dec_agg', + province: 'Buenos Aires', + country: 'Argentina', + merchant_email: 'merchant@mail.com', + merchant_phone: '2678433111' + } + } + + response = stub_comms(@gateway_for_purchase, :ssl_request) do + @gateway_for_purchase.purchase(@amount, @credit_card, @options.merge(options)) + end.check_request do |_method, _endpoint, data, _headers| + assert data =~ /"aggregate_data":{"indicator":1/ + assert data =~ /"identification_number":"308103480"/ + assert data =~ /"bill_to_pay":"test1"/ + assert data =~ /"bill_to_refund":"test2"/ + assert data =~ /"merchant_name":"Heavenly Buffaloes"/ + assert data =~ /"street":"Sesame"/ + assert data =~ /"number":"123"/ + assert data =~ /"postal_code":"22001"/ + assert data =~ /"category":"yum"/ + assert data =~ /"channel":"005"/ + assert data =~ /"geographic_code":"C1234"/ + assert data =~ /"city":"Ciudad de Buenos Aires"/ + assert data =~ /"merchant_id":"dec_agg"/ + assert data =~ /"province":"Buenos Aires"/ + assert data =~ /"country":"Argentina"/ + assert data =~ /"merchant_email":"merchant@mail.com"/ + assert data =~ /"merchant_phone":"2678433111"/ + end.respond_with(successful_purchase_response) + + assert_equal 7719132, response.authorization + assert_equal 'approved', response.message + assert response.test? + end + + def test_successful_purchase_with_fraud_detection + options = @options.merge(fraud_detection: @fraud_detection) + + 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_equal(@fraud_detection, JSON.parse(data, symbolize_names: true)[:fraud_detection]) + assert_match(/device_unique_identifier/, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_wallet_id + options = @options.merge(wallet_id: 'moto') + + 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_equal('moto', JSON.parse(data, symbolize_names: true)[:wallet_id]) + assert_match(/#{options[:wallet_id]}/, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_sub_payments + options = @options.merge(sub_payments: @sub_payments) + options[:installments] = 4 + options[:payment_type] = 'distributed' + + 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_equal(@sub_payments, JSON.parse(data, symbolize_names: true)[:sub_payments]) + assert_match(/#{options[:installments]}/, data) + assert_match(/#{options[:payment_type]}/, data) + end.respond_with(successful_purchase_response) + + 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) + + response = @gateway_for_purchase.purchase(@amount, @credit_card, @options) + assert_failure response + assert_equal 'TARJETA INVALIDA | invalid_number', response.message + assert_match Gateway::STANDARD_ERROR_CODE[:invalid_number], response.error_code + end + + def test_failed_purchase_with_invalid_field + @gateway_for_purchase.expects(:ssl_request).returns(failed_purchase_with_invalid_field_response) + + response = @gateway_for_purchase.purchase(@amount, @credit_card, @options.merge(installments: -1)) + assert_failure response + assert_equal 'invalid_param: installments', response.message + assert_match 'invalid_request_error', response.error_code + end + + def test_failed_purchase_with_preauth_mode + assert_raise(ArgumentError) do + @gateway_for_auth.purchase(@amount, @credit_card, @options) + end + end + + def test_failed_purchase_error_response + @gateway_for_purchase.expects(:ssl_request).returns(unique_purchase_error_response) + + response = @gateway_for_purchase.purchase(@amount, @credit_card, @options) + assert_failure response + assert_match 'invalid_request_error | invalid_param | payment_type', response.error_code + end + + def test_failed_purchase_error_response_with_error_code + @gateway_for_purchase.expects(:ssl_request).returns(error_response_with_error_code) + + response = @gateway_for_purchase.purchase(@amount, @credit_card, @options) + assert_failure response + assert_match '14, invalid_number', response.error_code + end + + def test_failed_purchase_with_unexpected_error_code + @gateway_for_purchase.expects(:ssl_request).returns(failed_purchase_response_with_unexpected_error) + + response = @gateway_for_purchase.purchase(@amount, @credit_card, @options) + assert_failure response + assert_equal ' | processing_error', response.message + assert_match Gateway::STANDARD_ERROR_CODE[:processing_error], response.error_code + end + + def test_successful_authorize + @gateway_for_auth.expects(:ssl_request).returns(successful_authorize_response) + + response = @gateway_for_auth.authorize(@amount, @credit_card, @options) + assert_success response + + assert_equal 7720214, response.authorization + assert_equal 'pre_approved', response.message + assert response.test? + end + + def test_failed_authorize + @gateway_for_auth.expects(:ssl_request).returns(failed_authorize_response) + + response = @gateway_for_auth.authorize(@amount, @credit_card, @options) + assert_failure response + + assert_equal 7719358, response.authorization + assert_equal 'TARJETA INVALIDA | invalid_number', response.message + assert response.test? + end + + def test_failed_authorize_without_preauth_mode + assert_raise(ArgumentError) do + @gateway_for_purchase.authorize(@amount, @credit_card, @options) + end + end + + def test_successful_capture + @gateway_for_auth.expects(:ssl_request).returns(successful_capture_response) + + response = @gateway_for_auth.capture(@amount, 7720214) + assert_success response + + assert_equal 7720214, response.authorization + assert_equal 'approved', response.message + assert response.test? + end + + def test_failed_partial_capture + @gateway_for_auth.expects(:ssl_request).returns(failed_partial_capture_response) + + response = @gateway_for_auth.capture(@amount, '') + assert_failure response + + assert_nil response.authorization + assert_equal 'amount: Amount out of ranges: 100 - 100', response.message + assert_equal 'invalid_request_error', response.error_code + assert response.test? + end + + def test_failed_capture + @gateway_for_auth.expects(:ssl_request).returns(failed_capture_response) + + response = @gateway_for_auth.capture(@amount, '') + assert_failure response + + assert_equal '', response.authorization + assert_equal 'not_found_error', response.message + assert response.test? + end + + def test_failed_capture_without_preauth_mode + assert_raise(ArgumentError) do + @gateway_for_purchase.capture(@amount, @credit_card, @options) + end + end + + def test_successful_refund + @gateway_for_purchase.expects(:ssl_request).returns(successful_refund_response) + + response = @gateway_for_purchase.refund(@amount, 81931, @options) + assert_success response + + assert_equal 81931, response.authorization + assert_equal 'approved', response.message + assert response.test? + end + + def test_partial_refund + @gateway_for_purchase.expects(:ssl_request).returns(partial_refund_response) + + response = @gateway_for_purchase.refund(@amount - 1, 81932, @options) + assert_success response + + assert_equal 81932, response.authorization + assert_equal 'approved', response.message + assert response.test? + end + + def test_failed_refund + @gateway_for_purchase.expects(:ssl_request).returns(failed_refund_response) + + response = @gateway_for_purchase.refund(@amount, '') + assert_failure response + + assert_equal '', response.authorization + assert_equal 'not_found_error', response.message + assert response.test? + end + + def test_successful_void + @gateway_for_auth.expects(:ssl_request).returns(successful_void_response) + + response = @gateway_for_auth.void(@amount, '') + assert_success response + + assert_equal 82814, response.authorization + assert_equal 'approved', response.message + assert response.test? + end + + def test_failed_void + @gateway_for_auth.expects(:ssl_request).returns(failed_void_response) + + response = @gateway_for_auth.void('') + assert_failure response + + assert_equal '', response.authorization + assert_equal 'not_found_error', response.message + assert response.test? + end + + def test_successful_verify + @gateway_for_auth.expects(:ssl_request).at_most(3).returns(successful_void_response) + + response = @gateway_for_auth.verify(@credit_card, @options) + assert_success response + + assert_equal 'approved', response.message + assert response.test? + end + + def test_successful_verify_with_failed_void + @gateway_for_auth.expects(:ssl_request).at_most(3).returns(failed_void_response) + + response = @gateway_for_auth.verify(@credit_card, @options) + assert_failure response + + assert_equal 'not_found_error', response.message + assert response.test? + end + + def test_successful_verify_with_failed_void_unique_error_message + @gateway_for_auth.expects(:ssl_request).at_most(3).returns(unique_void_error_response) + + response = @gateway_for_auth.verify(@credit_card, @options) + assert_failure response + + assert_equal 'invalid_status_error - status: refunded', response.message + assert response.test? + end + + def test_failed_verify + @gateway_for_auth.expects(:ssl_request).at_most(2).returns(failed_authorize_response) + + response = @gateway_for_auth.verify(@credit_card, @options) + assert_failure response + + assert_equal 'TARJETA INVALIDA | invalid_number', response.message + assert response.test? + end + + def test_failed_verify_for_without_preauth_mode + assert_raise(ArgumentError) do + @gateway_for_purchase.verify(@amount, @credit_card, @options) + end + end + + def test_successful_inquire_with_authorization + @gateway_for_purchase.expects(:ssl_request).returns(successful_inquire_response) + response = @gateway_for_purchase.inquire('818423490') + assert_success response + + assert_equal 544453, response.authorization + assert_equal 'rejected', response.message + assert response.test? + end + + def test_network_token_payment_method + options = { + card_holder_name: 'Tesest payway', + card_holder_door_number: 1234, + card_holder_birthday: '200988', + card_holder_identification_type: 'DNI', + card_holder_identification_number: '44444444', + last_4: @credit_card.last_digits + } + + response = stub_comms(@gateway_for_auth, :ssl_request) do + @gateway_for_auth.authorize(100, @network_token, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"cryptogram\":\"#{@network_token.payment_cryptogram}\"/, data) + assert_match(/"security_code\":\"#{@network_token.verification_value}\"/, data) + assert_match(/"expiration_month\":\"#{format(@network_token.month, :two_digits)}\"/, data) + assert_match(/"expiration_year\":\"#{format(@network_token.year, :two_digits)}\"/, data) + end.respond_with(successful_network_token_response) + + assert_success response + assert_equal 49120515, response.authorization + end + + def test_network_token_payment_method_without_cvv + 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 + } + @network_token.verification_value = nil + response = stub_comms(@gateway_for_auth, :ssl_request) do + @gateway_for_auth.authorize(100, @network_token, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"cryptogram\":\"#{@network_token.payment_cryptogram}\"/, data) + assert_not_match(/"security_code\":\"#{@network_token.verification_value}\"/, data) + end.respond_with(successful_network_token_response) + + 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 + end + + def test_transcript_scrubbing_network_token + assert_equal @gateway_for_purchase.scrub(pre_scrubbed_network_token), post_scrubbed_network_token + end + + def test_payment_method_id_with_visa + post = {} + @gateway_for_purchase.send(:add_auth_purchase_params, post, @amount, @credit_card, @options) + assert_equal 1, post[:payment_method_id] + end + + def test_payment_method_id_with_mastercard + post = {} + @gateway_for_purchase.send(:add_auth_purchase_params, post, @amount, credit_card('5299910010000015'), @options) + assert_equal 104, post[:payment_method_id] + end + + def test_payment_method_id_with_amex + post = {} + @gateway_for_purchase.send(:add_auth_purchase_params, post, @amount, credit_card('373953192351004'), @options) + assert_equal 65, post[:payment_method_id] + end + + def test_payment_method_id_with_diners + post = {} + @gateway_for_purchase.send(:add_auth_purchase_params, post, @amount, credit_card('36463664750005'), @options) + assert_equal 8, post[:payment_method_id] + end + + def test_payment_method_id_with_cabal + post = {} + credit_card = credit_card('5896570000000008') + @gateway_for_purchase.send(:add_auth_purchase_params, post, @amount, credit_card, @options) + assert_equal 63, post[:payment_method_id] + end + + def test_payment_method_id_with_naranja + post = {} + credit_card = credit_card('5895627823453005') + @gateway_for_purchase.send(:add_auth_purchase_params, post, @amount, credit_card, @options) + assert_equal 24, post[:payment_method_id] + end + + def test_payment_method_id_with_visa_debit + visa_debit_card = credit_card('4517721004856075') + debit_options = @options.merge(debit: true) + + stub_comms(@gateway_for_purchase, :ssl_request) do + @gateway_for_purchase.purchase(@amount, visa_debit_card, debit_options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"payment_method_id":31/, data) + end.respond_with(successful_purchase_response) + end + + def test_payment_method_id_with_mastercard_debit + # currently lacking a valid MasterCard debit card number, so using the MasterCard credit card number + mastercard = credit_card('5299910010000015') + debit_options = @options.merge(debit: true) + + stub_comms(@gateway_for_purchase, :ssl_request) do + @gateway_for_purchase.purchase(@amount, mastercard, debit_options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"payment_method_id":105/, data) + end.respond_with(successful_purchase_response) + end + + def test_payment_method_id_with_maestro_debit + # currently lacking a valid Maestro debit card number, so using a generated test card number + maestro_card = credit_card('6759649826438453') + debit_options = @options.merge(debit: true) + + stub_comms(@gateway_for_purchase, :ssl_request) do + @gateway_for_purchase.purchase(@amount, maestro_card, debit_options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"payment_method_id":106/, data) + end.respond_with(successful_purchase_response) + end + + def test_payment_method_id_with_cabal_debit + # currently lacking a valid Cabal debit card number, so using the Cabal credit card number + cabal_card = credit_card('5896570000000008') + debit_options = @options.merge(debit: true) + + stub_comms(@gateway_for_purchase, :ssl_request) do + @gateway_for_purchase.purchase(@amount, cabal_card, debit_options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"payment_method_id":108/, data) + end.respond_with(successful_purchase_response) + end + + private + + def pre_scrubbed + %q( + opening connection to developers.decidir.com:443... + opened + starting SSL for developers.decidir.com:443... + SSL established + <- "POST /api/v2/payments HTTP/1.1\r\nContent-Type: application/json\r\nApikey: 5df6b5764c3f4822aecdc82d56f26b9d\r\nCache-Control: no-cache\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: developers.decidir.com\r\nContent-Length: 414\r\n\r\n" + <- "{\"site_transaction_id\":\"d5972b68-87d5-46fd-8d3d-b2512902b9af\",\"payment_method_id\":1,\"bin\":\"450799\",\"payment_type\":\"single\",\"installments\":1,\"description\":\"Store Purchase\",\"sub_payments\":[],\"amount\":100,\"currency\":\"ARS\",\"card_data\":{\"card_number\":\"4507990000004905\",\"card_expiration_month\":\"09\",\"card_expiration_year\":\"20\",\"security_code\":\"123\",\"card_holder_name\":\"Longbob Longsen\",\"card_holder_identification\":{}}}" + -> "HTTP/1.1 201 Created\r\n" + -> "Date: Mon, 24 Jun 2019 18:38:42 GMT\r\n" + -> "Content-Type: application/json\r\n" + -> "Content-Length: 659\r\n" + -> "Connection: close\r\n" + -> "Access-Control-Allow-Origin: *\r\n" + -> "X-Kong-Upstream-Latency: 159\r\n" + -> "X-Kong-Proxy-Latency: 0\r\n" + -> "Via: kong/0.8.3\r\n" + -> "\r\n" + reading 659 bytes... + -> "{\"id\":7721017,\"site_transaction_id\":\"d5972b68-87d5-46fd-8d3d-b2512902b9af\",\"payment_method_id\":1,\"card_brand\":\"Visa\",\"amount\":100,\"currency\":\"ars\",\"status\":\"approved\",\"status_details\":{\"ticket\":\"7297\",\"card_authorization_code\":\"153842\",\"address_validation_code\":\"VTE0011\",\"error\":null},\"date\":\"2019-06-24T15:38Z\",\"customer\":null,\"bin\":\"450799\",\"installments\":1,\"first_installment_expiration_date\":null,\"payment_type\":\"single\",\"sub_payments\":[],\"site_id\":\"99999999\",\"fraud_detection\":{\"status\":null},\"aggregate_data\":null,\"establishment_name\":\"Heavenly Buffaloes\",\"spv\":null,\"confirmed\":null,\"pan\":\"345425f15b2c7c4584e0044357b6394d7e\",\"customer_token\":null,\"card_data\":\"/tokens/7721017\"}" + read 659 bytes + Conn close + ) + end + + def post_scrubbed + %q( + opening connection to developers.decidir.com:443... + opened + starting SSL for developers.decidir.com:443... + SSL established + <- "POST /api/v2/payments HTTP/1.1\r\nContent-Type: application/json\r\nApikey: [FILTERED]\r\nCache-Control: no-cache\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: developers.decidir.com\r\nContent-Length: 414\r\n\r\n" + <- "{\"site_transaction_id\":\"d5972b68-87d5-46fd-8d3d-b2512902b9af\",\"payment_method_id\":1,\"bin\":\"450799\",\"payment_type\":\"single\",\"installments\":1,\"description\":\"Store Purchase\",\"sub_payments\":[],\"amount\":100,\"currency\":\"ARS\",\"card_data\":{\"card_number\":\"[FILTERED]\",\"card_expiration_month\":\"09\",\"card_expiration_year\":\"20\",\"security_code\":\"[FILTERED]\",\"card_holder_name\":\"Longbob Longsen\",\"card_holder_identification\":{}}}" + -> "HTTP/1.1 201 Created\r\n" + -> "Date: Mon, 24 Jun 2019 18:38:42 GMT\r\n" + -> "Content-Type: application/json\r\n" + -> "Content-Length: 659\r\n" + -> "Connection: close\r\n" + -> "Access-Control-Allow-Origin: *\r\n" + -> "X-Kong-Upstream-Latency: 159\r\n" + -> "X-Kong-Proxy-Latency: 0\r\n" + -> "Via: kong/0.8.3\r\n" + -> "\r\n" + reading 659 bytes... + -> "{\"id\":7721017,\"site_transaction_id\":\"d5972b68-87d5-46fd-8d3d-b2512902b9af\",\"payment_method_id\":1,\"card_brand\":\"Visa\",\"amount\":100,\"currency\":\"ars\",\"status\":\"approved\",\"status_details\":{\"ticket\":\"7297\",\"card_authorization_code\":\"153842\",\"address_validation_code\":\"VTE0011\",\"error\":null},\"date\":\"2019-06-24T15:38Z\",\"customer\":null,\"bin\":\"450799\",\"installments\":1,\"first_installment_expiration_date\":null,\"payment_type\":\"single\",\"sub_payments\":[],\"site_id\":\"99999999\",\"fraud_detection\":{\"status\":null},\"aggregate_data\":null,\"establishment_name\":\"Heavenly Buffaloes\",\"spv\":null,\"confirmed\":null,\"pan\":\"345425f15b2c7c4584e0044357b6394d7e\",\"customer_token\":null,\"card_data\":\"/tokens/7721017\"}" + read 659 bytes + Conn close + ) + end + + def pre_scrubbed_network_token + %( + opening connection to developers.decidir.com:443... + opened + starting SSL for developers.decidir.com:443... + SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384 + <- "POST /api/v2/payments HTTP/1.1\\r\\nContent-Type: application/json\\r\\nApikey: 5df6b5764c3f4822aecdc82d56f26b9d\\r\\nCache-Control: no-cache\\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: developers.decidir.com\\r\\nContent-Length: 505\\r\\n\\r\\n\" + <- "{\\\"payment_method_id\\\":1,\\\"site_transaction_id\\\":\\\"59239287-c211-4d72-97b0-70fd701126a6\\\",\\\"bin\\\":\\\"401200\\\",\\\"payment_type\\\":\\\"single\\\",\\\"installments\\\":1,\\\"description\\\":\\\"Store Purchase\\\",\\\"amount\\\":100,\\\"currency\\\":\\\"ARS\\\",\\\"card_data\\\":{\\\"card_holder_identification\\\":{},\\\"card_holder_name\\\":\\\"Tesest payway\\\",\\\"last_four_digits\\\":null},\\\"is_tokenized_payment\\\":true,\\\"fraud_detection\\\":{\\\"sent_to_cs\\\":false},\\\"token_card_data\\\":{\\\"expiration_month\\\":\\\"09\\\",\\\"expiration_year\\\":\\\"25\\\",\\\"token\\\":\\\"4012001037141112\\\",\\\"eci\\\":\\\"05\\\",\\\"cryptogram\\\":\\\"/wBBBBBCd4HzpGYAmbmgguoBBBB=\\\"},\\\"sub_payments\\\":[]}\" + -> "HTTP/1.1 402 Payment Required\\r\\n\" + -> "Content-Type: application/json; charset=utf-8\\r\\n\" + -> "Content-Length: 826\\r\\n\" + -> "Connection: close\\r\\n\" + -> "date: Wed, 21 Aug 2024 16:35:34 GMT\\r\\n\" + -> "ETag: W/\\\"33a-JHilnlQgDvDXNEdqUzzsVialMcw\\\"\\r\\n\" + -> "vary: Origin\\r\\n\" + -> "Access-Control-Allow-Origin: *\\r\\n\" + -> "Access-Control-Expose-Headers: Accept,Accept-Version,Content-Length,Content-MD5,Content-Type,Date,X-Auth-Token,Access-Control-Allow-Origin,apikey,Set-Cookie,x-consumer-username\\r\\n\" + -> "X-Kong-Upstream-Latency: 325\\r\\n\" + -> "X-Kong-Proxy-Latency: 1\\r\\n\" + -> "Via: kong/2.0.5\\r\\n\" + -> "Strict-Transport-Security: max-age=16070400; includeSubDomains\\r\\n\" + -> "Set-Cookie: TS017a11a6=012e46d8ee27033640500a291b59a9176ef91d5ef14fa722c67ee9909e85848e261382cc63bbfa0cb5d092944db41533293bbb0e26; Path=/; Domain=.developers.decidir.com\\r\\n\" + -> "\\r\\n\"\nreading 826 bytes... + -> "{\\\"id\\\":1945684101,\\\"site_transaction_id\\\":\\\"59239287-c211-4d72-97b0-70fd701126a6\\\",\\\"payment_method_id\\\":1,\\\"card_brand\\\":\\\"Visa\\\",\\\"amount\\\":100,\\\"currency\\\":\\\"ars\\\",\\\"status\\\":\\\"rejected\\\",\\\"status_details\\\":{\\\"ticket\\\":\\\"4922\\\",\\\"card_authorization_code\\\":\\\"\\\",\\\"address_validation_code\\\":\\\"VTE2222\\\",\\\"error\\\":{\\\"type\\\":\\\"insufficient_amount\\\",\\\"reason\\\":{\\\"id\\\":13,\\\"description\\\":\\\"MONTO INVALIDO\\\",\\\"additional_description\\\":\\\"\\\"}}},\\\"date\\\":\\\"2024-08-21T13:35Z\\\",\\\"payment_mode\\\":null,\\\"customer\\\":null,\\\"bin\\\":\\\"401200\\\",\\\"installments\\\":1,\\\"first_installment_expiration_date\\\":null,\\\"payment_type\\\":\\\"single\\\",\\\"sub_payments\\\":[],\\\"site_id\\\":\\\"99999999\\\",\\\"fraud_detection\\\":null,\\\"aggregate_data\\\":null,\\\"establishment_name\\\":null,\\\"spv\\\":null,\\\"confirmed\\\":null,\\\"pan\\\":null,\\\"customer_token\\\":null,\\\"card_data\\\":\\\"/tokens/1945684101\\\",\\\"token\\\":\\\"4a08b19a-fbe2-45b2-8ef6-f3f12d4aa6ed\\\",\\\"authenticated_token\\\":false}\" + read 826 bytes + Conn close + ) + end + + def post_scrubbed_network_token + %( + opening connection to developers.decidir.com:443... + opened + starting SSL for developers.decidir.com:443... + SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384 + <- "POST /api/v2/payments HTTP/1.1\\r\\nContent-Type: application/json\\r\\nApikey: [FILTERED]\\r\\nCache-Control: no-cache\\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: developers.decidir.com\\r\\nContent-Length: 505\\r\\n\\r\\n\" + <- "{\\\"payment_method_id\\\":1,\\\"site_transaction_id\\\":\\\"59239287-c211-4d72-97b0-70fd701126a6\\\",\\\"bin\\\":\\\"401200\\\",\\\"payment_type\\\":\\\"single\\\",\\\"installments\\\":1,\\\"description\\\":\\\"Store Purchase\\\",\\\"amount\\\":100,\\\"currency\\\":\\\"ARS\\\",\\\"card_data\\\":{\\\"card_holder_identification\\\":{},\\\"card_holder_name\\\":\\\"Tesest payway\\\",\\\"last_four_digits\\\":null},\\\"is_tokenized_payment\\\":true,\\\"fraud_detection\\\":{\\\"sent_to_cs\\\":false},\\\"token_card_data\\\":{\\\"expiration_month\\\":\\\"09\\\",\\\"expiration_year\\\":\\\"25\\\",\\\"token\\\":\\\"[FILTERED]\\\",\\\"eci\\\":\\\"05\\\",\\\"cryptogram\\\":\\\"/[FILTERED]=\\\"},\\\"sub_payments\\\":[]}\" + -> "HTTP/1.1 402 Payment Required\\r\\n\" + -> "Content-Type: application/json; charset=utf-8\\r\\n\" + -> "Content-Length: 826\\r\\n\" + -> "Connection: close\\r\\n\" + -> "date: Wed, 21 Aug 2024 16:35:34 GMT\\r\\n\" + -> "ETag: W/\\\"33a-JHilnlQgDvDXNEdqUzzsVialMcw\\\"\\r\\n\" + -> "vary: Origin\\r\\n\" + -> "Access-Control-Allow-Origin: *\\r\\n\" + -> "Access-Control-Expose-Headers: Accept,Accept-Version,Content-Length,Content-MD5,Content-Type,Date,X-Auth-Token,Access-Control-Allow-Origin,apikey,Set-Cookie,x-consumer-username\\r\\n\" + -> "X-Kong-Upstream-Latency: 325\\r\\n\" + -> "X-Kong-Proxy-Latency: 1\\r\\n\" + -> "Via: kong/2.0.5\\r\\n\" + -> "Strict-Transport-Security: max-age=16070400; includeSubDomains\\r\\n\" + -> "Set-Cookie: TS017a11a6=012e46d8ee27033640500a291b59a9176ef91d5ef14fa722c67ee9909e85848e261382cc63bbfa0cb5d092944db41533293bbb0e26; Path=/; Domain=.developers.decidir.com\\r\\n\" + -> "\\r\\n\"\nreading 826 bytes... + -> "{\\\"id\\\":1945684101,\\\"site_transaction_id\\\":\\\"59239287-c211-4d72-97b0-70fd701126a6\\\",\\\"payment_method_id\\\":1,\\\"card_brand\\\":\\\"Visa\\\",\\\"amount\\\":100,\\\"currency\\\":\\\"ars\\\",\\\"status\\\":\\\"rejected\\\",\\\"status_details\\\":{\\\"ticket\\\":\\\"4922\\\",\\\"card_authorization_code\\\":\\\"\\\",\\\"address_validation_code\\\":\\\"VTE2222\\\",\\\"error\\\":{\\\"type\\\":\\\"insufficient_amount\\\",\\\"reason\\\":{\\\"id\\\":13,\\\"description\\\":\\\"MONTO INVALIDO\\\",\\\"additional_description\\\":\\\"\\\"}}},\\\"date\\\":\\\"2024-08-21T13:35Z\\\",\\\"payment_mode\\\":null,\\\"customer\\\":null,\\\"bin\\\":\\\"401200\\\",\\\"installments\\\":1,\\\"first_installment_expiration_date\\\":null,\\\"payment_type\\\":\\\"single\\\",\\\"sub_payments\\\":[],\\\"site_id\\\":\\\"99999999\\\",\\\"fraud_detection\\\":null,\\\"aggregate_data\\\":null,\\\"establishment_name\\\":null,\\\"spv\\\":null,\\\"confirmed\\\":null,\\\"pan\\\":null,\\\"customer_token\\\":null,\\\"card_data\\\":\\\"/tokens/1945684101\\\",\\\"token\\\":\\\"4a08b19a-fbe2-45b2-8ef6-f3f12d4aa6ed\\\",\\\"authenticated_token\\\":false}\" + read 826 bytes + Conn close + ) + end + + def successful_purchase_response + %( + {"id":7719132,"site_transaction_id":"ebcb2db7-7aab-4f33-a7d1-6617a5749fce","payment_method_id":1,"card_brand":"Visa","amount":100,"currency":"ars","status":"approved","status_details":{"ticket":"7156","card_authorization_code":"174838","address_validation_code":"VTE0011","error":null},"date":"2019-06-21T17:48Z","customer":null,"bin":"450799","installments":1,"establishment_name":"Heavenly Buffaloes","first_installment_expiration_date":null,"payment_type":"single","sub_payments":[],"site_id":"99999999","fraud_detection":{"status":null},"aggregate_data":null,"establishment_name":null,"spv":null,"confirmed":null,"pan":"345425f15b2c7c4584e0044357b6394d7e","customer_token":null,"card_data":"/tokens/7719132"} + ) + end + + def failed_purchase_response + %( + {"id":7719351,"site_transaction_id":"73e3ed66-37b1-4c97-8f69-f9cb96422383","payment_method_id":1,"card_brand":"Visa","amount":100,"currency":"ars","status":"rejected","status_details":{"ticket":"7162","card_authorization_code":"","address_validation_code":null,"error":{"type":"invalid_number","reason":{"id":14,"description":"TARJETA INVALIDA","additional_description":""}}},"date":"2019-06-21T17:57Z","customer":null,"bin":"400030","installments":1,"first_installment_expiration_date":null,"payment_type":"single","sub_payments":[],"site_id":"99999999","fraud_detection":null,"aggregate_data":null,"establishment_name":null,"spv":null,"confirmed":null,"pan":"11b076fbc8fa6a55783b2f5d03f6938d8a","customer_token":null,"card_data":"/tokens/7719351"} + ) + end + + def failed_purchase_with_invalid_field_response + %( + {\"error_type\":\"invalid_request_error\",\"validation_errors\":[{\"code\":\"invalid_param\",\"param\":\"installments\"}]} ) + end + + def failed_purchase_response_with_unexpected_error + %( + {"id":7719351,"site_transaction_id":"73e3ed66-37b1-4c97-8f69-f9cb96422383","payment_method_id":1,"card_brand":"Visa","amount":100,"currency":"ars","status":"rejected","status_details":{"ticket":"7162","card_authorization_code":"","address_validation_code":null,"error":{"type":"processing_error","reason":{"id":-1,"description":"","additional_description":""}}},"date":"2019-06-21T17:57Z","customer":null,"bin":"400030","installments":1,"first_installment_expiration_date":null,"payment_type":"single","sub_payments":[],"site_id":"99999999","fraud_detection":null,"aggregate_data":null,"establishment_name":null,"spv":null,"confirmed":null,"pan":"11b076fbc8fa6a55783b2f5d03f6938d8a","customer_token":null,"card_data":"/tokens/7719351"} + ) + end + + def successful_authorize_response + %( + {"id":7720214,"site_transaction_id":"0fcedc95-4fbc-4299-80dc-f77e9dd7f525","payment_method_id":1,"card_brand":"Visa","amount":100,"currency":"ars","status":"pre_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":null,"pan":"345425f15b2c7c4584e0044357b6394d7e","customer_token":null,"card_data":"/tokens/7720214"} + ) + end + + def failed_authorize_response + %( + {"id":7719358,"site_transaction_id":"ff1c12c1-fb6d-4c1a-bc20-2e77d4322c61","payment_method_id":1,"card_brand":"Visa","amount":100,"currency":"ars","status":"rejected","status_details":{"ticket":"8189","card_authorization_code":"","address_validation_code":null,"error":{"type":"invalid_number","reason":{"id":14,"description":"TARJETA INVALIDA","additional_description":""}}},"date":"2019-06-21T18:07Z","customer":null,"bin":"400030","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":null,"pan":"11b076fbc8fa6a55783b2f5d03f6938d8a","customer_token":null,"card_data":"/tokens/7719358"} + ) + 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"} + ) + end + + def failed_partial_capture_response + %( + {"error_type":"invalid_request_error","validation_errors":[{"code":"amount","param":"Amount out of ranges: 100 - 100"}]} + ) + end + + def failed_capture_response + %( + {"error_type":"not_found_error","entity_name":"","id":""} + ) + end + + def successful_refund_response + %( + {"id":81931,"amount":100,"sub_payments":null,"error":null,"status":"approved"} + ) + end + + def partial_refund_response + %( + {"id":81932,"amount":99,"sub_payments":null,"error":null,"status":"approved"} + ) + end + + def failed_refund_response + %( + {"error_type":"not_found_error","entity_name":"","id":""} + ) + end + + def successful_void_response + %( + {"id":82814,"amount":100,"sub_payments":null,"error":null,"status":"approved"} + ) + end + + def failed_void_response + %( + {"error_type":"not_found_error","entity_name":"","id":""} + ) + end + + def successful_inquire_response + %( + { "id": 544453,"site_transaction_id": "52139443","token": "ef4504fc-21f1-4608-bb75-3f73aa9b9ede","user_id": null,"card_brand": "visa","bin": "483621","amount": 10,"currency": "ars","installments": 1,"description": "","payment_type": "single","sub_payments": [],"status": "rejected","status_details": null,"date": "2016-12-15T15:12Z","merchant_id": null,"fraud_detection": {}} + ) + end + + def unique_purchase_error_response + %{ + {\"error\":{\"error_type\":\"invalid_request_error\",\"validation_errors\":[{\"code\":\"invalid_param\",\"param\":\"payment_type\"}]}} + } + end + + def unique_void_error_response + %{ + {\"error_type\":\"invalid_status_error\",\"validation_errors\":{\"status\":\"refunded\"}} + } + end + + def error_response_with_error_code + %{ + {\"error\":{\"type\":\"invalid_number\",\"reason\":{\"id\":14,\"description\":\"TARJETA INVALIDA\",\"additional_description\":\"\"}}} + } + end +end diff --git a/test/unit/gateways/deepstack_test.rb b/test/unit/gateways/deepstack_test.rb new file mode 100644 index 00000000000..4e251114561 --- /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:, + 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/dibs_test.rb b/test/unit/gateways/dibs_test.rb index af127ba0be8..0f639e4df34 100644 --- a/test/unit/gateways/dibs_test.rb +++ b/test/unit/gateways/dibs_test.rb @@ -62,7 +62,7 @@ def test_successful_authorize_and_capture capture = stub_comms do @gateway.capture(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/1066662996/, data) end.respond_with(successful_capture_response) @@ -97,7 +97,7 @@ def test_successful_void void = stub_comms do @gateway.void(response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/1066662996/, data) end.respond_with(successful_void_response) @@ -107,7 +107,7 @@ def test_successful_void def test_failed_void response = stub_comms do @gateway.void('5d53a33d960c46d00f5dc061947d998c') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/5d53a33d960c46d00f5dc061947d998c/, data) end.respond_with(failed_void_response) @@ -124,7 +124,7 @@ def test_successful_refund refund = stub_comms do @gateway.refund(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/1066662996/, data) end.respond_with(successful_refund_response) diff --git a/test/unit/gateways/digitzs_test.rb b/test/unit/gateways/digitzs_test.rb index ede14ff24f2..ef5b67a844b 100644 --- a/test/unit/gateways/digitzs_test.rb +++ b/test/unit/gateways/digitzs_test.rb @@ -38,8 +38,8 @@ def test_successful_purchase def test_successful_card_split_purchase response = stub_comms do @gateway.purchase(@amount, @credit_card, @options_with_split) - end.check_request do |endpoint, data, headers| - if data =~ /"cardSplit"/ + end.check_request do |_endpoint, data, _headers| + if /"cardSplit"/.match?(data) assert_match(%r(split), data) assert_match(%r("merchantId":"spreedly-susanswidg-32270590-2095203-148657924"), data) end @@ -52,8 +52,8 @@ def test_successful_card_split_purchase def test_successful_token_split_purchase response = stub_comms do @gateway.purchase(@amount, @credit_card, @options_with_split) - end.check_request do |endpoint, data, headers| - if data =~ /"tokenSplit"/ + end.check_request do |_endpoint, data, _headers| + if /"tokenSplit"/.match?(data) assert_match(%r(split), data) assert_match(%r("merchantId":"spreedly-susanswidg-32270590-2095203-148657924"), data) end @@ -103,7 +103,7 @@ def test_successful_store_creates_new_customer @gateway.expects(:ssl_get).returns(customer_id_exists_response) @gateway.expects(:ssl_post).times(3).returns(successful_app_token_response, successful_create_customer_response, successful_token_response) - assert response = @gateway.store(@credit_card, @options.merge({customer_id: 'pre_existing_customer_id'})) + assert response = @gateway.store(@credit_card, @options.merge({ customer_id: 'pre_existing_customer_id' })) assert_success response assert_equal 'spreedly-susanswidg-32268973-2091076-148408385-2894006614343495-148710226|c0302d83-a694-4bec-9086-d1886b9eefd9-148710226', response.authorization end @@ -231,7 +231,6 @@ def successful_split_purchase_response ) end - def failed_purchase_response %( {\"meta\":{},\"errors\":[{\"status\":\"400\",\"source\":{\"pointer\":\"/payments\"},\"title\":\"Bad Request\",\"detail\":\"Partner error: Credit card declined (transaction element shows reason for decline)\",\"code\":\"58\",\"meta\":{\"debug\":{\"message\":\"Include debug info with support request.\",\"resource\":\"/payments POST\",\"log\":\"2017/02/02/[23]eb325f3ca78b4f7eb2178a0d1e635a0e\",\"request\":\"73c22dc3-e980-11e6-9390-69c24d5ed1f4\"},\"transaction\":{\"code\":\"51\",\"message\":\"Insufficient funds\",\"invoice\":\"3d1f247d9112349e3db252f9f3327047\",\"authCode\":\"A11111\",\"avsResult\":\"T\"}}}]} diff --git a/test/unit/gateways/ebanx_test.rb b/test/unit/gateways/ebanx_test.rb index c5e93bc8e13..270e9486c98 100644 --- a/test/unit/gateways/ebanx_test.rb +++ b/test/unit/gateways/ebanx_test.rb @@ -1,6 +1,8 @@ require 'test_helper' class EbanxTest < Test::Unit::TestCase + include CommStub + def setup @gateway = EbanxGateway.new(integration_key: 'key') @credit_card = credit_card @@ -11,18 +13,250 @@ def setup billing_address: address, description: 'Store Purchase' } + + @network_token = network_tokenization_credit_card( + '4111111111111111', + brand: 'visa', + payment_cryptogram: 'network_token_example_cryptogram', + month: 12, + year: 2030 + ) end def test_successful_purchase @gateway.expects(:ssl_request).returns(successful_purchase_response) response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response assert_equal '592db57ad6933455efbb62a48d1dfa091dd7cd092109db99', response.authorization assert response.test? end + def test_successful_purchase_with_optional_processing_type_header + response = stub_comms(@gateway, :ssl_request) do + @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) + + assert_success response + end + + def test_successful_purchase_with_soft_descriptor + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge(soft_descriptor: 'ActiveMerchant')) + end.check_request do |_method, _endpoint, data, _headers| + assert_match %r{"soft_descriptor\":\"ActiveMerchant\"}, data + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_without_merchant_payment_code + # hexdigest of 1 is c4ca4238a0b923820dcc509a6f75849b + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match %r{"merchant_payment_code\":\"1\"}, data + assert_match %r{"merchant_payment_code\":\"c4ca4238a0b923820dcc509a6f75849b\"}, data + assert_match %r{"order_number\":\"1\"}, data + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_merchant_payment_code + # hexdigest of 2 is c81e728d9d4c2f636f067f89cc14862c + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge(merchant_payment_code: '2')) + end.check_request do |_method, _endpoint, data, _headers| + assert_match %r{"merchant_payment_code\":\"2\"}, data + assert_match %r{"merchant_payment_code\":\"c81e728d9d4c2f636f067f89cc14862c\"}, data + assert_match %r{"order_number\":\"1\"}, data + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_notification_url + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge(notification_url: 'https://notify.example.com/')) + end.check_request do |_method, _endpoint, data, _headers| + assert_match %r{"notification_url\":\"https://notify.example.com/\"}, data + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_default_payment_type_code + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match %r{"payment_type_code\":\"creditcard\"}, data + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_payment_type_code_override + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge({ payment_type_code: 'visa' })) + end.check_request do |_method, _endpoint, data, _headers| + assert_match %r{"payment_type_code\":\"visa\"}, data + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_stored_credentials_cardholder_recurring + options = @options.merge!({ + stored_credential: { + initial_transaction: true, + initiator: 'cardholder', + reason_type: 'recurring', + network_transaction_id: nil + } + }) + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match %r{"cof_type\":\"initial\"}, data + assert_match %r{"initiator\":\"CIT\"}, data + assert_match %r{"trans_type\":\"SCHEDULED_RECURRING\"}, data + assert_not_match %r{"mandate_id\"}, data + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_stored_credentials_cardholder_unscheduled + options = @options.merge!({ + stored_credential: { + initial_transaction: true, + initiator: 'cardholder', + reason_type: 'unscheduled', + network_transaction_id: nil + } + }) + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match %r{"cof_type\":\"initial\"}, data + assert_match %r{"initiator\":\"CIT\"}, data + assert_match %r{"trans_type\":\"CUSTOMER_COF\"}, data + assert_not_match %r{"mandate_id\"}, data + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_stored_credentials_cardholder_installment + options = @options.merge!({ + stored_credential: { + initial_transaction: true, + initiator: 'cardholder', + reason_type: 'installment', + network_transaction_id: nil + } + }) + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match %r{"cof_type\":\"initial\"}, data + assert_match %r{"initiator\":\"CIT\"}, data + assert_match %r{"trans_type\":\"INSTALLMENT\"}, data + assert_not_match %r{"mandate_id\"}, data + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_stored_credentials_merchant_installment + options = @options.merge!({ + stored_credential: { + initial_transaction: false, + initiator: 'merchant', + reason_type: 'installment', + network_transaction_id: '1234' + } + }) + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match %r{"cof_type\":\"stored\"}, data + assert_match %r{"initiator\":\"MIT\"}, data + assert_match %r{"trans_type\":\"INSTALLMENT\"}, data + assert_match %r{"mandate_id\":\"1234\"}, data + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_stored_credentials_merchant_unscheduled + options = @options.merge!({ + stored_credential: { + initial_transaction: false, + initiator: 'merchant', + reason_type: 'unscheduled', + network_transaction_id: '1234' + } + }) + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match %r{"cof_type\":\"stored\"}, data + assert_match %r{"initiator\":\"MIT\"}, data + assert_match %r{"trans_type\":\"MERCHANT_COF\"}, data + assert_match %r{"mandate_id\":\"1234\"}, data + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_stored_credentials_merchant_recurring + options = @options.merge!({ + stored_credential: { + initial_transaction: false, + initiator: 'merchant', + reason_type: 'recurring', + network_transaction_id: '1234' + } + }) + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match %r{"cof_type\":\"stored\"}, data + assert_match %r{"initiator\":\"MIT\"}, data + assert_match %r{"trans_type\":\"SCHEDULED_RECURRING\"}, data + assert_match %r{"mandate_id\":\"1234\"}, data + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_stored_credentials_cardholder_not_initial + options = @options.merge!({ + stored_credential: { + initial_transaction: false, + initiator: 'cardholder', + reason_type: 'unscheduled', + network_transaction_id: '1234' + } + }) + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match %r{"cof_type\":\"stored\"}, data + assert_match %r{"initiator\":\"CIT\"}, data + assert_match %r{"trans_type\":\"CUSTOMER_COF\"}, data + assert_match %r{"mandate_id\":\"1234\"}, data + end.respond_with(successful_purchase_response) + + assert_success response + end + def test_failed_purchase @gateway.expects(:ssl_request).returns(failed_purchase_response) @@ -54,11 +288,20 @@ def test_successful_capture response = @gateway.capture(@amount, 'authorization', @options) assert_success response - - assert_equal 'Sandbox - Test credit card, transaction captured', response.message + assert_equal '5dee94502bd59660b801c441ad5a703f2c4123f5fc892ccb', response.authorization + assert_equal 'Accepted', response.message assert response.test? end + def test_failed_partial_capture + @gateway.expects(:ssl_request).returns(failed_partial_capture_response) + + response = @gateway.capture(@amount, 'authorization', @options.merge(include_capture_amount: true)) + assert_failure response + assert_equal 'BP-CAP-11', response.error_code + assert_equal 'Partial capture not available', response.message + end + def test_failed_capture @gateway.expects(:ssl_request).returns(failed_capture_response) @@ -104,15 +347,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 @@ -120,11 +355,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 @@ -140,6 +375,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) @@ -153,11 +400,33 @@ def test_scrub assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed end + def test_successful_purchase_with_network_tokenization + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @network_token, @options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"network_token_pan\":\"#{@network_token.number}\"/, data) + assert_match(/"network_token_cryptogram\":\"#{@network_token.payment_cryptogram}\"/, data) + assert_match(/"network_token_expire_date\":\"#{@network_token.month}\/#{@network_token.year}\"/, data) + end.respond_with(successful_purchase_with_network_token) + + assert_success response + assert_equal '66e45f37b6700ed7119469c774a824a006a1da0293ffd204', response.authorization + end + + def test_scrub_network_token + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed_network_token), post_scrubbed_network_token + end + + def test_supported_countries + assert_equal %w[BR MX CO CL AR PE BO EC CR DO GT PA PY UY], EbanxGateway.supported_countries + end + private def pre_scrubbed %q( - request_body={\"integration_key\":\"1231000\",\"operation\":\"request\",\"payment\":{\"amount_total\":\"1.00\",\"currency_code\":\"USD\",\"merchant_payment_code\":\"2bed75b060e936834e354d944aeaa892\",\"name\":\"Longbob Longsen\",\"email\":\"unspecified@example.com\",\"document\":\"853.513.468-93\",\"payment_type_code\":\"visa\",\"creditcard\":{\"card_number\":\"4111111111111111\",\"card_name\":\"Longbob Longsen\",\"card_due_date\":\"9/2018\",\"card_cvv\":\"123\"},\"address\":\"Rua E\",\"street_number\":\"1040\",\"city\":\"Maracana\u{fa}\",\"state\":\"CE\",\"zipcode\":\"61919-230\",\"country\":\"BR\",\"phone_number\":\"(555)555-5555\"}} + request_body={\"integration_key\":\"Ac1EwnH0ud2UIndICS37l0\",\"operation\":\"request\",\"payment\":{\"amount_total\":\"1.00\",\"currency_code\":\"USD\",\"merchant_payment_code\":\"2bed75b060e936834e354d944aeaa892\",\"name\":\"Longbob Longsen\",\"email\":\"unspecified@example.com\",\"document\":\"853.513.468-93\",\"payment_type_code\":\"visa\",\"creditcard\":{\"card_number\":\"4111111111111111\",\"card_name\":\"Longbob Longsen\",\"card_due_date\":\"9/2018\",\"card_cvv\":\"123\"},\"address\":\"Rua E\",\"street_number\":\"1040\",\"city\":\"Maracana\u{fa}\",\"state\":\"CE\",\"zipcode\":\"61919-230\",\"country\":\"BR\",\"phone_number\":\"(555)555-5555\"}} ) end @@ -167,6 +436,18 @@ def post_scrubbed ) end + def pre_scrubbed_network_token + %q( + request_body={\"payment\":{\"amount_total\":\"1.00\",\"currency_code\":\"USD\",\"merchant_payment_code\":\"dc2df1269619de89d72ca6c8fc1ee52a\",\"instalments\":1,\"order_number\":\"d17a85de6bb15444b82320a7ab0ce846\",\"name\":\"Longbob Longsen\",\"email\":\"neymar@test.com\",\"document\":\"853.513.468-93\",\"payment_type_code\":\"creditcard\",\"creditcard\":{\"network_token_pan\":\"4111111111111111\",\"network_token_expire_date\":\"12/2030\",\"network_token_cryptogram\":\"example+/_cryptogram==\",\"soft_descriptor\":\"ActiveMerchant\"},\"address\":\"Rua E\",\"street_number\":\"1040\",\"city\":\"Maracana\u00FA\",\"state\":\"CE\",\"zipcode\":\"61919-230\",\"country\":\"br\",\"phone_number\":\"(555)555-5555\",\"tags\":[\"Spreedly\"]},\"integration_key\":\"test_ik_Gc1EwnH0ud2UIndICS37lA\",\"operation\":\"request\",\"device_id\":\"34c376b2767\",\"metadata\":{\"metadata_1\":\"test\",\"metadata_2\":\"test2\",\"merchant_payment_code\":\"d17a85de6bb15444b82320a7ab0ce846\"}} + ) + end + + def post_scrubbed_network_token + %q( + request_body={\"payment\":{\"amount_total\":\"1.00\",\"currency_code\":\"USD\",\"merchant_payment_code\":\"dc2df1269619de89d72ca6c8fc1ee52a\",\"instalments\":1,\"order_number\":\"d17a85de6bb15444b82320a7ab0ce846\",\"name\":\"Longbob Longsen\",\"email\":\"neymar@test.com\",\"document\":\"853.513.468-93\",\"payment_type_code\":\"creditcard\",\"creditcard\":{\"network_token_pan\":\"[FILTERED]\",\"network_token_expire_date\":\"12/2030\",\"network_token_cryptogram\":\"[FILTERED]\",\"soft_descriptor\":\"ActiveMerchant\"},\"address\":\"Rua E\",\"street_number\":\"1040\",\"city\":\"Maracana\u00FA\",\"state\":\"CE\",\"zipcode\":\"61919-230\",\"country\":\"br\",\"phone_number\":\"(555)555-5555\",\"tags\":[\"Spreedly\"]},\"integration_key\":\"[FILTERED]\",\"operation\":\"request\",\"device_id\":\"34c376b2767\",\"metadata\":{\"metadata_1\":\"test\",\"metadata_2\":\"test2\",\"merchant_payment_code\":\"d17a85de6bb15444b82320a7ab0ce846\"}} + ) + end + def successful_purchase_response %( {"payment":{"hash":"592db57ad6933455efbb62a48d1dfa091dd7cd092109db99","pin":"081043552","merchant_payment_code":"ca2251ed6ac582162b17d77dfd7fb98a","order_number":null,"status":"CO","status_date":"2017-05-30 15:10:01","open_date":"2017-05-30 15:10:01","confirm_date":"2017-05-30 15:10:01","transfer_date":null,"amount_br":"3.31","amount_ext":"1.00","amount_iof":"0.01","currency_rate":"3.3000","currency_ext":"USD","due_date":"2017-06-02","instalments":"1","payment_type_code":"visa","transaction_status":{"acquirer":"EBANX","code":"OK","description":"Sandbox - Test credit card, transaction captured"},"pre_approved":true,"capture_available":false,"customer":{"document":"85351346893","email":"unspecified@example.com","name":"LONGBOB LONGSEN","birth_date":null}},"status":"SUCCESS"} @@ -191,9 +472,27 @@ 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":"592dd65824427e4f5f50564c118f399869637bfb30d54f5b","pin":"081043654","merchant_payment_code":"8424e3000d64d056fbd58639957dc1c4","order_number":null,"status":"CO","status_date":"2017-05-30 17:30:16","open_date":"2017-05-30 17:30:15","confirm_date":"2017-05-30 17:30:16","transfer_date":null,"amount_br":"3.31","amount_ext":"1.00","amount_iof":"0.01","currency_rate":"3.3000","currency_ext":"USD","due_date":"2017-06-02","instalments":"1","payment_type_code":"visa","transaction_status":{"acquirer":"EBANX","code":"OK","description":"Sandbox - Test credit card, transaction captured"},"pre_approved":true,"capture_available":false,"customer":{"document":"85351346893","email":"unspecified@example.com","name":"LONGBOB LONGSEN","birth_date":null}},"status":"SUCCESS"} + {"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"} + ) + end + + def failed_partial_capture_response + %( + {"status":"ERROR", "status_code":"BP-CAP-11", "status_message":"Partial capture not available"} ) end @@ -244,4 +543,10 @@ def invalid_cred_response {"status":"ERROR","status_code":"DA-1","status_message":"Invalid integration key"} ) end + + def successful_purchase_with_network_token + %( + {"payment":{"hash":"66e45f37b6700ed7119469c774a824a006a1da0293ffd204","country":"br","merchant_payment_code":"dc2df1269619de89d72ca6c8fc1ee52a","order_number":"d17a85de6bb15444b82320a7ab0ce846","status":"CO","status_date":"2024-09-13 15:50:15","open_date":"2024-09-13 15:50:15","confirm_date":"2024-09-13 15:50:15","transfer_date":null,"amount_br":"5.85","amount_ext":"1.00","amount_iof":"0.02","currency_rate":"5.8300","currency_ext":"USD","due_date":"2024-09-16","instalments":"1","payment_type_code":"visa","details":{"billing_descriptor":"SPREEDLY"},"transaction_status":{"acquirer":"EBANX","code":"OK","description":"Accepted","authcode":"87017"},"pre_approved":true,"capture_available":false},"status":"SUCCESS"} + ) + end end diff --git a/test/unit/gateways/efsnet_test.rb b/test/unit/gateways/efsnet_test.rb index 1a3528186ef..15c79aecf1f 100644 --- a/test/unit/gateways/efsnet_test.rb +++ b/test/unit/gateways/efsnet_test.rb @@ -1,16 +1,15 @@ require 'test_helper' class EfsnetTest < Test::Unit::TestCase - def setup @gateway = EfsnetGateway.new( - :login => 'LOGIN', - :password => 'PASSWORD' + login: 'LOGIN', + password: 'PASSWORD' ) @credit_card = credit_card('4242424242424242') @amount = 100 - @options = { :order_id => 1, :billing_address => address } + @options = { order_id: 1, billing_address: address } end def test_successful_purchase @@ -22,7 +21,6 @@ def test_successful_purchase assert response.test? assert_equal '100018347764;1.00', response.authorization assert_equal 'Approved', response.message - end def test_unsuccessful_purchase @@ -37,28 +35,28 @@ def test_unsuccessful_purchase def test_credit @gateway.expects(:ssl_post).with(anything, regexp_matches(/AccountNumber>#{@credit_card.number}<\/AccountNumber/), anything).returns('') - @gateway.credit(@amount, @credit_card, :order_id => 5) + @gateway.credit(@amount, @credit_card, order_id: 5) end def test_deprecated_credit @gateway.expects(:ssl_post).with(anything, regexp_matches(/transaction_id<\/OriginalTransactionID>/), anything).returns('') assert_deprecation_warning(Gateway::CREDIT_DEPRECATION_MESSAGE) do - @gateway.credit(@amount, 'transaction_id', :order_id => 5) + @gateway.credit(@amount, 'transaction_id', order_id: 5) end end def test_refund @gateway.expects(:ssl_post).with(anything, regexp_matches(/transaction_id<\/OriginalTransactionID>/), anything).returns('') - @gateway.refund(@amount, 'transaction_id', :order_id => 5) + @gateway.refund(@amount, 'transaction_id', order_id: 5) end def test_authorize_is_valid_xml params = { - :order_id => 'order1', - :transaction_amount => '1.01', - :account_number => '4242424242424242', - :expiration_month => '12', - :expiration_year => '2029', + order_id: 'order1', + transaction_amount: '1.01', + account_number: '4242424242424242', + expiration_month: '12', + expiration_year: '2029' } assert data = @gateway.send(:post_data, :credit_card_authorize, params) @@ -67,10 +65,10 @@ def test_authorize_is_valid_xml def test_settle_is_valid_xml params = { - :order_id => 'order1', - :transaction_amount => '1.01', - :original_transaction_amount => '1.01', - :original_transaction_id => '1', + order_id: 'order1', + transaction_amount: '1.01', + original_transaction_amount: '1.01', + original_transaction_id: '1' } assert data = @gateway.send(:post_data, :credit_card_settle, params) @@ -92,49 +90,50 @@ def test_cvv_result end private + def successful_purchase_response - <<-XML - - - - 0 - 00 - APPROVED - 100018347764 - N - M - 123456 - 123456 - 080117 - 163222 - 1 - XXXXXXXXXXXX2224 - 1.00 - - + <<~XML + + + + 0 + 00 + APPROVED + 100018347764 + N + M + 123456 + 123456 + 080117 + 163222 + 1 + XXXXXXXXXXXX2224 + 1.00 + + XML end def unsuccessful_purchase_response - <<-XML - - - - 256 - 04 - DECLINED - 100018347784 - N - - - - 080117 - 163946 - 1 - XXXXXXXXXXXX2224 - 1.56 - - + <<~XML + + + + 256 + 04 + DECLINED + 100018347784 + N + + + + 080117 + 163946 + 1 + XXXXXXXXXXXX2224 + 1.56 + + XML end end diff --git a/test/unit/gateways/elavon_test.rb b/test/unit/gateways/elavon_test.rb index a5456d577d4..fc70e59102b 100644 --- a/test/unit/gateways/elavon_test.rb +++ b/test/unit/gateways/elavon_test.rb @@ -5,27 +5,55 @@ class ElavonTest < Test::Unit::TestCase def setup @gateway = ElavonGateway.new( - :login => 'login', - :user => 'user', - :password => 'password' - ) + login: 'login', + user: 'user', + password: 'password' + ) + + @multi_currency_gateway = ElavonGateway.new( + login: 'login', + user: 'user', + password: 'password', + multi_currency: true + ) + + @gateway_with_ssl_vendor_id = ElavonGateway.new( + login: 'login', + user: 'user', + password: 'password', + ssl_vendor_id: 'ABC123' + ) @credit_card = credit_card @amount = 100 + @stored_card = '4421912014039990' @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase' + order_id: '1', + 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 - @gateway.expects(:ssl_post).returns(successful_purchase_response) + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/123<\/ssl_cvv2cvc2>/, data) + end.respond_with(successful_purchase_response) - assert response = @gateway.purchase(@amount, @credit_card, @options) assert_success response - assert_equal '123456;00000000-0000-0000-0000-00000000000', response.authorization + assert_equal '093840;180820AD3-27AEE6EF-8CA7-4811-8D1F-E420C3B5041E', response.authorization assert response.test? end @@ -36,8 +64,8 @@ def test_successful_authorization assert_instance_of Response, response assert_success response - assert_equal '123456;00000000-0000-0000-0000-00000000000', response.authorization - assert_equal 'APPROVED', response.message + assert_equal '259404;150920ED4-3EB7A2DF-A5A7-48E6-97B6-D98A9DC0BD59', response.authorization + assert_equal 'APPROVAL', response.message assert response.test? end @@ -51,44 +79,44 @@ def test_failed_authorization def test_successful_capture @gateway.expects(:ssl_post).returns(successful_capture_response) - authorization = '123456;00000000-0000-0000-0000-00000000000' + authorization = '070213;110820ED4-23CA2F2B-A88C-40E1-AC46-9219F800A520' - assert response = @gateway.capture(@amount, authorization, :credit_card => @credit_card) + assert response = @gateway.capture(@amount, authorization, credit_card: @credit_card) assert_instance_of Response, response assert_success response - assert_equal '123456;00000000-0000-0000-0000-00000000000', response.authorization + assert_equal '070213;110820ED4-23CA2F2B-A88C-40E1-AC46-9219F800A520', response.authorization assert_equal 'APPROVAL', response.message assert response.test? end def test_successful_capture_with_auth_code @gateway.expects(:ssl_post).returns(successful_capture_response) - authorization = '123456;00000000-0000-0000-0000-00000000000' + authorization = '070213;110820ED4-23CA2F2B-A88C-40E1-AC46-9219F800A520' assert response = @gateway.capture(@amount, authorization) assert_instance_of Response, response assert_success response - assert_equal '123456;00000000-0000-0000-0000-00000000000', response.authorization + assert_equal '070213;110820ED4-23CA2F2B-A88C-40E1-AC46-9219F800A520', response.authorization assert_equal 'APPROVAL', response.message assert response.test? end def test_successful_capture_with_additional_options - authorization = '123456;00000000-0000-0000-0000-00000000000' + authorization = '070213;110820ED4-23CA2F2B-A88C-40E1-AC46-9219F800A520' response = stub_comms do - @gateway.capture(@amount, authorization, :test_mode => true, :partial_shipment_flag => true) - end.check_request do |endpoint, data, headers| - assert_match(/ssl_transaction_type=CCCOMPLETE/, data) - assert_match(/ssl_test_mode=TRUE/, data) - assert_match(/ssl_partial_shipment_flag=Y/, data) + @gateway.capture(@amount, authorization, test_mode: true, partial_shipment_flag: true) + end.check_request do |_endpoint, data, _headers| + assert_match(/CCCOMPLETE<\/ssl_transaction_type>/, data) + assert_match(/TRUE<\/ssl_test_mode>/, data) + assert_match(/Y<\/ssl_partial_shipment_flag>/, data) end.respond_with(successful_capture_response) assert_instance_of Response, response assert_success response - assert_equal '123456;00000000-0000-0000-0000-00000000000', response.authorization + assert_equal '070213;110820ED4-23CA2F2B-A88C-40E1-AC46-9219F800A520', response.authorization assert_equal 'APPROVAL', response.message assert response.test? end @@ -97,8 +125,7 @@ def test_successful_purchase_with_ip response = stub_comms do @gateway.purchase(@amount, @credit_card, @options.merge(ip: '203.0.113.0')) end.check_request do |_endpoint, data, _headers| - parsed = CGI.parse(data) - assert_equal ['203.0.113.0'], parsed['ssl_cardholder_ip'] + assert_match(/203.0.113.0/, data) end.respond_with(successful_purchase_response) assert_success response @@ -108,18 +135,109 @@ def test_successful_authorization_with_ip response = stub_comms do @gateway.authorize(@amount, @credit_card, @options.merge(ip: '203.0.113.0')) end.check_request do |_endpoint, data, _headers| - parsed = CGI.parse(data) - assert_equal ['203.0.113.0'], parsed['ssl_cardholder_ip'] + assert_match(/203.0.113.0<\/ssl_cardholder_ip>/, data) + end.respond_with(successful_authorization_response) + + assert_success response + end + + def test_successful_purchase_with_dynamic_dba + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(dba: 'MANYMAG*BAKERS MONTHLY')) + end.check_request do |_endpoint, data, _headers| + assert_match(/MANYMAG\*BAKERS MONTHLY<\/ssl_dynamic_dba>/, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_unscheduled + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(merchant_initiated_unscheduled: 'Y')) + end.check_request do |_endpoint, data, _headers| + assert_match(/Y<\/ssl_merchant_initiated_unscheduled>/, data) + 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%22version%22%3A%22EC_v1%22%2C %22data%22%3A%22QlzLxRFnNP9%2FGTaMhBwgmZ2ywntbr9%22%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%22version%22%3A%22EC_v1%22%2C %22data%22%3A%22QlzLxRFnNP9%2FGTaMhBwgmZ2ywntbr9%22%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')) + end.check_request do |_endpoint, data, _headers| + assert_match(/Y<\/ssl_add_token>/, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_sends_ssl_token_field + purchase_response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(ssl_token: '8675309')) + end.check_request do |_endpoint, data, _headers| + assert_match(/8675309<\/ssl_token>/, data) + refute_match(/8675309<\/ssl_token>/, data) + refute_match(/MANYMAG\*BAKERS MONTHLY<\/ssl_dynamic_dba>/, data) end.respond_with(successful_authorization_response) assert_success response end + def test_successful_purchase_with_multi_currency + response = stub_comms(@multi_currency_gateway) do + @multi_currency_gateway.purchase(@amount, @credit_card, @options.merge(currency: 'JPY')) + end.check_request do |_endpoint, data, _headers| + assert_match(/JPY<\/ssl_transaction_currency>/, data) + end.respond_with(successful_purchase_with_multi_currency_response) + + assert_success response + end + + def test_successful_purchase_without_multi_currency + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(currency: 'EUR', multi_currency: false)) + end.check_request do |_endpoint, data, _headers| + assert_no_match(/ssl_transaction_currency=EUR/, data) + end.respond_with(successful_purchase_response) + end + def test_failed_capture @gateway.expects(:ssl_post).returns(failed_authorization_response) authorization = '123456INVALID;00000000-0000-0000-0000-00000000000' - assert response = @gateway.capture(@amount, authorization, :credit_card => @credit_card) + assert response = @gateway.capture(@amount, authorization, credit_card: @credit_card) assert_instance_of Response, response assert_failure response end @@ -161,28 +279,20 @@ def test_unsuccessful_refund assert response = @gateway.refund(123, '456') assert_failure response - assert_equal 'The refund amount exceeds the original transaction amount.', response.message + assert_equal 'The amount exceeded the original transaction amount. Amount must be equal or lower than the original transaction amount.', response.message end def test_successful_verify response = stub_comms do @gateway.verify(@credit_card) - end.respond_with(successful_authorization_response, successful_void_response) + end.respond_with(successful_verify_response) assert_success response end - def test_successful_verify_failed_void - response = stub_comms do - @gateway.verify(@credit_card, @options) - end.respond_with(successful_authorization_response, failed_void_response) - assert_success response - assert_equal 'APPROVED', response.message - end - def test_unsuccessful_verify response = stub_comms do @gateway.verify(@credit_card, @options) - end.respond_with(failed_authorization_response, successful_void_response) + end.respond_with(failed_verify_response) assert_failure response assert_equal 'The Credit Card Number supplied in the authorization request appears to be invalid.', response.message end @@ -192,25 +302,25 @@ def test_invalid_login assert response = @gateway.purchase(@amount, @credit_card, @options) - assert_equal '7000', response.params['result'] - assert_equal 'The VirtualMerchant ID and/or User ID supplied in the authorization request is invalid.', response.message + assert_equal '4025', response.params['errorCode'] + assert_equal 'The credentials supplied in the authorization request are invalid.', response.message assert_failure response end def test_supported_card_types - assert_equal [:visa, :master, :american_express, :discover], ElavonGateway.supported_cardtypes + assert_equal %i[visa master american_express discover], ElavonGateway.supported_cardtypes end def test_avs_result @gateway.expects(:ssl_post).returns(successful_purchase_response) response = @gateway.purchase(@amount, @credit_card) - assert_equal 'X', response.avs_result['code'] + assert_equal 'M', response.avs_result['code'] end def test_cvv_result @gateway.expects(:ssl_post).returns(successful_purchase_response) response = @gateway.purchase(@amount, @credit_card) - assert_equal 'P', response.cvv_result['code'] + assert_equal 'M', response.cvv_result['code'] end def test_successful_store @@ -218,7 +328,7 @@ def test_successful_store assert response = @gateway.store(@credit_card, @options) assert_success response - assert_equal '7595301425001111', response.params['token'] + assert_equal '4421912014039990', response.params['token'] assert response.test? end @@ -252,7 +362,7 @@ def test_stripping_non_word_characters_from_zip @options[:billing_address][:zip] = bad_zip - @gateway.expects(:commit).with(anything, anything, has_entries(:avs_zip => stripped_zip), anything) + @gateway.expects(:commit).with(includes("#{stripped_zip}")) @gateway.purchase(@amount, @credit_card, @options) end @@ -260,18 +370,508 @@ def test_stripping_non_word_characters_from_zip def test_zip_codes_with_letters_are_left_intact @options[:billing_address][:zip] = '.K1%Z_5E3-' - @gateway.expects(:commit).with(anything, anything, has_entries(:avs_zip => 'K1Z5E3'), anything) + @gateway.expects(:commit).with(includes('K1Z5E3')) @gateway.purchase(@amount, @credit_card, @options) end + def test_strip_ampersands + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(shipping_address: { address1: 'Bats & Cats' })) + end.check_request do |_endpoint, data, _headers| + refute_match(/&/, data) + end.respond_with(successful_purchase_response) + end + + def test_split_full_network_transaction_id + oar_data = '010012318808182231420000047554200000000000093840023122123188' + ps2000_data = 'A8181831435010530042VE' + network_transaction_id = "#{oar_data}|#{ps2000_data}" + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential: { initiator: 'merchant', reason_type: 'recurring', network_transaction_id: })) + end.check_request do |_endpoint, data, _headers| + assert_match(/#{oar_data}<\/ssl_oar_data>/, data) + assert_match(/#{ps2000_data}<\/ssl_ps2000_data>/, data) + end.respond_with(successful_purchase_response) + end + + def test_oar_only_network_transaction_id + oar_data = '010012318808182231420000047554200000000000093840023122123188' + ps2000_data = nil + network_transaction_id = "#{oar_data}|#{ps2000_data}" + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential: { initiator: 'merchant', reason_type: 'recurring', network_transaction_id: })) + end.check_request do |_endpoint, data, _headers| + assert_match(/#{oar_data}<\/ssl_oar_data>/, data) + refute_match(/123<\/ssl_cvv2cvc2>/, data) + assert_match(/1<\/ssl_recurring_flag>/, data) + refute_match(/A7540295892588345510A<\/ssl_ps2000_data>/, data) + assert_match(/010012130901291622040000047554200000000000155836402916121309<\/ssl_oar_data>/, data) + assert_match(/1234566<\/ssl_approval_code>/, data) + assert_match(/1<\/ssl_recurring_flag>/, data) + refute_match(/A7540295892588345510A<\/ssl_ps2000_data>/, data) + assert_match(/010012130901291622040000047554200000000000155836402916121309<\/ssl_oar_data>/, data) + assert_match(/1234566<\/ssl_approval_code>/, data) + assert_match(/2<\/ssl_recurring_flag>/, data) + assert_match(/2<\/ssl_payment_number>/, data) + assert_match(/4<\/ssl_payment_count>/, data) + refute_match(/A7540295892588345510A<\/ssl_ps2000_data>/, data) + assert_match(/010012130901291622040000047554200000000000155836402916121309<\/ssl_oar_data>/, data) + assert_match(/1234566<\/ssl_approval_code>/, data) + assert_match(/Y<\/ssl_merchant_initiated_unscheduled>/, data) + assert_match(/12<\/ssl_entry_mode>/, data) + assert_match(/1234567890<\/ssl_par_value>/, data) + assert_match(/1<\/ssl_association_token_data>/, data) + assert_match(/1<\/ssl_recurring_flag>/, data) + refute_match(/1<\/ssl_recurring_flag>/, data) + refute_match(/2<\/ssl_recurring_flag>/, data) + assert_match(/2<\/ssl_payment_number>/, data) + assert_match(/4<\/ssl_payment_count>/, data) + refute_match(/Y<\/ssl_merchant_initiated_unscheduled>/, data) + assert_match(/1234567890<\/ssl_par_value>/, data) + assert_match(/1<\/ssl_association_token_data>/, data) + refute_match(/1<\/ssl_recurring_flag>/, data) + refute_match(/2<\/ssl_recurring_flag>/, data) + assert_match(/2<\/ssl_payment_number>/, data) + assert_match(/4<\/ssl_payment_count>/, data) + refute_match(/Y<\/ssl_merchant_initiated_unscheduled>/, data) + assert_match(/1234567890<\/ssl_par_value>/, data) + assert_match(/1<\/ssl_association_token_data>/, data) + refute_match(/#{ps2000_data}<\/ssl_ps2000_data>/, data) + end.respond_with(successful_purchase_response) + end + + def test_oar_transaction_id_without_pipe + oar_data = '010012318808182231420000047554200000000000093840023122123188' + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential: { initiator: 'merchant', reason_type: 'recurring', network_transaction_id: oar_data })) + end.check_request do |_endpoint, data, _headers| + assert_match(/#{oar_data}<\/ssl_oar_data>/, data) + refute_match(/#{ps2000_data}<\/ssl_ps2000_data>/, data) + end.respond_with(successful_purchase_response) + end + def test_custom_fields_in_request stub_comms do - @gateway.purchase(@amount, @credit_card, @options.merge(:customer_number => '123', :custom_fields => {:a_key => 'a value'})) - end.check_request do |endpoint, data, headers| - assert_match(/customer_number=123/, data) - assert_match(/a_key/, data) - refute_match(/ssl_a_key/, data) + @gateway.purchase(@amount, @credit_card, @options.merge(customer_number: '123', custom_fields: { a_key: 'a value' })) + end.check_request do |_endpoint, data, _headers| + assert_match(/123<\/ssl_customer_number>/, data) + assert_match(/a value<\/a_key>/, data) + end.respond_with(successful_purchase_response) + end + + def test_ssl_vendor_id_from_gateway_credentials + stub_comms do + @gateway_with_ssl_vendor_id.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/ABC123<\/ssl_vendor_id>/, data) + end.respond_with(successful_purchase_response) + end + + def test_ssl_vendor_id_from_options + stub_comms do + @gateway_with_ssl_vendor_id.purchase(@amount, @credit_card, @options.merge(ssl_vendor_id: 'My special ID')) + end.check_request do |_endpoint, data, _headers| + assert_match(/My special ID<\/ssl_vendor_id>/, data) + end.respond_with(successful_purchase_response) + end + + def test_truncate_special_characters + first_name = 'Ricky ™ Martínez įncogníto' + credit_card = @credit_card + credit_card.first_name = first_name + + stub_comms do + @gateway.purchase(@amount, credit_card, @options) + end.check_request do |_endpoint, data, _headers| + check = 'Ricky %E2%84%A2 Mart' + assert_match(/#{check}<\/ssl_first_name>/, data) + end.respond_with(successful_purchase_response) + end + + def test_successful_special_character_encoding_truncation + special_card = @credit_card + special_card.first_name = 'Fear & Loathing' + special_card.last_name = 'Castañeda' + + stub_comms do + @gateway.purchase(@amount, special_card, @options) + end.check_request do |_endpoint, data, _headers| + first = 'Fear %26amp; Loathin' + last = 'Casta%C3%B1eda' + assert_match(/#{first}<\/ssl_first_name>/, data) + assert_match(/#{last}<\/ssl_last_name>/, data) + end.respond_with(successful_purchase_response) + end + + def test_level_3_fields_in_request + level_3_data = { + customer_code: 'bob', + salestax: '3.45', + salestax_indicator: 'Y', + level3_indicator: 'Y', + ship_to_zip: '12345', + ship_to_country: 'US', + shipping_amount: '1234', + ship_from_postal_code: '54321', + discount_amount: '5', + duty_amount: '2', + national_tax_indicator: '0', + national_tax_amount: '10', + order_date: '280810', + other_tax: '3', + summary_commodity_code: '123', + merchant_vat_number: '222', + customer_vat_number: '333', + freight_tax_amount: '4', + vat_invoice_number: '26', + tracking_number: '45', + shipping_company: 'UFedzon', + other_fees: '2', + line_items: [ + { + description: 'thing', + product_code: '23', + commodity_code: '444', + quantity: '15', + unit_of_measure: 'kropogs', + unit_cost: '4.5', + discount_indicator: 'Y', + tax_indicator: 'Y', + discount_amount: '1', + tax_rate: '8.25', + tax_amount: '12', + tax_type: 'state', + extended_total: '500', + total: '525', + alternative_tax: '111' + }, + { + description: 'thing2', + product_code: '23', + commodity_code: '444', + quantity: '15', + unit_of_measure: 'kropogs', + unit_cost: '4.5', + discount_indicator: 'Y', + tax_indicator: 'Y', + discount_amount: '1', + tax_rate: '8.25', + tax_amount: '12', + tax_type: 'state', + extended_total: '500', + total: '525', + alternative_tax: '111' + } + ] + } + + options = @options.merge(level_3_data:) + stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/bob/, data) + assert_match(/3.45/, data) + assert_match(/Y/, data) + assert_match(/Y/, data) + assert_match(/12345/, data) + assert_match(/US/, data) + assert_match(/1234/, data) + assert_match(/54321/, data) + assert_match(/5/, data) + assert_match(/2/, data) + assert_match(/0/, data) + assert_match(/10/, data) + assert_match(/280810/, data) + assert_match(/3/, data) + assert_match(/123/, data) + assert_match(/222/, data) + assert_match(/333/, data) + assert_match(/4/, data) + assert_match(/26/, data) + assert_match(/45/, data) + assert_match(/UFedzon/, data) + assert_match(/2/, data) + assert_match(//, data) + assert_match(//, data) + assert_match(//, data) + assert_match(//, data) + assert_match(//, data) + assert_match(//, data) + assert_match(//, data) + assert_match(//, data) + assert_match(//, data) + assert_match(//, data) + assert_match(//, data) + assert_match(//, data) + assert_match(//, data) + assert_match(//, data) + assert_match(//, data) + end.respond_with(successful_purchase_response) + end + + def test_shipping_address_in_request + shipping_address = { + address1: '733 Foster St.', + city: 'Durham', + state: 'NC', + phone: '8887277750', + country: 'USA', + zip: '27701' + } + options = @options.merge(shipping_address:) + stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/733 Foster St./, data) + assert_match(/Durham/, data) + assert_match(/NC/, data) + assert_match(/8887277750/, data) + assert_match(/USA/, data) + assert_match(/27701/, data) end.respond_with(successful_purchase_response) end @@ -283,189 +883,434 @@ def test_transcript_scrubbing private def successful_purchase_response - "ssl_card_number=42********4242 - ssl_exp_date=0910 - ssl_amount=1.00 - ssl_invoice_number= - ssl_description=Test Transaction - ssl_result=0 - ssl_result_message=APPROVED - ssl_txn_id=00000000-0000-0000-0000-00000000000 - ssl_approval_code=123456 - ssl_cvv2_response=P - ssl_avs_response=X - ssl_account_balance=0.00 - ssl_txn_time=08/07/2009 09:54:18 PM" + <<-XML + + + 00 + Longsen + Widgets Inc + (555)555-5555 + 41**********9990 + + 010012318808182231420000047554200000000000093840023122123188 + 0 + 180820AD3-27AEE6EF-8CA7-4811-8D1F-E420C3B5041E + M + 093840 + paul@domain.com + 100.00 + K1C2N6 + 08/18/2020 06:31:42 PM + 0921 + VISA + + Apt 1 + CA + CREDITCARD + AUTHONLY + + 456 My Street + 0.00 + A8181831435010530042VE + ON + Ottawa + APPROVAL + Longbob + + M + VM + + XML + end + + def successful_purchase_with_multi_currency_response + <<-XML + + + 00 + + 41**********9990 + + 010012316708182238060000047554200000000000093864023122123167 + 0 + 180820ED3-1DD371B9-64DF-4902-B377-EBD095E6DAF0 + + M + 093864 + + 100 + JPY + 08/18/2020 06:38:06 PM + + 0921 + VISA + + CREDITCARD + + SALE + + 0.00 + + 0.00 + A8181838065010780213VE + APPROVAL + + + + VM + + XML end def successful_refund_response - "ssl_card_number=42*****2222 - ssl_exp_date= - ssl_amount=1.00 - ssl_customer_code= - ssl_invoice_number= - ssl_description= - ssl_company= - ssl_first_name= - ssl_last_name= - ssl_avs_address= - ssl_address2= - ssl_city= - ssl_state= - ssl_avs_zip= - ssl_country= - ssl_phone= - ssl_email= - ssl_result=0 - ssl_result_message=APPROVAL - ssl_txn_id=AA49315-C3D2B7BA-237C-1168-405A-CD5CAF928B0C - ssl_approval_code= - ssl_cvv2_response= - ssl_avs_response= - ssl_account_balance=0.00 - ssl_txn_time=08/21/2012 05:43:46 PM" + <<-XML + + + 00 + Longsen + Widgets Inc + (555)555-5555 + 41**********9990 + + 0 + 180820AD3-4BACDE38-63F3-427D-BFC1-1B3EB046056B + + 094012 + paul@domain.com + 100.00 + K1C2N6 + 08/18/2020 07:04:49 PM + 0921 + VISA + + Apt 1 + + CA + CREDITCARD + RETURN + + 456 My Street + 0.00 + ON + Ottawa + APPROVAL + Longbob + + + VM + + XML end def successful_void_response - "ssl_card_number=42*****2222 - ssl_exp_date=0913 - ssl_amount=1.00 - ssl_invoice_number= - ssl_description= - ssl_company= - ssl_first_name= - ssl_last_name= - ssl_avs_address= - ssl_address2= - ssl_city= - ssl_state= - ssl_avs_zip= - ssl_country= - ssl_phone= - ssl_email= - ssl_result=0 - ssl_result_message=APPROVAL - ssl_txn_id=AA49315-F04216E3-E556-E2E0-ADE9-4186A5F69105 - ssl_approval_code= - ssl_cvv2_response= - ssl_avs_response= - ssl_account_balance=1.00 - ssl_txn_time=08/21/2012 05:37:19 PM" + <<-XML + + + Longsen + + Widgets Inc + (555)555-5555 + 41**********9990 + 0 + 180820AD3-2E02E02D-A1FB-4926-A957-3930D3F7B869 + paul@domain.com + 100.00 + K1C2N6 + 08/18/2020 06:56:27 PM + 0921 + VISA + Apt 1 + + CA + CREDITCARD + DELETE + + 456 My Street + ON + Ottawa + APPROVAL + Longbob + + VM + + XML + end + + def successful_verify_response + <<-XML + + + 85 + CARDVERIFICATION + 41**********9990 + 010012309508182257450000047554200000000000093964023122123095 + 0 + 180820ED4-85DA9146-51AB-4FEC-8004-91C607047E5C + M + 093964 + 456 My Street + K1C2N6 + 08/18/2020 06:57:45 PM + 0.00 + A8181857455011610042VE + 0921 + APPROVAL + VISA + CREDITCARD + M + VM + + XML end def failed_purchase_response - "errorCode=5000 - errorName=Credit Card Number Invalid - errorMessage=The Credit Card Number supplied in the authorization request appears to be invalid." + <<-XML + + + 5000 + Credit Card Number Invalid + The Credit Card Number supplied in the authorization request appears to be invalid. + + XML end def failed_refund_response - "errorCode=5091 - errorName=Invalid Refund Amount - errorMessage=The refund amount exceeds the original transaction amount." + <<-XML + + + 5091 + Invalid amount + The amount exceeded the original transaction amount. Amount must be equal or lower than the original transaction amount. + + XML end def failed_void_response - "errorCode=5040 - errorName=Invalid Transaction ID - errorMessage=The transaction ID is invalid for this transaction type" + <<-XML + + + 5040 + Invalid Transaction ID + The transaction ID is invalid for this transaction type + + XML + end + + def failed_verify_response + <<-XML + + + 5000 + Credit Card Number Invalid + The Credit Card Number supplied in the authorization request appears to be invalid. + + XML end def invalid_login_response - <<-RESPONSE - ssl_result=7000\r - ssl_result_message=The VirtualMerchant ID and/or User ID supplied in the authorization request is invalid.\r - RESPONSE + <<-XML + + + 4025 + Invalid Credentials + The credentials supplied in the authorization request are invalid. + + XML end def successful_authorization_response - "ssl_card_number=42********4242 - ssl_exp_date=0910 - ssl_amount=1.00 - ssl_invoice_number= - ssl_description=Test Transaction - ssl_result=0 - ssl_result_message=APPROVED - ssl_txn_id=00000000-0000-0000-0000-00000000000 - ssl_approval_code=123456 - ssl_cvv2_response=P - ssl_avs_response=X - ssl_account_balance=0.00 - ssl_txn_time=08/07/2009 09:56:11 PM" + <<-XML + + + 00 + AUTHONLY + 41**********9990 + + 010012312309152159540000047554200000000000259404025921123123 + 0 + 150920ED4-3EB7A2DF-A5A7-48E6-97B6-D98A9DC0BD59 + M + 259404 + + 100.00 + 09/15/2020 05:59:54 PM + 0.00 + A9151759546571260030VE + 0921 + APPROVAL + VISA + + 3 + CREDITCARD + + M + 01 + + XML end def failed_authorization_response - "errorCode=5000 - errorName=Credit Card Number Invalid - errorMessage=The Credit Card Number supplied in the authorization request appears to be invalid." + <<-XML + + + 5000 + Credit Card Number Invalid + The Credit Card Number supplied in the authorization request appears to be invalid. + + XML end def successful_capture_response - "ssl_card_number=42********4242 - ssl_exp_date=0910 - ssl_amount=1.00 - ssl_customer_code= - ssl_salestax= - ssl_invoice_number= - ssl_result=0 - ssl_result_message=APPROVAL - ssl_txn_id=00000000-0000-0000-0000-00000000000 - ssl_approval_code=123456 - ssl_cvv2_response=P - ssl_avs_response=X - ssl_account_balance=0.00 - ssl_txn_time=08/07/2009 09:56:11 PM" + <<~XML + + Longsen + Widgets Inc + (555)555-5555 + 41**********9990 + + 0 + 110820ED4-23CA2F2B-A88C-40E1-AC46-9219F800A520 + + 070213 + paul@domain.com + 100.00 + K1C2N6 + 08/11/2020 10:08:14 PM + 0921 + VISA + + Apt 1 + CA + CREDITCARD + FORCE + + 456 My Street + 0.00 + ON + Ottawa + APPROVAL + Longbob + + + VM + + XML end def failed_capture_response - "errorCode=5040 - errorName=Invalid Transaction ID - errorMessage=The transaction ID is invalid for this transaction type" + <<-XML + + + 5004 + Invalid Approval Code + The FORCE Approval Code supplied in the authorization request appears to be invalid or blank. The FORCE Approval Code must be 6 or less alphanumeric characters. + + XML end def successful_store_response - "ssl_transaction_type=CCGETTOKEN - ssl_result=0 - ssl_token=7595301425001111 - ssl_card_number=41**********1111 - ssl_token_response=SUCCESS - ssl_add_token_response=Card Updated - vu_aamc_id=" + <<-XML + + + Longsen + Widgets Inc + (555)555-5555 + 41**********9990 + 0 + + + + paul@domain.com + K1C2N6 + 08/18/2020 07:01:16 PM + 0921 + VISA + Apt 1 + SUCCESS + CA + CREDITCARD + GETTOKEN + + 456 My Street + + 0.00 + ON + Ottawa + + Longbob + + + 4421912014039990 + Card Updated + + XML end def failed_store_response - "errorCode=5000 - errorName=Credit Card Number Invalid - errorMessage=The Credit Card Number supplied in the authorization request appears to be invalid." + <<-XML + + + 5000 + Credit Card Number Invalid + The Credit Card Number supplied in the authorization request appears to be invalid. + + XML end def successful_update_response - "ssl_token=7595301425001111 - ssl_card_type=VISA - ssl_card_number=************1111 - ssl_exp_date=1015 - ssl_company= - ssl_customer_id= - ssl_first_name=John - ssl_last_name=Doe - ssl_avs_address= - ssl_address2= - ssl_avs_zip= - ssl_city= - ssl_state= - ssl_country= - ssl_phone= - ssl_email= - ssl_description= - ssl_user_id=webpage - ssl_token_response=SUCCESS - ssl_result=0" + <<-XML + + + 4421912014039990 + VISA + ************9990 + 1021 + Widgets Inc + + Longbob + Longsen + 456 My Street + Apt 1 + Ottawa + ON + K1C2N6 + CA + (555)555-5555 + paul@domain.com + + webpage + SUCCESS + 0 + + XML end def failed_update_response - "errorCode=5000 - errorName=Credit Card Number Invalid - errorMessage=The Credit Card Number supplied in the authorization request appears to be invalid." + <<-XML + + + 4421912014039990 + VISA + ************9990 + 1021 + Widgets Inc + + Longbob + Longsen + 456 My Street + Apt 1 + Ottawa + ON + K1C2N6 + CA + (555)555-5555 + paul@domain.com + + apiuser + Failed + 1 + + XML end def pre_scrub @@ -474,35 +1319,38 @@ def pre_scrub opened starting SSL for api.demo.convergepay.com:443... SSL established -<- "POST /VirtualMerchantDemo/process.do 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: api.demo.convergepay.com\r\nContent-Length: 616\r\n\r\n" -<- "ssl_merchant_id=000127&ssl_pin=IERAOBEE5V0D6Q3Q6R51TG89XAIVGEQ3LGLKMKCKCVQBGGGAU7FN627GPA54P5HR&ssl_show_form=false&ssl_result_format=ASCII&ssl_user_id=ssltest&ssl_invoice_number=&ssl_description=Test+Transaction&ssl_card_number=4124939999999990&ssl_exp_date=0919&ssl_cvv2cvc2=123&ssl_cvv2cvc2_indicator=1&ssl_first_name=Longbob&ssl_last_name=Longsen&ssl_avs_address=456+My+Street&ssl_address2=Apt+1&ssl_avs_zip=K1C2N6&ssl_city=Ottawa&ssl_state=ON&ssl_company=Widgets+Inc&ssl_phone=%28555%29555-5555&ssl_country=CA&ssl_email=paul%40domain.com&ssl_cardholder_ip=203.0.113.0&ssl_amount=1.00&ssl_transaction_type=CCSALE" +<- "POST /VirtualMerchantDemo/processxml.do HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept: application/xml\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.demo.convergepay.com\r\nContent-Length: 1026\r\n\r\n" +<- "xmldata=\n 2020701\n apiuser\n ULV2VQJXA5UR19KFXZ8TUWEFWMFY5MYXJVVOS8JN69EWV8XTN8Y0HYCR8B11DIUU\n CCSALE\n 100\n 4124939999999990\n 0921\n 123\n 1\n Longbob\n Longsen\n \n Test Transaction\n 456 My Street\n Apt 1\n K1C2N6\n Ottawa\n ON\n Widgets Inc\n (555)555-5555\n CA\n paul@domain.com\n N\n\n" -> "HTTP/1.1 200 OK\r\n" --> "Date: Wed, 03 Jan 2018 21:40:26 GMT\r\n" --> "Pragma: no-cache\r\n" --> "Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0\r\n" +-> "Date: Tue, 15 Sep 2020 23:09:31 GMT\r\n" +-> "Server: Apache\r\n" +-> "X-Frame-Options: SAMEORIGIN\r\n" +-> "Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\r\n" -> "Expires: 0\r\n" --> "Content-Disposition: inline; filename=response.txt\r\n" +-> "Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0\r\n" -> "AuthApproved: true\r\n" +-> "Pragma: no-cache\r\n" +-> "X-Frame-Options: SAMEORIGIN\r\n" +-> "Content-Security-Policy: frame-ancestors 'self'\r\n" +-> "Content-Disposition: inline; filename=response.xml\r\n" +-> "CPID: ED4-dff741a6-df1a-463c-920e-2e4842eda7bf\r\n" -> "AuthResponse: AA\r\n" --> "Set-Cookie: JSESSIONID=00007wKfJV3-JFME8QiC_RCDjuI:14j4qkv92; HTTPOnly; Path=/; Secure\r\n" --> "Set-Cookie: JSESSIONID=0000uW6woWZ84eAJunhFLfJz8hS:14j4qkv92; HTTPOnly; Path=/; Secure\r\n" +-> "Content-Type: text/xml\r\n" +-> "Set-Cookie: JSESSIONID=UtM16S1VJSFsHChVlcYvM0cGVDWHMW1XD0vZ5T47.svplknxcnvrgdapp02; path=/VirtualMerchantDemo; secure; HttpOnly\r\n" -> "Connection: close\r\n" --> "Content-Type: text/plain\r\n" --> "Content-Language: en-US\r\n" --> "Content-Encoding: gzip\r\n" -> "Transfer-Encoding: chunked\r\n" -> "\r\n" --> "1A5 \r\n" -reading 421 bytes... --> "\x1F\x8B\b\x00\x00\x00\x00\x00\x00\x03MR\xEFk\xDB0\x10\xFD\xDE\xBF\xC2\x1F\xB7\x81[\xC9\xB1\x1D\xBB \x98\x7FtP\xD66!+\xDBGs\xB1o\x99\xC0\x96\x84%{q\xFF\xFA\xC9R\x12f\x10\xDC\xBDw\xBEw\xEF8\xAD\xFB\xA6\x85\xB1k\xC44\x1Cqd1\xFDr\xFB\xF2<'w\xDA\x16\xE0Y5\x1D\x18d$\xA7\xB9C`\x90\x930\x8C\xDE\x13_\xA1\xA1Gm\xE0\xCC\\\xC6\xC5,y\x8B\xD7\x9E\x0E\x130\xA0\x8FV9\x1Fu\xA8`4\xD3\x88\xBE\xFB\xDD;WM\xE1;\xFBJ9\xA8\x1E\r\x97\xE2Rp\x05A,\xEC\x17\xEFNht\xF0,Z\x87\xFF\xE6\xA36^\xE6E\x8A\xD3Q\x1E\x1D\xDC\xC3\xFF\xA8F\xE1P\x98u\x03]7\xA2\xD6,N\xD2\xE0u\t~\x98\x11\xD1x\xD63\x11+\x94\t\xA8W\xE5fa;c\xE0/\xB8\xDC\x9A\xB5\x03\xED\xDEn\xDD>\xB8b\xDFi\x15\xBD\xA5\xBE~u1.\xAC*\\\xAA\xFEH\x81\xECS\x92$\x9F\xED\v\xEDK\x1C\x8E\x03\xF0\x9E)\x98\xFA\xAF\x9D\xB4\xB1\xB8\xB7\xF6\x1Cc\a\x98z\xC3\xFCz}\xD2\fv(8!+\xF6\xFB\xC3\xEEg\xF1\xE28s\x16\r\xEF\x18\xD9\x10J\xB3\x82&!\xA9\xD3:O\xF3*|\x8A\xEA2\x8C\xB34\t\xB3o\xDB$,\xD3\xA2,\xB3tC\xB7E\xE9\xFE\x04\xA5F9\xC3:l\x87,\xDEnI\x1C9\xA2\x9D\xE7h\xD5TR\xE8\xCB\xD6W\x8B7\xE4\xE2\xBAu&\x9B#\xF4 Z{\x1C\xD7cX'2\xDCn\x9C\xD0\a\xB2y\x88\b\xCD\x02\x12?\xC6\xE41\xDA\x06\xFBW/\xB1\xDE\x9CY\x14\xB2\xEA\xF0T?\xBFW\xC5\xA1\xFE\aC\x85\x1DS\x8C\x02\x00\x00" -read 421 bytes +-> "44b\r\n" +reading 1099 bytes... +-> "\n00SALE41**********99900100123443091523092800000475542000000000002598490259231234430150920ED4-48E1CA31-F2C5-411B-9543-AEA81EFB81B9M259849100.0009/15/2020 07:09:28 PM0.00A9151909286574590030VE0921APPROVALVISA3CREDITCARDM01" +read 1099 bytes reading 2 bytes... -> "\r\n" read 2 bytes -> "0\r\n" -> "\r\n" Conn close -}} + } end def post_scrub @@ -511,34 +1359,37 @@ def post_scrub opened starting SSL for api.demo.convergepay.com:443... SSL established -<- "POST /VirtualMerchantDemo/process.do 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: api.demo.convergepay.com\r\nContent-Length: 616\r\n\r\n" -<- "ssl_merchant_id=000127&ssl_pin=[FILTERED]&ssl_show_form=false&ssl_result_format=ASCII&ssl_user_id=ssltest&ssl_invoice_number=&ssl_description=Test+Transaction&ssl_card_number=[FILTERED]&ssl_exp_date=0919&ssl_cvv2cvc2=[FILTERED]&ssl_cvv2cvc2_indicator=1&ssl_first_name=Longbob&ssl_last_name=Longsen&ssl_avs_address=456+My+Street&ssl_address2=Apt+1&ssl_avs_zip=K1C2N6&ssl_city=Ottawa&ssl_state=ON&ssl_company=Widgets+Inc&ssl_phone=%28555%29555-5555&ssl_country=CA&ssl_email=paul%40domain.com&ssl_cardholder_ip=203.0.113.0&ssl_amount=1.00&ssl_transaction_type=CCSALE" +<- "POST /VirtualMerchantDemo/processxml.do HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept: application/xml\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.demo.convergepay.com\r\nContent-Length: 1026\r\n\r\n" +<- "xmldata=\n 2020701\n apiuser\n [FILTERED]\n CCSALE\n 100\n [FILTERED]\n 0921\n [FILTERED]\n 1\n Longbob\n Longsen\n \n Test Transaction\n 456 My Street\n Apt 1\n K1C2N6\n Ottawa\n ON\n Widgets Inc\n (555)555-5555\n CA\n paul@domain.com\n N\n\n" -> "HTTP/1.1 200 OK\r\n" --> "Date: Wed, 03 Jan 2018 21:40:26 GMT\r\n" --> "Pragma: no-cache\r\n" --> "Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0\r\n" +-> "Date: Tue, 15 Sep 2020 23:09:31 GMT\r\n" +-> "Server: Apache\r\n" +-> "X-Frame-Options: SAMEORIGIN\r\n" +-> "Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\r\n" -> "Expires: 0\r\n" --> "Content-Disposition: inline; filename=response.txt\r\n" +-> "Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0\r\n" -> "AuthApproved: true\r\n" +-> "Pragma: no-cache\r\n" +-> "X-Frame-Options: SAMEORIGIN\r\n" +-> "Content-Security-Policy: frame-ancestors 'self'\r\n" +-> "Content-Disposition: inline; filename=response.xml\r\n" +-> "CPID: ED4-dff741a6-df1a-463c-920e-2e4842eda7bf\r\n" -> "AuthResponse: AA\r\n" --> "Set-Cookie: JSESSIONID=00007wKfJV3-JFME8QiC_RCDjuI:14j4qkv92; HTTPOnly; Path=/; Secure\r\n" --> "Set-Cookie: JSESSIONID=0000uW6woWZ84eAJunhFLfJz8hS:14j4qkv92; HTTPOnly; Path=/; Secure\r\n" +-> "Content-Type: text/xml\r\n" +-> "Set-Cookie: JSESSIONID=UtM16S1VJSFsHChVlcYvM0cGVDWHMW1XD0vZ5T47.svplknxcnvrgdapp02; path=/VirtualMerchantDemo; secure; HttpOnly\r\n" -> "Connection: close\r\n" --> "Content-Type: text/plain\r\n" --> "Content-Language: en-US\r\n" --> "Content-Encoding: gzip\r\n" -> "Transfer-Encoding: chunked\r\n" -> "\r\n" --> "1A5 \r\n" -reading 421 bytes... --> "\x1F\x8B\b\x00\x00\x00\x00\x00\x00\x03MR\xEFk\xDB0\x10\xFD\xDE\xBF\xC2\x1F\xB7\x81[\xC9\xB1\x1D\xBB \x98\x7FtP\xD66!+\xDBGs\xB1o\x99\xC0\x96\x84%{q\xFF\xFA\xC9R\x12f\x10\xDC\xBDw\xBEw\xEF8\xAD\xFB\xA6\x85\xB1k\xC44\x1Cqd1\xFDr\xFB\xF2<'w\xDA\x16\xE0Y5\x1D\x18d$\xA7\xB9C`\x90\x930\x8C\xDE\x13_\xA1\xA1Gm\xE0\xCC\\\xC6\xC5,y\x8B\xD7\x9E\x0E\x130\xA0\x8FV9\x1Fu\xA8`4\xD3\x88\xBE\xFB\xDD;WM\xE1;\xFBJ9\xA8\x1E\r\x97\xE2Rp\x05A,\xEC\x17\xEFNht\xF0,Z\x87\xFF\xE6\xA36^\xE6E\x8A\xD3Q\x1E\x1D\xDC\xC3\xFF\xA8F\xE1P\x98u\x03]7\xA2\xD6,N\xD2\xE0u\t~\x98\x11\xD1x\xD63\x11+\x94\t\xA8W\xE5fa;c\xE0/\xB8\xDC\x9A\xB5\x03\xED\xDEn\xDD>\xB8b\xDFi\x15\xBD\xA5\xBE~u1.\xAC*\\\xAA\xFEH\x81\xECS\x92$\x9F\xED\v\xEDK\x1C\x8E\x03\xF0\x9E)\x98\xFA\xAF\x9D\xB4\xB1\xB8\xB7\xF6\x1Cc\a\x98z\xC3\xFCz}\xD2\fv(8!+\xF6\xFB\xC3\xEEg\xF1\xE28s\x16\r\xEF\x18\xD9\x10J\xB3\x82&!\xA9\xD3:O\xF3*|\x8A\xEA2\x8C\xB34\t\xB3o\xDB$,\xD3\xA2,\xB3tC\xB7E\xE9\xFE\x04\xA5F9\xC3:l\x87,\xDEnI\x1C9\xA2\x9D\xE7h\xD5TR\xE8\xCB\xD6W\x8B7\xE4\xE2\xBAu&\x9B#\xF4 Z{\x1C\xD7cX'2\xDCn\x9C\xD0\a\xB2y\x88\b\xCD\x02\x12?\xC6\xE41\xDA\x06\xFBW/\xB1\xDE\x9CY\x14\xB2\xEA\xF0T?\xBFW\xC5\xA1\xFE\aC\x85\x1DS\x8C\x02\x00\x00" -read 421 bytes +-> "44b\r\n" +reading 1099 bytes... +-> "\n00SALE[FILTERED]0100123443091523092800000475542000000000002598490259231234430150920ED4-48E1CA31-F2C5-411B-9543-AEA81EFB81B9M259849100.0009/15/2020 07:09:28 PM0.00A9151909286574590030VE0921APPROVALVISA3CREDITCARDM01" +read 1099 bytes reading 2 bytes... -> "\r\n" read 2 bytes -> "0\r\n" -> "\r\n" Conn close -}} + } end end diff --git a/test/unit/gateways/element_test.rb b/test/unit/gateways/element_test.rb index c731345435d..694af43d9a9 100644 --- a/test/unit/gateways/element_test.rb +++ b/test/unit/gateways/element_test.rb @@ -1,6 +1,8 @@ require 'test_helper' class ElementTest < Test::Unit::TestCase + include CommStub + def setup @gateway = ElementGateway.new(account_id: '', account_token: '', application_id: '', acceptor_id: '', application_name: '', application_version: '') @credit_card = credit_card @@ -23,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) @@ -61,6 +75,7 @@ def test_failed_purchase_with_payment_account_token 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) @@ -127,32 +142,236 @@ def test_failed_void end def test_successful_verify - @gateway.expects(:ssl_post).times(2).returns(successful_authorize_response, successful_void_response) + @gateway.expects(:ssl_post).returns(successful_verify_response) response = @gateway.verify(@credit_card, @options) assert_success response end - def test_successful_verify_with_failed_void - @gateway.expects(:ssl_post).times(2).returns(successful_authorize_response, failed_void_response) + 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 'Present', data + end.respond_with(successful_purchase_response) - response = @gateway.verify(@credit_card, @options) assert_success response end - def test_failed_verify - @gateway.expects(:ssl_post).returns(failed_authorize_response) + 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 - response = @gateway.verify(@credit_card, @options) - assert_failure response + 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 'NotUsed', data + end.respond_with(successful_purchase_response) + + assert_success response end - def test_handles_error_response - @gateway.expects(:ssl_post).returns(error_response) + 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 'NotUsed', data + end.respond_with(successful_purchase_response) - response = @gateway.purchase(@amount, @credit_card, @options) - assert_equal response.message, 'TargetNamespace required' - assert_failure 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 'True', 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 'True', 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_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_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_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_no_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 'True', 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 'True', 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_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_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_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_no_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_merchant_descriptor + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(merchant_descriptor: 'Flowerpot Florists')) + end.check_request do |_endpoint, data, _headers| + assert_match 'Flowerpot Florists', 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 '1FoodRestaurant', data + assert_match '123', data + end.respond_with(successful_credit_response) end def test_scrub @@ -163,104 +382,116 @@ def test_scrub private def pre_scrubbed - <<-XML -\n\n \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
\n
\n
\n + <<~XML + \n\n \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
\n
\n
\n XML end def post_scrubbed - <<-XML -\n\n \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
\n
\n
\n + <<~XML + \n\n \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
\n
\n
\n XML end def error_response - <<-XML -103TargetNamespace required + <<~XML + 103TargetNamespace required XML end def successful_purchase_response - <<-XML -0Approved20151201104518UTC-05:00000APRegularTotals1962962.00FullBatchCurrentDefaultNMVisa2005831886000045SystemDefaultaVb001234567810425c0425d5e00FalseFalseFalseFalseNULL_PROCESSOR_TESTApproved1False1.00DefaultUnknownCreditCardNullNull
456 My StreetK1C2N6
OneTimeFutureFalseActiveCheckingPersonalNullNullFalseFalseFalseNotUsedUnknownUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultRegularNotUsedDefaultUnusedUnusedNoAdjustmentsFalseNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNonParticipantDefaultDefaultNull
+ <<~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 + 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 + 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 + 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 + 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 + 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 + 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 + 20Declined20151201120315UTC-05:00007DECLINEDRegularTotals1FullBatchCurrentDefaultNMVisa2005832537SystemDefaultaVb001234567810425c0425d5e00FalseFalseFalseFalseNULL_PROCESSOR_TESTDeclined2FalseDefaultUnknownCreditCardNullNull
456 My StreetK1C2N6
OneTimeFutureFalseActiveCheckingPersonalNullNullFalseFalseFalseNotUsedUnknownUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultRegularNotUsedDefaultUnusedUnusedNoAdjustmentsFalseNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNonParticipantDefaultDefaultNull
XML end def successful_capture_response - <<-XML -0Success20151201120222UTC-05:00000APRegularTotals1972963.00FullBatchCurrentDefaultVisa2005832535000002SystemDefaultaVb001234567810425c0425d5e00FalseFalseFalseFalseNULL_PROCESSOR_TESTApproved1FalseDefaultUnknownCreditCardNullNull
OneTimeFutureFalseActiveCheckingPersonalNullNullFalseFalseFalseNotUsedUnknownUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultRegularNotUsedDefaultUnusedUnusedNoAdjustmentsFalseNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNonParticipantDefaultDefaultNull + <<~XML + 0Success20151201120222UTC-05:00000APRegularTotals1972963.00FullBatchCurrentDefaultVisa2005832535000002SystemDefaultaVb001234567810425c0425d5e00FalseFalseFalseFalseNULL_PROCESSOR_TESTApproved1FalseDefaultUnknownCreditCardNullNull
OneTimeFutureFalseActiveCheckingPersonalNullNullFalseFalseFalseNotUsedUnknownUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultRegularNotUsedDefaultUnusedUnusedNoAdjustmentsFalseNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNonParticipantDefaultDefaultNull XML end def failed_capture_response - <<-XML -101TransactionID requiredUTC-05:00RegularTotalsFullBatchCurrentDefaultSystemDefaultFalseFalseFalseFalseFalseDefaultUnknownCreditCardNullNull
OneTimeFutureFalseActiveCheckingPersonalNullNullFalseFalseFalseNotUsedUnknownUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultRegularNotUsedDefaultUnusedUnusedNoAdjustmentsFalseNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNonParticipantDefaultDefaultNull + <<~XML + 101TransactionID requiredUTC-05:00RegularTotalsFullBatchCurrentDefaultSystemDefaultFalseFalseFalseFalseFalseDefaultUnknownCreditCardNullNull
OneTimeFutureFalseActiveCheckingPersonalNullNullFalseFalseFalseNotUsedUnknownUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultRegularNotUsedDefaultUnusedUnusedNoAdjustmentsFalseNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNonParticipantDefaultDefaultNull XML end def successful_refund_response - <<-XML -0Approved20151201120437UTC-05:00000APRegularTotals1992963.00FullBatchCurrentDefaultVisa2005832540000004SystemDefaultFalseFalseFalseFalseNULL_PROCESSOR_TESTApproved1FalseDefaultUnknownCreditCardNullNull
456 My StreetK1C2N6
OneTimeFutureFalseActiveCheckingPersonalNullNullFalseFalseFalseNotUsedUnknownUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultRegularNotUsedDefaultUnusedUnusedNoAdjustmentsFalseNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNonParticipantDefaultDefaultNull
+ <<~XML + 0Approved20151201120437UTC-05:00000APRegularTotals1992963.00FullBatchCurrentDefaultVisa2005832540000004SystemDefaultFalseFalseFalseFalseNULL_PROCESSOR_TESTApproved1FalseDefaultUnknownCreditCardNullNull
456 My StreetK1C2N6
OneTimeFutureFalseActiveCheckingPersonalNullNullFalseFalseFalseNotUsedUnknownUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultRegularNotUsedDefaultUnusedUnusedNoAdjustmentsFalseNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNonParticipantDefaultDefaultNull
XML end def failed_refund_response - <<-XML -101TransactionID requiredUTC-05:00RegularTotalsFullBatchCurrentDefaultSystemDefaultFalseFalseFalseFalseFalseDefaultUnknownCreditCardNullNull
OneTimeFutureFalseActiveCheckingPersonalNullNullFalseFalseFalseNotUsedUnknownUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultRegularNotUsedDefaultUnusedUnusedNoAdjustmentsFalseNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNonParticipantDefaultDefaultNull + <<~XML + 101TransactionID requiredUTC-05:00RegularTotalsFullBatchCurrentDefaultSystemDefaultFalseFalseFalseFalseFalseDefaultUnknownCreditCardNullNull
OneTimeFutureFalseActiveCheckingPersonalNullNullFalseFalseFalseNotUsedUnknownUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultRegularNotUsedDefaultUnusedUnusedNoAdjustmentsFalseNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNonParticipantDefaultDefaultNull XML end def successful_void_response - <<-XML -0Success20151201120516UTC-05:00006REVERSEDRegularTotalsFullBatchCurrentDefaultVisa2005832551000005SystemDefaultaVb001234567810425c0425d5e00FalseFalseFalseFalseNULL_PROCESSOR_TESTSuccess8FalseDefaultUnknownCreditCardNullNull
OneTimeFutureFalseActiveCheckingPersonalNullNullFalseFalseFalseNotUsedUnknownUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultRegularNotUsedDefaultUnusedUnusedNoAdjustmentsFalseNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNonParticipantDefaultDefaultNull + <<~XML + 0Success20151201120516UTC-05:00006REVERSEDRegularTotalsFullBatchCurrentDefaultVisa2005832551000005SystemDefaultaVb001234567810425c0425d5e00FalseFalseFalseFalseNULL_PROCESSOR_TESTSuccess8FalseDefaultUnknownCreditCardNullNull
OneTimeFutureFalseActiveCheckingPersonalNullNullFalseFalseFalseNotUsedUnknownUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultRegularNotUsedDefaultUnusedUnusedNoAdjustmentsFalseNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNonParticipantDefaultDefaultNull XML end def failed_void_response - <<-XML -101TransactionAmount requiredUTC-05:00RegularTotalsFullBatchCurrentDefaultSystemDefaultFalseFalseFalseFalseFalseDefaultUnknownCreditCardNullNull
OneTimeFutureFalseActiveCheckingPersonalNullNullFalseFalseFalseNotUsedUnknownUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultRegularNotUsedDefaultUnusedUnusedNoAdjustmentsFalseNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNonParticipantDefaultDefaultNull + <<~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/epay_test.rb b/test/unit/gateways/epay_test.rb index 462ecf1cbf4..91a03ed50e5 100644 --- a/test/unit/gateways/epay_test.rb +++ b/test/unit/gateways/epay_test.rb @@ -5,11 +5,12 @@ def setup Base.mode = :test @gateway = EpayGateway.new( - :login => '10100111001', - :password => 'http://example.com' + login: '10100111001', + password: 'http://example.com' ) @credit_card = credit_card + @options = { three_d_secure: { eci: '7', xid: '123', cavv: '456', version: '2', ds_transaction_id: '798' } } end def test_successful_purchase @@ -25,8 +26,23 @@ 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 + @gateway.expects(:raw_ssl_request).returns(valid_authorize_3ds_response) + + assert response = @gateway.authorize(100, @credit_card, @options) + assert_success response + assert_equal '123', response.authorization + end + + def test_failed_3ds_purchase + @gateway.expects(:raw_ssl_request).returns(invalid_authorize_3ds_response) + + assert response = @gateway.authorize(100, @credit_card, @options) + assert_success response + assert_equal '123', response.authorization end def test_invalid_characters_in_response @@ -34,12 +50,11 @@ 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 - @gateway.expects(:raw_ssl_request).returns(Net::HTTPBadRequest.new(1.0, 400,'Bad Request')) + @gateway.expects(:raw_ssl_request).returns(Net::HTTPBadRequest.new(1.0, 400, 'Bad Request')) assert response = @gateway.authorize(100, @credit_card) assert_equal 400, response.params['response_code'] @@ -105,13 +120,13 @@ def test_deprecated_credit def test_authorize_sends_order_number @gateway.expects(:raw_ssl_request).with(anything, anything, regexp_matches(/orderid=1234/), anything).returns(valid_authorize_response) - @gateway.authorize(100, '123', :order_id => '#1234') + @gateway.authorize(100, '123', order_id: '#1234') end def test_purchase_sends_order_number @gateway.expects(:raw_ssl_request).with(anything, anything, regexp_matches(/orderid=1234/), anything).returns(valid_authorize_response) - @gateway.purchase(100, '123', :order_id => '#1234') + @gateway.purchase(100, '123', order_id: '#1234') end def test_transcript_scrubbing @@ -132,6 +147,14 @@ def invalid_authorize_response_with_invalid_characters { 'Location' => 'https://ssl.ditonlinebetalingssystem.dk/auth/default.aspx?decline=1&error=209&errortext=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' } end + def valid_authorize_3ds_response + { 'Location' => 'https://ssl.ditonlinebetalingssystem.dk/auth/default.aspx?accept=1&tid=123&&amount=100&cur=208&date=20101117&time=2357&cardnopostfix=3000&fraud=1&cardid=18&transfee=0&eci=7&xci=123&cavv=456&threeds_version=2&ds_transaction_id=798' } + end + + def invalid_authorize_3ds_response + { 'Location' => 'https://ssl.ditonlinebetalingssystem.dk/auth/default.aspx?accept=1&tid=123&&amount=100&cur=208&date=20101117&time=2357&cardnopostfix=3000&fraud=1&cardid=18&transfee=0&eci=5&xci=1234&cavv=3456&threeds_version=1&ds_transaction_id=6798' } + end + def valid_capture_response 'true0-1' end diff --git a/test/unit/gateways/evo_ca_test.rb b/test/unit/gateways/evo_ca_test.rb index e699e1462a1..75b4513fa67 100644 --- a/test/unit/gateways/evo_ca_test.rb +++ b/test/unit/gateways/evo_ca_test.rb @@ -2,19 +2,19 @@ class EvoCaTest < Test::Unit::TestCase def setup - @gateway = EvoCaGateway.new(:username => 'demo', :password => 'password') + @gateway = EvoCaGateway.new(username: 'demo', password: 'password') @credit_card = credit_card @amount = 100 @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase', - :tracking_number => '123456789-0', - :shipping_carrier => 'fedex', - :email => 'evo@example.com', - :ip => '127.0.0.1' + order_id: '1', + billing_address: address, + description: 'Store Purchase', + tracking_number: '123456789-0', + shipping_carrier: 'fedex', + email: 'evo@example.com', + ip: '127.0.0.1' } end @@ -84,7 +84,7 @@ def test_successful_void def test_successful_update @gateway.expects(:ssl_post).returns(successful_update_response) - assert response = @gateway.update('1812639342', :tracking_number => '1234', :shipping_carrier => 'fedex') + assert response = @gateway.update('1812639342', tracking_number: '1234', shipping_carrier: 'fedex') assert_success response assert_equal '1812639342', response.authorization end @@ -99,7 +99,7 @@ def test_successful_refund def test_add_address result = {} - @gateway.send(:add_address, result, :address => {:address1 => '123 Main Street', :country => 'CA', :state => 'BC'} ) + @gateway.send(:add_address, result, address: { address1: '123 Main Street', country: 'CA', state: 'BC' }) assert_equal %w{address1 address2 city company country firstname lastname phone state zip}, result.stringify_keys.keys.sort assert_equal 'BC', result[:state] assert_equal '123 Main Street', result[:address1] @@ -109,7 +109,7 @@ def test_add_address def test_add_shipping_address result = {} - @gateway.send(:add_address, result, :shipping_address => {:address1 => '123 Main Street', :country => 'CA', :state => 'BC'} ) + @gateway.send(:add_address, result, shipping_address: { address1: '123 Main Street', country: 'CA', state: 'BC' }) assert_equal %w{shipping_address1 shipping_address2 shipping_city shipping_company shipping_country shipping_firstname shipping_lastname shipping_state shipping_zip}, result.stringify_keys.keys.sort assert_equal 'BC', result[:shipping_state] assert_equal '123 Main Street', result[:shipping_address1] diff --git a/test/unit/gateways/eway_managed_test.rb b/test/unit/gateways/eway_managed_test.rb index 5e8c914b611..a920ac70c67 100644 --- a/test/unit/gateways/eway_managed_test.rb +++ b/test/unit/gateways/eway_managed_test.rb @@ -4,70 +4,71 @@ class EwayManagedTest < Test::Unit::TestCase def setup Base.mode = :test - @gateway = EwayManagedGateway.new(:username => 'username', :login => 'login', :password => 'password') + @gateway = EwayManagedGateway.new(username: 'username', login: 'login', password: 'password') - @valid_card='4444333322221111' - @valid_customer_id='9876543211000' + @valid_card = '4444333322221111' + @valid_customer_id = '9876543211000' @credit_card = credit_card(@valid_card) @declined_card = credit_card('4444111111111111') @amount = 100 - @options = { :billing_address => { - :address1 => '1234 My Street', - :address2 => 'Apt 1', - :company => 'Widgets Inc', - :city => 'Ottawa', - :state => 'ON', - :zip => 'K1C2N6', - :country => 'au', - :title => 'Mr.', - :phone => '(555)555-5555' - }, - :email => 'someguy1232@fakeemail.net', - :order_id => '1000', - :customer => 'mycustomerref', - :description => 'My Description', - :invoice => 'invoice-4567' + @options = { + billing_address: { + address1: '1234 My Street', + address2: 'Apt 1', + company: 'Widgets Inc', + city: 'Ottawa', + state: 'ON', + zip: 'K1C2N6', + country: 'au', + title: 'Mr.', + phone: '(555)555-5555' + }, + email: 'someguy1232@fakeemail.net', + order_id: '1000', + customer: 'mycustomerref', + description: 'My Description', + invoice: 'invoice-4567' } end def test_should_require_billing_address_on_store assert_raise ArgumentError do - @gateway.store(@credit_card, { }) + @gateway.store(@credit_card, {}) end assert_raise ArgumentError do - @gateway.store(@credit_card, { :billing_address => {} }) + @gateway.store(@credit_card, { billing_address: {} }) end assert_raise ArgumentError do - @gateway.store(@credit_card, { :billing_address => { :title => 'Mr.' } }) + @gateway.store(@credit_card, { billing_address: { title: 'Mr.' } }) end assert_raise ArgumentError do - @gateway.store(@credit_card, { :billing_address => { :country => 'au' } }) + @gateway.store(@credit_card, { billing_address: { country: 'au' } }) end assert_nothing_raised do @gateway.expects(:ssl_post).returns(successful_store_response) - @gateway.store(@credit_card, { :billing_address => { :title => 'Mr.', :country => 'au' } }) + @gateway.store(@credit_card, { billing_address: { title: 'Mr.', country: 'au' } }) end end def test_should_require_billing_address_on_update assert_raise ArgumentError do - @gateway.update(@valid_customer_id, @credit_card, { }) + @gateway.update(@valid_customer_id, @credit_card, {}) end assert_raise ArgumentError do - @gateway.update(@valid_customer_id, @credit_card, { :billing_address => {} }) + @gateway.update(@valid_customer_id, @credit_card, { billing_address: {} }) end assert_raise ArgumentError do - @gateway.update(@valid_customer_id, @credit_card, { :billing_address => { :title => 'Mr.' } }) + @gateway.update(@valid_customer_id, @credit_card, { billing_address: { title: 'Mr.' } }) end assert_raise ArgumentError do - @gateway.update(@valid_customer_id, @credit_card, { :billing_address => { :country => 'au' } }) + @gateway.update(@valid_customer_id, @credit_card, { billing_address: { country: 'au' } }) end assert_nothing_raised do @gateway.expects(:ssl_post).returns(successful_update_response) - @gateway.update(@valid_customer_id, @credit_card, { :billing_address => { :title => 'Mr.', :country => 'au' } }) + @gateway.update(@valid_customer_id, @credit_card, { billing_address: { title: 'Mr.', country: 'au' } }) end end @@ -84,7 +85,7 @@ def test_successful_purchase end def test_expected_request_on_purchase - @gateway.expects(:ssl_post).with { |endpoint, data, headers| + @gateway.expects(:ssl_post).with { |_endpoint, data, _headers| # Compare the actual and expected XML documents, by converting them to Hashes first expected = Hash.from_xml(expected_purchase_request) actual = Hash.from_xml(data) @@ -100,7 +101,7 @@ def test_purchase_invoice_reference_comes_from_order_id_or_invoice options[:order_id] = 'order_id' options.delete(:invoice) - @gateway.expects(:ssl_post).with { |endpoint, data, headers| + @gateway.expects(:ssl_post).with { |_endpoint, data, _headers| request_hash = Hash.from_xml(data) request_hash['Envelope']['Body']['ProcessPayment']['invoiceReference'] == 'order_id' }.returns(successful_purchase_response) @@ -110,7 +111,7 @@ def test_purchase_invoice_reference_comes_from_order_id_or_invoice options[:invoice] = 'invoice' options.delete(:order_id) - @gateway.expects(:ssl_post).with { |endpoint, data, headers| + @gateway.expects(:ssl_post).with { |_endpoint, data, _headers| request_hash = Hash.from_xml(data) request_hash['Envelope']['Body']['ProcessPayment']['invoiceReference'] == 'invoice' }.returns(successful_purchase_response) @@ -120,12 +121,11 @@ def test_purchase_invoice_reference_comes_from_order_id_or_invoice options[:order_id] = 'order_id' options[:invoice] = 'invoice' - @gateway.expects(:ssl_post).with { |endpoint, data, headers| + @gateway.expects(:ssl_post).with { |_endpoint, data, _headers| request_hash = Hash.from_xml(data) request_hash['Envelope']['Body']['ProcessPayment']['invoiceReference'] == 'order_id' }.returns(successful_purchase_response) @gateway.purchase(@amount, @valid_customer_id, options) - end def test_invalid_customer_id @@ -148,7 +148,7 @@ def test_successful_store end def test_expected_request_on_store - @gateway.expects(:ssl_post).with { |endpoint, data, headers| + @gateway.expects(:ssl_post).with { |_endpoint, data, _headers| # Compare the actual and expected XML documents, by converting them to Hashes first expected = Hash.from_xml(expected_store_request) actual = Hash.from_xml(data) @@ -164,7 +164,7 @@ def test_email_on_store_may_come_from_options_root_or_billing_address options.delete(:email) options[:billing_address][:email] = 'email+billing@example.com' - @gateway.expects(:ssl_post).with { |endpoint, data, headers| + @gateway.expects(:ssl_post).with { |_endpoint, data, _headers| request_hash = Hash.from_xml(data) request_hash['Envelope']['Body']['CreateCustomer']['Email'] == 'email+billing@example.com' }.returns(successful_store_response) @@ -174,7 +174,7 @@ def test_email_on_store_may_come_from_options_root_or_billing_address options[:billing_address].delete(:email) options[:email] = 'email+root@example.com' - @gateway.expects(:ssl_post).with { |endpoint, data, headers| + @gateway.expects(:ssl_post).with { |_endpoint, data, _headers| request_hash = Hash.from_xml(data) request_hash['Envelope']['Body']['CreateCustomer']['Email'] == 'email+root@example.com' }.returns(successful_store_response) @@ -184,7 +184,7 @@ def test_email_on_store_may_come_from_options_root_or_billing_address options[:billing_address][:email] = 'email+billing@example.com' options[:email] = 'email+root@example.com' - @gateway.expects(:ssl_post).with { |endpoint, data, headers| + @gateway.expects(:ssl_post).with { |_endpoint, data, _headers| request_hash = Hash.from_xml(data) request_hash['Envelope']['Body']['CreateCustomer']['Email'] == 'email+billing@example.com' }.returns(successful_store_response) @@ -198,7 +198,7 @@ def test_customer_ref_on_store_may_come_from_options_root_or_billing_address options.delete(:customer) options[:billing_address][:customer_ref] = 'customer_ref+billing' - @gateway.expects(:ssl_post).with { |endpoint, data, headers| + @gateway.expects(:ssl_post).with { |_endpoint, data, _headers| request_hash = Hash.from_xml(data) request_hash['Envelope']['Body']['CreateCustomer']['CustomerRef'] == 'customer_ref+billing' }.returns(successful_store_response) @@ -208,7 +208,7 @@ def test_customer_ref_on_store_may_come_from_options_root_or_billing_address options[:billing_address].delete(:customer_ref) options[:customer] = 'customer_ref+root' - @gateway.expects(:ssl_post).with { |endpoint, data, headers| + @gateway.expects(:ssl_post).with { |_endpoint, data, _headers| request_hash = Hash.from_xml(data) request_hash['Envelope']['Body']['CreateCustomer']['CustomerRef'] == 'customer_ref+root' }.returns(successful_store_response) @@ -218,7 +218,7 @@ def test_customer_ref_on_store_may_come_from_options_root_or_billing_address options[:billing_address][:customer_ref] = 'customer_ref+billing' options[:customer] = 'customer_ref+root' - @gateway.expects(:ssl_post).with { |endpoint, data, headers| + @gateway.expects(:ssl_post).with { |_endpoint, data, _headers| request_hash = Hash.from_xml(data) request_hash['Envelope']['Body']['CreateCustomer']['CustomerRef'] == 'customer_ref+billing' }.returns(successful_store_response) @@ -246,7 +246,7 @@ def test_successful_retrieve end def test_expected_retrieve_response - @gateway.expects(:ssl_post).with { |endpoint, data, headers| + @gateway.expects(:ssl_post).with { |_endpoint, data, _headers| # Compare the actual and expected XML documents, by converting them to Hashes first expected = Hash.from_xml(expected_retrieve_request) actual = Hash.from_xml(data) @@ -262,53 +262,53 @@ def test_default_currency private def successful_purchase_response - <<-XML - - - - - - 00,Transaction Approved(Test Gateway) - True - 123456 - 100 - 123456 - - - - + <<~XML + + + + + + 00,Transaction Approved(Test Gateway) + True + 123456 + 100 + 123456 + + + + XML end def unsuccessful_authorization_response - <<-XML -soap:SenderLogin failed + <<~XML + soap:SenderLogin failed XML end def successful_store_response - <<-XML - - - - - 1234567 - - - + <<~XML + + + + + 1234567 + + + XML end def successful_update_response - <<-XML - - - - - true - - - + <<~XML + + + + + true + + + XML end @@ -322,8 +322,8 @@ def successful_retrieve_response #{@credit_card.first_name} #{@credit_card.last_name} #{@credit_card.number} - #{sprintf("%.2i", @credit_card.month)} - #{sprintf("%.4i", @credit_card.year)[-2..-1]} + #{sprintf('%.2i', @credit_card.month)} + #{sprintf('%.4i', @credit_card.year)[-2..-1]} @@ -333,70 +333,70 @@ def successful_retrieve_response # Documented here: https://www.eway.com.au/gateway/ManagedPaymentService/managedCreditCardPayment.asmx?op=CreateCustomer def expected_store_request - <<-XML - - - - - login - username - password - - - - - Mr. - #{@credit_card.first_name} - #{@credit_card.last_name} -
#{@options[:billing_address][:address1]}
- #{@options[:billing_address][:city]} - #{@options[:billing_address][:state]} - #{@options[:billing_address][:company]} - #{@options[:billing_address][:zip]} - #{@options[:billing_address][:country]} - #{@options[:email]} - - #{@options[:billing_address][:phone]} - - #{@options[:customer]} - - #{@options[:description]} - - #{@credit_card.number} - #{@credit_card.first_name} #{@credit_card.last_name} - #{sprintf("%.2i", @credit_card.month)} - #{sprintf("%.4i", @credit_card.year)[-2..-1]} -
-
-
+ <<~XML + + + + + login + username + password + + + + + Mr. + #{@credit_card.first_name} + #{@credit_card.last_name} +
#{@options[:billing_address][:address1]}
+ #{@options[:billing_address][:city]} + #{@options[:billing_address][:state]} + #{@options[:billing_address][:company]} + #{@options[:billing_address][:zip]} + #{@options[:billing_address][:country]} + #{@options[:email]} + + #{@options[:billing_address][:phone]} + + #{@options[:customer]} + + #{@options[:description]} + + #{@credit_card.number} + #{@credit_card.first_name} #{@credit_card.last_name} + #{sprintf('%.2i', @credit_card.month)} + #{sprintf('%.4i', @credit_card.year)[-2..-1]} +
+
+
XML end - # Documented here: https://www.eway.com.au/gateway/ManagedPaymentService/managedCreditCardPayment.asmx?op=CreateCustomer - def expected_purchase_request - <<-XML - - - - - login - username - password - - - - - #{@valid_customer_id} - #{@amount} - #{@options[:order_id] || @options[:invoice]} - #{@options[:description]} - - - - XML - end + # Documented here: https://www.eway.com.au/gateway/ManagedPaymentService/managedCreditCardPayment.asmx?op=CreateCustomer + def expected_purchase_request + <<~XML + + + + + login + username + password + + + + + #{@valid_customer_id} + #{@amount} + #{@options[:order_id] || @options[:invoice]} + #{@options[:description]} + + + + XML + end - # Documented here: https://www.eway.com.au/gateway/ManagedPaymentService/managedCreditCardPayment.asmx?op=QueryCustomer + # Documented here: https://www.eway.com.au/gateway/ManagedPaymentService/managedCreditCardPayment.asmx?op=QueryCustomer def expected_retrieve_request <<-XML @@ -416,5 +416,4 @@ def expected_retrieve_request XML end - end diff --git a/test/unit/gateways/eway_rapid_test.rb b/test/unit/gateways/eway_rapid_test.rb index 41aed4196d0..3c3d6ec68f7 100644 --- a/test/unit/gateways/eway_rapid_test.rb +++ b/test/unit/gateways/eway_rapid_test.rb @@ -6,12 +6,42 @@ class EwayRapidTest < Test::Unit::TestCase def setup ActiveMerchant::Billing::EwayRapidGateway.partner_id = nil @gateway = EwayRapidGateway.new( - :login => 'login', - :password => 'password' + login: 'login', + password: 'password' ) @credit_card = credit_card @amount = 100 + + @address = { + name: 'John Smith', + title: 'Test Title', + company: 'Test Company, Inc.', + address1: '14701 Test Road', + address2: 'Test Unit 100', + city: 'TestCity', + state: 'NC', + zip: '27517', + country: 'USA', + phone: '555-555-1000', + fax: '555-555-2000' + } + + @shipping_address = { + name: 'John Smith', + title: 'Test Title', + company: 'Test Company, Inc.', + address1: '14701 Test Road', + address2: 'Test Unit 100', + city: 'TestCity', + state: 'NC', + zip: '27517', + country: 'USA', + phone_number: '555-555-1000', + fax: '555-555-2000' + } + + @email = 'john.smith@example.com' end def test_successful_purchase @@ -25,16 +55,106 @@ def test_successful_purchase assert response.test? end + def test_purchase_passes_customer_data_from_payment_method_when_no_address_is_provided + stub_comms do + @gateway.purchase(@amount, @credit_card, { email: @email }) + end.check_request do |_endpoint, data, _headers| + assert_customer_data_passed( + data, + @credit_card.first_name, + @credit_card.last_name, + @email + ) + end.respond_with(successful_purchase_response) + end + + def test_purchase_passes_customer_data_from_billing_address + stub_comms do + @gateway.purchase(@amount, @credit_card, { billing_address: @address, email: @email }) + end.check_request do |_endpoint, data, _headers| + assert_customer_data_passed( + data, + @address[:name].split[0], + @address[:name].split[1], + @email, + @address + ) + end.respond_with(successful_purchase_response) + end + + def test_purchase_passes_customer_data_from_address + stub_comms do + @gateway.purchase(@amount, @credit_card, { address: @address, email: @email }) + end.check_request do |_endpoint, data, _headers| + assert_customer_data_passed( + data, + @address[:name].split[0], + @address[:name].split[1], + @email, + @address + ) + end.respond_with(successful_purchase_response) + end + + def test_purchase_passes_shipping_data + stub_comms do + @gateway.purchase(@amount, @credit_card, { shipping_address: @shipping_address, email: @email }) + end.check_request do |_endpoint, data, _headers| + assert_shipping_data_passed(data, @shipping_address, @email) + end.respond_with(successful_purchase_response) + end + + def test_purchase_3ds1_data + eci = '05' + cavv = 'AgAAAAAA4n1uzQPRaATeQAAAAAA=' + xid = 'AAAAAAAA4n1uzQPRaATeQAAAAAA=' + authentication_response_status = 'Y' + options_with_3ds1 = { + eci:, + cavv:, + xid:, + authentication_response_status: + } + + stub_comms do + @gateway.purchase(@amount, @credit_card, { three_d_secure: options_with_3ds1 }) + end.check_request do |_endpoint, data, _headers| + assert_3ds_data_passed(data, options_with_3ds1) + end.respond_with(successful_purchase_response) + end + + def test_purchase_3ds2_data + eci = '05' + cavv = 'AgAAAAAA4n1uzQPRaATeQAAAAAA=' + authentication_response_status = 'Y' + version = '2.1.0' + ds_transaction_id = '8fe2e850-a028-407e-9a18-c8cf7598ca10' + + options_with_3ds2 = { + version:, + eci:, + cavv:, + ds_transaction_id:, + authentication_response_status: + } + + stub_comms do + @gateway.purchase(@amount, @credit_card, { three_d_secure: options_with_3ds2 }) + end.check_request do |_endpoint, data, _headers| + assert_3ds_data_passed(data, options_with_3ds2) + end.respond_with(successful_purchase_response) + end + def test_localized_currency stub_comms do - @gateway.purchase(100, @credit_card, :currency => 'CAD') - end.check_request do |endpoint, data, headers| + @gateway.purchase(100, @credit_card, currency: 'CAD') + end.check_request do |_endpoint, data, _headers| assert_match '"TotalAmount":"100"', data end.respond_with(successful_purchase_response) stub_comms do - @gateway.purchase(100, @credit_card, :currency => 'JPY') - end.check_request do |endpoint, data, headers| + @gateway.purchase(100, @credit_card, currency: 'JPY') + end.check_request do |_endpoint, data, _headers| assert_match '"TotalAmount":"1"', data end.respond_with(successful_purchase_response) end @@ -74,45 +194,47 @@ def test_failed_purchase_with_multiple_messages def test_purchase_with_all_options response = stub_comms do - @gateway.purchase(200, @credit_card, - :transaction_type => 'CustomTransactionType', - :redirect_url => 'http://awesomesauce.com', - :ip => '0.0.0.0', - :application_id => 'Woohoo', - :partner_id => 'SomePartner', - :description => 'The Really Long Description More Than Sixty Four Characters Gets Truncated', - :order_id => 'orderid1', - :invoice => 'I1234', - :currency => 'INR', - :email => 'jim@example.com', - :billing_address => { - :title => 'Mr.', - :name => 'Jim Awesome Smith', - :company => 'Awesome Co', - :address1 => '1234 My Street', - :address2 => 'Apt 1', - :city => 'Ottawa', - :state => 'ON', - :zip => 'K1C2N6', - :country => 'CA', - :phone => '(555)555-5555', - :fax => '(555)555-6666' + @gateway.purchase( + 200, + @credit_card, + transaction_type: 'CustomTransactionType', + redirect_url: 'http://awesomesauce.com', + ip: '0.0.0.0', + application_id: 'Woohoo', + partner_id: 'SomePartner', + description: 'The Really Long Description More Than Sixty Four Characters Gets Truncated', + order_id: 'orderid1', + invoice: 'I1234', + currency: 'INR', + email: 'jim@example.com', + billing_address: { + title: 'Mr.', + name: 'Jim Awesome Smith', + company: 'Awesome Co', + address1: '1234 My Street', + address2: 'Apt 1', + city: 'Ottawa', + state: 'ON', + zip: 'K1C2N6', + country: 'CA', + phone: '(555)555-5555', + fax: '(555)555-6666' }, - :shipping_address => { - :title => 'Ms.', - :name => 'Baker', - :company => 'Elsewhere Inc.', - :address1 => '4321 Their St.', - :address2 => 'Apt 2', - :city => 'Chicago', - :state => 'IL', - :zip => '60625', - :country => 'US', - :phone => '1115555555', - :fax => '1115556666' + shipping_address: { + title: 'Ms.', + name: 'Baker', + company: 'Elsewhere Inc.', + address1: '4321 Their St.', + address2: 'Apt 2', + city: 'Chicago', + state: 'IL', + zip: '60625', + country: 'US', + phone: '1115555555', + fax: '1115556666' } ) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(%r{"TransactionType":"CustomTransactionType"}, data) assert_match(%r{"RedirectUrl":"http://awesomesauce.com"}, data) assert_match(%r{"CustomerIP":"0.0.0.0"}, data) @@ -150,7 +272,7 @@ def test_purchase_with_all_options assert_match(%r{"Country":"us"}, data) assert_match(%r{"Phone":"1115555555"}, data) assert_match(%r{"Fax":"1115556666"}, data) - assert_match(%r{"Email":null}, data) + assert_match(%r{"Email":"jim@example\.com"}, data) end.respond_with(successful_purchase_response) assert_success response @@ -162,7 +284,7 @@ def test_partner_id_class_attribute ActiveMerchant::Billing::EwayRapidGateway.partner_id = 'SomePartner' stub_comms do @gateway.purchase(200, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(%r{"PartnerID":"SomePartner"}, data) end.respond_with(successful_purchase_response) end @@ -171,7 +293,7 @@ def test_partner_id_params_overrides_class_attribute ActiveMerchant::Billing::EwayRapidGateway.partner_id = 'SomePartner' stub_comms do @gateway.purchase(200, @credit_card, partner_id: 'OtherPartner') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(%r{"PartnerID":"OtherPartner"}, data) end.respond_with(successful_purchase_response) end @@ -179,7 +301,7 @@ def test_partner_id_params_overrides_class_attribute def test_partner_id_is_omitted_when_not_set stub_comms do @gateway.purchase(200, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_no_match(%r{"PartnerID":}, data) end.respond_with(successful_purchase_response) end @@ -188,8 +310,8 @@ def test_partner_id_truncates_to_50_characters partner_string = 'EWay Rapid PartnerID is capped at 50 characters and will truncate if it is too long.' stub_comms do @gateway.purchase(200, @credit_card, partner_id: partner_string) - end.check_request do |endpoint, data, headers| - assert_match(%r{"PartnerID":"#{partner_string.slice(0,50)}"}, data) + end.check_request do |_endpoint, data, _headers| + assert_match(%r{"PartnerID":"#{partner_string.slice(0, 50)}"}, data) end.respond_with(successful_purchase_response) end @@ -203,6 +325,55 @@ def test_successful_authorize assert_equal 10774952, response.authorization end + def test_authorize_passes_customer_data_from_payment_method_when_no_address_is_provided + stub_comms do + @gateway.authorize(@amount, @credit_card, { email: @email }) + end.check_request do |_endpoint, data, _headers| + assert_customer_data_passed( + data, + @credit_card.first_name, + @credit_card.last_name, + @email + ) + end.respond_with(successful_authorize_response) + end + + def test_authorize_passes_customer_data_from_billing_address + stub_comms do + @gateway.authorize(@amount, @credit_card, { billing_address: @address, email: @email }) + end.check_request do |_endpoint, data, _headers| + assert_customer_data_passed( + data, + @address[:name].split[0], + @address[:name].split[1], + @email, + @address + ) + end.respond_with(successful_authorize_response) + end + + def test_authorize_passes_customer_data_from_address + stub_comms do + @gateway.authorize(@amount, @credit_card, { address: @address, email: @email }) + end.check_request do |_endpoint, data, _headers| + assert_customer_data_passed( + data, + @address[:name].split[0], + @address[:name].split[1], + @email, + @address + ) + end.respond_with(successful_authorize_response) + end + + def test_authorize_passes_shipping_data + stub_comms do + @gateway.authorize(@amount, @credit_card, { shipping_address: @shipping_address, email: @email }) + end.check_request do |_endpoint, data, _headers| + assert_shipping_data_passed(data, @shipping_address, @email) + end.respond_with(successful_authorize_response) + end + def test_successful_capture response = stub_comms do @gateway.capture(nil, 'auth') @@ -255,20 +426,20 @@ def test_failed_void def test_successful_store response = stub_comms do - @gateway.store(@credit_card, :billing_address => { - :title => 'Mr.', - :name => 'Jim Awesome Smith', - :company => 'Awesome Co', - :address1 => '1234 My Street', - :address2 => 'Apt 1', - :city => 'Ottawa', - :state => 'ON', - :zip => 'K1C2N6', - :country => 'CA', - :phone => '(555)555-5555', - :fax => '(555)555-6666' - }) - end.check_request do |endpoint, data, headers| + @gateway.store(@credit_card, billing_address: { + title: 'Mr.', + name: 'Jim Awesome Smith', + company: 'Awesome Co', + address1: '1234 My Street', + address2: 'Apt 1', + city: 'Ottawa', + state: 'ON', + zip: 'K1C2N6', + country: 'CA', + phone: '(555)555-5555', + fax: '(555)555-6666' + }) + end.check_request do |_endpoint, data, _headers| assert_match '"Method":"CreateTokenCustomer"', data end.respond_with(successful_store_response) @@ -278,9 +449,34 @@ def test_successful_store assert response.test? end + def test_store_passes_customer_data_from_billing_address + stub_comms do + @gateway.store(@credit_card, { billing_address: @address, email: @email }) + end.check_request do |_endpoint, data, _headers| + assert_customer_data_passed( + data, + @address[:name].split[0], + @address[:name].split[1], + @email, + @address + ) + end.respond_with(successful_store_response) + end + + def test_store_passes_shipping_data + stub_comms do + @gateway.store( + @credit_card, + { shipping_address: @shipping_address, billing_address: @address, email: @email } + ) + end.check_request do |_endpoint, data, _headers| + assert_shipping_data_passed(data, @shipping_address, @email) + end.respond_with(successful_store_response) + end + def test_failed_store response = stub_comms do - @gateway.store(@credit_card, :billing_address => {}) + @gateway.store(@credit_card, billing_address: {}) end.respond_with(failed_store_response) assert_failure response @@ -292,7 +488,7 @@ def test_failed_store def test_successful_update response = stub_comms do @gateway.update('faketoken', nil) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match '"Method":"UpdateTokenCustomer"', data end.respond_with(successful_update_response) @@ -302,10 +498,59 @@ def test_successful_update assert response.test? end + def test_update_passes_customer_data_from_payment_method_when_no_address_is_provided + stub_comms do + @gateway.update('token', @credit_card, { email: @email }) + end.check_request do |_endpoint, data, _headers| + assert_customer_data_passed( + data, + @credit_card.first_name, + @credit_card.last_name, + @email + ) + end.respond_with(successful_update_response) + end + + def test_update_passes_customer_data_from_billing_address + stub_comms do + @gateway.update('token', @credit_card, { billing_address: @address, email: @email }) + end.check_request do |_endpoint, data, _headers| + assert_customer_data_passed( + data, + @address[:name].split[0], + @address[:name].split[1], + @email, + @address + ) + end.respond_with(successful_update_response) + end + + def test_update_passes_customer_data_from_address + stub_comms do + @gateway.update('token', @credit_card, { address: @address, email: @email }) + end.check_request do |_endpoint, data, _headers| + assert_customer_data_passed( + data, + @address[:name].split[0], + @address[:name].split[1], + @email, + @address + ) + end.respond_with(successful_update_response) + end + + def test_update_passes_shipping_data + stub_comms do + @gateway.update('token', @credit_card, { shipping_address: @shipping_address, email: @email }) + end.check_request do |_endpoint, data, _headers| + assert_shipping_data_passed(data, @shipping_address, @email) + end.respond_with(successful_update_response) + end + def test_successful_refund response = stub_comms do @gateway.refund(@amount, '1234567') - end.check_request do |endpoint, data, headers| + end.check_request do |endpoint, data, _headers| assert_match %r{Transaction\/1234567\/Refund$}, endpoint json = JSON.parse(data) assert_equal '100', json['Refund']['TotalAmount'] @@ -332,7 +577,7 @@ def test_failed_refund def test_successful_stored_card_purchase response = stub_comms do @gateway.purchase(100, 'the_customer_token', transaction_type: 'MOTO') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match '"Method":"TokenPayment"', data assert_match '"TransactionType":"MOTO"', data end.respond_with(successful_store_purchase_response) @@ -346,7 +591,7 @@ def test_successful_stored_card_purchase def test_verification_results response = stub_comms do @gateway.purchase(100, @credit_card) - end.respond_with(successful_purchase_response(:verification_status => 'Valid')) + end.respond_with(successful_purchase_response(verification_status: 'Valid')) assert_success response assert_equal 'M', response.cvv_result['code'] @@ -354,7 +599,7 @@ def test_verification_results response = stub_comms do @gateway.purchase(100, @credit_card) - end.respond_with(successful_purchase_response(:verification_status => 'Invalid')) + end.respond_with(successful_purchase_response(verification_status: 'Invalid')) assert_success response assert_equal 'N', response.cvv_result['code'] @@ -362,7 +607,7 @@ def test_verification_results response = stub_comms do @gateway.purchase(100, @credit_card) - end.respond_with(successful_purchase_response(:verification_status => 'Unchecked')) + end.respond_with(successful_purchase_response(verification_status: 'Unchecked')) assert_success response assert_equal 'P', response.cvv_result['code'] @@ -375,6 +620,58 @@ def test_transcript_scrubbing private + def assert_customer_data_passed(data, first_name, last_name, email, address = nil) + parsed_data = JSON.parse(data) + customer = parsed_data['Customer'] + + assert_equal customer['FirstName'], first_name + assert_equal customer['LastName'], last_name + assert_equal customer['Email'], email + + if address + assert_equal customer['Title'], address[:title] + assert_equal customer['CompanyName'], address[:company] + assert_equal customer['Street1'], address[:address1] + assert_equal customer['Street2'], address[:address2] + assert_equal customer['City'], address[:city] + assert_equal customer['State'], address[:state] + assert_equal customer['PostalCode'], address[:zip] + assert_equal customer['Country'], address[:country].downcase + assert_equal customer['Phone'], address[:phone] + assert_equal customer['Fax'], address[:fax] + end + end + + def assert_3ds_data_passed(data, threedsoption) + parsed_data = JSON.parse(data) + threeds = parsed_data['PaymentInstrument']['ThreeDSecureAuth'] + + assert_equal threeds['Cryptogram'], threedsoption[:cavv] + assert_equal threeds['ECI'], threedsoption[:eci] + assert_equal threeds['XID'], threedsoption[:xid] + assert_equal threeds['AuthStatus'], threedsoption[:authentication_response_status] + assert_equal threeds['dsTransactionId'], threedsoption[:ds_transaction_id] + assert_equal threeds['Version'], threedsoption[:version] + end + + def assert_shipping_data_passed(data, address, email) + parsed_data = JSON.parse(data) + shipping = parsed_data['ShippingAddress'] + + assert_equal shipping['FirstName'], address[:name].split[0] + assert_equal shipping['LastName'], address[:name].split[1] + assert_equal shipping['Title'], address[:title] + assert_equal shipping['Street1'], address[:address1] + assert_equal shipping['Street2'], address[:address2] + assert_equal shipping['City'], address[:city] + assert_equal shipping['State'], address[:state] + assert_equal shipping['PostalCode'], address[:zip] + assert_equal shipping['Country'], address[:country].downcase + assert_equal shipping['Phone'], address[:phone_number] + assert_equal shipping['Fax'], address[:fax] + assert_equal shipping['Email'], email + end + def successful_purchase_response(options = {}) verification_status = options[:verification_status] || 0 verification_status = %Q{"#{verification_status}"} if verification_status.is_a? String diff --git a/test/unit/gateways/eway_test.rb b/test/unit/gateways/eway_test.rb index 5dbe984051a..898567d50b6 100644 --- a/test/unit/gateways/eway_test.rb +++ b/test/unit/gateways/eway_test.rb @@ -3,7 +3,7 @@ class EwayTest < Test::Unit::TestCase def setup @gateway = EwayGateway.new( - :login => '87654321' + login: '87654321' ) @amount = 100 @@ -11,17 +11,17 @@ def setup @credit_card = credit_card('4646464646464646') @options = { - :order_id => '1230123', - :email => 'bob@testbob.com', - :billing_address => { - :address1 => '1234 First St.', - :address2 => 'Apt. 1', - :city => 'Melbourne', - :state => 'ACT', - :country => 'AU', - :zip => '12345' + order_id: '1230123', + email: 'bob@testbob.com', + billing_address: { + address1: '1234 First St.', + address2: 'Apt. 1', + city: 'Melbourne', + state: 'ACT', + country: 'AU', + zip: '12345' }, - :description => 'purchased items' + description: 'purchased items' } end @@ -70,11 +70,11 @@ def test_failed_refund end def test_amount_style - assert_equal '1034', @gateway.send(:amount, 1034) + assert_equal '1034', @gateway.send(:amount, 1034) - assert_raise(ArgumentError) do - @gateway.send(:amount, '10.34') - end + assert_raise(ArgumentError) do + @gateway.send(:amount, '10.34') + end end def test_ensure_does_not_respond_to_authorize @@ -97,6 +97,7 @@ def test_transcript_scrubbing end private + def successful_purchase_response <<-XML diff --git a/test/unit/gateways/exact_test.rb b/test/unit/gateways/exact_test.rb index 0c33fd415c3..2986777bfa6 100644 --- a/test/unit/gateways/exact_test.rb +++ b/test/unit/gateways/exact_test.rb @@ -2,15 +2,15 @@ class ExactTest < Test::Unit::TestCase def setup - @gateway = ExactGateway.new( :login => 'A00427-01', - :password => 'testus' ) + @gateway = ExactGateway.new(login: 'A00427-01', + password: 'testus') @credit_card = credit_card @amount = 100 @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase' + order_id: '1', + billing_address: address, + description: 'Store Purchase' } end @@ -22,7 +22,7 @@ def test_successful_purchase assert response.test? assert_equal 'Transaction Normal - VER UNAVAILABLE', response.message - ExactGateway::SENSITIVE_FIELDS.each{ |f| assert !response.params.has_key?(f.to_s) } + ExactGateway::SENSITIVE_FIELDS.each { |f| assert !response.params.has_key?(f.to_s) } end def test_successful_refund @@ -48,11 +48,11 @@ def test_failed_purchase assert_failure response 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 @@ -64,11 +64,11 @@ def test_soap_fault end def test_supported_countries - assert_equal ['CA', 'US'], ExactGateway.supported_countries + assert_equal %w[CA US], ExactGateway.supported_countries end def test_supported_card_types - assert_equal [:visa, :master, :american_express, :jcb, :discover], ExactGateway.supported_cardtypes + assert_equal %i[visa master american_express jcb discover], ExactGateway.supported_cardtypes end def test_avs_result @@ -85,122 +85,123 @@ def test_cvv_result assert_equal 'M', response.cvv_result['code'] end - private + def successful_purchase_response - <<-RESPONSE -A00427-01#######00104242424242424242106625152ET17000909Longbob Longsen123100001Store Purchase0Processed by: -E-xact Transaction Gateway :- Version 8.4.0 B19b -Copyright 2006 -{34:2652}0falsetrue00Transaction Normal00VER UNAVAILABLE 377UM200801181700E-xact ConnectionShopSuite 400 - 1152 Mainland St.VancouverBCCanadaV6B 4X2www.e-xact.com========== TRANSACTION RECORD ========= + <<~RESPONSE + A00427-01#######00104242424242424242106625152ET17000909Longbob Longsen123100001Store Purchase0Processed by: + E-xact Transaction Gateway :- Version 8.4.0 B19b + Copyright 2006 + {34:2652}0falsetrue00Transaction Normal00VER UNAVAILABLE 377UM200801181700E-xact ConnectionShopSuite 400 - 1152 Mainland St.VancouverBCCanadaV6B 4X2www.e-xact.com========== TRANSACTION RECORD ========= -E-xact ConnectionShop -Suite 400 - 1152 Mainland St. -Vancouver, BC V6B 4X2 -www.e-xact.com + E-xact ConnectionShop + Suite 400 - 1152 Mainland St. + Vancouver, BC V6B 4X2 + www.e-xact.com -TYPE: Purchase + TYPE: Purchase -ACCT: Visa $1.00 USD + ACCT: Visa $1.00 USD -CARD NUMBER : ############4242 -TRANS. REF. : 1 -CARD HOLDER : Longbob Longsen -EXPIRY DATE : xx/xx -DATE/TIME : 18 Jan 08 14:17:00 -REFERENCE # : 5999 377 M -AUTHOR.# : ET1700 + CARD NUMBER : ############4242 + TRANS. REF. : 1 + CARD HOLDER : Longbob Longsen + EXPIRY DATE : xx/xx + DATE/TIME : 18 Jan 08 14:17:00 + REFERENCE # : 5999 377 M + AUTHOR.# : ET1700 - Approved - Thank You 00 + Approved - Thank You 00 -SIGNATURE + SIGNATURE -_______________________________________ + _______________________________________ - + RESPONSE end + def successful_refund_response - <<-RESPONSE -A00427-01#######00104242424242424242106625152ET17000909Longbob Longsen123100001Store Purchase0Processed by: -E-xact Transaction Gateway :- Version 8.4.0 B19b -Copyright 2006 -{34:2652}0falsetrue00Transaction Normal00VER UNAVAILABLE 377UM200801181700E-xact ConnectionShopSuite 400 - 1152 Mainland St.VancouverBCCanadaV6B 4X2www.e-xact.com========== TRANSACTION RECORD ========= + <<~RESPONSE + A00427-01#######00104242424242424242106625152ET17000909Longbob Longsen123100001Store Purchase0Processed by: + E-xact Transaction Gateway :- Version 8.4.0 B19b + Copyright 2006 + {34:2652}0falsetrue00Transaction Normal00VER UNAVAILABLE 377UM200801181700E-xact ConnectionShopSuite 400 - 1152 Mainland St.VancouverBCCanadaV6B 4X2www.e-xact.com========== TRANSACTION RECORD ========= -E-xact ConnectionShop -Suite 400 - 1152 Mainland St. -Vancouver, BC V6B 4X2 -www.e-xact.com + E-xact ConnectionShop + Suite 400 - 1152 Mainland St. + Vancouver, BC V6B 4X2 + www.e-xact.com -TYPE: Refund + TYPE: Refund -ACCT: Visa $1.00 USD + ACCT: Visa $1.00 USD -CARD NUMBER : ############4242 -TRANS. REF. : 1 -CARD HOLDER : Longbob Longsen -EXPIRY DATE : xx/xx -DATE/TIME : 18 Jan 08 14:17:00 -REFERENCE # : 5999 377 M + CARD NUMBER : ############4242 + TRANS. REF. : 1 + CARD HOLDER : Longbob Longsen + EXPIRY DATE : xx/xx + DATE/TIME : 18 Jan 08 14:17:00 + REFERENCE # : 5999 377 M - Approved - Thank You 00 + Approved - Thank You 00 -SIGNATURE + SIGNATURE -Please retain this copy for your records. + Please retain this copy for your records. - ========================================= + ========================================= - + RESPONSE end def failed_purchase_response - <<-RESPONSE -A00427-01#######005013041111111111111111066246680909Longbob Longsen123100001Store Purchase0Processed by: -E-xact Transaction Gateway :- Version 8.4.0 B19b -Copyright 2006 -{34:2652}0falsefalse00Transaction Normal13AMOUNT ERR376UME-xact ConnectionShopSuite 400 - 1152 Mainland St.VancouverBCCanadaV6B 4X2www.e-xact.com========== TRANSACTION RECORD ========= + <<~RESPONSE + A00427-01#######005013041111111111111111066246680909Longbob Longsen123100001Store Purchase0Processed by: + E-xact Transaction Gateway :- Version 8.4.0 B19b + Copyright 2006 + {34:2652}0falsefalse00Transaction Normal13AMOUNT ERR376UME-xact ConnectionShopSuite 400 - 1152 Mainland St.VancouverBCCanadaV6B 4X2www.e-xact.com========== TRANSACTION RECORD ========= -E-xact ConnectionShop -Suite 400 - 1152 Mainland St. -Vancouver, BC V6B 4X2 -www.e-xact.com + E-xact ConnectionShop + Suite 400 - 1152 Mainland St. + Vancouver, BC V6B 4X2 + www.e-xact.com -TYPE: Purchase + TYPE: Purchase -ACCT: Visa $5,013.00 USD + ACCT: Visa $5,013.00 USD -CARD NUMBER : ############1111 -TRANS. REF. : 1 -CARD HOLDER : Longbob Longsen -EXPIRY DATE : xx/xx -DATE/TIME : 18 Jan 08 14:11:09 -REFERENCE # : 5999 376 M -AUTHOR.# : + CARD NUMBER : ############1111 + TRANS. REF. : 1 + CARD HOLDER : Longbob Longsen + EXPIRY DATE : xx/xx + DATE/TIME : 18 Jan 08 14:11:09 + REFERENCE # : 5999 376 M + AUTHOR.# : - Transaction not Approved 13 + Transaction not Approved 13 - + RESPONSE end def soap_fault_response - <<-RESPONSE - - - - - soap:Client - Unable to handle request without a valid action parameter. Please supply a valid soap action. - - - - + <<~RESPONSE + + + + + soap:Client + Unable to handle request without a valid action parameter. Please supply a valid soap action. + + + + RESPONSE end end diff --git a/test/unit/gateways/ezic_test.rb b/test/unit/gateways/ezic_test.rb index 5fa29b4004b..d79f3ece845 100644 --- a/test/unit/gateways/ezic_test.rb +++ b/test/unit/gateways/ezic_test.rb @@ -166,5 +166,4 @@ def failed_void_response def successful_authorize_raw_response MockResponse.succeeded(successful_authorize_response) end - end diff --git a/test/unit/gateways/fat_zebra_test.rb b/test/unit/gateways/fat_zebra_test.rb index 2251655ff70..dfb33d78355 100644 --- a/test/unit/gateways/fat_zebra_test.rb +++ b/test/unit/gateways/fat_zebra_test.rb @@ -5,17 +5,28 @@ class FatZebraTest < Test::Unit::TestCase def setup @gateway = FatZebraGateway.new( - :username => 'TEST', - :token => 'TEST' - ) + username: 'TEST', + token: 'TEST' + ) @credit_card = credit_card @amount = 100 @options = { - :order_id => rand(10000), - :billing_address => address, - :description => 'Store Purchase' + order_id: rand(10000), + billing_address: address, + 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', + ds_transaction_id: 'f25084f0-5b16-4c0a-ae5d-b24808a95e4b', + authentication_response_status: 'Y' } end @@ -25,87 +36,99 @@ def test_successful_purchase assert response = @gateway.purchase(@amount, @credit_card, @options) assert_success response - assert_equal '001-P-12345AA', response.authorization + assert_equal '001-P-12345AA|purchases', response.authorization + assert response.test? + end + + def test_successful_purchase_with_metadata + @gateway.expects(:ssl_request).with { |_method, _url, body, _headers| + body.match '"metadata":{"foo":"bar"}' + }.returns(successful_purchase_response_with_metadata) + + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(metadata: { 'foo' => 'bar' })) + assert_success response + + assert_equal '001-P-12345AA|purchases', response.authorization assert response.test? end def test_successful_purchase_with_token - @gateway.expects(:ssl_request).with { |method, url, body, headers| + @gateway.expects(:ssl_request).with { |_method, _url, body, _headers| body.match '"card_token":"e1q7dbj2"' }.returns(successful_purchase_response) assert response = @gateway.purchase(@amount, 'e1q7dbj2', @options) assert_success response - assert_equal '001-P-12345AA', response.authorization + assert_equal '001-P-12345AA|purchases', response.authorization assert response.test? end def test_successful_purchase_with_token_string - @gateway.expects(:ssl_request).with { |method, url, body, headers| + @gateway.expects(:ssl_request).with { |_method, _url, body, _headers| body.match '"card_token":"e1q7dbj2"' }.returns(successful_purchase_response) assert response = @gateway.purchase(@amount, 'e1q7dbj2', @options) assert_success response - assert_equal '001-P-12345AA', response.authorization + assert_equal '001-P-12345AA|purchases', response.authorization assert response.test? end def test_successful_multi_currency_purchase - @gateway.expects(:ssl_request).with { |method, url, body, headers| + @gateway.expects(:ssl_request).with { |_method, _url, body, _headers| body.match '"currency":"USD"' }.returns(successful_purchase_response) - assert response = @gateway.purchase(@amount, 'e1q7dbj2', @options.merge(:currency => 'USD')) + assert response = @gateway.purchase(@amount, 'e1q7dbj2', @options.merge(currency: 'USD')) assert_success response - assert_equal '001-P-12345AA', response.authorization + assert_equal '001-P-12345AA|purchases', response.authorization assert response.test? end def test_successful_purchase_with_recurring_flag stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card, @options.merge(recurring: true)) - end.check_request do |method, endpoint, data, headers| - assert_match(%r("extra":{"ecm":"32"}), data) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(%r("extra":{"ecm":"32"), data) end.respond_with(successful_purchase_response) end def test_successful_purchase_with_descriptor - @gateway.expects(:ssl_request).with { |method, url, body, headers| + @gateway.expects(:ssl_request).with { |_method, _url, body, _headers| json = JSON.parse(body) json['extra']['name'] == 'Merchant' && json['extra']['location'] == 'Location' }.returns(successful_purchase_response) - assert response = @gateway.purchase(@amount, 'e1q7dbj2', @options.merge(:merchant => 'Merchant', :merchant_location => 'Location')) + assert response = @gateway.purchase(@amount, 'e1q7dbj2', @options.merge(merchant: 'Merchant', merchant_location: 'Location')) assert_success response - assert_equal '001-P-12345AA', response.authorization + assert_equal '001-P-12345AA|purchases', response.authorization assert response.test? end def test_successful_authorization - @gateway.expects(:ssl_request).with { |method, url, body, headers| + @gateway.expects(:ssl_request).with { |_method, _url, body, _headers| body.match '"capture":false' }.returns(successful_purchase_response) assert response = @gateway.authorize(@amount, 'e1q7dbj2', @options) assert_success response - assert_equal '001-P-12345AA', response.authorization + assert_equal '001-P-12345AA|purchases', response.authorization assert response.test? end def test_successful_capture - @gateway.expects(:ssl_request).with { |method, url, body, headers| + @gateway.expects(:ssl_request).with { |_method, url, _body, _headers| url =~ %r[purchases/e1q7dbj2/capture\z] }.returns(successful_purchase_response) response = @gateway.capture(@amount, 'e1q7dbj2', @options) assert_success response - assert_equal '001-P-12345AA', response.authorization + assert_equal '001-P-12345AA|purchases', response.authorization assert response.test? end @@ -147,7 +170,7 @@ def test_successful_tokenization assert response = @gateway.store(@credit_card) assert_success response - assert_equal 'e1q7dbj2', response.authorization + assert_equal 'e1q7dbj2|credit_cards', response.authorization end def test_unsuccessful_tokenization @@ -157,12 +180,32 @@ def test_unsuccessful_tokenization assert_failure response end + def test_successful_tokenization_without_cvv + credit_card = @credit_card + credit_card.verification_value = nil + @gateway.expects(:ssl_request).returns(successful_no_cvv_tokenize_response) + + assert response = @gateway.store(credit_card, recurring: true) + assert_success response + assert_equal 'ep3c05nzsqvft15wsf1z|credit_cards', response.authorization + end + + def test_unsuccessful_tokenization_without_cvv + credit_card = @credit_card + credit_card.verification_value = nil + @gateway.expects(:ssl_request).returns(failed_no_cvv_tokenize_response) + + assert response = @gateway.store(credit_card) + assert_failure response + assert_equal 'CVV is required', response.message + end + def test_successful_refund @gateway.expects(:ssl_request).returns(successful_refund_response) assert response = @gateway.refund(100, 'TEST') assert_success response - assert_equal '003-R-7MNIUMY6', response.authorization + assert_equal '003-R-7MNIUMY6|refunds', response.authorization assert response.test? end @@ -179,199 +222,345 @@ 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[:directory_server_txn_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 - <<-'PRE_SCRUBBED' -opening connection to gateway.sandbox.fatzebra.com.au:443... -opened -starting SSL for gateway.sandbox.fatzebra.com.au:443... -SSL established -<- "POST /v1.0/credit_cards HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAuthorization: Basic VEVTVDpURVNU\r\nUser-Agent: Fat Zebra v1.0/ActiveMerchant 1.56.0\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nConnection: close\r\nHost: gateway.sandbox.fatzebra.com.au\r\nContent-Length: 93\r\n\r\n" -<- "{\"card_number\":\"5123456789012346\",\"card_expiry\":\"5/2017\",\"cvv\":\"111\",\"card_holder\":\"Foo Bar\"}" --> "HTTP/1.1 200 OK\r\n" --> "Content-Type: application/json; charset=utf-8\r\n" --> "Connection: close\r\n" --> "Status: 200 OK\r\n" --> "Cache-control: no-store\r\n" --> "Pragma: no-cache\r\n" --> "X-Request-Id: 3BA78272_F214_AC10001D_01BB_566A58EC_222F1D_49F4\r\n" --> "X-Runtime: 0.142463\r\n" --> "Date: Fri, 11 Dec 2015 05:02:36 GMT\r\n" --> "X-Rack-Cache: invalidate, pass\r\n" --> "X-Sandbox: true\r\n" --> "X-Backend-Server: app-3\r\n" --> "\r\n" -reading all... --> "{\"successful\":true,\"response\":{\"token\":\"nkk9rhwu\",\"card_holder\":\"Foo Bar\",\"card_number\":\"512345XXXXXX2346\",\"card_expiry\":\"2017-05-31T23:59:59+10:00\",\"authorized\":true,\"transaction_count\":0},\"errors\":[],\"test\":true}" -read 214 bytes -Conn close + <<~'PRE_SCRUBBED' + opening connection to gateway.sandbox.fatzebra.com.au:443... + opened + starting SSL for gateway.sandbox.fatzebra.com.au:443... + SSL established + <- "POST /v1.0/credit_cards HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAuthorization: Basic VEVTVDpURVNU\r\nUser-Agent: Fat Zebra v1.0/ActiveMerchant 1.56.0\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nConnection: close\r\nHost: gateway.sandbox.fatzebra.com.au\r\nContent-Length: 93\r\n\r\n" + <- "{\"card_number\":\"5123456789012346\",\"card_expiry\":\"5/2017\",\"cvv\":\"111\",\"card_holder\":\"Foo Bar\"}" + -> "HTTP/1.1 200 OK\r\n" + -> "Content-Type: application/json; charset=utf-8\r\n" + -> "Connection: close\r\n" + -> "Status: 200 OK\r\n" + -> "Cache-control: no-store\r\n" + -> "Pragma: no-cache\r\n" + -> "X-Request-Id: 3BA78272_F214_AC10001D_01BB_566A58EC_222F1D_49F4\r\n" + -> "X-Runtime: 0.142463\r\n" + -> "Date: Fri, 11 Dec 2015 05:02:36 GMT\r\n" + -> "X-Rack-Cache: invalidate, pass\r\n" + -> "X-Sandbox: true\r\n" + -> "X-Backend-Server: app-3\r\n" + -> "\r\n" + reading all... + -> "{\"successful\":true,\"response\":{\"token\":\"nkk9rhwu\",\"card_holder\":\"Foo Bar\",\"card_number\":\"512345XXXXXX2346\",\"card_expiry\":\"2017-05-31T23:59:59+10:00\",\"authorized\":true,\"transaction_count\":0},\"errors\":[],\"test\":true}" + read 214 bytes + Conn close PRE_SCRUBBED end def post_scrubbed - <<-'POST_SCRUBBED' -opening connection to gateway.sandbox.fatzebra.com.au:443... -opened -starting SSL for gateway.sandbox.fatzebra.com.au:443... -SSL established -<- "POST /v1.0/credit_cards HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAuthorization: Basic [FILTERED]\r\nUser-Agent: Fat Zebra v1.0/ActiveMerchant 1.56.0\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nConnection: close\r\nHost: gateway.sandbox.fatzebra.com.au\r\nContent-Length: 93\r\n\r\n" -<- "{\"card_number\":\"[FILTERED]\",\"card_expiry\":\"5/2017\",\"cvv\":\"[FILTERED]\",\"card_holder\":\"Foo Bar\"}" --> "HTTP/1.1 200 OK\r\n" --> "Content-Type: application/json; charset=utf-8\r\n" --> "Connection: close\r\n" --> "Status: 200 OK\r\n" --> "Cache-control: no-store\r\n" --> "Pragma: no-cache\r\n" --> "X-Request-Id: 3BA78272_F214_AC10001D_01BB_566A58EC_222F1D_49F4\r\n" --> "X-Runtime: 0.142463\r\n" --> "Date: Fri, 11 Dec 2015 05:02:36 GMT\r\n" --> "X-Rack-Cache: invalidate, pass\r\n" --> "X-Sandbox: true\r\n" --> "X-Backend-Server: app-3\r\n" --> "\r\n" -reading all... --> "{\"successful\":true,\"response\":{\"token\":\"nkk9rhwu\",\"card_holder\":\"Foo Bar\",\"card_number\":\"[FILTERED]\",\"card_expiry\":\"2017-05-31T23:59:59+10:00\",\"authorized\":true,\"transaction_count\":0},\"errors\":[],\"test\":true}" -read 214 bytes -Conn close + <<~'POST_SCRUBBED' + opening connection to gateway.sandbox.fatzebra.com.au:443... + opened + starting SSL for gateway.sandbox.fatzebra.com.au:443... + SSL established + <- "POST /v1.0/credit_cards HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAuthorization: Basic [FILTERED]\r\nUser-Agent: Fat Zebra v1.0/ActiveMerchant 1.56.0\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nConnection: close\r\nHost: gateway.sandbox.fatzebra.com.au\r\nContent-Length: 93\r\n\r\n" + <- "{\"card_number\":\"[FILTERED]\",\"card_expiry\":\"5/2017\",\"cvv\":\"[FILTERED]\",\"card_holder\":\"Foo Bar\"}" + -> "HTTP/1.1 200 OK\r\n" + -> "Content-Type: application/json; charset=utf-8\r\n" + -> "Connection: close\r\n" + -> "Status: 200 OK\r\n" + -> "Cache-control: no-store\r\n" + -> "Pragma: no-cache\r\n" + -> "X-Request-Id: 3BA78272_F214_AC10001D_01BB_566A58EC_222F1D_49F4\r\n" + -> "X-Runtime: 0.142463\r\n" + -> "Date: Fri, 11 Dec 2015 05:02:36 GMT\r\n" + -> "X-Rack-Cache: invalidate, pass\r\n" + -> "X-Sandbox: true\r\n" + -> "X-Backend-Server: app-3\r\n" + -> "\r\n" + reading all... + -> "{\"successful\":true,\"response\":{\"token\":\"nkk9rhwu\",\"card_holder\":\"Foo Bar\",\"card_number\":\"[FILTERED]\",\"card_expiry\":\"2017-05-31T23:59:59+10:00\",\"authorized\":true,\"transaction_count\":0},\"errors\":[],\"test\":true}" + read 214 bytes + Conn close POST_SCRUBBED end + # Place raw successful response from gateway here def successful_purchase_response { - :successful => true, - :response => { - :authorization => '55355', - :id => '001-P-12345AA', - :card_number => 'XXXXXXXXXXXX1111', - :card_holder => 'John Smith', - :card_expiry => '10/2011', - :card_token => 'a1bhj98j', - :amount => 349, - :successful => true, - :reference => 'ABC123', - :message => 'Approved', + successful: true, + response: { + authorization: 55355, + id: '001-P-12345AA', + card_number: 'XXXXXXXXXXXX1111', + card_holder: 'John Smith', + card_expiry: '10/2011', + card_token: 'a1bhj98j', + amount: 349, + decimal_amount: 3.49, + successful: true, + message: 'Approved', + reference: 'ABC123', + currency: 'AUD', + transaction_id: '001-P-12345AA', + settlement_date: '2011-07-01', + transaction_date: '2011-07-01T12:00:00+11:00', + response_code: '08', + captured: true, + captured_amount: 349, + rrn: '000000000000', + cvv_match: 'U', + metadata: { + } + }, + test: true, + errors: [] + }.to_json + end + + def successful_purchase_response_with_metadata + { + successful: true, + response: { + authorization: 55355, + id: '001-P-12345AA', + card_number: 'XXXXXXXXXXXX1111', + card_holder: 'John Smith', + card_expiry: '2011-10-31', + card_token: 'a1bhj98j', + amount: 349, + decimal_amount: 3.49, + successful: true, + message: 'Approved', + reference: 'ABC123', + currency: 'AUD', + transaction_id: '001-P-12345AA', + settlement_date: '2011-07-01', + transaction_date: '2011-07-01T12:00:00+11:00', + response_code: '08', + captured: true, + captured_amount: 349, + rrn: '000000000000', + cvv_match: 'U', + metadata: { + 'foo' => 'bar' + } }, - :test => true, - :errors => [] + test: true, + errors: [] }.to_json end def declined_purchase_response { - :successful => true, - :response => { - :authorization_id => nil, - :id => nil, - :card_number => 'XXXXXXXXXXXX1111', - :card_holder => 'John Smith', - :card_expiry => '10/2011', - :amount => 100, - :authorized => false, - :reference => 'ABC123', - :message => 'Card Declined - check with issuer', + successful: true, + response: { + authorization: 0, + id: '001-P-12345AB', + card_number: 'XXXXXXXXXXXX1111', + card_holder: 'John Smith', + card_expiry: '10/2011', + amount: 100, + authorized: false, + reference: 'ABC123', + decimal_amount: 1.0, + successful: false, + message: 'Card Declined - check with issuer', + currency: 'AUD', + transaction_id: '001-P-12345AB', + settlement_date: nil, + transaction_date: '2011-07-01T12:00:00+11:00', + response_code: '01', + captured: false, + captured_amount: 0, + rrn: '000000000001', + cvv_match: 'U', + metadata: { + } }, - :test => true, - :errors => [] + test: true, + errors: [] }.to_json end def successful_refund_response { - :successful => true, - :response => { - :authorization => '1339973263', - :id => '003-R-7MNIUMY6', - :amount => -10, - :refunded => 'Approved', - :message => '08 Approved', - :card_holder => 'Harry Smith', - :card_number => 'XXXXXXXXXXXX4444', - :card_expiry => '2013-05-31', - :card_type => 'MasterCard', - :transaction_id => '003-R-7MNIUMY6', - :successful => true + successful: true, + response: { + authorization: 1339973263, + id: '003-R-7MNIUMY6', + amount: 10, + refunded: 'Approved', + message: 'Approved', + card_holder: 'Harry Smith', + card_number: 'XXXXXXXXXXXX4444', + card_expiry: '2013-05-31', + card_type: 'MasterCard', + transaction_id: '003-R-7MNIUMY6', + reference: '18280', + currency: 'USD', + successful: true, + transaction_date: '2013-07-01T12:00:00+11:00', + response_code: '08', + settlement_date: '2013-07-01', + metadata: { + }, + standalone: false, + rrn: '000000000002' }, - :errors => [ - - ], - :test => true + errors: [], + test: true }.to_json end def unsuccessful_refund_response { - :successful => false, - :response => { - :authorization => nil, - :id => nil, - :amount => nil, - :refunded => nil, - :message => nil, - :card_holder => 'Matthew Savage', - :card_number => 'XXXXXXXXXXXX4444', - :card_expiry => '2013-05-31', - :card_type => 'MasterCard', - :transaction_id => nil, - :successful => false + successful: false, + response: { + authorization: nil, + id: nil, + amount: nil, + refunded: nil, + message: nil, + card_holder: 'Matthew Savage', + card_number: 'XXXXXXXXXXXX4444', + card_expiry: '2013-05-31', + card_type: 'MasterCard', + transaction_id: nil, + successful: false }, - :errors => [ + errors: [ "Reference can't be blank" ], - :test => true + test: true }.to_json end def successful_tokenize_response { - :successful => true, - :response => { - :token => 'e1q7dbj2', - :card_holder => 'Bob Smith', - :card_number => 'XXXXXXXXXXXX2346', - :card_expiry => '2013-05-31T23:59:59+10:00', - :authorized => true, - :transaction_count => 0 + successful: true, + response: { + token: 'e1q7dbj2', + card_holder: 'Bob Smith', + card_number: 'XXXXXXXXXXXX2346', + card_expiry: '2013-05-31T23:59:59+10:00', + authorized: true, + transaction_count: 0 }, - :errors => [], - :test => true + errors: [], + test: true }.to_json end def failed_tokenize_response { - :successful => false, - :response => { - :token => nil, - :card_holder => 'Bob ', - :card_number => '512345XXXXXX2346', - :card_expiry => nil, - :authorized => false, - :transaction_count => 10 + successful: false, + response: { + token: nil, + card_holder: 'Bob ', + card_number: '512345XXXXXX2346', + card_expiry: nil, + authorized: false, + transaction_count: 10 }, - :errors => [ + errors: [ "Expiry date can't be blank" ], - :test => false + test: false + }.to_json + end + + def successful_no_cvv_tokenize_response + { + successful: true, + response: { + token: 'ep3c05nzsqvft15wsf1z', + card_holder: 'Bob ', + card_number: '512345XXXXXX2346', + card_expiry: nil, + authorized: true, + transaction_count: 0 + }, + errors: [], + test: false + }.to_json + end + + def failed_no_cvv_tokenize_response + { + successful: false, + response: { + token: nil, + card_holder: 'Bob ', + card_number: '512345XXXXXX2346', + card_expiry: nil, + authorized: false, + transaction_count: 0 + }, + errors: [ + 'CVV is required' + ], + test: false }.to_json end # Place raw failed response from gateway here def failed_purchase_response { - :successful => false, - :response => {}, - :test => true, - :errors => ['Invalid Card Number'] + successful: false, + response: {}, + test: true, + errors: ['Invalid Card Number'] }.to_json end def missing_data_response { - :successful => false, - :response => {}, - :test => true, - :errors => ['Card Number is required'] + successful: false, + response: {}, + test: true, + errors: ['Card Number is required'] }.to_json end end diff --git a/test/unit/gateways/federated_canada_test.rb b/test/unit/gateways/federated_canada_test.rb index 2e7daeb5718..c54a6c7e006 100644 --- a/test/unit/gateways/federated_canada_test.rb +++ b/test/unit/gateways/federated_canada_test.rb @@ -3,42 +3,41 @@ class FederatedCanadaTest < Test::Unit::TestCase def setup @gateway = FederatedCanadaGateway.new( - :login => 'demo', - :password => 'password' - ) + login: 'demo', + password: 'password' + ) @credit_card = credit_card('4111111111111111') @credit_card.verification_value = '999' @amount = 100 - @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase' + @options = { + order_id: '1', + billing_address: address, + description: 'Store Purchase' } end - + def test_successful_authorization @gateway.expects(:ssl_post).returns(successful_authorization_response) - options = {:billing_address => {:address1 => '888', :address2 => 'apt 13', :country => 'CA', :state => 'SK', :city => 'Big Beaver', :zip => '77777'}} + options = { billing_address: { address1: '888', address2: 'apt 13', country: 'CA', state: 'SK', city: 'Big Beaver', zip: '77777' } } assert response = @gateway.authorize(@amount, @credit_card, options) assert_instance_of Response, response assert_success response assert_equal '1355694937', response.authorization assert_equal 'auth', response.params['type'] end - - + def test_successful_purchase @gateway.expects(:ssl_post).returns(successful_purchase_response) assert response = @gateway.purchase(@amount, @credit_card, @options) assert_instance_of Response, response assert_success response assert_equal '1346648416', response.authorization - assert_equal 'sale', response.params['type'] + assert_equal 'sale', response.params['type'] assert response.test? end - + def test_unsuccessful_request @gateway.expects(:ssl_post).returns(failed_purchase_response) assert response = @gateway.purchase(@amount, @credit_card, @options) @@ -48,42 +47,41 @@ def test_unsuccessful_request def test_add_address result = {} - @gateway.send(:add_address, result, :billing_address => {:address1 => '123 Happy Town Road', :address2 => 'apt 13', :country => 'CA', :state => 'SK', :phone => '1234567890'} ) - assert_equal ['address1', 'address2', 'city', 'company', 'country', 'phone', 'state', 'zip'], result.stringify_keys.keys.sort + @gateway.send(:add_address, result, billing_address: { address1: '123 Happy Town Road', address2: 'apt 13', country: 'CA', state: 'SK', phone: '1234567890' }) + assert_equal %w[address1 address2 city company country phone state zip], result.stringify_keys.keys.sort assert_equal 'SK', result[:state] assert_equal '123 Happy Town Road', result[:address1] - assert_equal 'apt 13', result[:address2] + assert_equal 'apt 13', result[:address2] assert_equal 'CA', result[:country] end def test_add_invoice result = {} - @gateway.send(:add_invoice, result, :order_id => '#1001', :description => 'This is a great order') + @gateway.send(:add_invoice, result, order_id: '#1001', description: 'This is a great order') assert_equal '#1001', result[:orderid] assert_equal 'This is a great order', result[:orderdescription] end - + def test_purchase_is_valid_csv - params = {:amount => @amount} + params = { amount: @amount } @gateway.send(:add_creditcard, params, @credit_card) assert data = @gateway.send(:post_data, 'auth', params) assert_equal post_data_fixture.size, data.size end - - + def test_purchase_meets_minimum_requirements - params = {:amount => @amount} + params = { amount: @amount } @gateway.send(:add_creditcard, params, @credit_card) assert data = @gateway.send(:post_data, 'auth', params) minimum_requirements.each do |key| assert_not_nil(data.include?(key)) end end - + def test_expdate_formatting - assert_equal '0909', @gateway.send(:expdate, credit_card('4111111111111111', :month => '9', :year => '2009')) - assert_equal '0711', @gateway.send(:expdate, credit_card('4111111111111111', :month => '7', :year => '2011')) + assert_equal '0909', @gateway.send(:expdate, credit_card('4111111111111111', month: '9', year: '2009')) + assert_equal '0711', @gateway.send(:expdate, credit_card('4111111111111111', month: '7', year: '2011')) end def test_supported_countries @@ -91,7 +89,7 @@ def test_supported_countries end def test_supported_card_types - assert_equal @gateway.supported_cardtypes, [:visa, :master, :american_express, :discover] + assert_equal @gateway.supported_cardtypes, %i[visa master american_express discover] end def test_avs_result @@ -105,7 +103,7 @@ def test_cvv_result response = @gateway.purchase(@amount, @credit_card) assert_equal 'M', response.cvv_result['code'] end - + def test_amount assert_equal '1.00', @gateway.send(:amount, 100) assert_equal '10.00', @gateway.send(:amount, 1000) @@ -113,17 +111,17 @@ def test_amount @gateway.send(:amount, '10.00') end end - + private - + def post_data_fixture 'password=password&type=auth&ccnumber=4111111111111111&username=demo&ccexp=1111&amount=100&cvv=999' end - + def minimum_requirements %w{type username password amount ccnumber ccexp} end - + # Raw successful authorization response def successful_authorization_response 'response=1&responsetext=SUCCESS&authcode=123456&transactionid=1355694937&avsresponse=Y&cvvresponse=M&orderid=&type=auth&response_code=100' @@ -133,7 +131,7 @@ def successful_authorization_response def successful_purchase_response 'response=1&responsetext=SUCCESS&authcode=123456&transactionid=1346648416&avsresponse=N&cvvresponse=N&orderid=&type=sale&response_code=100' end - + # Raw failed sale response def failed_purchase_response 'response=2&responsetext=DECLINE&authcode=&transactionid=1346648595&avsresponse=N&cvvresponse=N&orderid=&type=sale&response_code=200' diff --git a/test/unit/gateways/finansbank_test.rb b/test/unit/gateways/finansbank_test.rb index 4d4553366fd..2f7e1cfabbd 100644 --- a/test/unit/gateways/finansbank_test.rb +++ b/test/unit/gateways/finansbank_test.rb @@ -1,4 +1,5 @@ # encoding: utf-8 + require 'test_helper' class FinansbankTest < Test::Unit::TestCase @@ -6,18 +7,18 @@ def setup @original_kcode = nil @gateway = FinansbankGateway.new( - :login => 'login', - :password => 'password', - :client_id => 'client_id' + login: 'login', + password: 'password', + client_id: 'client_id' ) @credit_card = credit_card @amount = 100 @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase' + order_id: '1', + billing_address: address, + description: 'Store Purchase' } end @@ -121,228 +122,228 @@ def test_failed_credit private def successful_purchase_response - <<-EOF - - 1 - 1 - Approved - 123456 - 123456 - 00 - 123456 - - - EOF + <<~XML + + 1 + 1 + Approved + 123456 + 123456 + 00 + 123456 + + + XML end def failed_purchase_response - <<-EOF - - 1 - 2 - Declined - - 123456 - 12 - 123456 - Not enough credit - - EOF + <<~XML + + 1 + 2 + Declined + + 123456 + 12 + 123456 + Not enough credit + + XML end def successful_authorize_response - <<-EOF - - 1 - 1 - Approved - 794573 - 305219419620 - 00 - 13052TpOI06012476 - - - 411 - 20130221 19:41:14 - - ISLEMINIZ ONAYLANDI - 00 - 000 - N - - - EOF + <<~XML + + 1 + 1 + Approved + 794573 + 305219419620 + 00 + 13052TpOI06012476 + + + 411 + 20130221 19:41:14 + + ISLEMINIZ ONAYLANDI + 00 + 000 + N + + + XML end def successful_capture_response - <<-EOF - - 1 - 1 - Approved - 794573 - 305219419622 - 00 - 13052TpPH06012485 - - - 411 - 20130221 19:41:15 - - 00 - - - EOF + <<~XML + + 1 + 1 + Approved + 794573 + 305219419622 + 00 + 13052TpPH06012485 + + + 411 + 20130221 19:41:15 + + 00 + + + XML end def capture_without_authorize_response - <<-EOF - - - - Error - - - 99 - 13052TtZF06012712 - PostAuth sadece iliskili bir PreAuth icin yapilabilir. - - - 20130221 19:45:25 - CORE-2115 - 992115 - - - EOF + <<~XML + + + + Error + + + 99 + 13052TtZF06012712 + PostAuth sadece iliskili bir PreAuth icin yapilabilir. + + + 20130221 19:45:25 + CORE-2115 + 992115 + + + XML end def successful_void_response - <<-EOF - - 1 - 1 - Approved - 794573 - 402310197597 - 00 - 14023KVGD18549 - - - 1363 - 20140123 10:21:05 - - 00 - - - EOF + <<~XML + + 1 + 1 + Approved + 794573 + 402310197597 + 00 + 14023KVGD18549 + + + 1363 + 20140123 10:21:05 + + 00 + + + XML end def failed_void_response - <<-EOF - - - - Error - - - 99 - 14023KvNI18702 - İptal edilmeye uygun satış işlemi bulunamadı. - - - 20140123 10:47:13 - CORE-2008 - 992008 - - - EOF + <<~XML + + + + Error + + + 99 + 14023KvNI18702 + İptal edilmeye uygun satış işlemi bulunamadı. + + + 20140123 10:47:13 + CORE-2008 + 992008 + + + XML end def success_refund_response - <<-EOF - - 1 - 1 - Approved - 811778 - 402410197809 - 00 - 14024KACE13836 - - - 1364 - 20140124 10:00:02 - - 000000001634 - 000000001634 - 00 - 3 - - - EOF + <<~XML + + 1 + 1 + Approved + 811778 + 402410197809 + 00 + 14024KACE13836 + + + 1364 + 20140124 10:00:02 + + 000000001634 + 000000001634 + 00 + 3 + + + XML end def failed_refund_response - <<-EOF - - - - Error - - - 99 - 14024KEwH13882 - Iade yapilamaz, siparis gunsonuna girmemis. - - - 20140124 10:04:48 - CORE-2508 - 992508 - - - EOF + <<~XML + + + + Error + + + 99 + 14024KEwH13882 + Iade yapilamaz, siparis gunsonuna girmemis. + + + 20140124 10:04:48 + CORE-2508 + 992508 + + + XML end def success_credit_response - <<-EOF - - ORDER-14024KUGB13953 - ORDER-14024KUGB13953 - Approved - 718160 - 402410197818 - 00 - 14024KUGD13955 - - - 1364 - 20140124 10:20:06 - - 00 - 3 - - - EOF + <<~XML + + ORDER-14024KUGB13953 + ORDER-14024KUGB13953 + Approved + 718160 + 402410197818 + 00 + 14024KUGD13955 + + + 1364 + 20140124 10:20:06 + + 00 + 3 + + + XML end def failed_credit_response - <<-EOF - - - - Error - - - 99 - 14024KUtG13966 - Kredi karti numarasi gecerli formatta degil. - - - 20140124 10:20:45 - CORE-2012 - 992012 - - - EOF + <<~XML + + + + Error + + + 99 + 14024KUtG13966 + Kredi karti numarasi gecerli formatta degil. + + + 20140124 10:20:45 + CORE-2012 + 992012 + + + XML end end 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..d729fe3016c --- /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\":\"#{@credit_card.year.to_s[-2..]}"/, 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\":\"#{@credit_card.year.to_s[-2..]}\"/, 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/first_pay_test.rb b/test/unit/gateways/first_pay_test.rb index 0b3de0f9c25..0db2ce8680a 100644 --- a/test/unit/gateways/first_pay_test.rb +++ b/test/unit/gateways/first_pay_test.rb @@ -21,7 +21,7 @@ def setup def test_successful_purchase response = stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/1234<\/FIELD>/, data) assert_match(/a91c38c3-7d7f-4d29-acc7-927b4dca0dbe<\/FIELD>/, data) assert_match(/sale<\/FIELD>/, data) @@ -62,7 +62,7 @@ def test_failed_purchase def test_successful_authorize response = stub_comms do @gateway.authorize(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/1234<\/FIELD>/, data) assert_match(/a91c38c3-7d7f-4d29-acc7-927b4dca0dbe<\/FIELD>/, data) assert_match(/auth<\/FIELD>/, data) @@ -101,7 +101,7 @@ def test_failed_authorize def test_successful_capture response = stub_comms do @gateway.capture(@amount, '47920') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/1234<\/FIELD>/, data) assert_match(/a91c38c3-7d7f-4d29-acc7-927b4dca0dbe<\/FIELD>/, data) assert_match(/settle<\/FIELD>/, data) @@ -129,7 +129,7 @@ def test_failed_capture def test_successful_refund response = stub_comms do @gateway.refund(@amount, '47925') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/1234<\/FIELD>/, data) assert_match(/a91c38c3-7d7f-4d29-acc7-927b4dca0dbe<\/FIELD>/, data) assert_match(/credit<\/FIELD>/, data) @@ -157,7 +157,7 @@ def test_failed_refund def test_successful_void response = stub_comms do @gateway.void('47934') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/1234<\/FIELD>/, data) assert_match(/a91c38c3-7d7f-4d29-acc7-927b4dca0dbe<\/FIELD>/, data) assert_match(/void<\/FIELD>/, data) @@ -182,21 +182,36 @@ def test_failed_void end def test_recurring_payments - @options[:recurring] = 'none' + @options[:recurring] = 1 @options[:recurring_start_date] = '01/01/1900' @options[:recurring_end_date] = '02/02/1901' + @options[:recurring_type] = 'monthly' response = stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| - assert_match(%r{none}, data) - assert_match(%r{01/01/1900}, data) - assert_match(%r{02/02/1901}, data) + end.check_request do |_endpoint, data, _headers| + assert_match(%r{1}, data) + assert_match(%r{01/01/1900}, data) + assert_match(%r{02/02/1901}, data) + assert_match(%r{monthly}, data) end.respond_with(successful_purchase_response) assert response assert_success response end + def test_error_message + @gateway.stubs(:ssl_post).returns(failed_login_response) + response = @gateway.void('1') + + assert_failure response + assert response.error_code.include?('Merchant: 1234 has encountered error #DTO-200-TC.') + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + private def successful_purchase_response @@ -360,6 +375,67 @@ def failed_void_response 1 +) + end + + def failed_login_response + %( + + 0 + + + + + + a0d2560dda18631ce325c07dcbda2a9880fd17fb344fd233 + Merchant: 1234 has encountered error #DTO-200-TC. Please call 888-638-7867 if you feel this is in error. + +) + end + + def pre_scrubbed + %( + + 77b61bfe08510e00852f2f20011e7952d80f9a4be17d27cf + 1.00visa + 4111111111111111 + 0919 + 789 + Jim Smith + 456 My Street + Apt 1 + Ottawa + ON + K1C2N6 + CA + (555)555-5555 + 1264 + a91c38c3-7d7f-4d29-acc7-927b4dca0dbe + sale + +) + end + + def post_scrubbed + %( + + 77b61bfe08510e00852f2f20011e7952d80f9a4be17d27cf + 1.00visa + + 0919 + + Jim Smith + 456 My Street + Apt 1 + Ottawa + ON + K1C2N6 + CA + (555)555-5555 + 1264 + + sale + ) end end diff --git a/test/unit/gateways/firstdata_e4_test.rb b/test/unit/gateways/firstdata_e4_test.rb index eb01abdebbd..d4dab1f763f 100755 --- a/test/unit/gateways/firstdata_e4_test.rb +++ b/test/unit/gateways/firstdata_e4_test.rb @@ -7,16 +7,16 @@ class FirstdataE4Test < Test::Unit::TestCase def setup @gateway = FirstdataE4Gateway.new( - :login => 'A00427-01', - :password => 'testus' + login: 'A00427-01', + password: 'testus' ) @credit_card = credit_card @amount = 100 @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase' + order_id: '1', + billing_address: address, + description: 'Store Purchase' } @authorization = 'ET1700;106625152;4738' end @@ -38,11 +38,11 @@ def test_successful_purchase assert response.test? assert_equal 'Transaction Normal - Approved', response.message - FirstdataE4Gateway::SENSITIVE_FIELDS.each{|f| assert !response.params.has_key?(f.to_s)} + FirstdataE4Gateway::SENSITIVE_FIELDS.each { |f| assert !response.params.has_key?(f.to_s) } end def test_successful_purchase_with_specified_currency - options_with_specified_currency = @options.merge({currency: 'GBP'}) + options_with_specified_currency = @options.merge({ currency: 'GBP' }) @gateway.expects(:ssl_post).returns(successful_purchase_with_specified_currency_response) assert response = @gateway.purchase(@amount, @credit_card, options_with_specified_currency) assert_success response @@ -51,7 +51,7 @@ def test_successful_purchase_with_specified_currency assert_equal 'Transaction Normal - Approved', response.message assert_equal 'GBP', response.params['currency'] - FirstdataE4Gateway::SENSITIVE_FIELDS.each{|f| assert !response.params.has_key?(f.to_s)} + FirstdataE4Gateway::SENSITIVE_FIELDS.each { |f| assert !response.params.has_key?(f.to_s) } end def test_successful_purchase_with_token @@ -61,10 +61,9 @@ def test_successful_purchase_with_token end def test_successful_purchase_with_specified_currency_and_token - options_with_specified_currency = @options.merge({currency: 'GBP'}) + 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 @@ -82,7 +81,7 @@ def test_successful_refund end def test_successful_refund_with_specified_currency - options_with_specified_currency = @options.merge({currency: 'GBP'}) + options_with_specified_currency = @options.merge({ currency: 'GBP' }) @gateway.expects(:ssl_post).returns(successful_refund_with_specified_currency_response) assert response = @gateway.refund(@amount, @authorization, options_with_specified_currency) assert_success response @@ -136,11 +135,11 @@ def test_no_transaction end def test_supported_countries - assert_equal ['CA', 'US'], FirstdataE4Gateway.supported_countries + assert_equal %w[CA US], FirstdataE4Gateway.supported_countries end def test_supported_cardtypes - assert_equal [:visa, :master, :american_express, :jcb, :discover], FirstdataE4Gateway.supported_cardtypes + assert_equal %i[visa master american_express jcb discover], FirstdataE4Gateway.supported_cardtypes end def test_avs_result @@ -160,15 +159,24 @@ def test_cvv_result def test_requests_include_verification_string stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match '456 My Street|K1C2N6|Ottawa|ON|CA', data end.respond_with(successful_purchase_response) end + def test_requests_scrub_newline_and_return_characters_from_verification_string_components + stub_comms do + options_with_newline_and_return_characters_in_address = @options.merge({ billing_address: address({ address1: "123 My\nStreet", address2: "K1C2N6\r", city: "Ottawa\r\n" }) }) + @gateway.purchase(@amount, @credit_card, options_with_newline_and_return_characters_in_address) + end.check_request do |_endpoint, data, _headers| + assert_match '123 My Street|K1C2N6|Ottawa|ON|CA', data + end.respond_with(successful_purchase_response) + end + def test_tax_fields_are_sent stub_comms do @gateway.purchase(@amount, @credit_card, @options.merge(tax1_amount: 830, tax1_number: 'Br59a')) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match '830', data assert_match 'Br59a', data end.respond_with(successful_purchase_response) @@ -177,7 +185,7 @@ def test_tax_fields_are_sent def test_customer_ref_is_sent stub_comms do @gateway.purchase(@amount, @credit_card, @options.merge(customer: '932')) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match '932', data end.respond_with(successful_purchase_response) end @@ -185,7 +193,7 @@ def test_customer_ref_is_sent def test_eci_default_value stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match '07', data end.respond_with(successful_purchase_response) end @@ -196,7 +204,7 @@ def test_eci_numeric_padding stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match '05', data end.respond_with(successful_purchase_response) @@ -205,7 +213,7 @@ def test_eci_numeric_padding stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match '05', data end.respond_with(successful_purchase_response) end @@ -213,7 +221,7 @@ def test_eci_numeric_padding def test_eci_option_value stub_comms do @gateway.purchase(@amount, @credit_card, @options.merge(eci: '05')) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match '05', data end.respond_with(successful_purchase_response) end @@ -225,7 +233,7 @@ def test_network_tokenization_requests_with_amex brand: 'american_express', transaction_id: '123', eci: '05', - payment_cryptogram: 'whatever_the_cryptogram_of_at_least_20_characters_is', + payment_cryptogram: 'whatever_the_cryptogram_of_at_least_20_characters_is' ) @gateway.purchase(@amount, credit_card, @options) @@ -244,7 +252,7 @@ def test_network_tokenization_requests_with_discover brand: 'discover', transaction_id: '123', eci: '05', - payment_cryptogram: 'whatever_the_cryptogram_is', + payment_cryptogram: 'whatever_the_cryptogram_is' ) @gateway.purchase(@amount, credit_card, @options) @@ -261,10 +269,10 @@ def test_network_tokenization_requests_with_other_brands stub_comms do credit_card = network_tokenization_credit_card( '378282246310005', - brand: brand, + brand:, transaction_id: '123', eci: '05', - payment_cryptogram: 'whatever_the_cryptogram_is', + payment_cryptogram: 'whatever_the_cryptogram_is' ) @gateway.purchase(@amount, credit_card, @options) @@ -287,7 +295,7 @@ def test_requests_include_card_authentication_data stub_comms do @gateway.purchase(@amount, @credit_card, options_with_authentication_data) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match '06', data assert_match 'SAMPLECAVV', data assert_match 'SAMPLEXID', data @@ -309,7 +317,7 @@ def test_add_swipe_data_with_creditcard stub_comms do @gateway.purchase(@amount, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match 'Track Data', data assert_match 'R', data end.respond_with(successful_purchase_response) @@ -394,777 +402,780 @@ def post_scrub end def successful_purchase_response - <<-RESPONSE - - - AD1234-56 - - 00 - 47.38 - - ############1111 - 106625152 - - - - ET1700 - 0913 - Fred Burfle - - 773 - 0 - - - - - - - - - - - - 77 - - - - 1.1.1.10 - - false - true - 00 - Transaction Normal - 100 - Approved - - 000040 - U - M - 3146117 - - USD - - false - Friendly Inc DEMO0983 - 123 King St - Toronto - Ontario - Canada - L7Z 3K8 - - 8938737759041111 - =========== TRANSACTION RECORD ========== -Friendly Inc DEMO0983 -123 King St -Toronto, ON L7Z 3K8 -Canada - - -TYPE: Purchase - -ACCT: Visa $ 47.38 USD - -CARD NUMBER : ############1111 -DATE/TIME : 28 Sep 12 07:54:48 -REFERENCE # : 000040 M -AUTHOR. # : ET120454 -TRANS. REF. : 77 - - Approved - Thank You 100 - - -Please retain this copy for your records. - -Cardholder will pay above amount to card -issuer pursuant to cardholder agreement. -========================================= - + <<~RESPONSE + + + AD1234-56 + + 00 + 47.38 + + ############1111 + 106625152 + + + + ET1700 + 0913 + Fred Burfle + + 773 + 0 + + + + + + + + + + + + 77 + + + + 1.1.1.10 + + false + true + 00 + Transaction Normal + 100 + Approved + + 000040 + U + M + 3146117 + + USD + + false + Friendly Inc DEMO0983 + 123 King St + Toronto + Ontario + Canada + L7Z 3K8 + + 8938737759041111 + =========== TRANSACTION RECORD ========== + Friendly Inc DEMO0983 + 123 King St + Toronto, ON L7Z 3K8 + Canada + + + TYPE: Purchase + + ACCT: Visa $ 47.38 USD + + CARD NUMBER : ############1111 + DATE/TIME : 28 Sep 12 07:54:48 + REFERENCE # : 000040 M + AUTHOR. # : ET120454 + TRANS. REF. : 77 + + Approved - Thank You 100 + + + Please retain this copy for your records. + + Cardholder will pay above amount to card + issuer pursuant to cardholder agreement. + ========================================= + RESPONSE end + def successful_purchase_with_specified_currency_response - <<-RESPONSE - - - AD1234-56 - - 00 - 47.38 - - ############1111 - 106625152 - - - - ET1700 - 0913 - Fred Burfle - - 773 - 0 - - - - - - - - - - - - 77 - - - - 1.1.1.10 - - false - true - 00 - Transaction Normal - 100 - Approved - - 000040 - U - M - 3146117 - - GBP - - false - Friendly Inc DEMO0983 - 123 King St - Toronto - Ontario - Canada - L7Z 3K8 - - 8938737759041111 - =========== TRANSACTION RECORD ========== -Friendly Inc DEMO0983 -123 King St -Toronto, ON L7Z 3K8 -Canada - - -TYPE: Purchase - -ACCT: Visa £ 47.38 GBP - -CARD NUMBER : ############1111 -DATE/TIME : 28 Sep 12 07:54:48 -REFERENCE # : 000040 M -AUTHOR. # : ET120454 -TRANS. REF. : 77 - - Approved - Thank You 100 - - -Please retain this copy for your records. - -Cardholder will pay above amount to card -issuer pursuant to cardholder agreement. -========================================= - + <<~RESPONSE + + + AD1234-56 + + 00 + 47.38 + + ############1111 + 106625152 + + + + ET1700 + 0913 + Fred Burfle + + 773 + 0 + + + + + + + + + + + + 77 + + + + 1.1.1.10 + + false + true + 00 + Transaction Normal + 100 + Approved + + 000040 + U + M + 3146117 + + GBP + + false + Friendly Inc DEMO0983 + 123 King St + Toronto + Ontario + Canada + L7Z 3K8 + + 8938737759041111 + =========== TRANSACTION RECORD ========== + Friendly Inc DEMO0983 + 123 King St + Toronto, ON L7Z 3K8 + Canada + + + TYPE: Purchase + + ACCT: Visa £ 47.38 GBP + + CARD NUMBER : ############1111 + DATE/TIME : 28 Sep 12 07:54:48 + REFERENCE # : 000040 M + AUTHOR. # : ET120454 + TRANS. REF. : 77 + + Approved - Thank You 100 + + + Please retain this copy for your records. + + Cardholder will pay above amount to card + issuer pursuant to cardholder agreement. + ========================================= + RESPONSE end + def successful_purchase_response_without_transarmor - <<-RESPONSE - - - AD1234-56 - - 00 - 47.38 - - ############1111 - 106625152 - - - - ET1700 - 0913 - Fred Burfle - - 773 - 0 - - - - - - - - - - - - 77 - - - - 1.1.1.10 - - false - true - 00 - Transaction Normal - 100 - Approved - - 000040 - U - M - 3146117 - - USD - - false - Friendly Inc DEMO0983 - 123 King St - Toronto - Ontario - Canada - L7Z 3K8 - - - =========== TRANSACTION RECORD ========== -Friendly Inc DEMO0983 -123 King St -Toronto, ON L7Z 3K8 -Canada - - -TYPE: Purchase - -ACCT: Visa $ 47.38 USD - -CARD NUMBER : ############1111 -DATE/TIME : 28 Sep 12 07:54:48 -REFERENCE # : 000040 M -AUTHOR. # : ET120454 -TRANS. REF. : 77 - - Approved - Thank You 100 - - -Please retain this copy for your records. - -Cardholder will pay above amount to card -issuer pursuant to cardholder agreement. -========================================= - + <<~RESPONSE + + + AD1234-56 + + 00 + 47.38 + + ############1111 + 106625152 + + + + ET1700 + 0913 + Fred Burfle + + 773 + 0 + + + + + + + + + + + + 77 + + + + 1.1.1.10 + + false + true + 00 + Transaction Normal + 100 + Approved + + 000040 + U + M + 3146117 + + USD + + false + Friendly Inc DEMO0983 + 123 King St + Toronto + Ontario + Canada + L7Z 3K8 + + + =========== TRANSACTION RECORD ========== + Friendly Inc DEMO0983 + 123 King St + Toronto, ON L7Z 3K8 + Canada + + + TYPE: Purchase + + ACCT: Visa $ 47.38 USD + + CARD NUMBER : ############1111 + DATE/TIME : 28 Sep 12 07:54:48 + REFERENCE # : 000040 M + AUTHOR. # : ET120454 + TRANS. REF. : 77 + + Approved - Thank You 100 + + + Please retain this copy for your records. + + Cardholder will pay above amount to card + issuer pursuant to cardholder agreement. + ========================================= + RESPONSE end + def successful_refund_response - <<-RESPONSE - - - AD1234-56 - - 34 - 123 - - ############1111 - 888 - - - - ET112216 - 0913 - Fred Burfle - - - 0 - - - - - - - - - - - - - - - - 1.1.1.10 - - false - true - 00 - Transaction Normal - 100 - Approved - - 000041 - - I - 9176784 - - USD - - false - Friendly Inc DEMO0983 - 123 King St - Toronto - Ontario - Canada - L7Z 3K8 - - =========== TRANSACTION RECORD ========== -Friendly Inc DEMO0983 -123 King St -Toronto, ON L7Z 3K8 -Canada - - -TYPE: Refund - -ACCT: Visa $ 23.69 USD - -CARD NUMBER : ############1111 -DATE/TIME : 28 Sep 12 08:31:23 -REFERENCE # : 000041 M -AUTHOR. # : ET112216 -TRANS. REF. : - - Approved - Thank You 100 - - -Please retain this copy for your records. - -========================================= - + <<~RESPONSE + + + AD1234-56 + + 34 + 123 + + ############1111 + 888 + + + + ET112216 + 0913 + Fred Burfle + + + 0 + + + + + + + + + + + + + + + + 1.1.1.10 + + false + true + 00 + Transaction Normal + 100 + Approved + + 000041 + + I + 9176784 + + USD + + false + Friendly Inc DEMO0983 + 123 King St + Toronto + Ontario + Canada + L7Z 3K8 + + =========== TRANSACTION RECORD ========== + Friendly Inc DEMO0983 + 123 King St + Toronto, ON L7Z 3K8 + Canada + + + TYPE: Refund + + ACCT: Visa $ 23.69 USD + + CARD NUMBER : ############1111 + DATE/TIME : 28 Sep 12 08:31:23 + REFERENCE # : 000041 M + AUTHOR. # : ET112216 + TRANS. REF. : + + Approved - Thank You 100 + + + Please retain this copy for your records. + + ========================================= + RESPONSE end def successful_refund_with_specified_currency_response - <<-RESPONSE - - - AD1234-56 - - 34 - 123 - - ############1111 - 888 - - - - ET112216 - 0913 - Fred Burfle - - - 0 - - - - - - - - - - - - - - - - 1.1.1.10 - - false - true - 00 - Transaction Normal - 100 - Approved - - 000041 - - I - 9176784 - - GBP - - false - Friendly Inc DEMO0983 - 123 King St - Toronto - Ontario - Canada - L7Z 3K8 - - =========== TRANSACTION RECORD ========== -Friendly Inc DEMO0983 -123 King St -Toronto, ON L7Z 3K8 -Canada - - -TYPE: Refund - -ACCT: Visa £ 23.69 GBP - -CARD NUMBER : ############1111 -DATE/TIME : 28 Sep 12 08:31:23 -REFERENCE # : 000041 M -AUTHOR. # : ET112216 -TRANS. REF. : - - Approved - Thank You 100 - - -Please retain this copy for your records. - -========================================= - + <<~RESPONSE + + + AD1234-56 + + 34 + 123 + + ############1111 + 888 + + + + ET112216 + 0913 + Fred Burfle + + + 0 + + + + + + + + + + + + + + + + 1.1.1.10 + + false + true + 00 + Transaction Normal + 100 + Approved + + 000041 + + I + 9176784 + + GBP + + false + Friendly Inc DEMO0983 + 123 King St + Toronto + Ontario + Canada + L7Z 3K8 + + =========== TRANSACTION RECORD ========== + Friendly Inc DEMO0983 + 123 King St + Toronto, ON L7Z 3K8 + Canada + + + TYPE: Refund + + ACCT: Visa £ 23.69 GBP + + CARD NUMBER : ############1111 + DATE/TIME : 28 Sep 12 08:31:23 + REFERENCE # : 000041 M + AUTHOR. # : ET112216 + TRANS. REF. : + + Approved - Thank You 100 + + + Please retain this copy for your records. + + ========================================= + RESPONSE end def failed_purchase_response - <<-RESPONSE - - - AD1234-56 - - 00 - 5013.0 - - ############1111 - 555555 - - - - - 0911 - Fred Burfle - - 773 - 0 - - - - - - - - - - - - 77 - - - - 1.1.1.10 - - - 0 - - false - false - 00 - Transaction Normal - 605 - Invalid Expiration Date - - 000033 - - - - - USD - - false - Friendly Inc DEMO0983 - 123 King St - Toronto - Ontario - Canada - L7Z 3K8 - - =========== TRANSACTION RECORD ========== -Friendly Inc DEMO0983 -123 King St -Toronto, ON L7Z 3K8 -Canada - - -TYPE: Purchase -ACCT: Visa $ 5,013.00 USD -CARD NUMBER : ############1111 -DATE/TIME : 25 Sep 12 07:27:00 -REFERENCE # : 000033 M -AUTHOR. # : -TRANS. REF. : 77 -Transaction not approved 605 -Please retain this copy for your records. -========================================= - + <<~RESPONSE + + + AD1234-56 + + 00 + 5013.0 + + ############1111 + 555555 + + + + + 0911 + Fred Burfle + + 773 + 0 + + + + + + + + + + + + 77 + + + + 1.1.1.10 + + + 0 + + false + false + 00 + Transaction Normal + 605 + Invalid Expiration Date + + 000033 + + + + + USD + + false + Friendly Inc DEMO0983 + 123 King St + Toronto + Ontario + Canada + L7Z 3K8 + + =========== TRANSACTION RECORD ========== + Friendly Inc DEMO0983 + 123 King St + Toronto, ON L7Z 3K8 + Canada + + + TYPE: Purchase + ACCT: Visa $ 5,013.00 USD + CARD NUMBER : ############1111 + DATE/TIME : 25 Sep 12 07:27:00 + REFERENCE # : 000033 M + AUTHOR. # : + TRANS. REF. : 77 + Transaction not approved 605 + Please retain this copy for your records. + ========================================= + RESPONSE end def successful_verify_response - <<-RESPONSE - - - AD2552-05 - - 05 - 0.0 - - ############4242 - 25101911 - - - - ET184931 - 0915 - Longbob Longsen - 1234 My Street|K1C2N6|Ottawa|ON|CA - 123 - 0 - - - - - - - - - - - - 1 - - Store Purchase - - 75.182.123.244 - - false - true - 00 - Transaction Normal - 100 - Approved - - 000040 - 1 - M - 7228838 - - USD - - false - FriendlyInc - 123 Main Street - Durham - North Carolina - United States - 27592 - - - Visa - - - - - false - =========== TRANSACTION RECORD ========== -FriendlyInc DEMO0 -123 Main Street -Durham, NC 27592 -United States - - -TYPE: Auth Only - -ACCT: Visa $ 0.00 USD - -CARDHOLDER NAME : Longbob Longsen -CARD NUMBER : ############4242 -DATE/TIME : 04 Jul 14 14:21:52 -REFERENCE # : 000040 M -AUTHOR. # : ET184931 -TRANS. REF. : 1 - - Approved - Thank You 100 - - -Please retain this copy for your records. - -Cardholder will pay above amount to card -issuer pursuant to cardholder agreement. -========================================= - + <<~RESPONSE + + + AD2552-05 + + 05 + 0.0 + + ############4242 + 25101911 + + + + ET184931 + 0915 + Longbob Longsen + 1234 My Street|K1C2N6|Ottawa|ON|CA + 123 + 0 + + + + + + + + + + + + 1 + + Store Purchase + + 75.182.123.244 + + false + true + 00 + Transaction Normal + 100 + Approved + + 000040 + 1 + M + 7228838 + + USD + + false + FriendlyInc + 123 Main Street + Durham + North Carolina + United States + 27592 + + + Visa + + + + + false + =========== TRANSACTION RECORD ========== + FriendlyInc DEMO0 + 123 Main Street + Durham, NC 27592 + United States + + + TYPE: Auth Only + + ACCT: Visa $ 0.00 USD + + CARDHOLDER NAME : Longbob Longsen + CARD NUMBER : ############4242 + DATE/TIME : 04 Jul 14 14:21:52 + REFERENCE # : 000040 M + AUTHOR. # : ET184931 + TRANS. REF. : 1 + + Approved - Thank You 100 + + + Please retain this copy for your records. + + Cardholder will pay above amount to card + issuer pursuant to cardholder agreement. + ========================================= + RESPONSE end def no_transaction_response - yamlexcep = <<-RESPONSE ---- !ruby/exception:ActiveMerchant::ResponseError -message: Failed with 400 Bad Request -message: -response: !ruby/object:Net::HTTPBadRequest - body: "Malformed request: Transaction Type is missing." - body_exist: true - code: "400" - header: - connection: - - Close - content-type: - - text/html; charset=utf-8 - server: - - Apache - date: - - Fri, 28 Sep 2012 18:21:37 GMT - content-length: - - "47" - status: - - "400" - cache-control: - - no-cache - http_version: "1.1" - message: Bad Request - read: true - socket: + yamlexcep = <<~RESPONSE + --- !ruby/exception:ActiveMerchant::ResponseError + message: Failed with 400 Bad Request + message: + response: !ruby/object:Net::HTTPBadRequest + body: "Malformed request: Transaction Type is missing." + body_exist: true + code: "400" + header: + connection: + - Close + content-type: + - text/html; charset=utf-8 + server: + - Apache + date: + - Fri, 28 Sep 2012 18:21:37 GMT + content-length: + - "47" + status: + - "400" + cache-control: + - no-cache + http_version: "1.1" + message: Bad Request + read: true + socket: RESPONSE - YAML.load(yamlexcep) + YAML.safe_load(yamlexcep, permitted_classes: ['Net::HTTPBadRequest', 'ActiveMerchant::ResponseError']) end def bad_credentials_response - yamlexcep = <<-RESPONSE ---- !ruby/exception:ActiveMerchant::ResponseError -message: -response: !ruby/object:Net::HTTPUnauthorized - code: '401' - message: Authorization Required - body: Unauthorized Request. Bad or missing credentials. - read: true - header: - cache-control: - - no-cache - content-type: - - text/html; charset=utf-8 - date: - - Tue, 30 Dec 2014 23:28:32 GMT - server: - - Apache - status: - - '401' - x-rack-cache: - - invalidate, pass - x-request-id: - - 4157e21cc5620a95ead8d2025b55bdf4 - x-ua-compatible: - - IE=Edge,chrome=1 - content-length: - - '49' - connection: - - Close - body_exist: true - http_version: '1.1' - socket: + yamlexcep = <<~RESPONSE + --- !ruby/exception:ActiveMerchant::ResponseError + message: + response: !ruby/object:Net::HTTPUnauthorized + code: '401' + message: Authorization Required + body: Unauthorized Request. Bad or missing credentials. + read: true + header: + cache-control: + - no-cache + content-type: + - text/html; charset=utf-8 + date: + - Tue, 30 Dec 2014 23:28:32 GMT + server: + - Apache + status: + - '401' + x-rack-cache: + - invalidate, pass + x-request-id: + - 4157e21cc5620a95ead8d2025b55bdf4 + x-ua-compatible: + - IE=Edge,chrome=1 + content-length: + - '49' + connection: + - Close + body_exist: true + http_version: '1.1' + socket: RESPONSE - YAML.load(yamlexcep) + YAML.safe_load(yamlexcep, permitted_classes: ['Net::HTTPUnauthorized', 'ActiveMerchant::ResponseError']) end def successful_void_response - <<-RESPONSE - - - AD1234-56 - - 33 - 11.45 - - ############1111 - 987123 - - - - ET112112 - 0913 - Fred Burfle - - - 0 - - - - - - - - - - - - - - - - 1.1.1.10 - - - 0 - - false - true - 00 - Transaction Normal - 100 - Approved - - 000166 - - I - 2046743 - - USD - - false - FreshBooks DEMO0785 - 35 Golden Ave - Toronto - Ontario - Canada - M6R 2J5 - -=========== TRANSACTION RECORD ========== -FreshBooks DEMO0785 -35 Golden Ave -Toronto, ON M6R 2J5 -Canada - - -TYPE: Void - -ACCT: Visa $ 47.38 USD - -CARD NUMBER : ############1111 -DATE/TIME : 15 Nov 12 08:20:36 -REFERENCE # : 000166 M -AUTHOR. # : ET112112 -TRANS. REF. : - -Approved - Thank You 100 - - -Please retain this copy for your records. - -Cardholder will pay above amount to card -issuer pursuant to cardholder agreement. -========================================= - -RESPONSE + <<~RESPONSE + + + AD1234-56 + + 33 + 11.45 + + ############1111 + 987123 + + + + ET112112 + 0913 + Fred Burfle + + + 0 + + + + + + + + + + + + + + + + 1.1.1.10 + + + 0 + + false + true + 00 + Transaction Normal + 100 + Approved + + 000166 + + I + 2046743 + + USD + + false + FreshBooks DEMO0785 + 35 Golden Ave + Toronto + Ontario + Canada + M6R 2J5 + + =========== TRANSACTION RECORD ========== + FreshBooks DEMO0785 + 35 Golden Ave + Toronto, ON M6R 2J5 + Canada + + + TYPE: Void + + ACCT: Visa $ 47.38 USD + + CARD NUMBER : ############1111 + DATE/TIME : 15 Nov 12 08:20:36 + REFERENCE # : 000166 M + AUTHOR. # : ET112112 + TRANS. REF. : + + Approved - Thank You 100 + + + Please retain this copy for your records. + + Cardholder will pay above amount to card + issuer pursuant to cardholder agreement. + ========================================= + + RESPONSE end end diff --git a/test/unit/gateways/firstdata_e4_v27_test.rb b/test/unit/gateways/firstdata_e4_v27_test.rb new file mode 100644 index 00000000000..3e9a76fd895 --- /dev/null +++ b/test/unit/gateways/firstdata_e4_v27_test.rb @@ -0,0 +1,1132 @@ +require 'test_helper' +require 'nokogiri' +require 'yaml' + +class FirstdataE4V27Test < Test::Unit::TestCase + include CommStub + + def setup + @gateway = FirstdataE4V27Gateway.new( + login: 'A00427-01', + password: 'testus', + key_id: '12345', + hmac_key: 'hexkey' + ) + + @credit_card = credit_card + @amount = 100 + @options = { + order_id: '1', + billing_address: address, + description: 'Store Purchase' + } + @authorization = 'ET1700;106625152;4738' + end + + def test_invalid_credentials + @gateway.expects(:ssl_post).raises(bad_credentials_response) + assert response = @gateway.store(@credit_card, {}) + assert_failure response + assert response.test? + assert_equal '', response.authorization + assert_equal 'Unauthorized Request. Bad or missing credentials.', response.message + end + + def test_successful_purchase + @gateway.expects(:ssl_post).returns(successful_purchase_response) + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'ET1700;106625152;4738', response.authorization + assert response.test? + assert_equal 'Transaction Normal - Approved', response.message + + FirstdataE4V27Gateway::SENSITIVE_FIELDS.each { |f| assert !response.params.has_key?(f.to_s) } + end + + def test_successful_purchase_with_token + @gateway.expects(:ssl_post).returns(successful_purchase_response) + assert response = @gateway.purchase(@amount, '8938737759041111;visa;Longbob;Longsen;9;2014') + assert_success response + end + + def test_successful_purchase_with_wallet + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge!({ wallet_provider_id: 3 })) + end.check_request do |_endpoint, data, _headers| + assert_match(/WalletProviderID>341UnewC/, data) + end.respond_with(successful_purchase_response_with_stored_credentials) + + assert_success response + assert_equal '732602247202501', response.params['stored_credentials_transaction_id'] + end + + def test_successful_void + @gateway.expects(:ssl_post).returns(successful_void_response) + assert response = @gateway.void(@authorization, @options) + assert_success response + end + + def test_successful_refund + @gateway.expects(:ssl_post).returns(successful_refund_response) + assert response = @gateway.refund(@amount, @authorization) + assert_success response + end + + def test_successful_store + @gateway.expects(:ssl_post).returns(successful_purchase_response) + assert response = @gateway.store(@credit_card, @options) + assert_success response + assert_equal '8938737759041111', response.params['transarmor_token'] + assert_equal "8938737759041111;visa;Longbob;Longsen;9;#{@credit_card.year}", response.authorization + end + + def test_failed_store_without_transarmor_support + @gateway.expects(:ssl_post).returns(successful_purchase_response_without_transarmor) + assert_raise StandardError do + @gateway.store(@credit_card, @options) + end + end + + def test_failed_purchase + @gateway.expects(:ssl_post).returns(failed_purchase_response) + + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_instance_of Response, response + assert_failure response + assert_equal response.error_code, 'invalid_expiry_date' + end + + def test_successful_verify + response = stub_comms do + @gateway.verify(@credit_card) + end.respond_with(successful_verify_response) + assert_success response + end + + def test_expdate + assert_equal( + '%02d%2s' % [@credit_card.month, @credit_card.year.to_s[-2..-1]], + @gateway.send(:expdate, @credit_card) + ) + end + + def test_no_transaction + @gateway.expects(:ssl_post).raises(no_transaction_response()) + assert response = @gateway.purchase(100, @credit_card, {}) + assert_failure response + assert response.test? + assert_equal 'Malformed request: Transaction Type is missing.', response.message + end + + def test_supported_countries + assert_equal %w[CA US], FirstdataE4V27Gateway.supported_countries + end + + def test_supported_cardtypes + assert_equal %i[visa master american_express jcb discover], FirstdataE4V27Gateway.supported_cardtypes + end + + def test_avs_result + @gateway.expects(:ssl_post).returns(successful_purchase_response) + + response = @gateway.purchase(@amount, @credit_card) + assert_equal 'U', response.avs_result['code'] + end + + def test_cvv_result + @gateway.expects(:ssl_post).returns(successful_purchase_response) + + response = @gateway.purchase(@amount, @credit_card) + assert_equal 'M', response.cvv_result['code'] + end + + def test_request_includes_address + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match '
456 My StreetApt 1OttawaONK1C2N6CA
', data + end.respond_with(successful_purchase_response) + end + + def test_requests_scrub_newline_and_return_characters_from_verification_string_components + stub_comms do + options_with_newline_and_return_characters_in_address = @options.merge({ billing_address: address({ address1: "456 My\nStreet", address2: nil, city: "Ottawa\r\n", state: 'ON', country: 'CA', zip: 'K1C2N6' }) }) + @gateway.purchase(@amount, @credit_card, options_with_newline_and_return_characters_in_address) + end.check_request do |_endpoint, data, _headers| + assert_match '
456 My StreetOttawaONK1C2N6CA
', data + end.respond_with(successful_purchase_response) + end + + def test_tax_fields_are_sent + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(tax1_amount: 830, tax1_number: 'Br59a')) + end.check_request do |_endpoint, data, _headers| + assert_match '830', data + assert_match 'Br59a', data + end.respond_with(successful_purchase_response) + end + + def test_customer_ref_is_sent + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(customer: '932')) + end.check_request do |_endpoint, data, _headers| + assert_match '932', data + end.respond_with(successful_purchase_response) + end + + def test_eci_default_value + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match '07', data + end.respond_with(successful_purchase_response) + end + + def test_eci_numeric_padding + @credit_card = network_tokenization_credit_card + @credit_card.eci = '5' + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match '05', data + end.respond_with(successful_purchase_response) + + @credit_card = network_tokenization_credit_card + @credit_card.eci = 5 + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match '05', data + end.respond_with(successful_purchase_response) + end + + def test_eci_option_value + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(eci: '05')) + end.check_request do |_endpoint, data, _headers| + assert_match '05', data + end.respond_with(successful_purchase_response) + end + + def test_network_tokenization_requests_with_amex + stub_comms do + credit_card = network_tokenization_credit_card( + '378282246310005', + brand: 'american_express', + transaction_id: '123', + eci: '05', + payment_cryptogram: 'whatever_the_cryptogram_of_at_least_20_characters_is' + ) + + @gateway.purchase(@amount, credit_card, @options) + end.check_request do |_, data, _| + assert_match '05', data + assert_match "mrLdtHIWq2nLXq7IrA==\n", data + assert_match "whateverthecryptogramofatlc=\n", data + assert_xml_valid_to_wsdl(data) + end.respond_with(successful_purchase_response) + end + + def test_network_tokenization_requests_with_discover + stub_comms do + credit_card = network_tokenization_credit_card( + '6011111111111117', + brand: 'discover', + transaction_id: '123', + eci: '04', + payment_cryptogram: 'whatever_the_cryptogram_is' + ) + + @gateway.purchase(@amount, credit_card, @options) + end.check_request do |_, data, _| + assert_match '05', data + assert_match '123', data + assert_match 'whatever_the_cryptogram_is', data + assert_xml_valid_to_wsdl(data) + end.respond_with(successful_purchase_response) + end + + def test_network_tokenization_requests_with_other_brands + %w(visa mastercard other).each do |brand| + stub_comms do + credit_card = network_tokenization_credit_card( + '378282246310005', + brand:, + transaction_id: '123', + eci: '05', + payment_cryptogram: 'whatever_the_cryptogram_is' + ) + + @gateway.purchase(@amount, credit_card, @options) + end.check_request do |_, data, _| + assert_match '05', data + assert_match '123', data + assert_match 'whatever_the_cryptogram_is', data + assert_xml_valid_to_wsdl(data) + end.respond_with(successful_purchase_response) + end + end + + def test_requests_include_card_authentication_data + authentication_hash = { + eci: '06', + cavv: 'SAMPLECAVV', + xid: 'SAMPLEXID' + } + options_with_authentication_data = @options.merge(authentication_hash) + + stub_comms do + @gateway.purchase(@amount, @credit_card, options_with_authentication_data) + end.check_request do |_endpoint, data, _headers| + assert_match '06', data + assert_match 'SAMPLECAVV', data + assert_match 'SAMPLEXID', data + assert_xml_valid_to_wsdl(data) + end.respond_with(successful_purchase_response) + end + + def test_card_type + assert_equal 'Visa', @gateway.send(:card_type, 'visa') + assert_equal 'Mastercard', @gateway.send(:card_type, 'master') + assert_equal 'American Express', @gateway.send(:card_type, 'american_express') + assert_equal 'JCB', @gateway.send(:card_type, 'jcb') + assert_equal 'Discover', @gateway.send(:card_type, 'discover') + end + + def test_add_swipe_data_with_creditcard + @credit_card.track_data = 'Track Data' + + stub_comms do + @gateway.purchase(@amount, @credit_card) + end.check_request do |_endpoint, data, _headers| + assert_match 'Track Data', data + assert_match 'R', data + end.respond_with(successful_purchase_response) + end + + def test_transcript_scrubbing + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrub), post_scrub + end + + def test_supports_network_tokenization + assert_instance_of TrueClass, @gateway.supports_network_tokenization? + end + + private + + def assert_xml_valid_to_wsdl(data) + xsd = Nokogiri::XML::Schema(File.open("#{File.dirname(__FILE__)}/../../schema/firstdata_e4/v27.xsd")) + doc = Nokogiri::XML(data) + errors = xsd.validate(doc) + assert_empty errors, "XSD validation errors in the following XML:\n#{doc}" + end + + def pre_scrub + <<-PRE_SCRUBBED + opening connection to api.demo.globalgatewaye4.firstdata.com:443... + opened + starting SSL for api.demo.globalgatewaye4.firstdata.com:443... + SSL established + <- "POST /transaction/v27 HTTP/1.1\r\nContent-Type: application/xml\r\nX-Gge4-Date: 2018-07-03T07:35:34Z\r\nX-Gge4-Content-Sha1: 5335f81daf59c493fe5d4c18910d17eba69558d4\r\nAuthorization: GGE4_API 397439:iwaxRr8f3GQIMSucb+dmDeiwoAk=\r\nAccepts: application/xml\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.demo.globalgatewaye4.firstdata.com\r\nContent-Length: 807\r\n\r\n" + <- "SD8821-67cBhEc4GENtZ5fnVtpb2qlrhKUDprqtar001.00USD42424242424242420919Longbob LongsenVisa071123
456 My StreetApt 1OttawaONK1C2N6CA
1Store Purchase
" + -> "HTTP/1.1 201 Created\r\n" + -> "Server: nginx\r\n" + -> "Date: Tue, 03 Jul 2018 07:35:34 GMT\r\n" + -> "Content-Type: application/xml; charset=utf-8\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "X-Frame-Options: SAMEORIGIN\r\n" + -> "X-XSS-Protection: 1; mode=block\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "X-GGE4-Date: 2018-07-03T07:35:34Z\r\n" + -> "X-GGE4-CONTENT-SHA1: 87ae8c40ae5afbfd060c7569645f3f6b4045994f\r\n" + -> "Authorization: GGE4_API 397439:dPOI+d2MkNJjBJhHTYUo0ieILw4=\r\n" + -> "Pragma: no-cache\r\n" + -> "Expires: Tue, 03 Jul 2018 06:35:34 GMT\r\n" + -> "Cache-Control: no-store, no-cache\r\n" + -> "X-Request-Id: 0f9ba7k2rd80a2tpch40\r\n" + -> "Location: https://api.demo.globalgatewaye4.firstdata.com/transaction/v27/2264726018\r\n" + -> "Status: 201 Created\r\n" + -> "Strict-Transport-Security: max-age=31536000; includeSubDomains\r\n" + -> "Strict-Transport-Security: max-age=315360000; includeSubdomains\r\n" + -> "\r\n" + -> "2da\r\n" + reading 2944 bytes... + -> "\n\n SD8821-67\n \n 00\n 1.0\n \n ############4242\n 2264726018\n \n \n \n \n ET121995\n 0919\n Longbob Longsen\n 1\n \n \n \n \n \n \n \n 7\n \n \n 1\n \n Store Purchase\n \n \n \n \n false\n true\n 00\n Transaction Normal\n 100\n Approved\n \n 001157\n 4\n M\n 1196543\n \n USD\n \n false\n Spreedly DEMO0095\n 123 Testing\n Durham\n North Carolina\n United States\n 27701\n \n \n Visa\n \n \n \n \n false\n ========== TRANSACTION RECORD ==========\nSpreedly DEMO0095\n123 Testing\nDurham, NC 27701\nUnited States\n\n\nTYPE: Purchase\n\nACCT: Visa $ 1.00 USD\n\nCARDHOLDER NAME : Longbob Longsen\nCARD NUMBER : ############4242\nDATE/TIME : 03 Jul 18 03:35:34\nREFERENCE # : 03 001157 M\nAUTHOR. # : ET121995\nTRANS. REF. : 1\n\n Approved - Thank You 100\n\n\nPlease retain this copy for your records.\n\nCardholder will pay above amount to\ncard issuer pursuant to cardholder\nagreement.\n========================================\n \n
\n 456 My Street\n Apt 1\n Ottawa\n ON\n K1C2N6\n CA\n
\n 123\n \n
\n" + read 2944 bytes + Conn close + PRE_SCRUBBED + end + + def post_scrub + <<-POST_SCRUBBED + opening connection to api.demo.globalgatewaye4.firstdata.com:443... + opened + starting SSL for api.demo.globalgatewaye4.firstdata.com:443... + SSL established + <- "POST /transaction/v27 HTTP/1.1\r\nContent-Type: application/xml\r\nX-Gge4-Date: 2018-07-03T07:35:34Z\r\nX-Gge4-Content-Sha1: 5335f81daf59c493fe5d4c18910d17eba69558d4\r\nAuthorization: GGE4_API 397439:iwaxRr8f3GQIMSucb+dmDeiwoAk=\r\nAccepts: application/xml\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.demo.globalgatewaye4.firstdata.com\r\nContent-Length: 807\r\n\r\n" + <- "SD8821-67[FILTERED]001.00USD[FILTERED]0919Longbob LongsenVisa071[FILTERED]
456 My StreetApt 1OttawaONK1C2N6CA
1Store Purchase
" + -> "HTTP/1.1 201 Created\r\n" + -> "Server: nginx\r\n" + -> "Date: Tue, 03 Jul 2018 07:35:34 GMT\r\n" + -> "Content-Type: application/xml; charset=utf-8\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "X-Frame-Options: SAMEORIGIN\r\n" + -> "X-XSS-Protection: 1; mode=block\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "X-GGE4-Date: 2018-07-03T07:35:34Z\r\n" + -> "X-GGE4-CONTENT-SHA1: 87ae8c40ae5afbfd060c7569645f3f6b4045994f\r\n" + -> "Authorization: GGE4_API 397439:dPOI+d2MkNJjBJhHTYUo0ieILw4=\r\n" + -> "Pragma: no-cache\r\n" + -> "Expires: Tue, 03 Jul 2018 06:35:34 GMT\r\n" + -> "Cache-Control: no-store, no-cache\r\n" + -> "X-Request-Id: 0f9ba7k2rd80a2tpch40\r\n" + -> "Location: https://api.demo.globalgatewaye4.firstdata.com/transaction/v27/2264726018\r\n" + -> "Status: 201 Created\r\n" + -> "Strict-Transport-Security: max-age=31536000; includeSubDomains\r\n" + -> "Strict-Transport-Security: max-age=315360000; includeSubdomains\r\n" + -> "\r\n" + -> "2da\r\n" + reading 2944 bytes... + -> "\n\n SD8821-67\n \n 00\n 1.0\n \n [FILTERED]\n 2264726018\n \n \n \n \n ET121995\n 0919\n Longbob Longsen\n 1\n \n \n \n \n \n \n \n 7\n \n \n 1\n \n Store Purchase\n \n \n \n \n false\n true\n 00\n Transaction Normal\n 100\n Approved\n \n 001157\n 4\n M\n 1196543\n \n USD\n \n false\n Spreedly DEMO0095\n 123 Testing\n Durham\n North Carolina\n United States\n 27701\n \n \n Visa\n \n \n \n \n false\n ========== TRANSACTION RECORD ==========\nSpreedly DEMO0095\n123 Testing\nDurham, NC 27701\nUnited States\n\n\nTYPE: Purchase\n\nACCT: Visa $ 1.00 USD\n\nCARDHOLDER NAME : Longbob Longsen\nCARD NUMBER : [FILTERED]\nDATE/TIME : 03 Jul 18 03:35:34\nREFERENCE # : 03 001157 M\nAUTHOR. # : ET121995\nTRANS. REF. : 1\n\n Approved - Thank You 100\n\n\nPlease retain this copy for your records.\n\nCardholder will pay above amount to\ncard issuer pursuant to cardholder\nagreement.\n========================================\n \n
\n 456 My Street\n Apt 1\n Ottawa\n ON\n K1C2N6\n CA\n
\n [FILTERED]\n \n
\n" + read 2944 bytes + Conn close + POST_SCRUBBED + end + + def successful_purchase_response + <<~RESPONSE + + + AD1234-56 + + 00 + 47.38 + + ############1111 + 106625152 + + + + ET1700 + 0913 + Fred Burfle + 0 + + + + + + + + + + + + 77 + + + + 1.1.1.10 + + false + true + 00 + Transaction Normal + 100 + Approved + + 000040 + U + M + 3146117 + + USD + + false + Friendly Inc DEMO0983 + 123 King St + Toronto + Ontario + Canada + L7Z 3K8 + + 8938737759041111 + =========== TRANSACTION RECORD ========== + Friendly Inc DEMO0983 + 123 King St + Toronto, ON L7Z 3K8 + Canada + + + TYPE: Purchase + + ACCT: Visa $ 47.38 USD + + CARD NUMBER : ############1111 + DATE/TIME : 28 Sep 12 07:54:48 + REFERENCE # : 000040 M + AUTHOR. # : ET120454 + TRANS. REF. : 77 + + Approved - Thank You 100 + + + Please retain this copy for your records. + + Cardholder will pay above amount to card + issuer pursuant to cardholder agreement. + ========================================= +
+ 456 My Street + Apt 1 + Ottawa + ON + K1C2N6 + CA +
+
+ RESPONSE + end + + def successful_purchase_response_with_stored_credentials + <<~RESPONSE + + + AD1234-56 + + 00 + 47.38 + + ############1111 + 106625152 + + + + ET1700 + 0913 + Fred Burfle + 0 + + + + + + + + + + + + 77 + + + + 1.1.1.10 + + false + true + 00 + Transaction Normal + 100 + Approved + + 000040 + U + M + 3146117 + + USD + + false + Friendly Inc DEMO0983 + 123 King St + Toronto + Ontario + Canada + L7Z 3K8 + + 8938737759041111 + =========== TRANSACTION RECORD ========== + Friendly Inc DEMO0983 + 123 King St + Toronto, ON L7Z 3K8 + Canada + + + TYPE: Purchase + + ACCT: Visa $ 47.38 USD + + CARD NUMBER : ############1111 + DATE/TIME : 28 Sep 12 07:54:48 + REFERENCE # : 000040 M + AUTHOR. # : ET120454 + TRANS. REF. : 77 + + Approved - Thank You 100 + + + Please retain this copy for your records. + + Cardholder will pay above amount to card + issuer pursuant to cardholder agreement. + ========================================= +
+ 456 My Street + Apt 1 + Ottawa + ON + K1C2N6 + CA +
+ + 1 + U + 732602247202501 + +
+ RESPONSE + end + + def successful_purchase_response_without_transarmor + <<~RESPONSE + + + AD1234-56 + + 00 + 47.38 + + ############1111 + 106625152 + + + + ET1700 + 0913 + Fred Burfle + 0 + + + + + + + + + + + + 77 + + + + 1.1.1.10 + + false + true + 00 + Transaction Normal + 100 + Approved + + 000040 + U + M + 3146117 + + USD + + false + Friendly Inc DEMO0983 + 123 King St + Toronto + Ontario + Canada + L7Z 3K8 + + + =========== TRANSACTION RECORD ========== + Friendly Inc DEMO0983 + 123 King St + Toronto, ON L7Z 3K8 + Canada + + + TYPE: Purchase + + ACCT: Visa $ 47.38 USD + + CARD NUMBER : ############1111 + DATE/TIME : 28 Sep 12 07:54:48 + REFERENCE # : 000040 M + AUTHOR. # : ET120454 + TRANS. REF. : 77 + + Approved - Thank You 100 + + + Please retain this copy for your records. + + Cardholder will pay above amount to card + issuer pursuant to cardholder agreement. + ========================================= + + RESPONSE + end + + def successful_refund_response + <<~RESPONSE + + + AD1234-56 + + 34 + 123 + + ############1111 + 888 + + + + ET112216 + 0913 + Fred Burfle + 0 + + + + + + + + + + + + + + + + 1.1.1.10 + + false + true + 00 + Transaction Normal + 100 + Approved + + 000041 + + I + 9176784 + + USD + + false + Friendly Inc DEMO0983 + 123 King St + Toronto + Ontario + Canada + L7Z 3K8 + + =========== TRANSACTION RECORD ========== + Friendly Inc DEMO0983 + 123 King St + Toronto, ON L7Z 3K8 + Canada + + + TYPE: Refund + + ACCT: Visa $ 23.69 USD + + CARD NUMBER : ############1111 + DATE/TIME : 28 Sep 12 08:31:23 + REFERENCE # : 000041 M + AUTHOR. # : ET112216 + TRANS. REF. : + + Approved - Thank You 100 + + + Please retain this copy for your records. + + ========================================= + + RESPONSE + end + + def failed_purchase_response + <<~RESPONSE + + + AD1234-56 + + 00 + 5013.0 + + ############1111 + 555555 + + + + + 0911 + Fred Burfle + 0 + + + + + + + + + + + + 77 + + + + 1.1.1.10 + + + 0 + + false + false + 00 + Transaction Normal + 605 + Invalid Expiration Date + + 000033 + + + + + USD + + false + Friendly Inc DEMO0983 + 123 King St + Toronto + Ontario + Canada + L7Z 3K8 + + =========== TRANSACTION RECORD ========== + Friendly Inc DEMO0983 + 123 King St + Toronto, ON L7Z 3K8 + Canada + + + TYPE: Purchase + ACCT: Visa $ 5,013.00 USD + CARD NUMBER : ############1111 + DATE/TIME : 25 Sep 12 07:27:00 + REFERENCE # : 000033 M + AUTHOR. # : + TRANS. REF. : 77 + Transaction not approved 605 + Please retain this copy for your records. + ========================================= + + RESPONSE + end + + def successful_verify_response + <<~RESPONSE + + + AD2552-05 + + 05 + 0.0 + + ############4242 + 25101911 + + + + ET184931 + 0915 + Longbob Longsen + 0 + + + + + + + + + + + + 1 + + Store Purchase + + 75.182.123.244 + + false + true + 00 + Transaction Normal + 100 + Approved + + 000040 + 1 + M + 7228838 + + USD + + false + FriendlyInc + 123 Main Street + Durham + North Carolina + United States + 27592 + + + Visa + + + + + false + =========== TRANSACTION RECORD ========== + FriendlyInc DEMO0 + 123 Main Street + Durham, NC 27592 + United States + + + TYPE: Auth Only + + ACCT: Visa $ 0.00 USD + + CARDHOLDER NAME : Longbob Longsen + CARD NUMBER : ############4242 + DATE/TIME : 04 Jul 14 14:21:52 + REFERENCE # : 000040 M + AUTHOR. # : ET184931 + TRANS. REF. : 1 + + Approved - Thank You 100 + + + Please retain this copy for your records. + + Cardholder will pay above amount to card + issuer pursuant to cardholder agreement. + ========================================= + + RESPONSE + end + + def no_transaction_response + yamlexcep = <<~RESPONSE + --- !ruby/exception:ActiveMerchant::ResponseError + message: Failed with 400 Bad Request + message: + response: !ruby/object:Net::HTTPBadRequest + body: "Malformed request: Transaction Type is missing." + body_exist: true + code: "400" + header: + connection: + - Close + content-type: + - text/html; charset=utf-8 + server: + - Apache + date: + - Fri, 28 Sep 2012 18:21:37 GMT + content-length: + - "47" + status: + - "400" + cache-control: + - no-cache + http_version: "1.1" + message: Bad Request + read: true + socket: + RESPONSE + YAML.safe_load(yamlexcep, permitted_classes: ['Net::HTTPBadRequest', 'ActiveMerchant::ResponseError']) + end + + def bad_credentials_response + yamlexcep = <<~RESPONSE + --- !ruby/exception:ActiveMerchant::ResponseError + message: + response: !ruby/object:Net::HTTPUnauthorized + code: '401' + message: Authorization Required + body: Unauthorized Request. Bad or missing credentials. + read: true + header: + cache-control: + - no-cache + content-type: + - text/html; charset=utf-8 + date: + - Tue, 30 Dec 2014 23:28:32 GMT + server: + - Apache + status: + - '401' + x-rack-cache: + - invalidate, pass + x-request-id: + - 4157e21cc5620a95ead8d2025b55bdf4 + x-ua-compatible: + - IE=Edge,chrome=1 + content-length: + - '49' + connection: + - Close + body_exist: true + http_version: '1.1' + socket: + RESPONSE + YAML.safe_load(yamlexcep, permitted_classes: ['Net::HTTPUnauthorized', 'ActiveMerchant::ResponseError']) + end + + def successful_void_response + <<~RESPONSE + + + AD1234-56 + + 33 + 11.45 + + ############1111 + 987123 + + + + ET112112 + 0913 + Fred Burfle + 0 + + + + + + + + + + + + + + + + 1.1.1.10 + + + 0 + + false + true + 00 + Transaction Normal + 100 + Approved + + 000166 + + I + 2046743 + + USD + + false + FreshBooks DEMO0785 + 35 Golden Ave + Toronto + Ontario + Canada + M6R 2J5 + + =========== TRANSACTION RECORD ========== + FreshBooks DEMO0785 + 35 Golden Ave + Toronto, ON M6R 2J5 + Canada + + + TYPE: Void + + ACCT: Visa $ 47.38 USD + + CARD NUMBER : ############1111 + DATE/TIME : 15 Nov 12 08:20:36 + REFERENCE # : 000166 M + AUTHOR. # : ET112112 + TRANS. REF. : + + Approved - Thank You 100 + + + Please retain this copy for your records. + + Cardholder will pay above amount to card + issuer pursuant to cardholder agreement. + ========================================= + + RESPONSE + end +end diff --git a/test/unit/gateways/flex_charge_test.rb b/test/unit/gateways/flex_charge_test.rb new file mode 100644 index 00000000000..e5ce5d2683d --- /dev/null +++ b/test/unit/gateways/flex_charge_test.rb @@ -0,0 +1,673 @@ +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'), + shipping_address: address.merge(name: 'Jhon Doe', country: 'US'), + sense_key: 'abc123', + extra_data: { hello: 'world' }.to_json + } + + @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_valid_homepage_url + assert @gateway.homepage_url.present? + assert_equal 'https://www.flexfactor.io/', @gateway.homepage_url + 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['senseKey'], 'abc123' + assert_equal request['Source'], 'Spreedly' + assert_equal request['ExtraData'], { hello: 'world' }.to_json + 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['transactionType'], 'Purchase' + assert_equal request['payer']['email'], @options[:email] + assert_equal request['description'], @options[:description] + + assert_equal request['billingInformation']['firstName'], 'Cure' + assert_equal request['billingInformation']['country'], 'CA' + assert_equal request['shippingInformation']['firstName'], 'Jhon' + assert_equal request['shippingInformation']['country'], 'US' + end + end.respond_with(successful_access_token_response, successful_purchase_response) + + assert_success response + + assert_equal 'ca7bb327-a750-412d-a9c3-050d72b3f0c5#USD', response.authorization + assert response.test? + end + + def test_successful_authorization + 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['transactionType'], 'Authorization' if /evaluate/.match?(endpoint) + end.respond_with(successful_access_token_response, successful_purchase_response) + 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#USD', 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_purchase_using_card_with_no_number + credit_card_with_no_number = credit_card + credit_card_with_no_number.number = nil + + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, credit_card_with_no_number, @options) + end.respond_with(successful_access_token_response, successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_token + payment = 'bb114473-43fc-46c4-9082-ea3dfb490509' + + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, payment, @options) + end.respond_with(successful_access_token_response, successful_purchase_response) + + assert_success response + 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_failed_purchase_idempotency_key + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.respond_with(successful_access_token_response, missed_idempotency_key_field) + + assert_failure response + assert_nil response.error_code + assert_equal '{"IdempotencyKey":["The IdempotencyKey field is required."]}', response.message + end + + def test_failed_purchase_expiry_date + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.respond_with(successful_access_token_response, invalid_expiry_date_utc) + + assert_failure response + assert_nil response.error_code + assert_equal '{"ExpiryDateUtc":["The field ExpiryDateUtc is invalid."]}', response.message + 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(:names_from_address, @options[:billing_address], @credit_card) + + assert_equal 'Cure', names.first + assert_equal 'Tester', names.last + end + + def test_address_names_from_credit_card + @options.delete(:billing_address) + names = @gateway.send(:names_from_address, {}, @credit_card) + + assert_equal 'Longbob', names.first + assert_equal 'Longsen', names.last + end + + def test_address_names_when_passing_string_token + names = @gateway.send(:names_from_address, @options[:billing_address], SecureRandom.uuid) + + assert_equal 'Cure', names.first + assert_equal 'Tester', 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 + + def test_address_when_billing_address_provided + address = @gateway.send(:address, @options) + assert_equal 'CA', address[:country] + end + + def test_address_when_address_is_provided_in_options + @options.delete(:billing_address) + @options[:address] = { country: 'US' } + address = @gateway.send(:address, @options) + assert_equal 'US', address[:country] + end + + def test_authorization_from_on_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_authorization_from_on_purchase + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.respond_with(successful_access_token_response, successful_purchase_response) + + assert_success response + assert_equal 'ca7bb327-a750-412d-a9c3-050d72b3f0c5#USD', response.authorization + end + + def test_add_base_data_without_idempotency_key + @options.delete(:idempotency_key) + post = {} + @gateway.send(:add_base_data, post, @options) + + assert_equal 5, post[:idempotencyKey].split('-').size + 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 + + def missed_idempotency_key_field + <<~RESPONSE + { + "TraceId": ["00-bf5a1XXXTRACEXXX174b8a-f58XXXIDXXX32-01"], + "IdempotencyKey": ["The IdempotencyKey field is required."], + "access_token": "SomeAccessTokenXXXX1ZWE5ZmY0LTM4MjUtNDc0ZC04ZDhhLTk2OGZjM2NlYTA5ZCIsImlhdCI6IjE3MjI1Mjc1ODI1MjIiLCJhdWQiOlsicGF5bWVudHMiLCJvcmRlcnMiLCJtZXJjaGFudHMiLCJlbGlnaWJpbGl0eS1zZnRwIiwiZWxpZ2liaWxpdHkiLCJjb250YWN0Il0sImN1c3RvbTptaWQiOiJkOWQwYjVmZC05NDMzLTQ0ZDMtODA1MS02M2ZlZTI4NzY4ZTgiLCJuYmYiOjE3MjI1Mjc1ODIsImV4cCI6MTcyMjUyODE4MiwiaXNzIjoiQXBpLUNsaWVudC1TZXJ2aWNlIn0.Q7b5CViX4x3Qmna-JmLS2pQD8kWbrI5-GLLT1Ki9t3o", + "token_expires": 1722528182522 + } + RESPONSE + end + + def invalid_expiry_date_utc + <<~RESPONSE + { + "TraceId": ["00-bf5a1XXXTRACEXXX174b8a-f58XXXIDXXX32-01"], + "ExpiryDateUtc":["The field ExpiryDateUtc is invalid."], + "access_token": "SomeAccessTokenXXXX1ZWE5ZmY0LTM4MjUtNDc0ZC04ZDhhLTk2OGZjM2NlYTA5ZCIsImlhdCI6IjE3MjI1Mjc1ODI1MjIiLCJhdWQiOlsicGF5bWVudHMiLCJvcmRlcnMiLCJtZXJjaGFudHMiLCJlbGlnaWJpbGl0eS1zZnRwIiwiZWxpZ2liaWxpdHkiLCJjb250YWN0Il0sImN1c3RvbTptaWQiOiJkOWQwYjVmZC05NDMzLTQ0ZDMtODA1MS02M2ZlZTI4NzY4ZTgiLCJuYmYiOjE3MjI1Mjc1ODIsImV4cCI6MTcyMjUyODE4MiwiaXNzIjoiQXBpLUNsaWVudC1TZXJ2aWNlIn0.Q7b5CViX4x3Qmna-JmLS2pQD8kWbrI5-GLLT1Ki9t3o", + "token_expires": 1722528182522 + } + RESPONSE + end +end diff --git a/test/unit/gateways/flo2cash_simple_test.rb b/test/unit/gateways/flo2cash_simple_test.rb index 627f6782e29..d6d2b580d6c 100644 --- a/test/unit/gateways/flo2cash_simple_test.rb +++ b/test/unit/gateways/flo2cash_simple_test.rb @@ -7,9 +7,9 @@ def setup Base.mode = :test @gateway = Flo2cashSimpleGateway.new( - :username => 'username', - :password => 'password', - :account_id => 'account_id' + username: 'username', + password: 'password', + account_id: 'account_id' ) @credit_card = credit_card @@ -19,7 +19,7 @@ def setup def test_successful_purchase response = stub_comms do @gateway.purchase(@amount, @credit_card, order_id: 'boom') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(%r{boom}, data) end.respond_with(successful_purchase_response) @@ -50,7 +50,7 @@ def test_successful_refund refund = stub_comms do @gateway.refund(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/P150200005007600/, data) end.respond_with(successful_refund_response) @@ -67,7 +67,7 @@ def test_empty_response_fails end def test_transcript_scrubbing - transcript = @gateway.scrub(successful_purchase_response) + transcript = @gateway.scrub(successful_purchase_response) assert_scrubbed(@credit_card.number, transcript) assert_scrubbed(@credit_card.verification_value, transcript) diff --git a/test/unit/gateways/flo2cash_test.rb b/test/unit/gateways/flo2cash_test.rb index 4564114db64..664a329739c 100644 --- a/test/unit/gateways/flo2cash_test.rb +++ b/test/unit/gateways/flo2cash_test.rb @@ -7,9 +7,9 @@ def setup Base.mode = :test @gateway = Flo2cashGateway.new( - :username => 'username', - :password => 'password', - :account_id => 'account_id' + username: 'username', + password: 'password', + account_id: 'account_id' ) @credit_card = credit_card @@ -19,7 +19,7 @@ def setup def test_successful_purchase response = stub_comms do @gateway.purchase(@amount, @credit_card, order_id: 'boom') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(%r{boom}, data) end.respond_with(successful_authorize_response, successful_capture_response) @@ -50,7 +50,7 @@ def test_successful_authorize_and_capture capture = stub_comms do @gateway.capture(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/P150100005006789/, data) end.respond_with(successful_capture_response) @@ -78,7 +78,7 @@ def test_successful_refund refund = stub_comms do @gateway.refund(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/P150100005006789/, data) end.respond_with(successful_refund_response) @@ -95,7 +95,7 @@ def test_empty_response_fails end def test_transcript_scrubbing - transcript = @gateway.scrub(successful_authorize_response) + transcript = @gateway.scrub(successful_authorize_response) assert_scrubbed(@credit_card.number, transcript) assert_scrubbed(@credit_card.verification_value, transcript) diff --git a/test/unit/gateways/forte_test.rb b/test/unit/gateways/forte_test.rb index 124d2b3a17f..fb448d46abb 100644 --- a/test/unit/gateways/forte_test.rb +++ b/test/unit/gateways/forte_test.rb @@ -28,7 +28,7 @@ def test_successful_purchase def test_purchase_passes_options options = { order_id: '1' } - @gateway.expects(:commit).with(anything, has_entries(:order_number => '1')) + @gateway.expects(:commit).with(anything, has_entries(order_number: '1')) stub_comms(@gateway, :raw_ssl_request) do @gateway.purchase(@amount, @credit_card, options) @@ -61,6 +61,26 @@ def test_failed_purchase_with_echeck assert_equal 'INVALID CREDIT CARD NUMBER', response.message end + def test_successful_purchase_with_service_fee + response = stub_comms(@gateway, :raw_ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.respond_with(MockedResponse.new(successful_purchase_with_service_fee_response)) + assert_success response + + assert_equal '.5', response.params['service_fee_amount'] + assert response.test? + end + + def test_successful_purchase_with_xdata + response = stub_comms(@gateway, :raw_ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.respond_with(MockedResponse.new(successful_purchase_with_xdata_response)) + + assert_success response + (1..9).each { |n| assert_equal 'some customer metadata', response.params['xdata']["xdata_#{n}"] } + assert response.test? + end + def test_successful_authorize response = stub_comms(@gateway, :raw_ssl_request) do @gateway.authorize(@amount, @credit_card, @options) @@ -157,7 +177,7 @@ def test_handles_improper_padding @gateway = ForteGateway.new(location_id: ' improperly-padded ', account_id: ' account_id ', api_key: 'api_key', secret: 'secret') response = stub_comms(@gateway, :raw_ssl_request) do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |type, url, parameters, headers| + end.check_request do |_type, url, _parameters, _headers| URI.parse(url) end.respond_with(MockedResponse.new(successful_purchase_response)) assert_success response @@ -171,7 +191,8 @@ def test_scrub private class MockedResponse - attr :code, :body + attr_reader :code, :body + def initialize(body, code = 200) @code = code @body = body @@ -193,7 +214,7 @@ def post_scrubbed end def successful_purchase_response - %q( + ' { "transaction_id":"trn_bb7687a7-3d3a-40c2-8fa9-90727a814249", "account_id":"act_300111", @@ -227,11 +248,11 @@ def successful_purchase_response "settlements":"https://sandbox.forte.net/API/v2/transactions/trn_bb7687a7-3d3a-40c2-8fa9-90727a814249/settlements" } } - ) + ' end def failed_purchase_response - %q( + ' { "transaction_id":"trn_e9ea64c4-5c2c-43dd-9138-f2661b59947c", "account_id":"act_300111", @@ -261,11 +282,11 @@ def failed_purchase_response "settlements":"https://sandbox.forte.net/API/v2/transactions/trn_e9ea64c4-5c2c-43dd-9138-f2661b59947c/settlements" } } - ) + ' end def successful_echeck_purchase_response - %q( + ' { "transaction_id":"trn_bb7687a7-3d3a-40c2-8fa9-90727a814249", "account_id":"act_300111", @@ -306,11 +327,11 @@ def successful_echeck_purchase_response "settlements":"https://sandbox.forte.net/API/v2/transactions/trn_bb7687a7-3d3a-40c2-8fa9-90727a814249/settlements" } } - ) + ' end def failed_echeck_purchase_response - %q( + ' { "transaction_id":"trn_bb7687a7-3d3a-40c2-8fa9-90727a814249", "account_id":"act_300111", @@ -348,11 +369,102 @@ def failed_echeck_purchase_response "settlements":"https://sandbox.forte.net/API/v2/transactions/trn_bb7687a7-3d3a-40c2-8fa9-90727a814249/settlements" } } - ) + ' + end + + def successful_purchase_with_service_fee_response + ' + { + "transaction_id":"trn_bb7687a7-3d3a-40c2-8fa9-90727a814249", + "account_id":"act_300111", + "location_id":"loc_176008", + "action":"sale", + "authorization_amount": 1.0, + "service_fee_amount": ".5", + "subtotal_amount": ".5", + "authorization_code":"123456", + "billing_address":{ + "first_name":"Jim", + "last_name":"Smith" + }, + "card": { + "name_on_card":"Longbob Longsen", + "masked_account_number":"****2224", + "expire_month":9, + "expire_year":2016, + "card_verification_value":"***", + "card_type":"visa" + }, + "response": { + "authorization_code":"123456", + "avs_result":"Y", + "cvv_code":"M", + "environment":"sandbox", + "response_type":"A", + "response_code":"A01", + "response_desc":"TEST APPROVAL" + }, + "links": { + "self":"https://sandbox.forte.net/API/v2/transactions/trn_bb7687a7-3d3a-40c2-8fa9-90727a814249", + "settlements":"https://sandbox.forte.net/API/v2/transactions/trn_bb7687a7-3d3a-40c2-8fa9-90727a814249/settlements" + } + } + ' + end + + def successful_purchase_with_xdata_response + ' + { + "transaction_id":"trn_bb7687a7-3d3a-40c2-8fa9-90727a814249", + "account_id":"act_300111", + "location_id":"loc_176008", + "action":"sale", + "authorization_amount": 1.0, + "service_fee_amount": ".5", + "subtotal_amount": ".5", + "authorization_code":"123456", + "billing_address":{ + "first_name":"Jim", + "last_name":"Smith" + }, + "xdata": { + "xdata_1": "some customer metadata", + "xdata_2": "some customer metadata", + "xdata_3": "some customer metadata", + "xdata_4": "some customer metadata", + "xdata_5": "some customer metadata", + "xdata_6": "some customer metadata", + "xdata_7": "some customer metadata", + "xdata_8": "some customer metadata", + "xdata_9": "some customer metadata" + }, + "card": { + "name_on_card":"Longbob Longsen", + "masked_account_number":"****2224", + "expire_month":9, + "expire_year":2016, + "card_verification_value":"***", + "card_type":"visa" + }, + "response": { + "authorization_code":"123456", + "avs_result":"Y", + "cvv_code":"M", + "environment":"sandbox", + "response_type":"A", + "response_code":"A01", + "response_desc":"TEST APPROVAL" + }, + "links": { + "self":"https://sandbox.forte.net/API/v2/transactions/trn_bb7687a7-3d3a-40c2-8fa9-90727a814249", + "settlements":"https://sandbox.forte.net/API/v2/transactions/trn_bb7687a7-3d3a-40c2-8fa9-90727a814249/settlements" + } + } + ' end def successful_authorize_response - %q( + ' { "transaction_id":"trn_527fdc8a-d3d0-4680-badc-bfa784c63c13", "account_id":"act_300111", @@ -386,11 +498,11 @@ def successful_authorize_response "settlements":"https://sandbox.forte.net/API/v2/transactions/trn_527fdc8a-d3d0-4680-badc-bfa784c63c13/settlements" } } - ) + ' end def failed_authorize_response - %q( + ' { "transaction_id":"trn_7c045645-98b3-4c8a-88d6-e8d686884564", "account_id":"act_300111", @@ -420,11 +532,11 @@ def failed_authorize_response "settlements":"https://sandbox.forte.net/API/v2/transactions/trn_7c045645-98b3-4c8a-88d6-e8d686884564/settlements" } } - ) + ' end def successful_capture_response - %q( + ' { "transaction_id":"trn_94a04a97-c847-4420-820b-fb153a1f0f64", "account_id":"act_300111", @@ -444,11 +556,11 @@ def successful_capture_response "settlements":"https://sandbox.forte.net/API/v2/transactions/trn_94a04a97-c847-4420-820b-fb153a1f0f64/settlements" } } - ) + ' end def failed_capture_response - %q( + ' { "account_id":"act_300111", "location_id":"loc_176008", @@ -459,11 +571,11 @@ def failed_capture_response "response_desc":"The field transaction_id is required." } } - ) + ' end def successful_credit_response - %q( + ' { "transaction_id":"trn_357b284e-1dde-42ba-b0a5-5f66e08c7d9f", "account_id":"act_300111", @@ -497,11 +609,11 @@ def successful_credit_response "settlements":"https://sandbox.forte.net/API/v2/transactions/trn_357b284e-1dde-42ba-b0a5-5f66e08c7d9f/settlements" } } - ) + ' end def failed_credit_response - %q( + ' { "transaction_id":"trn_ce70ce9a-6265-4892-9a83-5825cb869ed5", "account_id":"act_300111", @@ -523,11 +635,11 @@ def failed_credit_response "settlements":"https://sandbox.forte.net/API/v2/transactions/trn_ce70ce9a-6265-4892-9a83-5825cb869ed5/settlements" } } - ) + ' end def successful_void_response - %q( + ' { "transaction_id":"trn_6c9d049e-1971-45fb-a4da-a0c35c4ed274", "account_id":"act_300111", @@ -546,11 +658,11 @@ def successful_void_response "settlements":"https://sandbox.forte.net/API/v2/transactions/trn_6c9d049e-1971-45fb-a4da-a0c35c4ed274/settlements" } } - ) + ' end def failed_void_response - %q( + ' { "account_id":"act_300111", "location_id":"loc_176008", @@ -561,7 +673,7 @@ def failed_void_response "response_desc":"The field transaction_id is required." } } - ) + ' end def successful_refund_response diff --git a/test/unit/gateways/fortis_test.rb b/test/unit/gateways/fortis_test.rb new file mode 100644 index 00000000000..38d1634a7b5 --- /dev/null +++ b/test/unit/gateways/fortis_test.rb @@ -0,0 +1,293 @@ +require 'test_helper' + +class FortisTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = FortisGateway.new(user_id: 'abc', user_api_key: 'def', developer_id: 'ghi', location_id: 'jkl') + @credit_card = credit_card + @amount = 100 + + @options = { + order_id: '1', + billing_address: address, + description: 'Store Purchase' + } + end + + def test_raises_error_without_required_options + assert_raises(ArgumentError) { FortisGateway.new } + assert_raises(ArgumentError) { FortisGateway.new(user_id: 'abc') } + assert_raises(ArgumentError) { FortisGateway.new(user_id: 'abc', user_api_key: 'def') } + assert_nothing_raised { FortisGateway.new(user_id: 'abc', user_api_key: 'def', developer_id: 'ghi') } + end + + def test_parse_valid_json + body = '{"key": "value"}' + expected_result = { 'key' => 'value' }.with_indifferent_access + result = @gateway.send(:parse, body) + assert_equal expected_result, result + end + + def test_parse_invalid_json + body = 'invalid json' + result = @gateway.send(:parse, body) + assert_equal 'Unable to parse JSON response', result[:status] + assert_equal body, result[:errors] + assert result[:message].include?('unexpected token') + end + + def test_parse_empty_json + body = '' + result = @gateway.send(:parse, body) + assert_equal 'Unable to parse JSON response', result[:status] + assert_equal body, result[:errors] + assert result[:message].include?('unexpected token') + end + + def test_request_headers + expected = { + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + 'user-id' => 'abc', + 'user-api-key' => 'def', + 'developer-id' => 'ghi' + } + assert_equal expected, @gateway.send(:request_headers) + end + + def test_url_for_test_environment + @gateway.stubs(:test?).returns(true) + assert_equal 'https://api.sandbox.fortis.tech/v1/some_action', @gateway.send(:url, '/some_action') + end + + def test_url_for_live_environment + @gateway.stubs(:test?).returns(false) + assert_equal 'https://api.fortis.tech/v1/some_action', @gateway.send(:url, '/some_action') + end + + def test_success_from + assert @gateway.send(:success_from, 200, { data: { status_code: 101 } }) + refute @gateway.send(:success_from, 200, { data: { status_code: 301 } }) + refute @gateway.send(:success_from, 200, { data: { status_code: 999 } }) + end + + def test_message_from + assert_equal 'Transaction Approved', @gateway.send(:message_from, { data: { verbiaje: 'Transaction Approved' } }) + assert_equal 'CC - Approved / ACH - Accepted', @gateway.send(:message_from, { data: { reason_code_id: 1000 } }) + assert_equal 'Sale cc Approved', @gateway.send(:message_from, { data: { status_code: 101 } }) + assert_equal 'Reserved for Future Fraud Reason Codes', @gateway.send(:message_from, { data: { reason_code_id: 1302 } }) + assert_equal 999, @gateway.send(:message_from, { data: { status_code: 999 } }) + end + + def test_get_reason_description_from + assert_equal 'CC - Approved / ACH - Accepted', @gateway.send(:get_reason_description_from, { data: { reason_code_id: 1000 } }) + assert_equal 'Reserved for Future Fraud Reason Codes', @gateway.send(:get_reason_description_from, { data: { reason_code_id: 1302 } }) + assert_nil @gateway.send(:get_reason_description_from, { data: { reason_code_id: 9999 } }) + end + + def test_authorization_from + assert_equal '31efa3732483237895c9a23d|txn', @gateway.send(:authorization_from, { data: { id: '31efa3732483237895c9a23d' } }, false) + assert_equal '31efa3732483237895c9a23d|token', @gateway.send(:authorization_from, { data: { id: '31efa3732483237895c9a23d' } }, true) + assert_equal '|txn', @gateway.send(:authorization_from, { data: { id: nil } }, false) + assert_equal '|txn', @gateway.send(:authorization_from, { data: {} }, false) + assert_equal '|txn', @gateway.send(:authorization_from, {}, false) + end + + def test_successfully_build_an_authorize_request + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(699, @credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_equal '699', request['transaction_amount'] + assert_equal @options[:order_id], request['order_number'] + assert_equal @options[:order_id], request['transaction_api_id'] + assert_equal @credit_card.number, request['account_number'] + assert_equal @credit_card.month.to_s.rjust(2, '0') + @credit_card.year.to_s[-2..-1], request['exp_date'] + assert_equal @credit_card.verification_value, request['cvv'] + assert_equal @credit_card.name, request['account_holder_name'] + assert_equal @options[:billing_address][:address1], request['billing_address']['street'] + assert_equal @options[:billing_address][:city], request['billing_address']['city'] + assert_equal @options[:billing_address][:state], request['billing_address']['state'] + assert_equal @options[:billing_address][:zip], request['billing_address']['postal_code'] + assert_equal 'CAN', request['billing_address']['country'] + end.respond_with(successful_authorize_response) + end + + def test_on_purchase_point_to_the_sale_endpoint + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(699, @credit_card, @options) + end.check_request do |_method, endpoint, _data, _headers| + assert_match %r{sale}, endpoint + end.respond_with(successful_authorize_response) + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + def test_error_code_from_with_status_code_301 + response = { data: { status_code: 301, reason_code_id: 1622 } } + assert_equal '301 - 1622', @gateway.send(:error_code_from, 400, response) + end + + def test_error_code_from_with_status_code_101 + response = { data: { status_code: 101 } } + assert_nil @gateway.send(:error_code_from, 200, response) + end + + def test_error_code_from_with_status_code_500 + response = { data: { status_code: 500 } } + assert_equal '500', @gateway.send(:error_code_from, 500, response) + end + + def test_error_code_from_with_nil_status_code + response = { data: { status_code: nil } } + assert_nil @gateway.send(:error_code_from, 204, response) + end + + private + + def pre_scrubbed + <<~PRE + <- "POST /v1/transactions/cc/auth-only/keyed HTTP/1.1\r\ncontent-type: application/json\r\naccept: application/json\r\nuser-id: 11ef69fdc8fd8db2b07213de\r\nuser-api-key: 11ef9c5897f42ac2a072e521\r\ndeveloper-id: bEgKPZos\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.fortis.tech\r\ncontent-length: 154\r\n\r\n" + <- "{\"transaction_amount\":\"100\",\"order_number\":null,\"account_number\":\"5454545454545454\",\"exp_date\":\"0925\",\"cvv\":\"123\",\"account_holder_name\":\"Longbob Longsen\"}" + -> "HTTP/1.1 201 Created\r\n" + -> "Date: Fri, 15 Nov 2024 17:00:42 GMT\r\n" + -> "Content-Type: application/json\r\n" + -> "Content-Length: 2031\r\n" + -> "Connection: close\r\n" + -> "x-amzn-RequestId: dfd852a8-5c39-4558-9f05-8eaa14affac9\r\n" + -> "Access-Control-Allow-Origin: *\r\n" + -> "x-amz-apigw-id: BTCoUG6SIAMEsdw=\r\n" + -> "X-Amzn-Trace-Id: Root=1-67377e34-170c289b473cbb2e56aeab61;Parent=7e76c3e737bb48c3;Sampled=0;Lineage=1:ae593ade:0\r\n" + -> "Access-Control-Max-Age: 86400\r\n" + -> "Access-Control-Allow-Credentials: true\r\n" + -> "\r\n" + reading 2031 bytes... + -> "{\"type\":\"Transaction\",\"data\":{\"id\":\"31efa3732483237895c9a23d\",\"payment_method\":\"cc\",\"account_vault_id\":null,\"recurring_id\":null,\"first_six\":\"545454\",\"last_four\":\"5454\",\"account_holder_name\":\"Longbob Longsen\",\"transaction_amount\":100,\"description\":null,\"transaction_code\":null,\"avs\":null,\"batch\":null,\"verbiage\":\"Test 7957\",\"transaction_settlement_status\":null,\"effective_date\":null,\"return_date\":null,\"created_ts\":1731690040,\"modified_ts\":1731690040,\"transaction_api_id\":null,\"terms_agree\":null,\"notification_email_address\":null,\"notification_email_sent\":true,\"notification_phone\":null,\"response_message\":null,\"auth_amount\":100,\"auth_code\":\"a37325\",\"type_id\":20,\"location_id\":\"11ef69fdc684ae30b436c55b\",\"reason_code_id\":1000,\"contact_id\":null,\"product_transaction_id\":\"11ef69fdc6debc2cb1af505c\",\"tax\":0,\"customer_ip\":\"34.234.17.123\",\"customer_id\":null,\"po_number\":null,\"avs_enhanced\":\"V\",\"cvv_response\":\"N\",\"cavv_result\":null,\"clerk_number\":null,\"tip_amount\":0,\"created_user_id\":\"11ef69fdc8fd8db2b07213de\",\"modified_user_id\":\"11ef69fdc8fd8db2b07213de\",\"ach_identifier\":null,\"check_number\":null,\"recurring_flag\":\"no\",\"installment_counter\":null,\"installment_total\":null,\"settle_date\":null,\"charge_back_date\":null,\"void_date\":null,\"account_type\":\"mc\",\"is_recurring\":false,\"is_accountvault\":false,\"transaction_c1\":null,\"transaction_c2\":null,\"transaction_c3\":null,\"additional_amounts\":[],\"terminal_serial_number\":null,\"entry_mode_id\":\"K\",\"terminal_id\":null,\"quick_invoice_id\":null,\"ach_sec_code\":null,\"custom_data\":null,\"ebt_type\":null,\"voucher_number\":null,\"hosted_payment_page_id\":null,\"transaction_batch_id\":null,\"currency_code\":840,\"par\":\"ZZZZZZZZZZZZZZZZZZZZ545454545\",\"stan\":null,\"currency\":\"USD\",\"secondary_amount\":0,\"card_bin\":\"545454\",\"paylink_id\":null,\"emv_receipt_data\":null,\"status_code\":102,\"token_id\":null,\"wallet_type\":null,\"order_number\":\"963274518498\",\"routing_number\":null,\"trx_source_code\":12,\"billing_address\":{\"city\":null,\"state\":null,\"postal_code\":null,\"phone\":null,\"country\":null,\"street\":null},\"is_token\":false}}" + PRE + end + + def post_scrubbed + <<~PRE + <- "POST /v1/transactions/cc/auth-only/keyed HTTP/1.1\r\ncontent-type: application/json\r\naccept: application/json\r\nuser-id: [FILTERED]\r\nuser-api-key: [FILTERED]\r\ndeveloper-id: [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.fortis.tech\r\ncontent-length: 154\r\n\r\n" + <- "{\"transaction_amount\":\"100\",\"order_number\":null,\"account_number\":\"[FILTERED]\",\"exp_date\":\"0925\",\"cvv\":\"[FILTERED]\",\"account_holder_name\":\"Longbob Longsen\"}" + -> "HTTP/1.1 201 Created\r\n" + -> "Date: Fri, 15 Nov 2024 17:00:42 GMT\r\n" + -> "Content-Type: application/json\r\n" + -> "Content-Length: 2031\r\n" + -> "Connection: close\r\n" + -> "x-amzn-RequestId: dfd852a8-5c39-4558-9f05-8eaa14affac9\r\n" + -> "Access-Control-Allow-Origin: *\r\n" + -> "x-amz-apigw-id: BTCoUG6SIAMEsdw=\r\n" + -> "X-Amzn-Trace-Id: Root=1-67377e34-170c289b473cbb2e56aeab61;Parent=7e76c3e737bb48c3;Sampled=0;Lineage=1:ae593ade:0\r\n" + -> "Access-Control-Max-Age: 86400\r\n" + -> "Access-Control-Allow-Credentials: true\r\n" + -> "\r\n" + reading 2031 bytes... + -> "{\"type\":\"Transaction\",\"data\":{\"id\":\"31efa3732483237895c9a23d\",\"payment_method\":\"cc\",\"account_vault_id\":null,\"recurring_id\":null,\"first_six\":\"545454\",\"last_four\":\"5454\",\"account_holder_name\":\"Longbob Longsen\",\"transaction_amount\":100,\"description\":null,\"transaction_code\":null,\"avs\":null,\"batch\":null,\"verbiage\":\"Test 7957\",\"transaction_settlement_status\":null,\"effective_date\":null,\"return_date\":null,\"created_ts\":1731690040,\"modified_ts\":1731690040,\"transaction_api_id\":null,\"terms_agree\":null,\"notification_email_address\":null,\"notification_email_sent\":true,\"notification_phone\":null,\"response_message\":null,\"auth_amount\":100,\"auth_code\":\"a37325\",\"type_id\":20,\"location_id\":\"11ef69fdc684ae30b436c55b\",\"reason_code_id\":1000,\"contact_id\":null,\"product_transaction_id\":\"11ef69fdc6debc2cb1af505c\",\"tax\":0,\"customer_ip\":\"34.234.17.123\",\"customer_id\":null,\"po_number\":null,\"avs_enhanced\":\"V\",\"cvv_response\":\"N\",\"cavv_result\":null,\"clerk_number\":null,\"tip_amount\":0,\"created_user_id\":\"11ef69fdc8fd8db2b07213de\",\"modified_user_id\":\"11ef69fdc8fd8db2b07213de\",\"ach_identifier\":null,\"check_number\":null,\"recurring_flag\":\"no\",\"installment_counter\":null,\"installment_total\":null,\"settle_date\":null,\"charge_back_date\":null,\"void_date\":null,\"account_type\":\"mc\",\"is_recurring\":false,\"is_accountvault\":false,\"transaction_c1\":null,\"transaction_c2\":null,\"transaction_c3\":null,\"additional_amounts\":[],\"terminal_serial_number\":null,\"entry_mode_id\":\"K\",\"terminal_id\":null,\"quick_invoice_id\":null,\"ach_sec_code\":null,\"custom_data\":null,\"ebt_type\":null,\"voucher_number\":null,\"hosted_payment_page_id\":null,\"transaction_batch_id\":null,\"currency_code\":840,\"par\":\"ZZZZZZZZZZZZZZZZZZZZ545454545\",\"stan\":null,\"currency\":\"USD\",\"secondary_amount\":0,\"card_bin\":\"545454\",\"paylink_id\":null,\"emv_receipt_data\":null,\"status_code\":102,\"token_id\":null,\"wallet_type\":null,\"order_number\":\"963274518498\",\"routing_number\":null,\"trx_source_code\":12,\"billing_address\":{\"city\":null,\"state\":null,\"postal_code\":null,\"phone\":null,\"country\":null,\"street\":null},\"is_token\":false}}" + PRE + end + + def successful_authorize_response + <<-JSON + { + "type": "Transaction", + "data": { + "id": "31efa361a11da7588f260af5", + "payment_method": "cc", + "account_vault_id": null, + "recurring_id": null, + "first_six": "545454", + "last_four": "5454", + "account_holder_name": "smith", + "transaction_amount": 699, + "description": null, + "transaction_code": null, + "avs": null, + "batch": null, + "verbiage": "Test 4669", + "transaction_settlement_status": null, + "effective_date": null, + "return_date": null, + "created_ts": 1731682518, + "modified_ts": 1731682518, + "transaction_api_id": null, + "terms_agree": null, + "notification_email_address": null, + "notification_email_sent": true, + "notification_phone": null, + "response_message": null, + "auth_amount": 699, + "auth_code": "a361a2", + "type_id": 20, + "location_id": "11ef69fdc684ae30b436c55b", + "reason_code_id": 1000, + "contact_id": null, + "product_transaction_id": "11ef69fdc6debc2cb1af505c", + "tax": 0, + "customer_ip": "34.234.17.123", + "customer_id": null, + "po_number": null, + "avs_enhanced": "V", + "cvv_response": "N", + "cavv_result": null, + "clerk_number": null, + "tip_amount": 0, + "created_user_id": "11ef69fdc8fd8db2b07213de", + "modified_user_id": "11ef69fdc8fd8db2b07213de", + "ach_identifier": null, + "check_number": null, + "recurring_flag": "no", + "installment_counter": null, + "installment_total": null, + "settle_date": null, + "charge_back_date": null, + "void_date": null, + "account_type": "mc", + "is_recurring": false, + "is_accountvault": false, + "transaction_c1": null, + "transaction_c2": null, + "transaction_c3": null, + "additional_amounts": [], + "terminal_serial_number": null, + "entry_mode_id": "K", + "terminal_id": null, + "quick_invoice_id": null, + "ach_sec_code": null, + "custom_data": null, + "ebt_type": null, + "voucher_number": null, + "hosted_payment_page_id": null, + "transaction_batch_id": null, + "currency_code": 840, + "par": "ZZZZZZZZZZZZZZZZZZZZ545454545", + "stan": null, + "currency": "USD", + "secondary_amount": 0, + "card_bin": "545454", + "paylink_id": null, + "emv_receipt_data": null, + "status_code": 102, + "token_id": null, + "wallet_type": null, + "order_number": "865934726945", + "routing_number": null, + "trx_source_code": 12, + "billing_address": { + "city": null, + "state": null, + "postal_code": null, + "phone": null, + "country": null, + "street": null + }, + "is_token": false + } + } + JSON + end +end diff --git a/test/unit/gateways/garanti_test.rb b/test/unit/gateways/garanti_test.rb index 5b8287bb018..ad8ed645b9c 100644 --- a/test/unit/gateways/garanti_test.rb +++ b/test/unit/gateways/garanti_test.rb @@ -7,15 +7,15 @@ def setup @original_kcode = nil Base.mode = :test - @gateway = GarantiGateway.new(:login => 'a', :password => 'b', :terminal_id => 'c', :merchant_id => 'd') + @gateway = GarantiGateway.new(login: 'a', password: 'b', terminal_id: 'c', merchant_id: 'd') - @credit_card = credit_card(4242424242424242) - @amount = 1000 #1000 cents, 10$ + @credit_card = credit_card('4242424242424242') + @amount = 1000 # 1000 cents, 10$ @options = { - :order_id => 'db4af18c5222503d845180350fbda516', - :billing_address => address, - :description => 'Store Purchase' + order_id: 'db4af18c5222503d845180350fbda516', + billing_address: address, + description: 'Store Purchase' } end @@ -58,11 +58,11 @@ def test_nil_normalization end def test_strip_invalid_xml_chars - xml = < Parse the First & but not this ˜ &x002a; -EOF +XML parsed_xml = @gateway.send(:strip_invalid_xml_chars, xml) assert REXML::Document.new(parsed_xml) @@ -75,71 +75,71 @@ def test_strip_invalid_xml_chars # Place raw successful response from gateway here def successful_purchase_response - <<-EOF - - - - db4af18c5222503d845180350fbda516 - - - - - HOST - 00 - 00 - Approved - - - - 035208609374 - 784260 - 000089 - 000008 - 20101218 08:56:39 - - Company Name & Another Name - - - - - - - - EOF + <<~XML + + + + db4af18c5222503d845180350fbda516 + + + + + HOST + 00 + 00 + Approved + + + + 035208609374 + 784260 + 000089 + 000008 + 20101218 08:56:39 + + Company Name & Another Name + + + + + + + + XML end # Place raw failed response from gateway here def failed_purchase_response - <<-EOF - - - - db4af18c5222503d845180350fbda516 - - - - - GVPS - 92 - 0651 - Declined - - ErrorId: 0651 - - - - - - 20101220 01:58:41 - - - - - - - - - - EOF + <<~XML + + + + db4af18c5222503d845180350fbda516 + + + + + GVPS + 92 + 0651 + Declined + + ErrorId: 0651 + + + + + + 20101220 01:58:41 + + + + + + + + + + XML end end diff --git a/test/unit/gateways/gateway_test.rb b/test/unit/gateways/gateway_test.rb index 3334a916a07..0d7cb986e1d 100644 --- a/test/unit/gateways/gateway_test.rb +++ b/test/unit/gateways/gateway_test.rb @@ -10,11 +10,11 @@ def teardown end def test_should_detect_if_a_card_is_supported - Gateway.supported_cardtypes = [:visa, :bogus] - assert [:visa, :bogus].all? { |supported_cardtype| Gateway.supports?(supported_cardtype) } + Gateway.supported_cardtypes = %i[visa bogus] + assert(%i[visa bogus].all? { |supported_cardtype| Gateway.supports?(supported_cardtype) }) Gateway.supported_cardtypes = [] - assert_false [:visa, :bogus].all? { |invalid_cardtype| Gateway.supports?(invalid_cardtype) } + assert_false(%i[visa bogus].all? { |invalid_cardtype| Gateway.supports?(invalid_cardtype) }) end def test_should_validate_supported_countries @@ -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 @@ -46,20 +45,20 @@ def test_should_be_able_to_look_for_test_mode end def test_amount_style - assert_equal '10.34', @gateway.send(:amount, 1034) + assert_equal '10.34', @gateway.send(:amount, 1034) - assert_raise(ArgumentError) do - @gateway.send(:amount, '10.34') - end + assert_raise(ArgumentError) do + @gateway.send(:amount, '10.34') + end end def test_card_brand - credit_card = stub(:brand => 'visa') + credit_card = stub(brand: 'visa') assert_equal 'visa', Gateway.card_brand(credit_card) end def test_card_brand_using_type - credit_card = stub(:type => 'String') + credit_card = stub(type: 'String') assert_equal 'string', Gateway.card_brand(credit_card) end @@ -103,7 +102,7 @@ def test_localized_amount_returns_three_decimal_places_for_three_decimal_currenc end def test_split_names - assert_equal ['Longbob', 'Longsen'], @gateway.send(:split_names, 'Longbob Longsen') + assert_equal %w[Longbob Longsen], @gateway.send(:split_names, 'Longbob Longsen') end def test_split_names_with_single_name @@ -116,7 +115,6 @@ def test_split_names_with_empty_names assert_equal [nil, nil], @gateway.send(:split_names, ' ') end - def test_supports_scrubbing? gateway = Gateway.new refute gateway.supports_scrubbing? @@ -132,11 +130,11 @@ def test_should_not_allow_scrubbing_if_unsupported end def test_strip_invalid_xml_chars - xml = < Parse the First & but not this ˜ &x002a; -EOF + XML parsed_xml = @gateway.send(:strip_invalid_xml_chars, xml) assert REXML::Document.new(parsed_xml) @@ -144,4 +142,30 @@ def test_strip_invalid_xml_chars REXML::Document.new(xml) end end + + def test_add_field_to_post_if_present + order_id = 'abc123' + + post = {} + options = { order_id:, do_not_add: 24 } + + @gateway.add_field_to_post_if_present(post, options, :order_id) + + assert_equal post[:order_id], order_id + assert_false post.key?(:do_not_add) + end + + def test_add_fields_to_post_if_present + order_id = 'abc123' + transaction_number = 500 + + post = {} + options = { order_id:, transaction_number:, do_not_add: 24 } + + @gateway.add_fields_to_post_if_present(post, options, %i[order_id transaction_number]) + + assert_equal post[:order_id], order_id + assert_equal post[:transaction_number], transaction_number + assert_false post.key?(:do_not_add) + end end diff --git a/test/unit/gateways/global_collect_test.rb b/test/unit/gateways/global_collect_test.rb index d7c9a81801b..4b888bfcbe1 100644 --- a/test/unit/gateways/global_collect_test.rb +++ b/test/unit/gateways/global_collect_test.rb @@ -7,8 +7,26 @@ def setup @gateway = GlobalCollectGateway.new(merchant_id: '1234', api_key_id: '39u4193urng12', secret_api_key: '109H/288H*50Y18W4/0G8571F245KA=') + @gateway_direct = GlobalCollectGateway.new(fixtures(:global_collect_direct)) + @gateway_direct.options[:url_override] = 'ogone_direct' @credit_card = credit_card('4567350000427977') + @apple_pay_network_token = network_tokenization_credit_card( + '4444333322221111', + month: 10, + year: 24, + first_name: 'John', + last_name: 'Smith', + eci: '05', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + source: :apple_pay + ) + + @google_pay_network_token = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ + source: :google_pay, + payment_data: { 'version' => 'EC_v1', 'data' => 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9' } + }) + @declined_card = credit_card('5424180279791732') @accepted_amount = 4005 @rejected_amount = 2997 @@ -16,27 +34,275 @@ def setup billing_address: address, description: 'Store Purchase' } + @options_3ds2 = @options.merge( + 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', + flow: 'frictionless' + } + ) + end + + def test_url + url = @gateway.test? ? @gateway.test_url : @gateway.live_url + merchant_id = @gateway.options[:merchant_id] + assert_equal "#{url}/v1/#{merchant_id}/payments", @gateway.send(:url, :authorize, nil) + assert_equal "#{url}/v1/#{merchant_id}/payments", @gateway.send(:url, :verify, nil) + assert_equal "#{url}/v1/#{merchant_id}/payments/0000/approve", @gateway.send(:url, :capture, '0000') + assert_equal "#{url}/v1/#{merchant_id}/payments/0000/refund", @gateway.send(:url, :refund, '0000') + assert_equal "#{url}/v1/#{merchant_id}/payments/0000/cancel", @gateway.send(:url, :void, '0000') + assert_equal "#{url}/v1/#{merchant_id}/payments/0000", @gateway.send(:url, :inquire, '0000') + end + + def test_ogone_url + url = @gateway_direct.test? ? @gateway_direct.ogone_direct_test : @gateway_direct.ogone_direct_live + merchant_id = @gateway_direct.options[:merchant_id] + assert_equal "#{url}/v2/#{merchant_id}/payments", @gateway_direct.send(:url, :authorize, nil) + assert_equal "#{url}/v2/#{merchant_id}/payments", @gateway_direct.send(:url, :verify, nil) + assert_equal "#{url}/v2/#{merchant_id}/payments/0000/capture", @gateway_direct.send(:url, :capture, '0000') + assert_equal "#{url}/v2/#{merchant_id}/payments/0000/refund", @gateway_direct.send(:url, :refund, '0000') + assert_equal "#{url}/v2/#{merchant_id}/payments/0000/cancel", @gateway_direct.send(:url, :void, '0000') + assert_equal "#{url}/v2/#{merchant_id}/payments/0000", @gateway_direct.send(:url, :inquire, '0000') + end + + def test_supported_card_types + assert_equal GlobalCollectGateway.supported_cardtypes, %i[visa master american_express discover naranja cabal tuya patagonia_365 tarjeta_sol] end def test_successful_authorize_and_capture - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@accepted_amount, @credit_card, @options) end.respond_with(successful_authorize_response) assert_success response assert_equal '000000142800000000920000100001', response.authorization - capture = stub_comms do + capture = stub_comms(@gateway, :ssl_request) do @gateway.capture(@accepted_amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_method, endpoint, _data, _headers| assert_match(/000000142800000000920000100001/, endpoint) end.respond_with(successful_capture_response) assert_success capture end + def test_successful_preproduction_url + @gateway = GlobalCollectGateway.new( + merchant_id: '1234', + api_key_id: '39u4193urng12', + secret_api_key: '109H/288H*50Y18W4/0G8571F245KA=', + url_override: 'preproduction' + ) + + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@accepted_amount, @credit_card) + end.check_request do |_method, endpoint, _data, _headers| + assert_match(/api\.preprod\.connect\.worldline-solutions\.com\/v1\/#{@gateway.options[:merchant_id]}/, endpoint) + end.respond_with(successful_authorize_response) + end + + # When requires_approval is true (or not present), + # a `purchase` makes two calls (`auth` and `capture`). + def test_successful_purchase_with_requires_approval_true + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@accepted_amount, @credit_card, @options.merge(requires_approval: true)) + end.check_request do |_method, _endpoint, _data, _headers| + end.respond_with(successful_authorize_response, successful_capture_response) + end + + 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, { 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.to_s&.gsub('=>', ':'), JSON.parse(data)['mobilePaymentMethodSpecificInput']['encryptedPaymentData'] + end + end + + def test_purchase_request_with_google_pay + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@accepted_amount, @google_pay_network_token) + end.check_request(skip_response: true) do |_method, _endpoint, data, _headers| + assert_equal '320', JSON.parse(data)['mobilePaymentMethodSpecificInput']['paymentProductId'] + end + end + + def test_add_payment_for_credit_card + post = {} + options = {} + payment = @credit_card + @gateway.send('add_payment', post, payment, options) + assert_includes post.keys, 'cardPaymentMethodSpecificInput' + assert_equal post['cardPaymentMethodSpecificInput']['paymentProductId'], '1' + assert_equal post['cardPaymentMethodSpecificInput']['authorizationMode'], 'FINAL_AUTHORIZATION' + assert_includes post['cardPaymentMethodSpecificInput'].keys, 'card' + assert_equal post['cardPaymentMethodSpecificInput']['card']['cvv'], '123' + assert_equal post['cardPaymentMethodSpecificInput']['card']['cardNumber'], '4567350000427977' + end + + def test_add_payment_for_google_pay + post = {} + options = {} + payment = @google_pay_network_token + @gateway.send('add_payment', post, payment, options) + assert_includes post.keys.first, 'mobilePaymentMethodSpecificInput' + assert_equal post['mobilePaymentMethodSpecificInput']['paymentProductId'], '320' + assert_equal post['mobilePaymentMethodSpecificInput']['authorizationMode'], 'FINAL_AUTHORIZATION' + assert_equal post['mobilePaymentMethodSpecificInput']['encryptedPaymentData'], @google_pay_network_token.payment_data.to_s&.gsub('=>', ':') + end + + def test_add_payment_for_apple_pay + post = {} + options = {} + payment = @apple_pay_network_token + @gateway.send('add_payment', post, payment, options) + assert_includes post.keys, 'mobilePaymentMethodSpecificInput' + assert_equal post['mobilePaymentMethodSpecificInput']['paymentProductId'], '302' + assert_equal post['mobilePaymentMethodSpecificInput']['authorizationMode'], 'FINAL_AUTHORIZATION' + assert_includes post['mobilePaymentMethodSpecificInput'].keys, 'decryptedPaymentData' + assert_equal post['mobilePaymentMethodSpecificInput']['decryptedPaymentData']['dpan'], '4444333322221111' + assert_equal post['mobilePaymentMethodSpecificInput']['decryptedPaymentData']['cryptogram'], 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + assert_equal post['mobilePaymentMethodSpecificInput']['decryptedPaymentData']['eci'], '05' + assert_equal post['mobilePaymentMethodSpecificInput']['decryptedPaymentData']['expiryDate'], '1024' + end + + def test_purchase_request_with_apple_pay + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@accepted_amount, @apple_pay_network_token) + end.check_request(skip_response: true) do |_method, _endpoint, data, _headers| + assert_equal '302', JSON.parse(data)['mobilePaymentMethodSpecificInput']['paymentProductId'] + end + end + + # When requires_approval is false, a `purchase` makes one call (`auth`). + def test_successful_purchase_with_requires_approval_false + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@accepted_amount, @credit_card, @options.merge(requires_approval: false)) + end.check_request do |_method, _endpoint, data, _headers| + assert_equal false, JSON.parse(data)['cardPaymentMethodSpecificInput']['requiresApproval'] + end.respond_with(successful_authorize_response) + end + + def test_successful_purchase_airline_fields + options = @options.merge( + airline_data: { + code: 111, + name: 'Spreedly Airlines', + flight_date: '20190810', + passenger_name: 'Randi Smith', + agent_numeric_code: '12345', + flight_legs: [ + { arrival_airport: 'BDL', + origin_airport: 'RDU', + date: '20190810', + carrier_code: 'SA', + number: 596, + airline_class: 'ZZ' }, + { arrival_airport: 'RDU', + origin_airport: 'BDL', + date: '20190817', + carrier_code: 'SA', + number: 597, + airline_class: 'ZZ' } + ] + } + ) + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@accepted_amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_equal 111, JSON.parse(data)['order']['additionalInput']['airlineData']['code'] + assert_equal '20190810', JSON.parse(data)['order']['additionalInput']['airlineData']['flightDate'] + assert_equal 2, JSON.parse(data)['order']['additionalInput']['airlineData']['flightLegs'].length + end.respond_with(successful_authorize_response, successful_capture_response) + end + + def test_successful_purchase_lodging_fields + options = @options.merge( + lodging_data: { + charges: [ + { charge_amount: '1000', + charge_amount_currency_code: 'USD', + charge_type: 'giftshop' } + ], + check_in_date: '20211223', + check_out_date: '20211227', + folio_number: 'randAssortmentofChars', + is_confirmed_reservation: 'true', + is_facility_fire_safety_conform: 'true', + is_no_show: 'false', + is_preference_smoking_room: 'false', + number_of_adults: '2', + number_of_nights: '1', + number_of_rooms: '1', + program_code: 'advancedDeposit', + property_customer_service_phone_number: '5555555555', + property_phone_number: '5555555555', + renter_name: 'Guy', + rooms: [ + { daily_room_rate: '25000', + daily_room_rate_currency_code: 'USD', + daily_room_tax_amount: '5', + daily_room_tax_amount_currency_code: 'USD', + number_of_nights_at_room_rate: '1', + room_location: 'Courtyard', + type_of_bed: 'Queen', + type_of_room: 'Walled' } + ] + } + ) + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@accepted_amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_equal 'advancedDeposit', JSON.parse(data)['order']['additionalInput']['lodgingData']['programCode'] + assert_equal '20211223', JSON.parse(data)['order']['additionalInput']['lodgingData']['checkInDate'] + assert_equal '1000', JSON.parse(data)['order']['additionalInput']['lodgingData']['charges'][0]['chargeAmount'] + end.respond_with(successful_authorize_response, successful_capture_response) + end + + def test_successful_purchase_passenger_fields + options = @options.merge( + airline_data: { + passengers: [ + { first_name: 'Randi', + surname: 'Smith', + surname_prefix: 'S', + title: 'Mr' }, + { first_name: 'Julia', + surname: 'Smith', + surname_prefix: 'S', + title: 'Mrs' } + ] + } + ) + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@accepted_amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_equal 'Julia', JSON.parse(data)['order']['additionalInput']['airlineData']['passengers'][1]['firstName'] + assert_equal 2, JSON.parse(data)['order']['additionalInput']['airlineData']['passengers'].length + end.respond_with(successful_authorize_response, successful_capture_response) + end + + def test_purchase_passes_installments + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@accepted_amount, @credit_card, @options.merge(number_of_installments: '3')) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"numberOfInstallments\":\"3\"/, data) + end.respond_with(successful_authorize_response, successful_capture_response) + end + def test_purchase_does_not_run_capture_if_authorize_auto_captured - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@accepted_amount, @credit_card, @options) end.respond_with(successful_capture_response) @@ -46,19 +312,19 @@ def test_purchase_does_not_run_capture_if_authorize_auto_captured end def test_authorize_with_pre_authorization_flag - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@accepted_amount, @credit_card, @options.merge(pre_authorization: true)) - end.check_request do |endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/PRE_AUTHORIZATION/, data) - end.respond_with(successful_authorize_response) + end.respond_with(successful_authorize_response_with_pre_authorization_flag) assert_success response end def test_authorize_without_pre_authorization_flag - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@accepted_amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/FINAL_AUTHORIZATION/, data) end.respond_with(successful_authorize_response) @@ -68,32 +334,84 @@ def test_authorize_without_pre_authorization_flag def test_successful_authorization_with_extra_options options = @options.merge( { + customer: '123987', + email: 'example@example.com', order_id: '123', ip: '127.0.0.1', fraud_fields: { 'website' => 'www.example.com', 'giftMessage' => 'Happy Day!' - } + }, + payment_product_id: '123ABC' } ) - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@accepted_amount, @credit_card, options) - end.check_request do |endpoint, data, headers| - assert_match %r("fraudFields":{"website":"www.example.com","giftMessage":"Happy Day!","customerIpAddress":"127.0.0.1"}), data + end.check_request do |_method, _endpoint, data, _headers| + 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":"My Street","houseNumber":"456","additionalInfo":"Apt 1","zip":"K1C2N6","city":"Ottawa","state":"ON","countryCode":"CA"}}}), data + assert_match %r("paymentProductId":"123ABC"), data end.respond_with(successful_authorize_response) assert_success response end - def test_trucates_first_name_to_15_chars + 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/, data) + assert_match(/externalCardholderAuthenticationData/, data) + assert_match(/"eci\":\"05\"/, data) + assert_match(/"cavv\":\"jJ81HADVRtXfCBATEp01CJUAAAA=\"/, data) + assert_match(/"xid\":\"BwABBJQ1AgAAAAAgJDUCAAAAAAA=\"/, data) + assert_match(/"threeDSecureVersion\":\"2.1.0\"/, data) + assert_match(/"directoryServerTransactionId\":\"97267598-FAE6-48F2-8083-C23433990FBC\"/, data) + assert_match(/"acsTransactionId\":\"13c701a3-5a88-4c45-89e9-ef65e50a8bf9\"/, data) + assert_match(/"cavvAlgorithm\":1/, data) + assert_match(/"validationResult\":\"Y\"/, data) + end.respond_with(successful_authorize_with_3ds2_data_response) + + assert_success response + end + + def test_does_not_send_3ds_auth_when_empty + response = stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@accepted_amount, @credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + assert_not_match(/threeDSecure/, data) + assert_not_match(/externalCardholderAuthenticationData/, data) + assert_not_match(/cavv/, data) + assert_not_match(/xid/, data) + assert_not_match(/threeDSecureVersion/, data) + assert_not_match(/directoryServerTransactionId/, data) + assert_not_match(/acsTransactionId/, data) + assert_not_match(/cavvAlgorithm/, data) + assert_not_match(/validationResult/, data) + end.respond_with(successful_authorize_response) + + 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' }) - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@accepted_amount, credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/thisisaverylong/, data) end.respond_with(successful_authorize_response) @@ -101,8 +419,37 @@ def test_trucates_first_name_to_15_chars assert_equal '000000142800000000920000100001', response.authorization end + def test_handles_blank_names + credit_card = credit_card('4567350000427977', { first_name: nil, last_name: nil }) + + response = stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@accepted_amount, credit_card, @options) + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_truncates_split_address_fields + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@accepted_amount, @credit_card, { + billing_address: { + address1: '1234 Supercalifragilisticexpialidociousthiscantbemorethanfiftycharacters', + address2: 'Unit 6', + city: '‎Portland', + state: 'ME', + zip: '09901', + country: 'US' + } + }) + end.check_request do |_method, _endpoint, data, _headers| + assert_equal(JSON.parse(data)['order']['customer']['billingAddress']['houseNumber'], '1234') + assert_equal(JSON.parse(data)['order']['customer']['billingAddress']['street'], 'Supercalifragilisticexpialidociousthiscantbemoreth') + end.respond_with(successful_capture_response) + assert_success response + end + def test_failed_authorize - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@rejected_amount, @declined_card, @options) end.respond_with(failed_authorize_response) @@ -111,72 +458,107 @@ def test_failed_authorize end def test_failed_capture - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.capture(100, '', @options) end.respond_with(failed_capture_response) assert_failure response end + def test_successful_inquire + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@accepted_amount, @credit_card, @options) + end.respond_with(successful_capture_response) + + assert_success response + + response = stub_comms(@gateway, :ssl_request) do + @gateway.inquire(response.authorization) + end.respond_with(successful_inquire_response) + + assert_success response + end + def test_successful_void - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@accepted_amount, @credit_card, @options) end.respond_with(successful_capture_response) assert_success response - assert_equal '000000142800000000920000100001', response.authorization - void = stub_comms do + void = stub_comms(@gateway, :ssl_request) do @gateway.void(response.authorization) - end.check_request do |endpoint, data, headers| - assert_match(/000000142800000000920000100001/, endpoint) end.respond_with(successful_void_response) assert_success void end def test_failed_void - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.void('5d53a33d960c46d00f5dc061947d998c') - end.check_request do |endpoint, data, headers| + end.check_request do |_method, endpoint, _data, _headers| assert_match(/5d53a33d960c46d00f5dc061947d998c/, endpoint) end.respond_with(failed_void_response) assert_failure response end + def test_successful_provider_unresponsive_void + response = stub_comms(@gateway, :ssl_request) do + @gateway.void('5d53a33d960c46d00f5dc061947d998c') + end.respond_with(successful_provider_unresponsive_void_response) + + assert_success response + end + + def test_failed_provider_unresponsive_void + response = stub_comms(@gateway, :ssl_request) do + @gateway.void('5d53a33d960c46d00f5dc061947d998c') + end.respond_with(failed_provider_unresponsive_void_response) + + assert_failure response + end + def test_successful_verify - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.verify(@credit_card, @options) end.respond_with(successful_verify_response) + assert_equal '000000219600000096240000100001', response.authorization + + assert_success response + end + + def test_successful_verify_ogone_direct + response = stub_comms(@gateway_direct, :ssl_request) do + @gateway_direct.verify(@credit_card, @options) + end.respond_with(successful_authorize_response, successful_void_response) assert_equal '000000142800000000920000100001', response.authorization assert_success response end - def test_failed_verify - response = stub_comms do - @gateway.verify(@credit_card, @options) - end.respond_with(failed_verify_response) - assert_equal 'cee09c50-5d9d-41b8-b740-8c7bf06d2c66', response.authorization + def test_failed_verify_ogone_direct + response = stub_comms(@gateway_direct, :ssl_request) do + @gateway_direct.verify(@credit_card, @options) + end.respond_with(failed_authorize_response) assert_failure response end def test_successful_refund - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@accepted_amount, @credit_card, @options) end.respond_with(successful_authorize_response) assert_equal '000000142800000000920000100001', response.authorization - capture = stub_comms do + capture = stub_comms(@gateway, :ssl_request) do @gateway.capture(@accepted_amount, response.authorization) end.respond_with(successful_capture_response) - refund = stub_comms do + refund = stub_comms(@gateway, :ssl_request) do @gateway.refund(@accepted_amount, capture.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_method, endpoint, _data, _headers| assert_match(/000000142800000000920000100001/, endpoint) end.respond_with(successful_refund_response) @@ -184,15 +566,15 @@ def test_successful_refund end def test_refund_passes_currency_code - stub_comms do - @gateway.refund(@accepted_amount, '000000142800000000920000100001', {currency: 'COP'}) - end.check_request do |endpoint, data, headers| + stub_comms(@gateway, :ssl_request) do + @gateway.refund(@accepted_amount, '000000142800000000920000100001', { currency: 'COP' }) + end.check_request do |_method, _endpoint, data, _headers| assert_match(/"currencyCode\":\"COP\"/, data) end.respond_with(failed_refund_response) end def test_failed_refund - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.refund(nil, '') end.respond_with(failed_refund_response) @@ -200,7 +582,7 @@ def test_failed_refund end def test_rejected_refund - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.refund(@accepted_amount, '000000142800000000920000100001') end.respond_with(rejected_refund_response) @@ -209,11 +591,38 @@ def test_rejected_refund assert_equal 'Status: REJECTED', response.message end + def test_invalid_raw_response + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@accepted_amount, @credit_card, @options) + end.respond_with(invalid_json_response) + + assert_failure response + assert_match %r{^Invalid response received from the Ingenico ePayments}, response.message + end + def test_scrub assert @gateway.supports_scrubbing? assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed end + def test_scrub_invalid_response + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@accepted_amount, @credit_card, @options) + end.respond_with(invalid_json_plus_card_data).message + + assert_equal @gateway.scrub(response), scrubbed_invalid_json_plus + end + + def test_authorize_with_optional_idempotency_key_header + response = stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@accepted_amount, @credit_card, @options.merge(idempotency_key: 'test123')) + end.check_request do |_method, _endpoint, _data, headers| + assert_equal headers['X-GCS-Idempotence-Key'], 'test123' + end.respond_with(successful_authorize_response) + + assert_success response + end + private def pre_scrubbed @@ -329,15 +738,27 @@ def post_scrubbed end def successful_authorize_response - %({\n \"creationOutput\" : {\n \"additionalReference\" : \"00000014280000000092\",\n \"externalReference\" : \"000000142800000000920000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000142800000000920000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 100,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"0917\"\n },\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n }\n }\n },\n \"status\" : \"PENDING_APPROVAL\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCode\" : 600,\n \"statusCodeChangeDateTime\" : \"20160316205952\",\n \"isAuthorized\" : true\n }\n }\n}) + "{\n \"creationOutput\" : {\n \"additionalReference\" : \"00000014280000000092\",\n \"externalReference\" : \"000000142800000000920000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000142800000000920000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 4005,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"0920\"\n }\n }\n },\n \"status\" : \"PENDING_APPROVAL\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCategory\" : \"PENDING_MERCHANT\",\n \"statusCode\" : 600,\n \"statusCodeChangeDateTime\" : \"20191203162910\",\n \"isAuthorized\" : true,\n \"isRefundable\" : false\n }\n }\n}" + end + + def successful_verify_response + "{\n \"creationOutput\" : {\n \"additionalReference\" : \"00000021960000009624\",\n \"externalReference\" : \"000000219600000096240000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000219600000096240000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 0,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"cardholderName\" : \"Longbob Longsen\",\n \"expiryDate\" : \"0925\"\n }\n }\n },\n \"status\" : \"ACCOUNT_VERIFIED\",\n \"statusOutput\" : {\n \"isCancellable\" : false,\n \"isRetriable\" : false,\n \"statusCategory\" : \"ACCOUNT_VERIFIED\",\n \"statusCode\" : 300,\n \"statusCodeChangeDateTime\" : \"20241010205138\",\n \"isAuthorized\" : false,\n \"isRefundable\" : false\n }\n }\n}" + end + + def successful_authorize_with_3ds2_data_response + %({\"creationOutput\":{\"additionalReference\":\"00000021960000002279\",\"externalReference\":\"000000219600000022790000100001\"},\"payment\":{\"id\":\"000000219600000022790000100001\",\"paymentOutput\":{\"amountOfMoney\":{\"amount\":100,\"currencyCode\":\"USD\"},\"references\":{\"paymentReference\":\"0\"},\"paymentMethod\":\"card\",\"cardPaymentMethodSpecificOutput\":{\"paymentProductId\":1,\"authorisationCode\":\"OK1131\",\"fraudResults\":{\"fraudServiceResult\":\"no-advice\",\"avsResult\":\"0\",\"cvvResult\":\"0\"},\"threeDSecureResults\":{\"cavv\":\"jJ81HADVRtXfCBATEp01CJUAAAA=\",\"directoryServerTransactionId\":\"97267598-FAE6-48F2-8083-C23433990FBC\",\"eci\":\"5\",\"threeDSecureVersion\":\"2.1.0\"},\"card\":{\"cardNumber\":\"************7977\",\"expiryDate\":\"0921\"}}},\"status\":\"PENDING_APPROVAL\",\"statusOutput\":{\"isCancellable\":true,\"statusCategory\":\"PENDING_MERCHANT\",\"statusCode\":600,\"statusCodeChangeDateTime\":\"20201029212921\",\"isAuthorized\":true,\"isRefundable\":false}}}) end def failed_authorize_response %({\n \"errorId\" : \"460ec7ed-f8be-4bd7-bf09-a4cbe07f774e\",\n \"errors\" : [ {\n \"code\" : \"430330\",\n \"message\" : \"Not authorised\"\n } ],\n \"paymentResult\" : {\n \"creationOutput\" : {\n \"additionalReference\" : \"00000014280000000064\",\n \"externalReference\" : \"000000142800000000640000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000142800000000640000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 100,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1\n }\n },\n \"status\" : \"REJECTED\",\n \"statusOutput\" : {\n \"errors\" : [ {\n \"code\" : \"430330\",\n \"requestId\" : \"55635\",\n \"message\" : \"Not authorised\"\n } ],\n \"isCancellable\" : false,\n \"statusCode\" : 100,\n \"statusCodeChangeDateTime\" : \"20160316154235\",\n \"isAuthorized\" : false\n }\n }\n }\n}) end + def successful_authorize_response_with_pre_authorization_flag + %({\n \"creationOutput\" : {\n \"additionalReference\" : \"00000021960000000968\",\n \"externalReference\" : \"000000219600000009680000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000219600000009680000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 4005,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"0920\"\n }\n }\n },\n \"status\" : \"PENDING_APPROVAL\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCategory\" : \"PENDING_MERCHANT\",\n \"statusCode\" : 600,\n \"statusCodeChangeDateTime\" : \"20191017153833\",\n \"isAuthorized\" : true,\n \"isRefundable\" : false\n }\n }\n}) + end + def successful_capture_response - %({\n \"payment\" : {\n \"id\" : \"000000142800000000920000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 100,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"0917\"\n },\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n }\n }\n },\n \"status\" : \"CAPTURE_REQUESTED\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCode\" : 800,\n \"statusCodeChangeDateTime\" : \"20160317191047\",\n \"isAuthorized\" : true\n }\n }\n}) + "{\n \"payment\" : {\n \"id\" : \"000000142800000000920000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 4005,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"0920\"\n }\n }\n },\n \"status\" : \"CAPTURE_REQUESTED\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCategory\" : \"PENDING_CONNECT_OR_3RD_PARTY\",\n \"statusCode\" : 800,\n \"statusCodeChangeDateTime\" : \"20191203163030\",\n \"isAuthorized\" : true,\n \"isRefundable\" : false\n }\n }\n}" end def failed_capture_response @@ -356,19 +777,63 @@ def rejected_refund_response %({\n \"id\" : \"00000022184000047564000-100001\",\n \"refundOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 627000,\n \"currencyCode\" : \"COP\"\n },\n \"references\" : {\n \"merchantReference\" : \"17091GTgZmcC\",\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardRefundMethodSpecificOutput\" : {\n }\n },\n \"status\" : \"REJECTED\",\n \"statusOutput\" : {\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 1850,\n \"statusCodeChangeDateTime\" : \"20170313230631\"\n }\n}) end + def successful_inquire_response + %({\n \"payment\" : {\n \"id\" : \"000001263340000255950000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 126000,\n \"currencyCode\" : \"ARS\"\n },\n \"references\" : {\n \"merchantReference\" : \"10032994586\",\n \"paymentReference\" : \"0\",\n \"providerId\" : \"88\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"002792\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"493768******8095\",\n \"expiryDate\" : \"0824\"\n }\n }\n },\n \"status\" : \"PENDING_APPROVAL\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCode\" : 600,\n \"statusCodeChangeDateTime\" : \"20220214193408\",\n \"isAuthorized\" : true\n }\n }\n }) + end + def successful_void_response - %({\n \"payment\" : {\n \"id\" : \"000000142800000000920000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 100,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"0917\"\n },\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n }\n }\n },\n \"status\" : \"CANCELLED\",\n \"statusOutput\" : {\n \"isCancellable\" : false,\n \"statusCode\" : 99999,\n \"statusCodeChangeDateTime\" : \"20160317191526\"\n }\n }\n}) + %({\n \"payment\" : {\n \"id\" : \"000001263340000255950000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 126000,\n \"currencyCode\" : \"ARS\"\n },\n \"references\" : {\n \"merchantReference\" : \"10032994586\",\n \"paymentReference\" : \"0\",\n \"providerId\" : \"88\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"002792\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"493768******8095\",\n \"expiryDate\" : \"0824\"\n }\n }\n },\n \"status\" : \"CANCELLED\",\n \"statusOutput\" : {\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 99999,\n \"statusCodeChangeDateTime\" : \"20220214193408\",\n \"isAuthorized\" : false,\n \"isRefundable\" : false\n }\n },\n \"cardPaymentMethodSpecificOutput\" : {\n \"voidResponseId\" : \"00\"\n }\n}) end def failed_void_response %({\n \"errorId\" : \"9e38736e-15f3-4d6b-8517-aad3029619b9\",\n \"errors\" : [ {\n \"code\" : \"1002\",\n \"propertyName\" : \"paymentId\",\n \"message\" : \"INVALID_PAYMENT_ID\"\n } ]\n}) end - def successful_verify_response - %({\n \"payment\" : {\n \"id\" : \"000000142800000000920000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 100,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"0917\"\n },\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n }\n }\n },\n \"status\" : \"CANCELLED\",\n \"statusOutput\" : {\n \"isCancellable\" : false,\n \"statusCode\" : 99999,\n \"statusCodeChangeDateTime\" : \"20160318170240\"\n }\n }\n}) + def successful_provider_unresponsive_void_response + %({\n \"payment\" : {\n \"id\" : \"000001263340000255950000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 126000,\n \"currencyCode\" : \"ARS\"\n },\n \"references\" : {\n \"merchantReference\" : \"10032994586\",\n \"paymentReference\" : \"0\",\n \"providerId\" : \"88\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"002792\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"493768******8095\",\n \"expiryDate\" : \"0824\"\n }\n }\n },\n \"status\" : \"CANCELLED\",\n \"statusOutput\" : {\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 99999,\n \"statusCodeChangeDateTime\" : \"20220214193408\",\n \"isAuthorized\" : false,\n \"isRefundable\" : false\n }\n },\n \"cardPaymentMethodSpecificOutput\" : {\n \"voidResponseId\" : \"98\"\n }\n}) + end + + def failed_provider_unresponsive_void_response + %({\n \"payment\" : {\n \"id\" : \"000000142800000000920000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 100,\n \"currencyCode\" : \"ARS\"\n },\n \"references\" : {\n \"merchantReference\" : \"0\",\n \"paymentReference\" : \"0\",\n \"providerId\" : \"88\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 3,\n \"authorisationCode\" : \"123456\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"0125\"\n }\n }\n },\n \"status\" : \"REJECTED\",\n \"statusOutput\" : {\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 99999,\n \"statusCodeChangeDateTime\" : \"20191011201122\",\n \"isAuthorized\" : false,\n \"isRefundable\" : false\n }\n },\n \"cardPaymentMethodSpecificOutput\" : {\n \"voidResponseId\" : \"98\"\n }\n}) + end + + def invalid_json_response + ' + + 502 Proxy Error + +

Proxy Error

+

The proxy server received an invalid + response from an upstream server.
+ The proxy server could not handle the request POST /v1/9040/payments.

+ Reason: Error reading from remote server

+ ' + end + + def invalid_json_plus_card_data + %q( + + 502 Proxy Error + + opening connection to api-sandbox.globalcollect.com:443... + opened + starting SSL for api-sandbox.globalcollect.com:443... + SSL established + <- "POST //v1/1428/payments HTTP/1.1\r\nContent-Type: application/json\r\nAuthorization: GCS v1HMAC:96f16a41890565d0:Bqv5QtSXi+SdqXUyoBBeXUDlRvi5DzSm49zWuJTLX9s=\r\nDate: Tue, 15 Mar 2016 14:32:13 GMT\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: api-sandbox.globalcollect.com\r\nContent-Length: 560\r\n\r\n" + <- "{\"order\":{\"amountOfMoney\":{\"amount\":\"100\",\"currencyCode\":\"USD\"},\"customer\":{\"merchantCustomerId\":null,\"personalInformation\":{\"name\":{\"firstName\":null,\"surname\":null}},\"billingAddress\":{\"street\":\"456 My Street\",\"additionalInfo\":\"Apt 1\",\"zip\":\"K1C2N6\",\"city\":\"Ottawa\",\"state\":\"ON\",\"countryCode\":\"CA\"}},\"contactDetails\":{\"emailAddress\":null}},\"cardPaymentMethodSpecificInput\":{\"paymentProductId\":\"1\",\"skipAuthentication\":\"true\",\"skipFraudService\":\"true\",\"card\":{\"cvv\":\"123\",\"cardNumber\":\"4567350000427977\",\"expiryDate\":\"0917\",\"cardholderName\":\"Longbob Longsen\"}}}" + -> "HTTP/1.1 201 Created\r\n" + -> "Date: Tue, 15 Mar 2016 18:32:14 GMT\r\n" + -> "Server: Apache/2.4.16 (Unix) OpenSSL/1.0.1p\r\n" + -> "Location: https://api-sandbox.globalcollect.com:443/v1/1428/payments/000000142800000000300000100001\r\n" + -> "X-Powered-By: Servlet/3.0 JSP/2.2\r\n" + -> "Connection: close\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Content-Type: application/json\r\n" + -> "\r\n" + -> "457\r\n") end - def failed_verify_response - %({\n \"errorId\" : \"cee09c50-5d9d-41b8-b740-8c7bf06d2c66\",\n \"errors\" : [ {\n \"code\" : \"430330\",\n \"message\" : \"Not authorised\"\n } ],\n \"paymentResult\" : {\n \"creationOutput\" : {\n \"additionalReference\" : \"00000014280000000134\",\n \"externalReference\" : \"000000142800000000920000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000142800000000920000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 100,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1\n }\n },\n \"status\" : \"REJECTED\",\n \"statusOutput\" : {\n \"errors\" : [ {\n \"code\" : \"430330\",\n \"requestId\" : \"64357\",\n \"message\" : \"Not authorised\"\n } ],\n \"isCancellable\" : false,\n \"statusCode\" : 100,\n \"statusCodeChangeDateTime\" : \"20160318170253\",\n \"isAuthorized\" : false\n }\n }\n }\n}) + def scrubbed_invalid_json_plus + 'Invalid response received from the Ingenico ePayments (formerly GlobalCollect) API. Please contact Ingenico ePayments if you continue to receive this message. (The raw response returned by the API was "\\n \\n 502 Proxy Error\\n \\n opening connection to api-sandbox.globalcollect.com:443...\\n opened\\n starting SSL for api-sandbox.globalcollect.com:443...\\n SSL established\\n <- \\"POST //v1/1428/payments HTTP/1.1\\\\r\\\\nContent-Type: application/json\\\\r\\\\nAuthorization: [FILTERED]\\\\r\\\\nDate: Tue, 15 Mar 2016 14:32:13 GMT\\\\r\\\\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\\\\r\\\\nAccept: */*\\\\r\\\\nUser-Agent: Ruby\\\\r\\\\nConnection: close\\\\r\\\\nHost: api-sandbox.globalcollect.com\\\\r\\\\nContent-Length: 560\\\\r\\\\n\\\\r\\\\n\\"\\n <- \\"{\\\\\\"order\\\\\\":{\\\\\\"amountOfMoney\\\\\\":{\\\\\\"amount\\\\\\":\\\\\\"100\\\\\\",\\\\\\"currencyCode\\\\\\":\\\\\\"USD\\\\\\"},\\\\\\"customer\\\\\\":{\\\\\\"merchantCustomerId\\\\\\":null,\\\\\\"personalInformation\\\\\\":{\\\\\\"name\\\\\\":{\\\\\\"firstName\\\\\\":null,\\\\\\"surname\\\\\\":null}},\\\\\\"billingAddress\\\\\\":{\\\\\\"street\\\\\\":\\\\\\"456 My Street\\\\\\",\\\\\\"additionalInfo\\\\\\":\\\\\\"Apt 1\\\\\\",\\\\\\"zip\\\\\\":\\\\\\"K1C2N6\\\\\\",\\\\\\"city\\\\\\":\\\\\\"Ottawa\\\\\\",\\\\\\"state\\\\\\":\\\\\\"ON\\\\\\",\\\\\\"countryCode\\\\\\":\\\\\\"CA\\\\\\"}},\\\\\\"contactDetails\\\\\\":{\\\\\\"emailAddress\\\\\\":null}},\\\\\\"cardPaymentMethodSpecificInput\\\\\\":{\\\\\\"paymentProductId\\\\\\":\\\\\\"1\\\\\\",\\\\\\"skipAuthentication\\\\\\":\\\\\\"true\\\\\\",\\\\\\"skipFraudService\\\\\\":\\\\\\"true\\\\\\",\\\\\\"card\\\\\\":{\\\\\\"cvv\\\\\\":\\\\\\"[FILTERED]\\\\\\",\\\\\\"cardNumber\\\\\\":\\\\\\"[FILTERED]\\\\\\",\\\\\\"expiryDate\\\\\\":\\\\\\"0917\\\\\\",\\\\\\"cardholderName\\\\\\":\\\\\\"Longbob Longsen\\\\\\"}}}\\"\\n -> \\"HTTP/1.1 201 Created\\\\r\\\\n\\"\\n -> \\"Date: Tue, 15 Mar 2016 18:32:14 GMT\\\\r\\\\n\\"\\n -> \\"Server: Apache/2.4.16 (Unix) OpenSSL/1.0.1p\\\\r\\\\n\\"\\n -> \\"Location: https://api-sandbox.globalcollect.com:443/v1/1428/payments/000000142800000000300000100001\\\\r\\\\n\\"\\n -> \\"X-Powered-By: Servlet/3.0 JSP/2.2\\\\r\\\\n\\"\\n -> \\"Connection: close\\\\r\\\\n\\"\\n -> \\"Transfer-Encoding: chunked\\\\r\\\\n\\"\\n -> \\"Content-Type: application/json\\\\r\\\\n\\"\\n -> \\"\\\\r\\\\n\\"\\n -> \\"457\\\\r\\\\n\\"")' end end diff --git a/test/unit/gateways/global_transport_test.rb b/test/unit/gateways/global_transport_test.rb index b64fd765323..637ace0ed49 100644 --- a/test/unit/gateways/global_transport_test.rb +++ b/test/unit/gateways/global_transport_test.rb @@ -9,7 +9,7 @@ def setup @options = { order_id: '1', - billing_address: address, + billing_address: address } end @@ -21,7 +21,7 @@ def test_successful_purchase assert_equal '3648838', response.authorization assert response.test? assert_equal 'CVV matches', response.cvv_result['message'] - assert_equal 'Street address and postal code do not match.', response.avs_result['message'] + assert_equal 'Street address and postal code do not match. For American Express: Card member\'s name, street address and postal code do not match.', response.avs_result['message'] end def test_failed_purchase @@ -52,7 +52,7 @@ def test_successful_authorize_and_capture capture = stub_comms do @gateway.capture(100, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/PNRef=3648890/, data) end.respond_with(successful_capture_response) @@ -70,7 +70,7 @@ def test_successful_partial_authorize_and_capture capture = stub_comms do @gateway.capture(150, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/PNRef=8869269/, data) end.respond_with(successful_partial_capture_response) @@ -103,7 +103,7 @@ def test_successful_refund refund = stub_comms do @gateway.refund(100, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/PNRef=3648838/, data) end.respond_with(successful_refund_response) @@ -127,7 +127,7 @@ def test_successful_void void = stub_comms do @gateway.void(response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/PNRef=3648838/, data) end.respond_with(successful_void_response) @@ -162,7 +162,7 @@ def test_failed_verify def test_truncation stub_comms do @gateway.purchase(100, credit_card, order_id: 'a' * 17) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/&InvNum=a{16}&/, data) end.respond_with(successful_purchase_response) end diff --git a/test/unit/gateways/hdfc_test.rb b/test/unit/gateways/hdfc_test.rb index 9913adedf19..935c0adf433 100644 --- a/test/unit/gateways/hdfc_test.rb +++ b/test/unit/gateways/hdfc_test.rb @@ -7,8 +7,8 @@ def setup Base.mode = :test @gateway = HdfcGateway.new( - :login => 'login', - :password => 'password' + login: 'login', + password: 'password' ) @credit_card = credit_card @@ -47,7 +47,7 @@ def test_authorize_and_capture capture = stub_comms do @gateway.capture(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/2441955352022771/, data) end.respond_with(successful_capture_response) @@ -64,7 +64,7 @@ def test_refund refund = stub_comms do @gateway.refund(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/849768440022761/, data) end.respond_with(successful_refund_response) @@ -74,45 +74,45 @@ def test_refund def test_passing_cvv stub_comms do @gateway.purchase(@amount, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/#{@credit_card.verification_value}/, data) end.respond_with(successful_purchase_response) end def test_passing_currency stub_comms do - @gateway.purchase(@amount, @credit_card, :currency => 'INR') - end.check_request do |endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, currency: 'INR') + end.check_request do |_endpoint, data, _headers| assert_match(/currencycode>356 'AOA') + @gateway.purchase(@amount, @credit_card, currency: 'AOA') end end def test_passing_order_id stub_comms do - @gateway.purchase(@amount, @credit_card, :order_id => '932823723') - end.check_request do |endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, order_id: '932823723') + end.check_request do |_endpoint, data, _headers| assert_match(/932823723/, data) end.respond_with(successful_purchase_response) end def test_passing_description stub_comms do - @gateway.purchase(@amount, @credit_card, :description => 'Awesome Services By Us') - end.check_request do |endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, description: 'Awesome Services By Us') + end.check_request do |_endpoint, data, _headers| assert_match(/Awesome Services By Us/, data) end.respond_with(successful_purchase_response) end def test_escaping stub_comms do - @gateway.purchase(@amount, @credit_card, :order_id => 'a' * 41, :description => "This has 'Hack Characters' ~`!\#$%^=+|\\:'\",;<>{}[]() and non-Hack Characters -_@.") - end.check_request do |endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, order_id: 'a' * 41, description: "This has 'Hack Characters' ~`!\#$%^=+|\\:'\",;<>{}[]() and non-Hack Characters -_@.") + end.check_request do |_endpoint, data, _headers| assert_match(/>This has Hack Characters and non-Hack Characters -_@.#{"a" * 40} address) - end.check_request do |endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, billing_address: address) + end.check_request do |_endpoint, data, _headers| assert_match(/udf4>Jim Smith\nWidgets Inc\n456 My Street\nApt 1\nOttawa ON K1C2N6\nCA/, data) end.respond_with(successful_purchase_response) end def test_passing_phone_number stub_comms do - @gateway.purchase(@amount, @credit_card, :billing_address => address) - end.check_request do |endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, billing_address: address) + end.check_request do |_endpoint, data, _headers| assert_match(/udf3>555555-5555 address(:phone => nil)) - end.check_request do |endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, billing_address: address(phone: nil)) + end.check_request do |_endpoint, data, _headers| assert_no_match(/udf3/, data) end.respond_with(successful_purchase_response) end def test_passing_eci stub_comms do - @gateway.purchase(@amount, @credit_card, :eci => 22) - end.check_request do |endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, eci: 22) + end.check_request do |_endpoint, data, _headers| assert_match(/eci>22 '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 f6371b26394..ad8ac0552fd 100644 --- a/test/unit/gateways/hps_test.rb +++ b/test/unit/gateways/hps_test.rb @@ -1,11 +1,15 @@ require 'test_helper' class HpsTest < Test::Unit::TestCase + include CommStub + def setup - @gateway = HpsGateway.new({:secret_api_key => '12'}) + @gateway = HpsGateway.new({ secret_api_key: '12' }) @credit_card = credit_card @amount = 100 + @check = check(account_number: '1357902468', routing_number: '122000030', number: '1234', account_type: 'SAVINGS') + @check_amount = 2000 @options = { order_id: '1', @@ -34,6 +38,52 @@ def test_successful_purchase_no_address assert_success response end + def test_successful_zip_formatting + @options[:billing_address][:zip] = '12345-1234 ' + + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/123451234<\/hps:CardHolderZip>/, data) + end.respond_with(successful_swipe_purchase_response) + + assert_success response + end + + def test_successful_check_purchase + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@check_amount, @check, @options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/SALE<\/hps:CheckAction>/, data) + end.respond_with(successful_check_purchase_response) + + assert_success response + end + + def test_check_purchase_does_not_raise_no_method_error_when_account_type_missing + check = @check.dup + check.account_type = nil + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@check_amount, check, @options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/SALE<\/hps:CheckAction>/, data) + end.respond_with(successful_check_purchase_response) + + assert_success response + end + + def test_check_purchase_does_not_raise_no_method_error_when_account_holder_type_missing + check = @check.dup + check.account_holder_type = nil + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@check_amount, check, @options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/SALE<\/hps:CheckAction>/, data) + end.respond_with(successful_check_purchase_response) + + assert_success response + end + def test_failed_purchase @gateway.expects(:ssl_post).returns(failed_charge_response) @@ -90,7 +140,7 @@ def test_failed_capture def test_successful_refund @gateway.expects(:ssl_post).returns(successful_refund_response) - refund = @gateway.refund(@amount,'transaction_id') + refund = @gateway.refund(@amount, 'transaction_id') assert_instance_of Response, refund assert_success refund assert_equal '0', refund.params['GatewayRspCode'] @@ -99,11 +149,25 @@ def test_successful_refund def test_failed_refund @gateway.expects(:ssl_post).returns(failed_refund_response) - refund = @gateway.refund(@amount,'169054') + refund = @gateway.refund(@amount, '169054') assert_instance_of Response, refund assert_failure refund end + def test_successful_credit + @gateway.expects(:ssl_post).returns(successful_refund_response) + + credit = @gateway.credit(@amount, @credit_card) + assert_success credit + end + + def test_failed_credit + @gateway.expects(:ssl_post).returns(failed_refund_response) + + credit = @gateway.refund(@amount, @credit_card) + assert_failure credit + end + def test_successful_void @gateway.expects(:ssl_post).returns(successful_void_response) @@ -112,6 +176,16 @@ def test_successful_void assert_success void end + def test_successful_check_void + void = stub_comms(@gateway, :ssl_request) do + @gateway.void('169054', check_void: true) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(//, data) + end.respond_with(successful_check_void_response) + + assert_success void + end + def test_failed_void @gateway.expects(:ssl_post).returns(failed_refund_response) @@ -120,11 +194,23 @@ def test_failed_void assert_failure void end + def test_successful_recurring_purchase + stored_credential_params = { + reason_type: 'recurring' + } + + @gateway.expects(:ssl_post).returns(successful_charge_response) + + response = @gateway.purchase(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_params })) + assert_instance_of Response, response + assert_success response + end + def test_successful_purchase_with_swipe_no_encryption @gateway.expects(:ssl_post).returns(successful_swipe_purchase_response) @credit_card.track_data = '%B547888879888877776?;5473500000000014=25121019999888877776?' - response = @gateway.purchase(@amount,@credit_card,@options) + response = @gateway.purchase(@amount, @credit_card, @options) assert_success response assert_equal 'Success', response.message end @@ -133,7 +219,7 @@ def test_failed_purchase_with_swipe_bad_track_data @gateway.expects(:ssl_post).returns(failed_swipe_purchase_response) @credit_card.track_data = '%B547888879888877776?;?' - response = @gateway.purchase(@amount,@credit_card,@options) + response = @gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal 'Transaction was rejected because the track data could not be read.', response.message @@ -144,7 +230,7 @@ def test_successful_purchase_with_swipe_encryption_type_01 @options[:encryption_type] = '01' @credit_card.track_data = '<E1052711%B5473501000000014^MC TEST CARD^251200000000000000000000000000000000?|GVEY/MKaKXuqqjKRRueIdCHPPoj1gMccgNOtHC41ymz7bIvyJJVdD3LW8BbwvwoenI+|+++++++C4cI2zjMp|11;5473501000000014=25120000000000000000?|8XqYkQGMdGeiIsgM0pzdCbEGUDP|+++++++C4cI2zjMp|00|||/wECAQECAoFGAgEH2wYcShV78RZwb3NAc2VjdXJlZXhjaGFuZ2UubmV0PX50qfj4dt0lu9oFBESQQNkpoxEVpCW3ZKmoIV3T93zphPS3XKP4+DiVlM8VIOOmAuRrpzxNi0TN/DWXWSjUC8m/PI2dACGdl/hVJ/imfqIs68wYDnp8j0ZfgvM26MlnDbTVRrSx68Nzj2QAgpBCHcaBb/FZm9T7pfMr2Mlh2YcAt6gGG1i2bJgiEJn8IiSDX5M2ybzqRT86PCbKle/XCTwFFe1X|>' - response = @gateway.purchase(@amount,@credit_card,@options) + response = @gateway.purchase(@amount, @credit_card, @options) assert_success response assert_equal 'Success', response.message @@ -157,7 +243,7 @@ def test_successful_purchase_with_swipe_encryption_type_02 @options[:encrypted_track_number] = 2 @options[:ktb] = '/wECAQECAoFGAgEH3QgVTDT6jRZwb3NAc2VjdXJlZXhjaGFuZ2UubmV0Nkt08KRSPigRYcr1HVgjRFEvtUBy+VcCKlOGA3871r3SOkqDvH2+30insdLHmhTLCc4sC2IhlobvWnutAfylKk2GLspH/pfEnVKPvBv0hBnF4413+QIRlAuGX6+qZjna2aMl0kIsjEY4N6qoVq2j5/e5I+41+a2pbm61blv2PEMAmyuCcAbN3/At/1kRZNwN6LSUg9VmJO83kOglWBe1CbdFtncq' @credit_card.track_data = '7SV2BK6ESQPrq01iig27E74SxMg' - response = @gateway.purchase(@amount,@credit_card,@options) + response = @gateway.purchase(@amount, @credit_card, @options) assert_success response assert_equal 'Success', response.message @@ -195,418 +281,1058 @@ def test_transcript_scrubbing assert_equal @gateway.scrub(pre_scrub), post_scrub end + def test_account_number_scrubbing + assert_equal @gateway.scrub(pre_scrubbed_account_number), post_scrubbed_account_number + end + + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + eci: '05', + source: :apple_pay + ) + assert response = @gateway.purchase(@amount, credit_card, @options) + assert_success response + assert_equal 'Success', response.message + end + + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + eci: '05', + source: :apple_pay + ) + assert response = @gateway.purchase(@amount, credit_card, @options) + assert_failure response + assert_equal 'The card was declined.', response.message + end + + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + source: :apple_pay + ) + assert response = @gateway.purchase(@amount, credit_card, @options) + assert_success response + assert_equal 'Success', response.message + end + + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + source: :apple_pay + ) + assert response = @gateway.purchase(@amount, credit_card, @options) + assert_failure response + assert_equal 'The card was declined.', response.message + end + + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + eci: '05', + source: :apple_pay + ) + assert response = @gateway.authorize(@amount, credit_card, @options) + assert_success response + assert_equal 'Success', response.message + end + + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + eci: '05', + source: :apple_pay + ) + assert response = @gateway.authorize(@amount, credit_card, @options) + assert_failure response + assert_equal 'The card was declined.', response.message + end + + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + source: :apple_pay + ) + assert response = @gateway.authorize(@amount, credit_card, @options) + assert_success response + assert_equal 'Success', response.message + end + + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + source: :apple_pay + ) + assert response = @gateway.authorize(@amount, credit_card, @options) + assert_failure response + assert_equal 'The card was declined.', response.message + end + + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + eci: '05', + source: :android_pay + ) + assert response = @gateway.purchase(@amount, credit_card, @options) + assert_success response + assert_equal 'Success', response.message + end + + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + eci: '05', + source: :android_pay + ) + assert response = @gateway.purchase(@amount, credit_card, @options) + assert_failure response + assert_equal 'The card was declined.', response.message + end + + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + source: :android_pay + ) + assert response = @gateway.purchase(@amount, credit_card, @options) + assert_success response + assert_equal 'Success', response.message + end + + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + source: :android_pay + ) + assert response = @gateway.purchase(@amount, credit_card, @options) + assert_failure response + assert_equal 'The card was declined.', response.message + end + + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + eci: '05', + source: :android_pay + ) + assert response = @gateway.authorize(@amount, credit_card, @options) + assert_success response + assert_equal 'Success', response.message + end + + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + eci: '05', + source: :android_pay + ) + assert response = @gateway.authorize(@amount, credit_card, @options) + assert_failure response + assert_equal 'The card was declined.', response.message + end + + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + source: :android_pay + ) + assert response = @gateway.authorize(@amount, credit_card, @options) + assert_success response + assert_equal 'Success', response.message + end + + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + source: :android_pay + ) + assert response = @gateway.authorize(@amount, credit_card, @options) + assert_failure response + assert_equal 'The card was declined.', response.message + end + + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + eci: '05', + source: :google_pay + ) + assert response = @gateway.purchase(@amount, credit_card, @options) + assert_success response + assert_equal 'Success', response.message + end + + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + eci: '05', + source: :google_pay + ) + assert response = @gateway.purchase(@amount, credit_card, @options) + assert_failure response + assert_equal 'The card was declined.', response.message + end + + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + source: :google_pay + ) + assert response = @gateway.purchase(@amount, credit_card, @options) + assert_success response + assert_equal 'Success', response.message + end + + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + source: :google_pay + ) + assert response = @gateway.purchase(@amount, credit_card, @options) + assert_failure response + assert_equal 'The card was declined.', response.message + end + + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + eci: '05', + source: :google_pay + ) + assert response = @gateway.authorize(@amount, credit_card, @options) + assert_success response + assert_equal 'Success', response.message + end + + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + eci: '05', + source: :google_pay + ) + assert response = @gateway.authorize(@amount, credit_card, @options) + assert_failure response + assert_equal 'The card was declined.', response.message + end + + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + source: :google_pay + ) + assert response = @gateway.authorize(@amount, credit_card, @options) + assert_success response + assert_equal 'Success', response.message + end + + 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + verification_value: nil, + source: :google_pay + ) + assert response = @gateway.authorize(@amount, credit_card, @options) + assert_failure response + assert_equal 'The card was declined.', response.message + end + + def test_three_d_secure_visa + @credit_card.number = '4012002000060016' + @credit_card.brand = 'visa' + + options = { + three_d_secure: { + version: '2.2.0', + cavv: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + eci: '05', + ds_transaction_id: 'TTBCSkVTa1ZpbDI1bjRxbGk5ODE=' + } + } + + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/(.*)<\/hps:Secure3D>/, data) + assert_match(/#{options[:three_d_secure][:version]}<\/hps:Version>/, data) + assert_match(/#{options[:three_d_secure][:cavv]}<\/hps:AuthenticationValue>/, data) + assert_match(/5<\/hps:ECI>/, data) + assert_match(/#{options[:three_d_secure][:ds_transaction_id]}<\/hps:DirectoryServerTxnId>/, data) + end.respond_with(successful_charge_response) + + assert_success response + assert_equal 'Success', response.message + end + + def test_three_d_secure_mastercard + @credit_card.number = '5473500000000014' + @credit_card.brand = 'master' + + options = { + three_d_secure: { + version: '2.2.0', + cavv: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + eci: '05', + ds_transaction_id: 'TTBCSkVTa1ZpbDI1bjRxbGk5ODE=' + } + } + + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/(.*)<\/hps:Secure3D>/, data) + assert_match(/#{options[:three_d_secure][:version]}<\/hps:Version>/, data) + assert_match(/#{options[:three_d_secure][:cavv]}<\/hps:AuthenticationValue>/, data) + assert_match(/5<\/hps:ECI>/, data) + assert_match(/#{options[:three_d_secure][:ds_transaction_id]}<\/hps:DirectoryServerTxnId>/, data) + end.respond_with(successful_charge_response) + + assert_success response + assert_equal 'Success', response.message + end + + def test_three_d_secure_discover + @credit_card.number = '6011000990156527' + @credit_card.brand = 'discover' + + options = { + three_d_secure: { + cavv: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + eci: '5', + ds_transaction_id: 'TTBCSkVTa1ZpbDI1bjRxbGk5ODE=' + } + } + + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/(.*)<\/hps:Secure3D>/, data) + assert_match(/#{options[:three_d_secure][:cavv]}<\/hps:AuthenticationValue>/, data) + assert_match(/5<\/hps:ECI>/, data) + assert_match(/#{options[:three_d_secure][:ds_transaction_id]}<\/hps:DirectoryServerTxnId>/, data) + end.respond_with(successful_charge_response) + + assert_success response + assert_equal 'Success', response.message + end + + def test_three_d_secure_amex + @credit_card.number = '372700699251018' + @credit_card.brand = 'american_express' + + options = { + three_d_secure: { + cavv: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + eci: '05', + ds_transaction_id: 'TTBCSkVTa1ZpbDI1bjRxbGk5ODE=' + } + } + + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/(.*)<\/hps:Secure3D>/, data) + assert_match(/#{options[:three_d_secure][:cavv]}<\/hps:AuthenticationValue>/, data) + assert_match(/5<\/hps:ECI>/, data) + assert_match(/#{options[:three_d_secure][:ds_transaction_id]}<\/hps:DirectoryServerTxnId>/, data) + end.respond_with(successful_charge_response) + + assert_success response + assert_equal 'Success', response.message + end + + def test_three_d_secure_jcb + @credit_card.number = '372700699251018' + @credit_card.brand = 'jcb' + + options = { + three_d_secure: { + cavv: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + eci: '5', + xid: 'TTBCSkVTa1ZpbDI1bjRxbGk5ODE=' + } + } + + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + refute_match(/(.*)<\/hps:SecureECommerce>/, data) + refute_match(/(.*)<\/hps:PaymentDataSource>/, data) + refute_match(/3DSecure<\/hps:TypeOfPaymentData>/, data) + refute_match(/#{options[:three_d_secure][:cavv]}<\/hps:PaymentData>/, data) + refute_match(/5<\/hps:ECommerceIndicator>/, data) + refute_match(/#{options[:three_d_secure][:xid]}<\/hps:XID>/, data) + end.respond_with(successful_charge_response) + + assert_success response + assert_equal 'Success', response.message + end + + def test_successful_auth_with_stored_credentials + stored_credential_params = { + initial_transaction: false, + reason_type: 'recurring', + initiator: 'customer', + network_transaction_id: 12345 + } + + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_params })) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/C<\/hps:CardOnFile>/, data) + assert_match(/12345<\/hps:CardBrandTxnId>/, data) + assert_match(/N<\/hps:OneTime>/, data) + end.respond_with(successful_charge_response) + + assert_success response + assert_equal 'Success', response.message + end + + def test_truncates_long_invoicenbr + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@check_amount, @check, @options.merge(order_id: '04863692e6b56aaed85760b3d0879afd18b980da0521f6454c007a838435e561')) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/04863692e6b56aaed85760b3d0879afd18b980da0521f6454c007a838435<\/hps:InvoiceNbr>/, data) + end.respond_with(successful_check_purchase_response) + + assert_success response + assert_equal 'Success', response.message + end + private def successful_charge_response - <<-RESPONSE - - - - - -
- 95878 - 95881 - 2409000 - 15927453 - 0 - Success - 2014-03-14T15:40:25.4686202 -
- - - 00 - APPROVAL - 36987A - 0 - M - 407313649105 - ACCEPT - ACCEPT - Visa - AVS Not Requested. - Match. - - -
-
-
-
- RESPONSE + <<~XML + + + + + +
+ 95878 + 95881 + 2409000 + 15927453 + 0 + Success + 2014-03-14T15:40:25.4686202 +
+ + + 00 + APPROVAL + 36987A + 0 + M + 407313649105 + ACCEPT + ACCEPT + Visa + AVS Not Requested. + Match. + + +
+
+
+
+ XML + end + + def successful_check_purchase_response + <<~XML + + + + + +
+ 144379 + 144474 + 6407594 + 1284694345 + 0 + Success + 2020-01-13T15:11:24.735047 +
+ + + 0 + Transaction Approved. BatchID:31796 + + +
+
+
+
+ XML end def failed_charge_response - <<-RESPONSE - - - - - -
- 21229 - 21232 - 1525997 - 16099851 - 0 - Success - 2014-03-17T13:01:55.851307 -
- - - 02 - CALL - - 0 - 407613674802 - Visa - AVS Not Requested. - - -
-
-
-
- RESPONSE + <<~XML + + + + + +
+ 21229 + 21232 + 1525997 + 16099851 + 0 + Success + 2014-03-17T13:01:55.851307 +
+ + + 02 + CALL + + 0 + 407613674802 + Visa + AVS Not Requested. + + +
+
+
+
+ XML + end + + def failed_charge_response_decline + <<~XML + + + + + +
+ 21229 + 21232 + 1525997 + 16099851 + 0 + Success + 2014-03-17T13:01:55.851307 +
+ + + 05 + DECLINE + + 0 + 407613674802 + Visa + AVS Not Requested. + + +
+
+
+
+ XML end def successful_authorize_response - <<-RESPONSE - - - - - -
- 21229 - 21232 - 1525997 - 16072891 - 0 - Success - 2014-03-17T13:05:34.5819712 -
- - - 00 - APPROVAL - 43204A - 0 - M - 407613674895 - ACCEPT - ACCEPT - Visa - AVS Not Requested. - Match. - - -
-
-
-
- RESPONSE + <<~XML + + + + + +
+ 21229 + 21232 + 1525997 + 16072891 + 0 + Success + 2014-03-17T13:05:34.5819712 +
+ + + 00 + APPROVAL + 43204A + 0 + M + 407613674895 + ACCEPT + ACCEPT + Visa + AVS Not Requested. + Match. + + +
+
+
+
+ XML end def failed_authorize_response - <<-RESPONSE - - - - - -
- 21229 - 21232 - 1525997 - 16088893 - 0 - Success - 2014-03-17T13:06:45.449707 -
- - - 54 - EXPIRED CARD - - 0 - 407613674811 - Visa - AVS Not Requested. - - -
-
-
-
- RESPONSE + <<~XML + + + + + +
+ 21229 + 21232 + 1525997 + 16088893 + 0 + Success + 2014-03-17T13:06:45.449707 +
+ + + 54 + EXPIRED CARD + + 0 + 407613674811 + Visa + AVS Not Requested. + + +
+
+
+
+ XML + end + + def failed_authorize_response_decline + <<~XML + + + + + +
+ 21229 + 21232 + 1525997 + 16088893 + 0 + Success + 2014-03-17T13:06:45.449707 +
+ + + 05 + DECLINE + + 0 + 407613674811 + Visa + AVS Not Requested. + + +
+
+
+
+ XML end def successful_capture_response - <<-RESPONSE - - - - - -
- 21229 - 21232 - 1525997 - 17213037 - 0 - Success - 2014-05-16T14:45:48.9906929 -
- - - -
-
-
-
- RESPONSE + <<~XML + + + + + +
+ 21229 + 21232 + 1525997 + 17213037 + 0 + Success + 2014-05-16T14:45:48.9906929 +
+ + + +
+
+
+
+ XML end def failed_capture_response - <<-Response - - - - - -
- 21229 - 21232 - 1525997 - 16104055 - 3 - Transaction rejected because the referenced original transaction is invalid. Subject '216072899'. Original transaction not found. - 2014-03-17T14:20:32.355307 -
-
-
-
-
- Response + <<~XML + + + + + +
+ 21229 + 21232 + 1525997 + 16104055 + 3 + Transaction rejected because the referenced original transaction is invalid. Subject '216072899'. Original transaction not found. + 2014-03-17T14:20:32.355307 +
+
+
+
+
+ XML end def successful_refund_response - <<-RESPONSE - - - - - -
- 21229 - 21232 - 1525997 - - 16092738 - 0 - Success - 2014-03-17T13:31:42.0231712 -
- - - -
-
-
-
- RESPONSE + <<~XML + + + + + +
+ 21229 + 21232 + 1525997 + + 16092738 + 0 + Success + 2014-03-17T13:31:42.0231712 +
+ + + +
+
+
+
+ XML end def failed_refund_response - <<-RESPONSE - - - - - -
- 21229 - 21232 - 1525997 - - 16092766 - 3 - Transaction rejected because the referenced original transaction is invalid. - 2014-03-17T13:48:55.3203712 -
-
-
-
-
- RESPONSE + <<~XML + + + + + +
+ 21229 + 21232 + 1525997 + + 16092766 + 3 + Transaction rejected because the referenced original transaction is invalid. + 2014-03-17T13:48:55.3203712 +
+
+
+
+
+ XML end def successful_void_response - <<-RESPONSE - - - - - -
- 21229 - 21232 - 1525997 - 16092767 - 0 - Success - 2014-03-17T13:53:43.6863712 -
- - - -
-
-
-
- RESPONSE + <<~XML + + + + + +
+ 21229 + 21232 + 1525997 + 16092767 + 0 + Success + 2014-03-17T13:53:43.6863712 +
+ + + +
+
+
+
+ XML + end + + def successful_check_void_response + <<~XML + + + + + +
+ 144379 + 144474 + 6407594 + 1284696436 + 0 + Success + 2020-01-13T15:44:24.3568038 +
+ + + 0 + Transaction Approved. + + +
+
+
+
+ XML end def failed_void_response - <<-RESPONSE - - - - - -
- 21229 - 21232 - 1525997 - 16103858 - 3 - Transaction rejected because the referenced original transaction is invalid. Subject '169054'. Original transaction not found. - 2014-03-17T13:55:56.8947712 -
-
-
-
-
- RESPONSE + <<~XML + + + + + +
+ 21229 + 21232 + 1525997 + 16103858 + 3 + Transaction rejected because the referenced original transaction is invalid. Subject '169054'. Original transaction not found. + 2014-03-17T13:55:56.8947712 +
+
+
+
+
+ XML end def successful_swipe_purchase_response - <<-RESPONSE - - - - - -
- 95878 - 95881 - 2409000 - 17596558 - 0 - Success - 2014-05-26T10:27:30.4211513 -
- - - 00 - APPROVAL - 037677 - 0 - 414614470800 - ACCEPT - MC - AVS Not Requested. - - -
-
-
-
- RESPONSE + <<~XML + + + + + +
+ 95878 + 95881 + 2409000 + 17596558 + 0 + Success + 2014-05-26T10:27:30.4211513 +
+ + + 00 + APPROVAL + 037677 + 0 + 414614470800 + ACCEPT + MC + AVS Not Requested. + + +
+
+
+
+ XML end def failed_swipe_purchase_response - <<-RESPONSE - - - - - -
- 95878 - 95881 - 2409000 - 17602711 - 8 - Transaction was rejected because the track data could not be read. - 2014-05-26T10:42:44.5031513 -
-
-
-
-
- RESPONSE + <<~XML + + + + + +
+ 95878 + 95881 + 2409000 + 17602711 + 8 + Transaction was rejected because the track data could not be read. + 2014-05-26T10:42:44.5031513 +
+
+
+
+
+ XML end def successful_verify_response - <<-RESPONSE - - - - - -
- 95878 - 95881 - 2409000 - - 20153225 - 0 - Success - 2014-09-04T14:43:49.6015895 -
- - - 85 - CARD OK - 65557A - 0 - M - 424715929580 - Visa - AVS Not Requested. - Match. - - -
-
-
-
- RESPONSE + <<~XML + + + + + +
+ 95878 + 95881 + 2409000 + + 20153225 + 0 + Success + 2014-09-04T14:43:49.6015895 +
+ + + 85 + CARD OK + 65557A + 0 + M + 424715929580 + Visa + AVS Not Requested. + Match. + + +
+
+
+
+ XML end def failed_verify_response - <<-RESPONSE - - - - - -
- 95878 - 95881 - 2409000 - - 20155097 - 14 - Transaction rejected because the manually entered card number is invalid. - 2014-09-04T15:42:47.983634 -
-
-
-
-
- RESPONSE + <<~XML + + + + + +
+ 95878 + 95881 + 2409000 + + 20155097 + 14 + Transaction rejected because the manually entered card number is invalid. + 2014-09-04T15:42:47.983634 +
+
+
+
+
+ XML end def pre_scrub @@ -616,7 +1342,7 @@ def pre_scrub starting SSL for posgateway.cert.secureexchange.net:443... SSL established <- "POST /Hps.Exchange.PosGateway/PosGatewayService.asmx?wsdl HTTP/1.1\r\nContent-Type: text/xml\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: posgateway.cert.secureexchange.net\r\nContent-Length: 1295\r\n\r\n" -<- "skapi_cert_MYl2AQAowiQAbLp5JesGKh7QFkcizOP2jcX9BrEMqQ1.00YLongbobLongsen456 My StreetOttawaONK1C2N6Store Purchase1400010001111222492019123NNN" +<- "skapi_cert_MYl2AQAowiQAbLp5JesGKh7QFkcizOP2jcX9BrEMqQ1.00YLongbobLongsen456 My StreetOttawaONK1C2N6Store Purchase1400010001111222492019123NNNApplePay3DSecureEHuWW9PiBkWvqE5juRwDzAUFBAk5abc123" -> "HTTP/1.1 200 OK\r\n" -> "Cache-Control: private, max-age=0\r\n" -> "Content-Type: text/xml; charset=utf-8\r\n" @@ -646,7 +1372,7 @@ def post_scrub starting SSL for posgateway.cert.secureexchange.net:443... SSL established <- "POST /Hps.Exchange.PosGateway/PosGatewayService.asmx?wsdl HTTP/1.1\r\nContent-Type: text/xml\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: posgateway.cert.secureexchange.net\r\nContent-Length: 1295\r\n\r\n" -<- "[FILTERED]1.00YLongbobLongsen456 My StreetOttawaONK1C2N6Store Purchase1[FILTERED]92019[FILTERED]NNN" +<- "[FILTERED]1.00YLongbobLongsen456 My StreetOttawaONK1C2N6Store Purchase1[FILTERED]92019[FILTERED]NNNApplePay3DSecure[FILTERED]5abc123" -> "HTTP/1.1 200 OK\r\n" -> "Cache-Control: private, max-age=0\r\n" -> "Content-Type: text/xml; charset=utf-8\r\n" @@ -669,4 +1395,57 @@ def post_scrub } end + def pre_scrubbed_account_number + <<~PRE_SCRUBBED + opening connection to posgateway.secureexchange.net:443... + opened + starting SSL for posgateway.secureexchange.net:443... + SSL established, protocol: TLSv1.2, cipher: DHE-RSA-AES256-SHA256 + <- "POST /Hps.Exchange.PosGateway/PosGatewayService.asmx?wsdl HTTP/1.1\r\nContent-Type: text/xml\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: posgateway.secureexchange.net\r\nContent-Length: 1029\r\n\r\n" + <- "SALE12200003013579024681234SAVINGSPERSONAL20.00WEBJimSmithHot Buttered Toast IncorporatedStore Purchase1" + -> "HTTP/1.1 200 OK\r\n" + -> "Cache-Control: no-cache,no-store\r\n" + -> "Pragma: no-cache\r\n" + -> "Content-Type: text/xml; charset=utf-8\r\n" + -> "Expires: -1\r\n" + -> "X-Frame-Options: DENY\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "Strict-Transport-Security: max-age=31536000; includeSubDomains;\r\n" + -> "Date: Tue, 12 Oct 2021 15:17:29 GMT\r\n" + -> "Connection: close\r\n" + -> "Content-Length: 543\r\n" + -> "\r\n" + reading 543 bytes... + -> "
1589181322-2Authentication Error
" + read 543 bytes + Conn close + PRE_SCRUBBED + end + + def post_scrubbed_account_number + <<~POST_SCRUBBED + opening connection to posgateway.secureexchange.net:443... + opened + starting SSL for posgateway.secureexchange.net:443... + SSL established, protocol: TLSv1.2, cipher: DHE-RSA-AES256-SHA256 + <- "POST /Hps.Exchange.PosGateway/PosGatewayService.asmx?wsdl HTTP/1.1\r\nContent-Type: text/xml\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: posgateway.secureexchange.net\r\nContent-Length: 1029\r\n\r\n" + <- "SALE[FILTERED][FILTERED]1234SAVINGSPERSONAL20.00WEBJimSmithHot Buttered Toast IncorporatedStore Purchase1" + -> "HTTP/1.1 200 OK\r\n" + -> "Cache-Control: no-cache,no-store\r\n" + -> "Pragma: no-cache\r\n" + -> "Content-Type: text/xml; charset=utf-8\r\n" + -> "Expires: -1\r\n" + -> "X-Frame-Options: DENY\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "Strict-Transport-Security: max-age=31536000; includeSubDomains;\r\n" + -> "Date: Tue, 12 Oct 2021 15:17:29 GMT\r\n" + -> "Connection: close\r\n" + -> "Content-Length: 543\r\n" + -> "\r\n" + reading 543 bytes... + -> "
1589181322-2Authentication Error
" + read 543 bytes + Conn close + POST_SCRUBBED + end end diff --git a/test/unit/gateways/iats_payments_test.rb b/test/unit/gateways/iats_payments_test.rb index 0a063e2d2b4..5cb3aa396d1 100644 --- a/test/unit/gateways/iats_payments_test.rb +++ b/test/unit/gateways/iats_payments_test.rb @@ -5,18 +5,31 @@ class IatsPaymentsTest < Test::Unit::TestCase def setup @gateway = IatsPaymentsGateway.new( - :agent_code => 'login', - :password => 'password', - :region => 'uk' + agent_code: 'login', + password: 'password', + region: 'uk' ) @amount = 100 @credit_card = credit_card @check = check + @address = { + name: 'Jim Smith', + address1: '456 My Street', + address2: 'Apt 1', + company: 'Widgets Inc', + city: 'Ottawa', + state: 'ON', + zip: 'K1C2N6', + country: 'CA', + phone: '555-555-5555', + fax: '(555)555-6666', + email: 'jimsmith@example.com' + } @options = { - :ip => '71.65.249.145', - :order_id => generate_unique_id, - :billing_address => address, - :description => 'Store purchase' + ip: '71.65.249.145', + order_id: generate_unique_id, + billing_address: @address, + description: 'Store purchase' } end @@ -38,9 +51,12 @@ def test_successful_purchase assert_match(/#{@options[:billing_address][:city]}<\/city>/, data) assert_match(/#{@options[:billing_address][:state]}<\/state>/, data) assert_match(/#{@options[:billing_address][:zip]}<\/zipCode>/, data) + assert_match(/#{@options[:billing_address][:phone]}<\/phone>/, data) + assert_match(/#{@options[:billing_address][:country]}<\/country>/, data) assert_match(/1.00<\/total>/, data) assert_match(/#{@options[:description]}<\/comment>/, data) - assert_equal endpoint, 'https://www.uk.iatspayments.com/NetGate/ProcessLink.asmx?op=ProcessCreditCardV1' + assert_match(/#{@options[:billing_address][:email]}<\/email>/, data) + assert_equal endpoint, 'https://www.uk.iatspayments.com/NetGate/ProcessLinkv3.asmx?op=ProcessCreditCard' assert_equal headers['Content-Type'], 'application/soap+xml; charset=utf-8' end.respond_with(successful_purchase_response) @@ -66,7 +82,7 @@ def test_successful_check_purchase response = stub_comms do @gateway.purchase(@amount, @check, @options) end.check_request do |endpoint, data, headers| - assert_match(/login<\/agentCode>/, data) assert_match(/password<\/password>/, data) assert_match(/#{@options[:ip]}<\/customerIPAddress>/, data) @@ -79,9 +95,12 @@ def test_successful_check_purchase assert_match(/#{@options[:billing_address][:city]}<\/city>/, data) assert_match(/#{@options[:billing_address][:state]}<\/state>/, data) assert_match(/#{@options[:billing_address][:zip]}<\/zipCode>/, data) + assert_match(/#{@options[:billing_address][:phone]}<\/phone>/, data) + assert_match(/#{@options[:billing_address][:country]}<\/country>/, data) assert_match(/1.00<\/total>/, data) + assert_match(/#{@options[:billing_address][:email]}<\/email>/, data) assert_match(/#{@options[:description]}<\/comment>/, data) - assert_equal endpoint, 'https://www.uk.iatspayments.com/NetGate/ProcessLink.asmx?op=ProcessACHEFTV1' + assert_equal endpoint, 'https://www.uk.iatspayments.com/NetGate/ProcessLinkv3.asmx?op=ProcessACHEFT' assert_equal headers['Content-Type'], 'application/soap+xml; charset=utf-8' end.respond_with(successful_check_purchase_response) @@ -92,6 +111,17 @@ def test_successful_check_purchase assert_equal 'Success', response.message end + def test_successful_purchase_with_customer_details + @options[:email] = 'jsmith2@example.com' + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/#{@options[:email]}<\/email>/, data) + end.respond_with(successful_purchase_response) + + assert response + end + def test_failed_check_purchase response = stub_comms do @gateway.purchase(@amount, @check, @options) @@ -107,7 +137,7 @@ def test_failed_check_purchase def test_successful_refund response = stub_comms do @gateway.refund(@amount, '1234', @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/1234<\/transactionId>/, data) assert_match(/-1.00<\/total>/, data) end.respond_with(successful_refund_response) @@ -121,13 +151,13 @@ def test_successful_check_refund response = stub_comms do @gateway.refund(@amount, 'ref|check', @options) end.check_request do |endpoint, data, headers| - assert_match(/login<\/agentCode>/, data) assert_match(/password<\/password>/, data) assert_match(/#{@options[:ip]}<\/customerIPAddress>/, data) assert_match(/-1.00<\/total>/, data) assert_match(/#{@options[:description]}<\/comment>/, data) - assert_equal endpoint, 'https://www.uk.iatspayments.com/NetGate/ProcessLink.asmx?op=ProcessACHEFTRefundWithTransactionIdV1' + assert_equal endpoint, 'https://www.uk.iatspayments.com/NetGate/ProcessLinkv3.asmx?op=ProcessACHEFTRefundWithTransactionId' assert_equal headers['Content-Type'], 'application/soap+xml; charset=utf-8' end.respond_with(successful_check_refund_response) @@ -153,7 +183,7 @@ def test_failed_check_refund def test_failed_refund response = stub_comms do @gateway.refund(@amount, '1234', @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/1234<\/transactionId>/, data) assert_match(/-1.00<\/total>/, data) end.respond_with(failed_refund_response) @@ -166,7 +196,7 @@ def test_failed_refund def test_successful_store response = stub_comms do @gateway.store(@credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/beginDate/, data) assert_match(/endDate/, data) assert_match(%r{#{@credit_card.number}}, data) @@ -180,6 +210,18 @@ def test_successful_store assert_equal 'Success', response.message end + def test_successful_purchase_with_customer_code + response = stub_comms do + @gateway.purchase(@amount, 'CustomerCode', @options) + end.check_request do |_endpoint, data, _headers| + assert_match(%r{CustomerCode}, data) + end.respond_with(successful_purchase_response) + + assert response + assert_success response + assert_equal 'Success', response.message + end + def test_failed_store response = stub_comms do @gateway.store(@credit_card, @options) @@ -193,7 +235,7 @@ def test_failed_store def test_successful_unstore response = stub_comms do @gateway.unstore('TheAuthorization', @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(%r{TheAuthorization}, data) end.respond_with(successful_unstore_response) @@ -205,17 +247,17 @@ def test_successful_unstore def test_deprecated_options assert_deprecation_warning("The 'login' option is deprecated in favor of 'agent_code' and will be removed in a future version.") do @gateway = IatsPaymentsGateway.new( - :login => 'login', - :password => 'password' + login: 'login', + password: 'password' ) end response = stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |endpoint, data, _headers| assert_match(/login<\/agentCode>/, data) assert_match(/password<\/password>/, data) - assert_equal endpoint, 'https://www.iatspayments.com/NetGate/ProcessLink.asmx?op=ProcessCreditCardV1' + assert_equal endpoint, 'https://www.iatspayments.com/NetGate/ProcessLinkv3.asmx?op=ProcessCreditCard' end.respond_with(successful_purchase_response) assert_success response @@ -223,15 +265,15 @@ def test_deprecated_options def test_region_urls @gateway = IatsPaymentsGateway.new( - :agent_code => 'code', - :password => 'password', - :region => 'na' #North america + agent_code: 'code', + password: 'password', + region: 'na' # North america ) response = stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| - assert_equal endpoint, 'https://www.iatspayments.com/NetGate/ProcessLink.asmx?op=ProcessCreditCardV1' + end.check_request do |endpoint, _data, _headers| + assert_equal endpoint, 'https://www.iatspayments.com/NetGate/ProcessLinkv3.asmx?op=ProcessCreditCard' end.respond_with(successful_purchase_response) assert_success response @@ -264,196 +306,196 @@ def test_supports_scrubbing? private def successful_purchase_response - <<-XML - - - - - - - Success - - - OK - - 04/22/2014 - 04/23/2014 - A6DE6F24 - - - - - - + <<~XML + + + + + + + Success + + + OK + + 04/22/2014 + 04/23/2014 + A6DE6F24 + + + + + + XML end def failed_purchase_response - <<-XML - - - - - - - Success - - - REJECT: 15 - - 04/22/2014 - 04/23/2014 - A6DE6F24 - - - - - - + <<~XML + + + + + + + Success + + + REJECT: 15 + + 04/22/2014 + 04/23/2014 + A6DE6F24 + + + + + + XML end def successful_check_purchase_response - <<-XML - - - - - - - Success - - - OK: 555555 - - A7F8B8B3 - - - - - - + <<~XML + + + + + + + Success + + + OK: 555555 + + A7F8B8B3 + + + + + + XML end def failed_check_purchase_response - <<-XML - - - - - - - Success - - - REJECT: 40 - - - - - - - - + <<~XML + + + + + + + Success + + + REJECT: 40 + + + + + + + + XML end def successful_refund_response - <<-XML - - - - - - - Success - - - OK: 678594: - - 04/22/2014 - 04/23/2014 - A6DEA654 - - - - - - + <<~XML + + + + + + + Success + + + OK: 678594: + + 04/22/2014 + 04/23/2014 + A6DEA654 + + + + + + XML end def successful_check_refund_response - <<-XML - - - - - - - Success - - - OK: 555555 - - A7F8B8B3 - - - - - - + <<~XML + + + + + + + Success + + + OK: 555555 + + A7F8B8B3 + + + + + + XML end def failed_check_refund_response - <<-XML - - - - - - - Success - - - REJECT: 39 - - 06/11/2015 - 06/12/2015 - - - - - - - + <<~XML + + + + + + + Success + + + REJECT: 39 + + 06/11/2015 + 06/12/2015 + + + + + + + XML end def failed_refund_response - <<-XML - - - - - - - Success - - - REJECT: 15 - - 04/22/2014 - 04/23/2014 - A6DEA654 - - - - - - + <<~XML + + + + + + + Success + + + REJECT: 15 + + 04/22/2014 + 04/23/2014 + A6DEA654 + + + + + + XML end @@ -462,8 +504,8 @@ def successful_store_response - - + + Success @@ -472,8 +514,8 @@ def successful_store_response A12181132 - - + + XML @@ -484,8 +526,8 @@ def failed_store_response - - + + Success @@ -494,8 +536,8 @@ def failed_store_response - - + + XML @@ -506,8 +548,8 @@ def successful_unstore_response - - + + Success @@ -516,8 +558,8 @@ def successful_unstore_response "A12181132" is deleted - - + + XML @@ -528,16 +570,16 @@ def failed_connection_response - - + + Failure Server Error - - + + XML @@ -549,8 +591,8 @@ def pre_scrub opened starting SSL for www.iatspayments.com:443... SSL established - <- "POST /NetGate/ProcessLink.asmx?op=ProcessCreditCardV1 HTTP/1.1\r\nContent-Type: application/soap+xml; charset=utf-8\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: www.iatspayments.com\r\nContent-Length: 779\r\n\r\n" - <- "TEST88TEST8863b5dd7098e8e3a9ff9a6f0992fdb6d51.00LongbobLongsen422222222222222009/17123VISA
456 My Street
OttawaONK1C2N6Store purchase
" + <- "POST /NetGate/ProcessLink.asmx?op=ProcessCreditCard HTTP/1.1\r\nContent-Type: application/soap+xml; charset=utf-8\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: www.iatspayments.com\r\nContent-Length: 779\r\n\r\n" + <- "TEST88TEST8863b5dd7098e8e3a9ff9a6f0992fdb6d51.00LongbobLongsen422222222222222009/17123VISA
456 My Street
OttawaONK1C2N6Store purchase
" -> "HTTP/1.1 200 OK\r\n" -> "Cache-Control: private, max-age=0\r\n" -> "Content-Type: application/soap+xml; charset=utf-8\r\n" @@ -562,7 +604,7 @@ def pre_scrub -> "Via: 1.1 sjc1-10\r\n" -> "\r\n" reading 719 bytes... - -> "Success OK: 678594:\n 09/28/2016\n 09/29/2016\nA92E3B72\n" + -> "Success OK: 678594:\n 09/28/2016\n 09/29/2016\nA92E3B72\n" read 719 bytes Conn close XML @@ -574,8 +616,8 @@ def post_scrub opened starting SSL for www.iatspayments.com:443... SSL established - <- "POST /NetGate/ProcessLink.asmx?op=ProcessCreditCardV1 HTTP/1.1\r\nContent-Type: application/soap+xml; charset=utf-8\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: www.iatspayments.com\r\nContent-Length: 779\r\n\r\n" - <- "[FILTERED][FILTERED]63b5dd7098e8e3a9ff9a6f0992fdb6d51.00LongbobLongsen[FILTERED]09/17[FILTERED]VISA
456 My Street
OttawaONK1C2N6Store purchase
" + <- "POST /NetGate/ProcessLink.asmx?op=ProcessCreditCard HTTP/1.1\r\nContent-Type: application/soap+xml; charset=utf-8\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: www.iatspayments.com\r\nContent-Length: 779\r\n\r\n" + <- "[FILTERED][FILTERED]63b5dd7098e8e3a9ff9a6f0992fdb6d51.00LongbobLongsen[FILTERED]09/17[FILTERED]VISA
456 My Street
OttawaONK1C2N6Store purchase
" -> "HTTP/1.1 200 OK\r\n" -> "Cache-Control: private, max-age=0\r\n" -> "Content-Type: application/soap+xml; charset=utf-8\r\n" @@ -587,7 +629,7 @@ def post_scrub -> "Via: 1.1 sjc1-10\r\n" -> "\r\n" reading 719 bytes... - -> "Success OK: 678594:\n 09/28/2016\n 09/29/2016\nA92E3B72\n" + -> "Success OK: 678594:\n 09/28/2016\n 09/29/2016\nA92E3B72\n" read 719 bytes Conn close XML diff --git a/test/unit/gateways/in_context_paypal_express_test.rb b/test/unit/gateways/in_context_paypal_express_test.rb index 4760bf50e5b..30bf8efffcc 100644 --- a/test/unit/gateways/in_context_paypal_express_test.rb +++ b/test/unit/gateways/in_context_paypal_express_test.rb @@ -8,9 +8,9 @@ class InContextPaypalExpressTest < Test::Unit::TestCase def setup @gateway = InContextPaypalExpressGateway.new( - :login => 'cody', - :password => 'test', - :pem => 'PEM' + login: 'cody', + password: 'test', + pem: 'PEM' ) Base.mode = :test @@ -40,4 +40,3 @@ def test_test_redirect_url_without_review assert_equal TEST_REDIRECT_URL_WITHOUT_REVIEW, @gateway.redirect_url_for('1234567890', review: false) end end - diff --git a/test/unit/gateways/inspire_test.rb b/test/unit/gateways/inspire_test.rb index 1bb0ea565e8..230db39427c 100644 --- a/test/unit/gateways/inspire_test.rb +++ b/test/unit/gateways/inspire_test.rb @@ -5,12 +5,12 @@ class InspireTest < Test::Unit::TestCase def setup @gateway = InspireGateway.new( - :login => 'LOGIN', - :password => 'PASSWORD' + login: 'LOGIN', + password: 'PASSWORD' ) @credit_card = credit_card('4242424242424242') @amount = 100 - @options = { :billing_address => address } + @options = { billing_address: address } end def test_successful_purchase @@ -60,8 +60,8 @@ def test_failed_refund def test_add_address result = {} - @gateway.send(:add_address, result, nil, :billing_address => {:address1 => '164 Waverley Street', :country => 'US', :state => 'CO'} ) - assert_equal ['address1', 'city', 'company', 'country', 'phone', 'state', 'zip'], result.stringify_keys.keys.sort + @gateway.send(:add_address, result, nil, billing_address: { address1: '164 Waverley Street', country: 'US', state: 'CO' }) + assert_equal %w[address1 city company country phone state zip], result.stringify_keys.keys.sort assert_equal 'CO', result[:state] assert_equal '164 Waverley Street', result[:address1] assert_equal 'US', result[:country] @@ -72,32 +72,32 @@ def test_supported_countries end def test_supported_card_types - assert_equal [:visa, :master, :american_express], InspireGateway.supported_cardtypes + assert_equal %i[visa master american_express], InspireGateway.supported_cardtypes end def test_adding_store_adds_vault_id_flag result = {} - @gateway.send(:add_creditcard, result, @credit_card, :store => true) - assert_equal ['ccexp', 'ccnumber', 'customer_vault', 'cvv', 'firstname', 'lastname'], result.stringify_keys.keys.sort + @gateway.send(:add_creditcard, result, @credit_card, store: true) + assert_equal %w[ccexp ccnumber customer_vault cvv firstname lastname], result.stringify_keys.keys.sort assert_equal 'add_customer', result[:customer_vault] end def test_blank_store_doesnt_add_vault_flag result = {} - @gateway.send(:add_creditcard, result, @credit_card, {} ) - assert_equal ['ccexp', 'ccnumber', 'cvv', 'firstname', 'lastname'], result.stringify_keys.keys.sort + @gateway.send(:add_creditcard, result, @credit_card, {}) + assert_equal %w[ccexp ccnumber cvv firstname lastname], result.stringify_keys.keys.sort assert_nil result[:customer_vault] end def test_accept_check post = {} - check = Check.new(:name => 'Fred Bloggs', - :routing_number => '111000025', - :account_number => '123456789012', - :account_holder_type => 'personal', - :account_type => 'checking') + check = Check.new(name: 'Fred Bloggs', + routing_number: '111000025', + account_number: '123456789012', + account_holder_type: 'personal', + account_type: 'checking') @gateway.send(:add_check, post, check) assert_equal %w[account_holder_type account_type checkaba checkaccount checkname payment], post.stringify_keys.keys.sort end @@ -140,4 +140,3 @@ def failed_refund_response 'response=3&responsetext=Invalid Transaction ID specified REFID:3150951931&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=refund&response_code=300' end end - diff --git a/test/unit/gateways/instapay_test.rb b/test/unit/gateways/instapay_test.rb index 2f8af425809..bc58c48efea 100644 --- a/test/unit/gateways/instapay_test.rb +++ b/test/unit/gateways/instapay_test.rb @@ -2,7 +2,7 @@ class InstapayTest < Test::Unit::TestCase def setup - @gateway = InstapayGateway.new(:login => 'TEST0') + @gateway = InstapayGateway.new(login: 'TEST0') @credit_card = credit_card @amount = 100 end @@ -11,7 +11,7 @@ def test_successful_purchase @gateway.expects(:ssl_post).returns(successful_purchase_response) assert response = @gateway.purchase(@amount, @credit_card) - assert_instance_of Response, response + assert_instance_of Response, response assert_success response assert_equal '118583850', response.authorization end @@ -29,7 +29,7 @@ def test_successful_auth @gateway.expects(:ssl_post).returns(successful_purchase_response) assert response = @gateway.authorize(@amount, @credit_card) - assert_instance_of Response, response + assert_instance_of Response, response assert_success response assert_equal '118583850', response.authorization end @@ -42,31 +42,31 @@ def test_unsuccessful_auth assert_failure response assert_nil response.authorization end - + def test_avs_result @gateway.expects(:ssl_post).returns(successful_purchase_response) - + response = @gateway.purchase(@amount, @credit_card) assert_equal 'X', response.avs_result['code'] end - + def test_cvv_result @gateway.expects(:ssl_post).returns(successful_purchase_response) - + response = @gateway.purchase(@amount, @credit_card) assert_equal 'M', response.cvv_result['code'] end - + def test_successful_capture @gateway.expects(:ssl_post).returns(successful_capture_response) - + response = @gateway.capture(100, '123456') assert_equal InstapayGateway::SUCCESS_MESSAGE, response.message end - + def test_failed_capture @gateway.expects(:ssl_post).returns(failed_capture_response) - + response = @gateway.capture(100, '123456') assert_equal 'Post amount exceeds Auth amount', response.message end @@ -82,7 +82,7 @@ def successful_purchase_response def failed_purchase_response "\r\nDeclined=DECLINED:0720930009:CVV2 MISMATCH:N7\r\nhistoryid=118583848\r\norderid=92886713\r\nACCOUNTNUMBER=************2220\r\nDeclined=DECLINED:0720930009:CVV2 MISMATCH:N7\r\nhistoryid=118583848\r\norderid=92886713\r\nrcode=0720930009\r\nReason=DECLINED:0720930009:CVV2 MISMATCH:N7\r\nrecurid=0\r\nresult=0\r\nStatus=Declined\r\ntransid=80410586\r\n" end - + def successful_auth_response "<html><body><plaintext>\r\nAccepted=AUTH:TEST:::118585994:::\r\nhistoryid=118585994\r\norderid=92888143\r\nAccepted=AUTH:TEST:::118585994:::\r\nACCOUNTNUMBER=************5454\r\nauthcode=TEST\r\nAuthNo=AUTH:TEST:::118585994:::\r\nhistoryid=118585994\r\norderid=92888143\r\nrecurid=0\r\nrefcode=118585994-TEST\r\nresult=1\r\nStatus=Accepted\r\ntransid=0\r\n" end @@ -90,13 +90,12 @@ def successful_auth_response def failed_auth_response "<html><body><plaintext>\r\nDeclined=DECLINED:0720930009:CVV2 MISMATCH:N7\r\nhistoryid=118585991\r\norderid=92888142\r\nACCOUNTNUMBER=************2220\r\nDeclined=DECLINED:0720930009:CVV2 MISMATCH:N7\r\nhistoryid=118585991\r\norderid=92888142\r\nrcode=0720930009\r\nReason=DECLINED:0720930009:CVV2 MISMATCH:N7\r\nrecurid=0\r\nresult=0\r\nStatus=Declined\r\ntransid=80412271\r\n" end - + def successful_capture_response "<html><body><plaintext>\r\nAccepted=AVSAUTH:TEST:::121609962::::DUPLICATE\r\nhistoryid=121609962\r\norderid=95009583\r\nAccepted=AVSAUTH:TEST:::121609962::::DUPLICATE\r\nACCOUNTNUMBER=************5454\r\nauthcode=TEST\r\nAuthNo=AVSAUTH:TEST:::121609962::::DUPLICATE\r\nDUPLICATE=1\r\nhistoryid=121609962\r\norderid=95009583\r\nrecurid=0\r\nrefcode=121609962-TEST\r\nresult=1\r\nStatus=Accepted\r\ntransid=0\r\n" end - + def failed_capture_response "<html><body><plaintext>\r\nDeclined=DECLINED:1101450002:Post amount exceeds Auth amount:\r\nhistoryid=\r\norderid=\r\nDeclined=DECLINED:1101450002:Post amount exceeds Auth amount:\r\nrcode=1101450002\r\nReason=DECLINED:1101450002:Post amount exceeds Auth amount:\r\nresult=0\r\nStatus=Declined\r\ntransid=0\r\n" end end - diff --git a/test/unit/gateways/ipg_test.rb b/test/unit/gateways/ipg_test.rb new file mode 100644 index 00000000000..738bd4564f3 --- /dev/null +++ b/test/unit/gateways/ipg_test.rb @@ -0,0 +1,802 @@ +require 'test_helper' + +class IpgTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = IpgGateway.new(fixtures(:ipg)) + @gateway_ma = IpgGateway.new(fixtures(:ipg_ma)) + @credit_card = credit_card + @amount = 100 + + @options = { + currency: 'ARS' + } + end + + def test_successful_purchase + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + 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 + end + + def test_successful_transaction_with_single_digit_card + @credit_card.month = 3 + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + doc = REXML::Document.new(data) + assert_match('03', REXML::XPath.first(doc, '//v1:CreditCardData//v1:ExpMonth').text) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_stored_credentials + stored_credential = { + initial_transaction: true, + reason_type: '', + initiator: 'merchant', + network_transaction_id: nil + } + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge({ stored_credential:, order_id: '123' })) + end.check_request do |_endpoint, data, _headers| + doc = REXML::Document.new(data) + assert_match('FIRST', REXML::XPath.first(doc, '//v1:recurringType').text) + end.respond_with(successful_purchase_response) + + stored_credential = { + initial_transaction: false, + reason_type: '', + initiator: 'merchant', + network_transaction_id: response.params['IpgTransactionId'] + } + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge({ stored_credential:, order_id: '123' })) + end.check_request do |_endpoint, data, _headers| + doc = REXML::Document.new(data) + assert_match('REPEAT', REXML::XPath.first(doc, '//v1:recurringType').text) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_submerchant + submerchant = { + mcc: '6513', + legal_name: 'KINDERLAND', + address: { + address1: 'ALVARADO 494', + address2: 'Street 2', + zip: '1704', + city: 'BUENOS AIRES', + state: 'BUENOS AIRES', + country: 'ARG' + }, + document: { + type: 'SINGLE_CODE_OF_LABOR_IDENTIFICATION', + number: '30710655479' + }, + merchant_id: '12345678' + } + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge({ submerchant: })) + end.check_request do |_endpoint, data, _headers| + doc = REXML::Document.new(data) + assert_match(submerchant[:mcc], REXML::XPath.first(doc, '//v1:SubMerchant//v1:Mcc').text) + assert_match(submerchant[:legal_name], REXML::XPath.first(doc, '//v1:SubMerchant//v1:LegalName').text) + assert_match(submerchant[:address][:address1], REXML::XPath.first(doc, '//v1:SubMerchant//v1:Address//v1:Address1').text) + assert_match(submerchant[:address][:address2], REXML::XPath.first(doc, '//v1:SubMerchant//v1:Address//v1:Address2').text) + assert_match(submerchant[:address][:zip], REXML::XPath.first(doc, '//v1:SubMerchant//v1:Address//v1:Zip').text) + assert_match(submerchant[:address][:city], REXML::XPath.first(doc, '//v1:SubMerchant//v1:Address//v1:City').text) + assert_match(submerchant[:address][:state], REXML::XPath.first(doc, '//v1:SubMerchant//v1:Address//v1:State').text) + assert_match(submerchant[:address][:country], REXML::XPath.first(doc, '//v1:SubMerchant//v1:Address//v1:Country').text) + assert_match(submerchant[:document][:type], REXML::XPath.first(doc, '//v1:SubMerchant//v1:Document//v1:Type').text) + assert_match(submerchant[:document][:number], REXML::XPath.first(doc, '//v1:SubMerchant//v1:Document//v1:Number').text) + assert_match(submerchant[:merchant_id], REXML::XPath.first(doc, '//v1:SubMerchant//v1:MerchantID').text) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_recurring_type + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge({ recurring_type: 'FIRST' })) + end.check_request do |_endpoint, data, _headers| + doc = REXML::Document.new(data) + assert_match('FIRST', REXML::XPath.first(doc, '//v1:recurringType').text) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_store_id + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge({ store_id: '1234' })) + end.check_request do |_endpoint, data, _headers| + doc = REXML::Document.new(data) + assert_match('1234', REXML::XPath.first(doc, '//v1:StoreId').text) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_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' + + response = stub_comms do + @gateway.purchase(@amount, payment_token, @options) + end.check_request do |_endpoint, data, _headers| + doc = REXML::Document.new(data) + assert_match(payment_token, REXML::XPath.first(doc, '//v1:Payment//v1:HostedDataID').text) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_3ds2 + three_d_options = { + three_d_secure: { + version: '2.1.0', + cavv: 'jEET5Odser3oCRAyNTY5BVgAAAA=', + xid: 'jHDMyjJJF9bLBCFT/YUbqMhoQ0s=', + directory_response_status: 'Y', + authentication_response_status: 'Y', + ds_transaction_id: '925a0317-9143-5130-8000-0000000f8742' + } + } + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(three_d_options)) + end.check_request do |_endpoint, data, _headers| + doc = REXML::Document.new(data) + assert_match(three_d_options[:three_d_secure][:cavv], REXML::XPath.first(doc, '//v1:CreditCard3DSecure//v1:AuthenticationValue').text) + assert_match(three_d_options[:three_d_secure][:version], REXML::XPath.first(doc, '//v1:CreditCard3DSecure//v1:Secure3DProtocolVersion').text) + assert_match(three_d_options[:three_d_secure][:xid], REXML::XPath.first(doc, '//v1:CreditCard3DSecure//v1:XID').text) + assert_match(three_d_options[:three_d_secure][:authentication_response_status], REXML::XPath.first(doc, '//v1:CreditCard3DSecure//v1:Secure3D2AuthenticationResponse').text) + assert_match(three_d_options[:three_d_secure][:ds_transaction_id], REXML::XPath.first(doc, '//v1:CreditCard3DSecure//v1:DirectoryServerTransactionId').text) + assert_match(three_d_options[:three_d_secure][:directory_response_status], REXML::XPath.first(doc, '//v1:CreditCard3DSecure//v1:Secure3D2TransactionStatus').text) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_failed_purchase + @gateway.expects(:ssl_post).returns(failed_purchase_response) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_match 'DECLINED', response.message + end + + def test_successful_authorize + order_id = generate_unique_id + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge!({ order_id: })) + end.check_request do |_endpoint, data, _headers| + doc = REXML::Document.new(data) + assert_match('preAuth', REXML::XPath.first(doc, '//v1:CreditCardTxType//v1:Type').text) + assert_match(order_id, REXML::XPath.first(doc, '//v1:TransactionDetails//v1:OrderId').text) + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_failed_authorize + @gateway.expects(:ssl_post).returns(failed_authorize_response) + + response = @gateway.authorize(@amount, @credit_card, @options.merge!({ order_id: 'ORD03' })) + assert_failure response + assert_match 'FAILED', response.message + end + + def test_successful_capture + order_id = generate_unique_id + response = stub_comms do + @gateway.capture(@amount, order_id, @options) + end.check_request do |_endpoint, data, _headers| + doc = REXML::Document.new(data) + assert_match('postAuth', REXML::XPath.first(doc, '//v1:CreditCardTxType//v1:Type').text) + assert_match(order_id, REXML::XPath.first(doc, '//v1:TransactionDetails//v1:OrderId').text) + end.respond_with(successful_capture_response) + + assert_success response + end + + def test_failed_capture + @gateway.expects(:ssl_post).returns(failed_capture_response) + + response = @gateway.capture(@amount, '123', @options) + assert_failure response + assert_match 'FAILED', response.message + end + + def test_successful_refund + order_id = generate_unique_id + response = stub_comms do + @gateway.refund(@amount, order_id, @options) + end.check_request do |_endpoint, data, _headers| + doc = REXML::Document.new(data) + assert_match('return', REXML::XPath.first(doc, '//v1:CreditCardTxType//v1:Type').text) + assert_match(order_id, REXML::XPath.first(doc, '//v1:TransactionDetails//v1:OrderId').text) + end.respond_with(successful_refund_response) + + assert_success response + end + + def test_failed_refund + @gateway.expects(:ssl_post).returns(failed_refund_response) + + response = @gateway.refund(@amount, '123', @options) + assert_failure response + assert_match 'FAILED', response.message + end + + def test_successful_void + order_id = generate_unique_id + response = stub_comms do + @gateway.void(order_id, @options) + end.check_request do |_endpoint, data, _headers| + doc = REXML::Document.new(data) + assert_match('void', REXML::XPath.first(doc, '//v1:CreditCardTxType//v1:Type').text) + assert_match(order_id, REXML::XPath.first(doc, '//v1:TransactionDetails//v1:OrderId').text) + end.respond_with(successful_void_response) + + assert_success response + end + + def test_failed_void + @gateway.expects(:ssl_post).returns(failed_void_response) + + response = @gateway.void('', @options) + assert_failure response + assert_match 'FAILED', response.message + end + + def test_successful_verify + response = stub_comms do + @gateway.verify(@credit_card) + end.check_request do |_endpoint, data, _headers| + doc = REXML::Document.new(data) + assert_match('032', REXML::XPath.first(doc, '//v1:Payment//v1:Currency').text) if REXML::XPath.first(doc, '//v1:CreditCardTxType//v1:Type')&.text == 'preAuth' + end.respond_with(successful_authorize_response, successful_void_response) + assert_success response + end + + def test_successful_verify_with_currency_code + response = stub_comms do + @gateway.verify(@credit_card, { currency: 'ARS' }) + end.check_request do |_endpoint, data, _headers| + doc = REXML::Document.new(data) + assert_match('032', REXML::XPath.first(doc, '//v1:Payment//v1:Currency').text) if REXML::XPath.first(doc, '//v1:CreditCardTxType//v1:Type')&.text == 'preAuth' + end.respond_with(successful_authorize_response, successful_void_response) + assert_success response + end + + def test_successful_verify_with_failed_void + response = stub_comms do + @gateway.verify(@credit_card, @options) + end.respond_with(successful_authorize_response, failed_void_response) + assert_success response + end + + def test_failed_verify + response = stub_comms do + @gateway.verify(@credit_card, @options) + end.respond_with(failed_authorize_response, successful_void_response) + assert_failure response + end + + def test_successful_store + payment_token = generate_unique_id + response = stub_comms do + @gateway.store(@credit_card, @options.merge!({ hosted_data_id: payment_token })) + end.check_request do |_endpoint, data, _headers| + doc = REXML::Document.new(data) + assert_match(payment_token, REXML::XPath.first(doc, '//ns2:HostedDataID').text) + end.respond_with(successful_store_response) + + assert_success response + end + + def test_successful_unstore + payment_token = generate_unique_id + response = stub_comms do + @gateway.unstore(payment_token) + end.check_request do |_endpoint, data, _headers| + doc = REXML::Document.new(data) + assert_match(payment_token, REXML::XPath.first(doc, '//ns2:HostedDataID').text) + end.respond_with(successful_store_response) + + assert_success response + end + + def test_failed_store + @gateway.expects(:ssl_post).returns(failed_store_response) + + response = @gateway.store(@credit_card, @options.merge!({ hosted_data_id: '123' })) + assert_failure response + assert_equal response.params['tpv_error_code'], 'SGSDAS-020300' + assert !response.params['tpv_error_msg'].nil? + end + + def test_scrub + assert @gateway.supports_scrubbing? + 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 + <<~RESPONSE + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> + <SOAP-ENV:Header/> + <SOAP-ENV:Body> + <ipgapi:IPGApiOrderResponse xmlns:a1="http://ipg-online.com/ipgapi/schemas/a1" xmlns:ipgapi="http://ipg-online.com/ipgapi/schemas/ipgapi" xmlns:v1="http://ipg-online.com/ipgapi/schemas/v1"> + <ipgapi:ApprovalCode>Y:019349:4578600880:PPXX:0193497665</ipgapi:ApprovalCode> + <ipgapi:AVSResponse>PPX</ipgapi:AVSResponse> + <ipgapi:Brand>MASTERCARD</ipgapi:Brand> + <ipgapi:Country>ARG</ipgapi:Country> + <ipgapi:CommercialServiceProvider>FDCS</ipgapi:CommercialServiceProvider> + <ipgapi:ExternalMerchantID>5921102002</ipgapi:ExternalMerchantID> + <ipgapi:OrderId>A-5e3d7dc2-0454-4d60-aae8-7edf35eb28c7</ipgapi:OrderId> + <ipgapi:IpgTransactionId>84578600880</ipgapi:IpgTransactionId> + <ipgapi:PaymentType>CREDITCARD</ipgapi:PaymentType> + <ipgapi:ProcessorApprovalCode>019349</ipgapi:ProcessorApprovalCode> + <ipgapi:ProcessorReceiptNumber>7665</ipgapi:ProcessorReceiptNumber> + <ipgapi:ProcessorBatchNumber>090</ipgapi:ProcessorBatchNumber> + <ipgapi:ProcessorEndpointID>TXSP ARGENTINA VIA CAFEX VISA</ipgapi:ProcessorEndpointID> + <ipgapi:ProcessorCCVResponse>X</ipgapi:ProcessorCCVResponse> + <ipgapi:ProcessorReferenceNumber>019349019349</ipgapi:ProcessorReferenceNumber> + <ipgapi:ProcessorResponseCode>00</ipgapi:ProcessorResponseCode> + <ipgapi:ProcessorResponseMessage>Function performed error-free</ipgapi:ProcessorResponseMessage> + <ipgapi:ProcessorTraceNumber>019349</ipgapi:ProcessorTraceNumber> + <ipgapi:TDate>1635149370</ipgapi:TDate> + <ipgapi:TDateFormatted>2021.10.25 10:09:30 (CEST)</ipgapi:TDateFormatted> + <ipgapi:TerminalID>98000000</ipgapi:TerminalID> + <ipgapi:TransactionResult>APPROVED</ipgapi:TransactionResult> + <ipgapi:TransactionTime>1635149370</ipgapi:TransactionTime> + </ipgapi:IPGApiOrderResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> + RESPONSE + end + + def failed_purchase_response + <<~RESPONSE + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> + <SOAP-ENV:Header/> + <SOAP-ENV:Body> + <SOAP-ENV:Fault> + <faultcode>SOAP-ENV:Client</faultcode> + <faultstring xml:lang="en">ProcessingException</faultstring> + <detail> + <ipgapi:IPGApiOrderResponse xmlns:a1="http://ipg-online.com/ipgapi/schemas/a1" xmlns:ipgapi="http://ipg-online.com/ipgapi/schemas/ipgapi" xmlns:v1="http://ipg-online.com/ipgapi/schemas/v1"> + <ipgapi:ApprovalCode>N:05:Do not honour</ipgapi:ApprovalCode> + <ipgapi:AVSResponse>PPX</ipgapi:AVSResponse> + <ipgapi:Brand>VISA</ipgapi:Brand> + <ipgapi:CommercialServiceProvider>FDCS</ipgapi:CommercialServiceProvider> + <ipgapi:ErrorMessage>SGS-050005: Do not honour</ipgapi:ErrorMessage> + <ipgapi:ExternalMerchantID>5921102002</ipgapi:ExternalMerchantID> + <ipgapi:OrderId>A-5c70b8fc-43d8-40f4-93de-46590dbf6d01</ipgapi:OrderId> + <ipgapi:IpgTransactionId>84578606308</ipgapi:IpgTransactionId> + <ipgapi:PaymentType>CREDITCARD</ipgapi:PaymentType> + <ipgapi:ProcessorReceiptNumber>7668</ipgapi:ProcessorReceiptNumber> + <ipgapi:ProcessorBatchNumber>090</ipgapi:ProcessorBatchNumber> + <ipgapi:ProcessorEndpointID>TXSP ARGENTINA VIA CAFEX VISA</ipgapi:ProcessorEndpointID> + <ipgapi:ProcessorCCVResponse>X</ipgapi:ProcessorCCVResponse> + <ipgapi:ProcessorResponseCode>05</ipgapi:ProcessorResponseCode> + <ipgapi:ProcessorResponseMessage>Do not honour</ipgapi:ProcessorResponseMessage> + <ipgapi:ProcessorTraceNumber>034209</ipgapi:ProcessorTraceNumber> + <ipgapi:TDate>1635152461</ipgapi:TDate> + <ipgapi:TDateFormatted>2021.10.25 11:01:01 (CEST)</ipgapi:TDateFormatted> + <ipgapi:TerminalID>98000000</ipgapi:TerminalID> + <ipgapi:TransactionResult>DECLINED</ipgapi:TransactionResult> + <ipgapi:TransactionTime>1635152461</ipgapi:TransactionTime> + </ipgapi:IPGApiOrderResponse> + </detail> + </SOAP-ENV:Fault> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> + RESPONSE + end + + def successful_authorize_response + <<~RESPONSE + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> + <SOAP-ENV:Header/> + <SOAP-ENV:Body> + <ipgapi:IPGApiOrderResponse xmlns:a1="http://ipg-online.com/ipgapi/schemas/a1" xmlns:ipgapi="http://ipg-online.com/ipgapi/schemas/ipgapi" xmlns:v1="http://ipg-online.com/ipgapi/schemas/v1"> + <ipgapi:ApprovalCode>Y:014593:4578595466:PPXX:0145937641</ipgapi:ApprovalCode> + <ipgapi:AVSResponse>PPX</ipgapi:AVSResponse> + <ipgapi:Brand>MASTERCARD</ipgapi:Brand> + <ipgapi:Country>ARG</ipgapi:Country> + <ipgapi:CommercialServiceProvider>FDCS</ipgapi:CommercialServiceProvider> + <ipgapi:ExternalMerchantID>5921102002</ipgapi:ExternalMerchantID> + <ipgapi:OrderId>ORD02</ipgapi:OrderId> + <ipgapi:IpgTransactionId>84578595466</ipgapi:IpgTransactionId> + <ipgapi:PaymentType>CREDITCARD</ipgapi:PaymentType> + <ipgapi:ProcessorApprovalCode>014593</ipgapi:ProcessorApprovalCode> + <ipgapi:ProcessorReceiptNumber>7641</ipgapi:ProcessorReceiptNumber> + <ipgapi:ProcessorBatchNumber>090</ipgapi:ProcessorBatchNumber> + <ipgapi:ProcessorEndpointID>TXSP ARGENTINA VIA CAFEX VISA</ipgapi:ProcessorEndpointID> + <ipgapi:ProcessorCCVResponse>X</ipgapi:ProcessorCCVResponse> + <ipgapi:ProcessorReferenceNumber>014593014593</ipgapi:ProcessorReferenceNumber> + <ipgapi:ProcessorResponseCode>00</ipgapi:ProcessorResponseCode> + <ipgapi:ProcessorResponseMessage>Function performed error-free</ipgapi:ProcessorResponseMessage> + <ipgapi:ProcessorTraceNumber>014593</ipgapi:ProcessorTraceNumber> + <ipgapi:TDate>1635146125</ipgapi:TDate> + <ipgapi:TDateFormatted>2021.10.25 09:15:25 (CEST)</ipgapi:TDateFormatted> + <ipgapi:TerminalID>98000000</ipgapi:TerminalID> + <ipgapi:TransactionResult>APPROVED</ipgapi:TransactionResult> + <ipgapi:TransactionTime>1635146125</ipgapi:TransactionTime> + </ipgapi:IPGApiOrderResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> + RESPONSE + end + + def failed_authorize_response + <<~RESPONSE + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> + <SOAP-ENV:Header/> + <SOAP-ENV:Body> + <SOAP-ENV:Fault> + <faultcode>SOAP-ENV:Client</faultcode> + <faultstring xml:lang="en">ProcessingException</faultstring> + <detail> + <ipgapi:IPGApiOrderResponse xmlns:a1="http://ipg-online.com/ipgapi/schemas/a1" xmlns:ipgapi="http://ipg-online.com/ipgapi/schemas/ipgapi" xmlns:v1="http://ipg-online.com/ipgapi/schemas/v1"> + <ipgapi:ApprovalCode>N:-5003:The order already exists in the database.</ipgapi:ApprovalCode> + <ipgapi:ErrorMessage>SGS-005003: The order already exists in the database.</ipgapi:ErrorMessage> + <ipgapi:OrderId>ORD03</ipgapi:OrderId> + <ipgapi:TDate>1635156782</ipgapi:TDate> + <ipgapi:TDateFormatted>2021.10.25 12:13:02 (CEST)</ipgapi:TDateFormatted> + <ipgapi:TransactionResult>FAILED</ipgapi:TransactionResult> + <ipgapi:TransactionTime>1635156782</ipgapi:TransactionTime> + <ipgapi:Secure3DResponse/> + </ipgapi:IPGApiOrderResponse> + </detail> + </SOAP-ENV:Fault> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> + RESPONSE + end + + def successful_capture_response + <<~RESPONSE + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> + <SOAP-ENV:Header/> + <SOAP-ENV:Body> + <ipgapi:IPGApiOrderResponse xmlns:a1="http://ipg-online.com/ipgapi/schemas/a1" xmlns:ipgapi="http://ipg-online.com/ipgapi/schemas/ipgapi" xmlns:v1="http://ipg-online.com/ipgapi/schemas/v1"> + <ipgapi:ApprovalCode>Y:034747:4578608047:PPXX:0347477672</ipgapi:ApprovalCode> + <ipgapi:AVSResponse>PPX</ipgapi:AVSResponse> + <ipgapi:Brand>MASTERCARD</ipgapi:Brand> + <ipgapi:Country>ARG</ipgapi:Country> + <ipgapi:CommercialServiceProvider>FDCS</ipgapi:CommercialServiceProvider> + <ipgapi:ExternalMerchantID>5921102002</ipgapi:ExternalMerchantID> + <ipgapi:OrderId>ORD04</ipgapi:OrderId> + <ipgapi:IpgTransactionId>84578608047</ipgapi:IpgTransactionId> + <ipgapi:PaymentType>CREDITCARD</ipgapi:PaymentType> + <ipgapi:ProcessorApprovalCode>034747</ipgapi:ProcessorApprovalCode> + <ipgapi:ProcessorReceiptNumber>7672</ipgapi:ProcessorReceiptNumber> + <ipgapi:ProcessorBatchNumber>090</ipgapi:ProcessorBatchNumber> + <ipgapi:ProcessorEndpointID>TXSP ARGENTINA VIA CAFEX VISA</ipgapi:ProcessorEndpointID> + <ipgapi:ProcessorCCVResponse>X</ipgapi:ProcessorCCVResponse> + <ipgapi:ProcessorReferenceNumber>034747034747</ipgapi:ProcessorReferenceNumber> + <ipgapi:ProcessorResponseCode>00</ipgapi:ProcessorResponseCode> + <ipgapi:ProcessorResponseMessage>Function performed error-free</ipgapi:ProcessorResponseMessage> + <ipgapi:ProcessorTraceNumber>034747</ipgapi:ProcessorTraceNumber> + <ipgapi:ReferencedTDate>1635157266</ipgapi:ReferencedTDate> + <ipgapi:TDate>1635157275</ipgapi:TDate> + <ipgapi:TDateFormatted>2021.10.25 12:21:15 (CEST)</ipgapi:TDateFormatted> + <ipgapi:TerminalID>98000000</ipgapi:TerminalID> + <ipgapi:TransactionResult>APPROVED</ipgapi:TransactionResult> + <ipgapi:TransactionTime>1635157275</ipgapi:TransactionTime> + </ipgapi:IPGApiOrderResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> + RESPONSE + end + + def failed_capture_response + <<~RESPONSE + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> + <SOAP-ENV:Header/> + <SOAP-ENV:Body> + <SOAP-ENV:Fault> + <faultcode>SOAP-ENV:Client</faultcode> + <faultstring xml:lang="en">ProcessingException</faultstring> + <detail> + <ipgapi:IPGApiOrderResponse xmlns:a1="http://ipg-online.com/ipgapi/schemas/a1" xmlns:ipgapi="http://ipg-online.com/ipgapi/schemas/ipgapi" xmlns:v1="http://ipg-online.com/ipgapi/schemas/v1"> + <ipgapi:ApprovalCode>N:-5008:Order does not exist.</ipgapi:ApprovalCode> + <ipgapi:CommercialServiceProvider>FDCS</ipgapi:CommercialServiceProvider> + <ipgapi:ErrorMessage>SGS-005008: Order does not exist.</ipgapi:ErrorMessage> + <ipgapi:OrderId>ORD090</ipgapi:OrderId> + <ipgapi:IpgTransactionId>84578608161</ipgapi:IpgTransactionId> + <ipgapi:PaymentType>CREDITCARD</ipgapi:PaymentType> + <ipgapi:TransactionResult>FAILED</ipgapi:TransactionResult> + <ipgapi:TransactionTime>1635157307</ipgapi:TransactionTime> + </ipgapi:IPGApiOrderResponse> + </detail> + </SOAP-ENV:Fault> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> + RESPONSE + end + + def successful_refund_response + <<~RESPONSE + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> + <SOAP-ENV:Header/> + <SOAP-ENV:Body> + <ipgapi:IPGApiOrderResponse xmlns:a1="http://ipg-online.com/ipgapi/schemas/a1" xmlns:ipgapi="http://ipg-online.com/ipgapi/schemas/ipgapi" xmlns:v1="http://ipg-online.com/ipgapi/schemas/v1"> + <ipgapi:ApprovalCode>Y:034889:4578608244:PPXX:0348897676</ipgapi:ApprovalCode> + <ipgapi:AVSResponse>PPX</ipgapi:AVSResponse> + <ipgapi:Brand>MASTERCARD</ipgapi:Brand> + <ipgapi:Country>ARG</ipgapi:Country> + <ipgapi:CommercialServiceProvider>FDCS</ipgapi:CommercialServiceProvider> + <ipgapi:ExternalMerchantID>5921102002</ipgapi:ExternalMerchantID> + <ipgapi:OrderId>A-8b75ffc2-95dd-4861-a91b-9c3816075f82</ipgapi:OrderId> + <ipgapi:IpgTransactionId>84578608244</ipgapi:IpgTransactionId> + <ipgapi:PaymentType>CREDITCARD</ipgapi:PaymentType> + <ipgapi:ProcessorApprovalCode>034889</ipgapi:ProcessorApprovalCode> + <ipgapi:ProcessorReceiptNumber>7676</ipgapi:ProcessorReceiptNumber> + <ipgapi:ProcessorBatchNumber>090</ipgapi:ProcessorBatchNumber> + <ipgapi:ProcessorEndpointID>TXSP ARGENTINA VIA CAFEX VISA</ipgapi:ProcessorEndpointID> + <ipgapi:ProcessorCCVResponse>X</ipgapi:ProcessorCCVResponse> + <ipgapi:ProcessorReferenceNumber>034889034889</ipgapi:ProcessorReferenceNumber> + <ipgapi:ProcessorResponseCode>00</ipgapi:ProcessorResponseCode> + <ipgapi:ProcessorResponseMessage>Function performed error-free</ipgapi:ProcessorResponseMessage> + <ipgapi:ProcessorTraceNumber>034889</ipgapi:ProcessorTraceNumber> + <ipgapi:ReferencedTDate>1635157480</ipgapi:ReferencedTDate> + <ipgapi:TDate>1635157594</ipgapi:TDate> + <ipgapi:TDateFormatted>2021.10.25 12:26:34 (CEST)</ipgapi:TDateFormatted> + <ipgapi:TerminalID>98000000</ipgapi:TerminalID> + <ipgapi:TransactionResult>APPROVED</ipgapi:TransactionResult> + <ipgapi:TransactionTime>1635157594</ipgapi:TransactionTime> + </ipgapi:IPGApiOrderResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> + RESPONSE + end + + def failed_refund_response + <<~RESPONSE + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> + <SOAP-ENV:Header/> + <SOAP-ENV:Body> + <SOAP-ENV:Fault> + <faultcode>SOAP-ENV:Client</faultcode> + <faultstring xml:lang="en">ProcessingException</faultstring> + <detail> + <ipgapi:IPGApiOrderResponse xmlns:a1="http://ipg-online.com/ipgapi/schemas/a1" xmlns:ipgapi="http://ipg-online.com/ipgapi/schemas/ipgapi" xmlns:v1="http://ipg-online.com/ipgapi/schemas/v1"> + <ipgapi:ApprovalCode>N:-5008:Order does not exist.</ipgapi:ApprovalCode> + <ipgapi:CommercialServiceProvider>FDCS</ipgapi:CommercialServiceProvider> + <ipgapi:ErrorMessage>SGS-005008: Order does not exist.</ipgapi:ErrorMessage> + <ipgapi:OrderId>182</ipgapi:OrderId> + <ipgapi:IpgTransactionId>84578608249</ipgapi:IpgTransactionId> + <ipgapi:PaymentType>CREDITCARD</ipgapi:PaymentType> + <ipgapi:TransactionResult>FAILED</ipgapi:TransactionResult> + <ipgapi:TransactionTime>1635157647</ipgapi:TransactionTime> + </ipgapi:IPGApiOrderResponse> + </detail> + </SOAP-ENV:Fault> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> + RESPONSE + end + + def successful_void_response + <<~RESPONSE + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> + <SOAP-ENV:Header/> + <SOAP-ENV:Body> + <ipgapi:IPGApiOrderResponse xmlns:a1="http://ipg-online.com/ipgapi/schemas/a1" xmlns:ipgapi="http://ipg-online.com/ipgapi/schemas/ipgapi" xmlns:v1="http://ipg-online.com/ipgapi/schemas/v1"> + <ipgapi:ApprovalCode>Y:035631:4578609369:PPXX:0356317745</ipgapi:ApprovalCode> + <ipgapi:AVSResponse>PPX</ipgapi:AVSResponse> + <ipgapi:Brand>MASTERCARD</ipgapi:Brand> + <ipgapi:Country>ARG</ipgapi:Country> + <ipgapi:CommercialServiceProvider>FDCS</ipgapi:CommercialServiceProvider> + <ipgapi:ExternalMerchantID>5921102002</ipgapi:ExternalMerchantID> + <ipgapi:OrderId>ORD07</ipgapi:OrderId> + <ipgapi:IpgTransactionId>84578609369</ipgapi:IpgTransactionId> + <ipgapi:PaymentType>CREDITCARD</ipgapi:PaymentType> + <ipgapi:ProcessorApprovalCode>035631</ipgapi:ProcessorApprovalCode> + <ipgapi:ProcessorReceiptNumber>7745</ipgapi:ProcessorReceiptNumber> + <ipgapi:ProcessorBatchNumber>090</ipgapi:ProcessorBatchNumber> + <ipgapi:ProcessorEndpointID>TXSP ARGENTINA VIA CAFEX VISA</ipgapi:ProcessorEndpointID> + <ipgapi:ProcessorCCVResponse>X</ipgapi:ProcessorCCVResponse> + <ipgapi:ProcessorReferenceNumber>035631035631</ipgapi:ProcessorReferenceNumber> + <ipgapi:ProcessorResponseCode>00</ipgapi:ProcessorResponseCode> + <ipgapi:ProcessorResponseMessage>Function performed error-free</ipgapi:ProcessorResponseMessage> + <ipgapi:ProcessorTraceNumber>035631</ipgapi:ProcessorTraceNumber> + <ipgapi:ReferencedTDate>1635158863</ipgapi:ReferencedTDate> + <ipgapi:TDate>1635158884</ipgapi:TDate> + <ipgapi:TDateFormatted>2021.10.25 12:48:04 (CEST)</ipgapi:TDateFormatted> + <ipgapi:TerminalID>98000000</ipgapi:TerminalID> + <ipgapi:TransactionResult>APPROVED</ipgapi:TransactionResult> + <ipgapi:TransactionTime>1635158884</ipgapi:TransactionTime> + </ipgapi:IPGApiOrderResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> + RESPONSE + end + + def failed_void_response + <<~RESPONSE + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> + <SOAP-ENV:Header/> + <SOAP-ENV:Body> + <SOAP-ENV:Fault> + <faultcode>SOAP-ENV:Client</faultcode> + <faultstring xml:lang="en">ProcessingException</faultstring> + <detail> + <ipgapi:IPGApiOrderResponse xmlns:a1="http://ipg-online.com/ipgapi/schemas/a1" xmlns:ipgapi="http://ipg-online.com/ipgapi/schemas/ipgapi" xmlns:v1="http://ipg-online.com/ipgapi/schemas/v1"> + <ipgapi:ApprovalCode>N:-5019:Transaction not voidable</ipgapi:ApprovalCode> + <ipgapi:CommercialServiceProvider>FDCS</ipgapi:CommercialServiceProvider> + <ipgapi:ErrorMessage>SGS-005019: The transaction to be voided is not voidable</ipgapi:ErrorMessage> + <ipgapi:OrderId>ORD07</ipgapi:OrderId> + <ipgapi:IpgTransactionId>84578609426</ipgapi:IpgTransactionId> + <ipgapi:PaymentType>CREDITCARD</ipgapi:PaymentType> + <ipgapi:TDate>1635158863</ipgapi:TDate> + <ipgapi:TDateFormatted>2021.10.25 12:47:43 (CEST)</ipgapi:TDateFormatted> + <ipgapi:TransactionResult>FAILED</ipgapi:TransactionResult> + <ipgapi:TransactionTime>1635158863</ipgapi:TransactionTime> + </ipgapi:IPGApiOrderResponse> + </detail> + </SOAP-ENV:Fault> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> + RESPONSE + end + + def successful_store_response + <<~RESPONSE + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> + <SOAP-ENV:Header/> + <SOAP-ENV:Body> + <ipgapi:IPGApiActionResponse xmlns:a1="http://ipg-online.com/ipgapi/schemas/a1" xmlns:ipgapi="http://ipg-online.com/ipgapi/schemas/ipgapi" xmlns:v1="http://ipg-online.com/ipgapi/schemas/v1"> + <ipgapi:successfully>true</ipgapi:successfully> + </ipgapi:IPGApiActionResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> + RESPONSE + end + + def failed_store_response + <<~RESPONSE + <ns4:IPGApiActionResponse xmlns:ns4="http://ipg-online.com/ipgapi/schemas/ipgapi" xmlns:ns2="http://ipg-online.com/ipgapi/schemas/a1" xmlns:ns3="http://ipg-online.com/ipgapi/schemas/v1"> + <ns4:successfully>true</ns4:successfully> + <ns2:Error Code="SGSDAS-020300"> + <ns2:ErrorMessage> + Could not store the hosted data id: + 691c7cb3-a752-4d6d-abde-83cad63de258. + Reason: An internal error has occured while + processing your request + </ns2:ErrorMessage> + </ns2:Error> + </ns4:IPGApiActionResponse> + RESPONSE + end + + def pre_scrubbed + <<-PRE_SCRUBBED + opening connection to test.ipg-online.com:443... + opened + 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 V1M1OTIxMTAyMDAyLl8uMTpuOU1DXTJzO25m\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" + <- "<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:ipg=\"http://ipg-online.com/ipgapi/schemas/ipgapi\" xmlns:v1=\"http://ipg-online.com/ipgapi/schemas/v1\">\n <soapenv:Header/>\n <soapenv:Body>\n <ipg:IPGApiOrderRequest>\n <v1:Transaction>\n <v1:CreditCardTxType>\n <v1:StoreId>5921102002</v1:StoreId>\n <v1:Type>sale</v1:Type>\n </v1:CreditCardTxType>\n<v1:CreditCardData>\n <v1:CardNumber>5165850000000008</v1:CardNumber>\n <v1:ExpMonth>12</v1:ExpMonth>\n <v1:ExpYear>22</v1:ExpYear>\n <v1:CardCodeValue>123</v1:CardCodeValue>\n</v1:CreditCardData>\n<v1:Payment>\n <v1:ChargeTotal>100</v1:ChargeTotal>\n <v1:Currency>032</v1:Currency>\n</v1:Payment>\n<v1:TransactionDetails>\n</v1:TransactionDetails>\n </v1:Transaction>\n </ipg:IPGApiOrderRequest>\n </soapenv:Body>\n</soapenv:Envelope>\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" + -> "X-XSS-Protection: 1; mode=block\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "Cache-Control: no-cache, no-store, must-revalidate\r\n" + -> "Pragma: no-cache\r\n" + -> "X-Frame-Options: SAMEORIGIN\r\n" + -> "Content-Security-Policy: default-src 'self' *.googleapis.com *.klarna.com *.masterpass.com *.mastercard.com *.npci.org.in 'unsafe-eval' 'unsafe-inline'; frame-ancestors 'self'\r\n" + -> "Accept: text/xml, text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2\r\n" + -> "SOAPAction: \"\"\r\n" + -> "Expires: 0\r\n" + -> "Content-Type: text/xml;charset=utf-8\r\n" + -> "Content-Length: 1808\r\n" + -> "Set-Cookie: JSESSIONID=08B9B3093F010FFB653B645616E0A258.dc; Path=/ipgapi; Secure; HttpOnly;HttpOnly;Secure;SameSite=Lax\r\n" + -> "Connection: close\r\n" + -> "Set-Cookie: TS0108ee57=0167ad6846753d9e71cb1e6ee74e68d3fd44879a5754a362817ba3e6f52bd01c4c794c29e5cd962b66ea0104c43957e17bc40d819c; Path=/\r\n" + -> "Set-Cookie: TS01c97684=0167ad6846d1db53410992975f8e679ecc1ec0624e54a362817ba3e6f52bd01c4c794c29e5a3f3b525308fafc99af65129fab2b19ce5715c3f475bc6c349b8428ffd87beac; path=/ipgapi\r\n" + -> "\r\n" + reading 1808 bytes... + -> "" + -> "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\"><SOAP-ENV:Header/><SOAP-ENV:Body><ipgapi:IPGApiOrderResponse xmlns:a1=\"http://ipg-online.com/ipgapi/schemas/a1\" xmlns:ipgapi=\"http://ipg-online.com/ipgapi/schemas/ipgapi\" xmlns:v1=\"http://ipg-online.com/ipgapi/schemas/v1\"><ipgapi:ApprovalCode>Y:334849:4579259603:PPXX:3348490741</ipgapi:ApprovalCode><ipgapi:AVSResponse>PPX</ipgapi:AVSResponse><ipgapi:Brand>MASTERCARD</ipgapi:Brand><ipgapi:Country>ARG</ipgapi:Country><ipgapi:CommercialServiceProvider>FDCS</ipgapi:CommercialServiceProvider><ipgapi:ExternalMerchantID>5921102002</ipgapi:ExternalMerchantID><ipgapi:OrderId>A-2e68e140-6024-41bb-b49c-a92d4984ae01</ipgapi:OrderId><ipgapi:IpgTransactionId>84579259603</ipgapi:IpgTransactionId><ipgapi:PaymentType>CREDITCARD</ipgapi:PaymentType><ipgapi:ProcessorApprovalCode>334849</ipgapi:ProcessorApprovalCode><ipgapi:ProcessorReceiptNumber>0741</ipgapi:ProcessorReceiptNumber><ipgapi:ProcessorBatchNumber>090</ipgapi:ProcessorBatchNumber><ipgapi:ProcessorEndpointID>TXSP ARGENTINA VIA CAFEX VISA</ipgapi:ProcessorEndpointID><ipgapi:ProcessorCCVResponse>X</ipgapi:ProcessorCCVResponse><ipgapi:ProcessorReferenceNumber>334849334849</ipgapi:ProcessorReferenceNumber><ipgapi:ProcessorResponseCode>00</ipgapi:ProcessorResponseCode><ipgapi:ProcessorResponseMessage>Function performed error-free</ipgapi:ProcessorResponseMessage><ipgapi:ProcessorTraceNumber>334849</ipgapi:ProcessorTraceNumber><ipgapi:TDate>1635535883</ipgapi:TDate><ipgapi:TDateFormatted>2021.10.29 21:31:23 (CEST)</ipgapi:TDateFormatted><ipgapi:TerminalID>98000000</ipgapi:TerminalID><ipgapi:TransactionResult>APPROVED</ipgapi:TransactionResult><ipgapi:TransactionTime>1635535883</ipgapi:TransactionTime></ipgapi:IPGApiOrderResponse></SOAP-ENV:Body></SOAP-ENV:Envelope>" + read 1808 bytes + Conn close + PRE_SCRUBBED + end + + def post_scrubbed + <<-POST_SCRUBBED + opening connection to test.ipg-online.com:443... + opened + 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" + <- "<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:ipg=\"http://ipg-online.com/ipgapi/schemas/ipgapi\" xmlns:v1=\"http://ipg-online.com/ipgapi/schemas/v1\">\n <soapenv:Header/>\n <soapenv:Body>\n <ipg:IPGApiOrderRequest>\n <v1:Transaction>\n <v1:CreditCardTxType>\n <v1:StoreId>5921102002</v1:StoreId>\n <v1:Type>sale</v1:Type>\n </v1:CreditCardTxType>\n<v1:CreditCardData>\n <v1:CardNumber>[FILTERED]</v1:CardNumber>\n <v1:ExpMonth>12</v1:ExpMonth>\n <v1:ExpYear>22</v1:ExpYear>\n <v1:CardCodeValue>[FILTERED]</v1:CardCodeValue>\n</v1:CreditCardData>\n<v1:Payment>\n <v1:ChargeTotal>100</v1:ChargeTotal>\n <v1:Currency>032</v1:Currency>\n</v1:Payment>\n<v1:TransactionDetails>\n</v1:TransactionDetails>\n </v1:Transaction>\n </ipg:IPGApiOrderRequest>\n </soapenv:Body>\n</soapenv:Envelope>\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" + -> "X-XSS-Protection: 1; mode=block\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "Cache-Control: no-cache, no-store, must-revalidate\r\n" + -> "Pragma: no-cache\r\n" + -> "X-Frame-Options: SAMEORIGIN\r\n" + -> "Content-Security-Policy: default-src 'self' *.googleapis.com *.klarna.com *.masterpass.com *.mastercard.com *.npci.org.in 'unsafe-eval' 'unsafe-inline'; frame-ancestors 'self'\r\n" + -> "Accept: text/xml, text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2\r\n" + -> "SOAPAction: \"\"\r\n" + -> "Expires: 0\r\n" + -> "Content-Type: text/xml;charset=utf-8\r\n" + -> "Content-Length: 1808\r\n" + -> "Set-Cookie: JSESSIONID=08B9B3093F010FFB653B645616E0A258.dc; Path=/ipgapi; Secure; HttpOnly;HttpOnly;Secure;SameSite=Lax\r\n" + -> "Connection: close\r\n" + -> "Set-Cookie: TS0108ee57=0167ad6846753d9e71cb1e6ee74e68d3fd44879a5754a362817ba3e6f52bd01c4c794c29e5cd962b66ea0104c43957e17bc40d819c; Path=/\r\n" + -> "Set-Cookie: TS01c97684=0167ad6846d1db53410992975f8e679ecc1ec0624e54a362817ba3e6f52bd01c4c794c29e5a3f3b525308fafc99af65129fab2b19ce5715c3f475bc6c349b8428ffd87beac; path=/ipgapi\r\n" + -> "\r\n" + reading 1808 bytes... + -> "" + -> "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\"><SOAP-ENV:Header/><SOAP-ENV:Body><ipgapi:IPGApiOrderResponse xmlns:a1=\"http://ipg-online.com/ipgapi/schemas/a1\" xmlns:ipgapi=\"http://ipg-online.com/ipgapi/schemas/ipgapi\" xmlns:v1=\"http://ipg-online.com/ipgapi/schemas/v1\"><ipgapi:ApprovalCode>Y:334849:4579259603:PPXX:3348490741</ipgapi:ApprovalCode><ipgapi:AVSResponse>PPX</ipgapi:AVSResponse><ipgapi:Brand>MASTERCARD</ipgapi:Brand><ipgapi:Country>ARG</ipgapi:Country><ipgapi:CommercialServiceProvider>FDCS</ipgapi:CommercialServiceProvider><ipgapi:ExternalMerchantID>5921102002</ipgapi:ExternalMerchantID><ipgapi:OrderId>A-2e68e140-6024-41bb-b49c-a92d4984ae01</ipgapi:OrderId><ipgapi:IpgTransactionId>84579259603</ipgapi:IpgTransactionId><ipgapi:PaymentType>CREDITCARD</ipgapi:PaymentType><ipgapi:ProcessorApprovalCode>334849</ipgapi:ProcessorApprovalCode><ipgapi:ProcessorReceiptNumber>0741</ipgapi:ProcessorReceiptNumber><ipgapi:ProcessorBatchNumber>090</ipgapi:ProcessorBatchNumber><ipgapi:ProcessorEndpointID>TXSP ARGENTINA VIA CAFEX VISA</ipgapi:ProcessorEndpointID><ipgapi:ProcessorCCVResponse>X</ipgapi:ProcessorCCVResponse><ipgapi:ProcessorReferenceNumber>334849334849</ipgapi:ProcessorReferenceNumber><ipgapi:ProcessorResponseCode>00</ipgapi:ProcessorResponseCode><ipgapi:ProcessorResponseMessage>Function performed error-free</ipgapi:ProcessorResponseMessage><ipgapi:ProcessorTraceNumber>334849</ipgapi:ProcessorTraceNumber><ipgapi:TDate>1635535883</ipgapi:TDate><ipgapi:TDateFormatted>2021.10.29 21:31:23 (CEST)</ipgapi:TDateFormatted><ipgapi:TerminalID>98000000</ipgapi:TerminalID><ipgapi:TransactionResult>APPROVED</ipgapi:TransactionResult><ipgapi:TransactionTime>1635535883</ipgapi:TransactionTime></ipgapi:IPGApiOrderResponse></SOAP-ENV:Body></SOAP-ENV:Envelope>" + read 1808 bytes + Conn close + POST_SCRUBBED + end +end diff --git a/test/unit/gateways/ipp_test.rb b/test/unit/gateways/ipp_test.rb index f338efad9e8..0174a7b7a93 100644 --- a/test/unit/gateways/ipp_test.rb +++ b/test/unit/gateways/ipp_test.rb @@ -6,7 +6,7 @@ class IppTest < Test::Unit::TestCase def setup @gateway = IppGateway.new( username: 'username', - password: 'password', + password: 'password' ) @amount = 100 @@ -16,7 +16,7 @@ def setup def test_successful_purchase response = stub_comms do @gateway.purchase(@amount, @credit_card, order_id: 1) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(%r{<SubmitSinglePayment }, data) assert_match(%r{<UserName>username<}, data) assert_match(%r{<Password>password<}, data) @@ -48,7 +48,7 @@ def test_failed_purchase def test_successful_authorize response = stub_comms do @gateway.authorize(@amount, @credit_card, order_id: 1) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(%r{<SubmitSinglePayment }, data) assert_match(%r{<CustRef>1<}, data) assert_match(%r{<TrnType>2<}, data) @@ -61,7 +61,7 @@ def test_successful_authorize def test_successful_capture response = stub_comms do @gateway.capture(@amount, 'receipt') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(%r{<SubmitSingleCapture }, data) assert_match(%r{<Receipt>receipt<}, data) assert_match(%r{<Amount>100<}, data) @@ -73,7 +73,7 @@ def test_successful_capture def test_successful_refund response = stub_comms do @gateway.refund(@amount, 'receipt') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(%r{<SubmitSingleRefund }, data) assert_match(%r{<Receipt>receipt<}, data) assert_match(%r{<Amount>100<}, data) @@ -90,122 +90,122 @@ def test_scrub private def pre_scrubbed - <<-'PRE_SCRUBBED' -opening connection to demo.ippayments.com.au:443... -opened -starting SSL for demo.ippayments.com.au:443... -SSL established -<- "POST /interface/api/dts.asmx HTTP/1.1\r\nContent-Type: text/xml; charset=utf-8\r\nSoapaction: http://www.ippayments.com.au/interface/api/dts/SubmitSinglePayment\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: demo.ippayments.com.au\r\nContent-Length: 822\r\n\r\n" -<- "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<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/\">\n <soap:Body>\n <SubmitSinglePayment xmlns=\"http://www.ippayments.com.au/interface/api/dts\">\n <trnXML>\n <![CDATA[<Transaction>\n <CustRef>1</CustRef>\n <Amount/>\n <TrnType>1</TrnType>\n <CreditCard Registered=\"False\">\n <CardNumber>4005550000000001</CardNumber>\n <ExpM>09</ExpM>\n <ExpY>2015</ExpY>\n <CVN>123</CVN>\n <CardHolderName>Longbob Longsen</CardHolderName>\n </CreditCard>\n <Security>\n <UserName>nmi.api</UserName>\n <Password>qwerty123</Password>\n </Security>\n <TrnSource/>\n</Transaction>\n]]>\n </trnXML>\n </SubmitSinglePayment>\n </soap:Body>\n</soap:Envelope>\n" --> "HTTP/1.1 200 OK\r\n" --> "Server: Microsoft-IIS/6.0\r\n" --> "X-Robots-Tag: noindex\r\n" --> "X-Powered-By: ASP.NET\r\n" --> "Cache-Control: private, max-age=0\r\n" --> "Content-Type: text/xml; charset=utf-8\r\n" --> "Content-Length: 767\r\n" --> "Date: Fri, 19 Dec 2014 19:55:13 GMT\r\n" --> "Connection: close\r\n" --> "\r\n" -reading 767 bytes... --> "<?xml version=\"1.0\" encoding=\"utf-8\"?><soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"><soap:Body><SubmitSinglePaymentResponse xmlns=\"http://www.ippayments.com.au/interface/api/dts\"><SubmitSinglePaymentResult>&lt;Response&gt;\r\n\t&lt;ResponseCode&gt;1&lt;/ResponseCode&gt;\r\n\t&lt;Timestamp&gt;20-Dec-2014 06:55:17&lt;/Timestamp&gt;\r\n\t&lt;Receipt&gt;&lt;/Receipt&gt;\r\n\t&lt;SettlementDate&gt;&lt;/SettlementDate&gt;\r\n\t&lt;DeclinedCode&gt;183&lt;/DeclinedCode&gt;\r\n\t&lt;DeclinedMessage&gt;Exception parsing transaction XML&lt;/DeclinedMessage&gt;\r\n&lt;/Response&gt;\r\n</SubmitSinglePaymentResult></SubmitSinglePaymentResponse></soap:Body></soap:Envelope>" -read 767 bytes -Conn close + <<~'PRE_SCRUBBED' + opening connection to demo.ippayments.com.au:443... + opened + starting SSL for demo.ippayments.com.au:443... + SSL established + <- "POST /interface/api/dts.asmx HTTP/1.1\r\nContent-Type: text/xml; charset=utf-8\r\nSoapaction: http://www.ippayments.com.au/interface/api/dts/SubmitSinglePayment\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: demo.ippayments.com.au\r\nContent-Length: 822\r\n\r\n" + <- "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<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/\">\n <soap:Body>\n <SubmitSinglePayment xmlns=\"http://www.ippayments.com.au/interface/api/dts\">\n <trnXML>\n <![CDATA[<Transaction>\n <CustRef>1</CustRef>\n <Amount/>\n <TrnType>1</TrnType>\n <CreditCard Registered=\"False\">\n <CardNumber>4005550000000001</CardNumber>\n <ExpM>09</ExpM>\n <ExpY>2015</ExpY>\n <CVN>123</CVN>\n <CardHolderName>Longbob Longsen</CardHolderName>\n </CreditCard>\n <Security>\n <UserName>nmi.api</UserName>\n <Password>qwerty123</Password>\n </Security>\n <TrnSource/>\n</Transaction>\n]]>\n </trnXML>\n </SubmitSinglePayment>\n </soap:Body>\n</soap:Envelope>\n" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: Microsoft-IIS/6.0\r\n" + -> "X-Robots-Tag: noindex\r\n" + -> "X-Powered-By: ASP.NET\r\n" + -> "Cache-Control: private, max-age=0\r\n" + -> "Content-Type: text/xml; charset=utf-8\r\n" + -> "Content-Length: 767\r\n" + -> "Date: Fri, 19 Dec 2014 19:55:13 GMT\r\n" + -> "Connection: close\r\n" + -> "\r\n" + reading 767 bytes... + -> "<?xml version=\"1.0\" encoding=\"utf-8\"?><soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"><soap:Body><SubmitSinglePaymentResponse xmlns=\"http://www.ippayments.com.au/interface/api/dts\"><SubmitSinglePaymentResult>&lt;Response&gt;\r\n\t&lt;ResponseCode&gt;1&lt;/ResponseCode&gt;\r\n\t&lt;Timestamp&gt;20-Dec-2014 06:55:17&lt;/Timestamp&gt;\r\n\t&lt;Receipt&gt;&lt;/Receipt&gt;\r\n\t&lt;SettlementDate&gt;&lt;/SettlementDate&gt;\r\n\t&lt;DeclinedCode&gt;183&lt;/DeclinedCode&gt;\r\n\t&lt;DeclinedMessage&gt;Exception parsing transaction XML&lt;/DeclinedMessage&gt;\r\n&lt;/Response&gt;\r\n</SubmitSinglePaymentResult></SubmitSinglePaymentResponse></soap:Body></soap:Envelope>" + read 767 bytes + Conn close PRE_SCRUBBED end def post_scrubbed - <<-'POST_SCRUBBED' -opening connection to demo.ippayments.com.au:443... -opened -starting SSL for demo.ippayments.com.au:443... -SSL established -<- "POST /interface/api/dts.asmx HTTP/1.1\r\nContent-Type: text/xml; charset=utf-8\r\nSoapaction: http://www.ippayments.com.au/interface/api/dts/SubmitSinglePayment\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: demo.ippayments.com.au\r\nContent-Length: 822\r\n\r\n" -<- "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<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/\">\n <soap:Body>\n <SubmitSinglePayment xmlns=\"http://www.ippayments.com.au/interface/api/dts\">\n <trnXML>\n <![CDATA[<Transaction>\n <CustRef>1</CustRef>\n <Amount/>\n <TrnType>1</TrnType>\n <CreditCard Registered=\"False\">\n <CardNumber>[FILTERED]</CardNumber>\n <ExpM>09</ExpM>\n <ExpY>2015</ExpY>\n <CVN>[FILTERED]</CVN>\n <CardHolderName>Longbob Longsen</CardHolderName>\n </CreditCard>\n <Security>\n <UserName>nmi.api</UserName>\n <Password>[FILTERED]</Password>\n </Security>\n <TrnSource/>\n</Transaction>\n]]>\n </trnXML>\n </SubmitSinglePayment>\n </soap:Body>\n</soap:Envelope>\n" --> "HTTP/1.1 200 OK\r\n" --> "Server: Microsoft-IIS/6.0\r\n" --> "X-Robots-Tag: noindex\r\n" --> "X-Powered-By: ASP.NET\r\n" --> "Cache-Control: private, max-age=0\r\n" --> "Content-Type: text/xml; charset=utf-8\r\n" --> "Content-Length: 767\r\n" --> "Date: Fri, 19 Dec 2014 19:55:13 GMT\r\n" --> "Connection: close\r\n" --> "\r\n" -reading 767 bytes... --> "<?xml version=\"1.0\" encoding=\"utf-8\"?><soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"><soap:Body><SubmitSinglePaymentResponse xmlns=\"http://www.ippayments.com.au/interface/api/dts\"><SubmitSinglePaymentResult>&lt;Response&gt;\r\n\t&lt;ResponseCode&gt;1&lt;/ResponseCode&gt;\r\n\t&lt;Timestamp&gt;20-Dec-2014 06:55:17&lt;/Timestamp&gt;\r\n\t&lt;Receipt&gt;&lt;/Receipt&gt;\r\n\t&lt;SettlementDate&gt;&lt;/SettlementDate&gt;\r\n\t&lt;DeclinedCode&gt;183&lt;/DeclinedCode&gt;\r\n\t&lt;DeclinedMessage&gt;Exception parsing transaction XML&lt;/DeclinedMessage&gt;\r\n&lt;/Response&gt;\r\n</SubmitSinglePaymentResult></SubmitSinglePaymentResponse></soap:Body></soap:Envelope>" -read 767 bytes -Conn close + <<~'POST_SCRUBBED' + opening connection to demo.ippayments.com.au:443... + opened + starting SSL for demo.ippayments.com.au:443... + SSL established + <- "POST /interface/api/dts.asmx HTTP/1.1\r\nContent-Type: text/xml; charset=utf-8\r\nSoapaction: http://www.ippayments.com.au/interface/api/dts/SubmitSinglePayment\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: demo.ippayments.com.au\r\nContent-Length: 822\r\n\r\n" + <- "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<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/\">\n <soap:Body>\n <SubmitSinglePayment xmlns=\"http://www.ippayments.com.au/interface/api/dts\">\n <trnXML>\n <![CDATA[<Transaction>\n <CustRef>1</CustRef>\n <Amount/>\n <TrnType>1</TrnType>\n <CreditCard Registered=\"False\">\n <CardNumber>[FILTERED]</CardNumber>\n <ExpM>09</ExpM>\n <ExpY>2015</ExpY>\n <CVN>[FILTERED]</CVN>\n <CardHolderName>Longbob Longsen</CardHolderName>\n </CreditCard>\n <Security>\n <UserName>nmi.api</UserName>\n <Password>[FILTERED]</Password>\n </Security>\n <TrnSource/>\n</Transaction>\n]]>\n </trnXML>\n </SubmitSinglePayment>\n </soap:Body>\n</soap:Envelope>\n" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: Microsoft-IIS/6.0\r\n" + -> "X-Robots-Tag: noindex\r\n" + -> "X-Powered-By: ASP.NET\r\n" + -> "Cache-Control: private, max-age=0\r\n" + -> "Content-Type: text/xml; charset=utf-8\r\n" + -> "Content-Length: 767\r\n" + -> "Date: Fri, 19 Dec 2014 19:55:13 GMT\r\n" + -> "Connection: close\r\n" + -> "\r\n" + reading 767 bytes... + -> "<?xml version=\"1.0\" encoding=\"utf-8\"?><soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"><soap:Body><SubmitSinglePaymentResponse xmlns=\"http://www.ippayments.com.au/interface/api/dts\"><SubmitSinglePaymentResult>&lt;Response&gt;\r\n\t&lt;ResponseCode&gt;1&lt;/ResponseCode&gt;\r\n\t&lt;Timestamp&gt;20-Dec-2014 06:55:17&lt;/Timestamp&gt;\r\n\t&lt;Receipt&gt;&lt;/Receipt&gt;\r\n\t&lt;SettlementDate&gt;&lt;/SettlementDate&gt;\r\n\t&lt;DeclinedCode&gt;183&lt;/DeclinedCode&gt;\r\n\t&lt;DeclinedMessage&gt;Exception parsing transaction XML&lt;/DeclinedMessage&gt;\r\n&lt;/Response&gt;\r\n</SubmitSinglePaymentResult></SubmitSinglePaymentResponse></soap:Body></soap:Envelope>" + read 767 bytes + Conn close POST_SCRUBBED end def successful_purchase_response - <<-XML -<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><SubmitSinglePaymentResponse xmlns="http://www.ippayments.com.au/interface/api/dts"><SubmitSinglePaymentResult>&lt;Response&gt; - &lt;ResponseCode&gt;0&lt;/ResponseCode&gt; - &lt;Timestamp&gt;20-Dec-2014 04:07:39&lt;/Timestamp&gt; - &lt;Receipt&gt;89435577&lt;/Receipt&gt; - &lt;SettlementDate&gt;22-Dec-2014&lt;/SettlementDate&gt; - &lt;DeclinedCode&gt;&lt;/DeclinedCode&gt; - &lt;DeclinedMessage&gt;&lt;/DeclinedMessage&gt; -&lt;/Response&gt; -</SubmitSinglePaymentResult></SubmitSinglePaymentResponse></soap:Body></soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><SubmitSinglePaymentResponse xmlns="http://www.ippayments.com.au/interface/api/dts"><SubmitSinglePaymentResult>&lt;Response&gt; + &lt;ResponseCode&gt;0&lt;/ResponseCode&gt; + &lt;Timestamp&gt;20-Dec-2014 04:07:39&lt;/Timestamp&gt; + &lt;Receipt&gt;89435577&lt;/Receipt&gt; + &lt;SettlementDate&gt;22-Dec-2014&lt;/SettlementDate&gt; + &lt;DeclinedCode&gt;&lt;/DeclinedCode&gt; + &lt;DeclinedMessage&gt;&lt;/DeclinedMessage&gt; + &lt;/Response&gt; + </SubmitSinglePaymentResult></SubmitSinglePaymentResponse></soap:Body></soap:Envelope> XML end def failed_purchase_response - <<-XML -<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><SubmitSinglePaymentResponse xmlns="http://www.ippayments.com.au/interface/api/dts"><SubmitSinglePaymentResult>&lt;Response&gt; - &lt;ResponseCode&gt;1&lt;/ResponseCode&gt; - &lt;Timestamp&gt;20-Dec-2014 04:14:56&lt;/Timestamp&gt; - &lt;Receipt&gt;&lt;/Receipt&gt; - &lt;SettlementDate&gt;22-Dec-2014&lt;/SettlementDate&gt; - &lt;DeclinedCode&gt;05&lt;/DeclinedCode&gt; - &lt;DeclinedMessage&gt;Do Not Honour&lt;/DeclinedMessage&gt; -&lt;/Response&gt; -</SubmitSinglePaymentResult></SubmitSinglePaymentResponse></soap:Body></soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><SubmitSinglePaymentResponse xmlns="http://www.ippayments.com.au/interface/api/dts"><SubmitSinglePaymentResult>&lt;Response&gt; + &lt;ResponseCode&gt;1&lt;/ResponseCode&gt; + &lt;Timestamp&gt;20-Dec-2014 04:14:56&lt;/Timestamp&gt; + &lt;Receipt&gt;&lt;/Receipt&gt; + &lt;SettlementDate&gt;22-Dec-2014&lt;/SettlementDate&gt; + &lt;DeclinedCode&gt;05&lt;/DeclinedCode&gt; + &lt;DeclinedMessage&gt;Do Not Honour&lt;/DeclinedMessage&gt; + &lt;/Response&gt; + </SubmitSinglePaymentResult></SubmitSinglePaymentResponse></soap:Body></soap:Envelope> XML end def successful_authorize_response - <<-XML -<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><SubmitSinglePaymentResponse xmlns="http://www.ippayments.com.au/interface/api/dts"><SubmitSinglePaymentResult>&lt;Response&gt; - &lt;ResponseCode&gt;0&lt;/ResponseCode&gt; - &lt;Timestamp&gt;20-Dec-2014 04:18:13&lt;/Timestamp&gt; - &lt;Receipt&gt;89435583&lt;/Receipt&gt; - &lt;SettlementDate&gt;22-Dec-2014&lt;/SettlementDate&gt; - &lt;DeclinedCode&gt;&lt;/DeclinedCode&gt; - &lt;DeclinedMessage&gt;&lt;/DeclinedMessage&gt; -&lt;/Response&gt; -</SubmitSinglePaymentResult></SubmitSinglePaymentResponse></soap:Body></soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><SubmitSinglePaymentResponse xmlns="http://www.ippayments.com.au/interface/api/dts"><SubmitSinglePaymentResult>&lt;Response&gt; + &lt;ResponseCode&gt;0&lt;/ResponseCode&gt; + &lt;Timestamp&gt;20-Dec-2014 04:18:13&lt;/Timestamp&gt; + &lt;Receipt&gt;89435583&lt;/Receipt&gt; + &lt;SettlementDate&gt;22-Dec-2014&lt;/SettlementDate&gt; + &lt;DeclinedCode&gt;&lt;/DeclinedCode&gt; + &lt;DeclinedMessage&gt;&lt;/DeclinedMessage&gt; + &lt;/Response&gt; + </SubmitSinglePaymentResult></SubmitSinglePaymentResponse></soap:Body></soap:Envelope> XML end def successful_capture_response - <<-XML -<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><SubmitSingleCaptureResponse xmlns="http://www.ippayments.com.au/interface/api/dts"><SubmitSingleCaptureResult>&lt;Response&gt; - &lt;ResponseCode&gt;0&lt;/ResponseCode&gt; - &lt;Timestamp&gt;20-Dec-2014 04:18:15&lt;/Timestamp&gt; - &lt;Receipt&gt;89435584&lt;/Receipt&gt; - &lt;SettlementDate&gt;22-Dec-2014&lt;/SettlementDate&gt; - &lt;DeclinedCode&gt;&lt;/DeclinedCode&gt; - &lt;DeclinedMessage&gt;&lt;/DeclinedMessage&gt; -&lt;/Response&gt; -</SubmitSingleCaptureResult></SubmitSingleCaptureResponse></soap:Body></soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><SubmitSingleCaptureResponse xmlns="http://www.ippayments.com.au/interface/api/dts"><SubmitSingleCaptureResult>&lt;Response&gt; + &lt;ResponseCode&gt;0&lt;/ResponseCode&gt; + &lt;Timestamp&gt;20-Dec-2014 04:18:15&lt;/Timestamp&gt; + &lt;Receipt&gt;89435584&lt;/Receipt&gt; + &lt;SettlementDate&gt;22-Dec-2014&lt;/SettlementDate&gt; + &lt;DeclinedCode&gt;&lt;/DeclinedCode&gt; + &lt;DeclinedMessage&gt;&lt;/DeclinedMessage&gt; + &lt;/Response&gt; + </SubmitSingleCaptureResult></SubmitSingleCaptureResponse></soap:Body></soap:Envelope> XML end def successful_refund_response - <<-XML -<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><SubmitSingleRefundResponse xmlns="http://www.ippayments.com.au/interface/api/dts"><SubmitSingleRefundResult>&lt;Response&gt; - &lt;ResponseCode&gt;0&lt;/ResponseCode&gt; - &lt;Timestamp&gt;20-Dec-2014 04:24:51&lt;/Timestamp&gt; - &lt;Receipt&gt;89435596&lt;/Receipt&gt; - &lt;SettlementDate&gt;22-Dec-2014&lt;/SettlementDate&gt; - &lt;DeclinedCode&gt;&lt;/DeclinedCode&gt; - &lt;DeclinedMessage&gt;&lt;/DeclinedMessage&gt; -&lt;/Response&gt; -</SubmitSingleRefundResult></SubmitSingleRefundResponse></soap:Body></soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><SubmitSingleRefundResponse xmlns="http://www.ippayments.com.au/interface/api/dts"><SubmitSingleRefundResult>&lt;Response&gt; + &lt;ResponseCode&gt;0&lt;/ResponseCode&gt; + &lt;Timestamp&gt;20-Dec-2014 04:24:51&lt;/Timestamp&gt; + &lt;Receipt&gt;89435596&lt;/Receipt&gt; + &lt;SettlementDate&gt;22-Dec-2014&lt;/SettlementDate&gt; + &lt;DeclinedCode&gt;&lt;/DeclinedCode&gt; + &lt;DeclinedMessage&gt;&lt;/DeclinedMessage&gt; + &lt;/Response&gt; + </SubmitSingleRefundResult></SubmitSingleRefundResponse></soap:Body></soap:Envelope> XML end end diff --git a/test/unit/gateways/iridium_test.rb b/test/unit/gateways/iridium_test.rb index b772f91408e..865b183acac 100644 --- a/test/unit/gateways/iridium_test.rb +++ b/test/unit/gateways/iridium_test.rb @@ -1,18 +1,20 @@ require 'test_helper' class IridiumTest < Test::Unit::TestCase + include CommStub + def setup Base.mode = :test - @gateway = IridiumGateway.new(:login => 'login', :password => 'password') + @gateway = IridiumGateway.new(login: 'login', password: 'password') @credit_card = credit_card @amount = 100 @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase' + order_id: '1', + billing_address: address, + description: 'Store Purchase' } end @@ -36,7 +38,6 @@ def test_unsuccessful_request assert response.test? end - def test_successful_authorize @gateway.expects(:ssl_post).returns(successful_authorize_response) @@ -90,7 +91,7 @@ def test_override_currency @gateway.expects(:ssl_post). with(anything, all_of(regexp_matches(/Amount="400"/), regexp_matches(/CurrencyCode="484"/)), anything). returns(successful_purchase_response) - assert_success @gateway.purchase(400, @credit_card, @options.merge(:currency => 'MXN')) + assert_success @gateway.purchase(400, @credit_card, @options.merge(currency: 'MXN')) end def test_do_not_depend_on_expiry_date_class @@ -103,19 +104,25 @@ def test_do_not_depend_on_expiry_date_class def test_use_ducktyping_for_credit_card @gateway.expects(:ssl_post).returns(successful_purchase_response) - credit_card = stub(:number => '4242424242424242', :verification_value => '123', :name => 'Hans Tester', :year => 2012, :month => 1) + credit_card = stub(number: '4242424242424242', verification_value: '123', name: 'Hans Tester', year: 2012, month: 1) assert_nothing_raised do assert_success @gateway.purchase(@amount, credit_card, @options) end end + def test_nonfractional_currency_handling + stub_comms do + @gateway.authorize(14200, @credit_card, @options.merge(currency: 'JPY')) + end.check_request do |_endpoint, data, _headers| + assert_match(/<TransactionDetails Amount=\"142\"/, data) + end.respond_with(successful_authorize_response) + end def test_transcript_scrubbing assert_equal post_scrubbed, @gateway.scrub(pre_scrubbed) end - private # Place raw successful response from gateway here @@ -344,5 +351,4 @@ def post_scrubbed <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><CardDetailsTransactionResponse xmlns="https://www.thepaymentgateway.net/"><CardDetailsTransactionResult AuthorisationAttempted="True"><StatusCode>0</StatusCode><Message>AuthCode: 608724</Message></CardDetailsTransactionResult><TransactionOutputData CrossReference="110428221508160201608724"><AuthCode>608724</AuthCode><AddressNumericCheckResult>PASSED</AddressNumericCheckResult><PostCodeCheckResult>PASSED</PostCodeCheckResult><CV2CheckResult>PASSED</CV2CheckResult><GatewayEntryPoints><GatewayEntryPoint EntryPointURL="https://gw1.iridiumcorp.net/" Metric="100" /><GatewayEntryPoint EntryPointURL="https://gw2.iridiumcorp.net/" Metric="200" /><GatewayEntryPoint EntryPointURL="https://gw3.iridiumcorp.net/" Metric="300" /></GatewayEntryPoints></TransactionOutputData></CardDetailsTransactionResponse></soap:Body></soap:Envelope> POST_SCRUBBED end - end diff --git a/test/unit/gateways/itransact_test.rb b/test/unit/gateways/itransact_test.rb index e1fe37ef8e3..46e019ebc95 100644 --- a/test/unit/gateways/itransact_test.rb +++ b/test/unit/gateways/itransact_test.rb @@ -3,31 +3,31 @@ class ItransactTest < Test::Unit::TestCase def setup @gateway = ItransactGateway.new( - :login => 'login', - :password => 'password', - :gateway_id => '09999' - ) + login: 'login', + password: 'password', + gateway_id: '09999' + ) @credit_card = credit_card @check = check @amount = 1014 # = $10.14 - - @options = { - :email => 'name@domain.com', - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase', - :email_text => ['line1', 'line2', 'line3'] + + @options = { + email: 'name@domain.com', + order_id: '1', + billing_address: address, + description: 'Store Purchase', + email_text: %w[line1 line2 line3] } end - + def test_successful_card_purchase @gateway.expects(:ssl_post).returns(successful_card_purchase_response) - + assert response = @gateway.purchase(@amount, @credit_card, @options) assert_instance_of Response, response assert_success response - + assert_equal '9999999999', response.authorization assert response.test? end @@ -45,19 +45,19 @@ def test_successful_check_purchase def test_unsuccessful_card_request @gateway.expects(:ssl_post).returns(failed_purchase_response) - + assert response = @gateway.purchase(@amount, @credit_card, @options) assert_failure response assert response.test? end private - + def successful_card_purchase_response "<?xml version=\"1.0\" standalone=\"yes\"?> <GatewayInterface><TransactionResponse><TransactionResult><Status>ok</Status><ErrorCategory></ErrorCategory><ErrorMessage></ErrorMessage><WarningMessage></WarningMessage><AuthCode></AuthCode><AVSCategory></AVSCategory><AVSResponse></AVSResponse><CVV2Response></CVV2Response><TimeStamp>20081216141214</TimeStamp><TestMode>TRUE</TestMode><Total>1.0</Total><XID>9999999999</XID><CustomerData><BillingAddress><Address1>1234 My Street</Address1><City>Ottawa</City><FirstName>Longbob</FirstName><LastName>Longsen</LastName><State>ON</State><Zip>K1C2N6</Zip><Country>CA</Country><Phone>(555)555-5555</Phone></BillingAddress><ShippingAddress><Address1></Address1><City></City><FirstName></FirstName><LastName></LastName><State></State><Zip></Zip><Country></Country><Phone></Phone></ShippingAddress></CustomerData></TransactionResult></TransactionResponse></GatewayInterface>" end - + def failed_purchase_response '<?xml version="1.0" standalone="yes"?> <GatewayInterface><TransactionResponse><TransactionResult><Status>FAILED</Status><ErrorCategory>REQUEST_FORMAT</ErrorCategory><ErrorMessage>Form does not contain xml parameter</ErrorMessage><AuthCode></AuthCode><AVSCategory></AVSCategory><AVSResponse></AVSResponse><CVV2Response></CVV2Response><TimeStamp></TimeStamp><TestMode></TestMode><Total></Total><XID></XID><CustomerData><BillingAddress><Address1 /><City></City><FirstName></FirstName><LastName></LastName><State></State><Zip></Zip><Country></Country><Phone></Phone></BillingAddress><ShippingAddress><Address1></Address1><City></City><FirstName></FirstName><LastName></LastName><State></State><Zip></Zip><Country></Country><Phone></Phone></ShippingAddress></CustomerData></TransactionResult></TransactionResponse></GatewayInterface>' @@ -67,5 +67,4 @@ def successful_check_purchase_response "<?xml version=\"1.0\" standalone=\"yes\"?> <GatewayInterface><TransactionResponse><TransactionResult><Status>ok</Status><ErrorCategory></ErrorCategory><ErrorMessage></ErrorMessage><WarningMessage></WarningMessage><TimeStamp>20081216141214</TimeStamp><TestMode>TRUE</TestMode><Total>1.0</Total><XID>9999999999</XID><CustomerData><BillingAddress><Address1>1234 My Street</Address1><City>Ottawa</City><FirstName>Longbob</FirstName><LastName>Longsen</LastName><State>ON</State><Zip>K1C2N6</Zip><Country>CA</Country><Phone>(555)555-5555</Phone></BillingAddress><ShippingAddress><Address1></Address1><City></City><FirstName></FirstName><LastName></LastName><State></State><Zip></Zip><Country></Country><Phone></Phone></ShippingAddress></CustomerData></TransactionResult></TransactionResponse></GatewayInterface>" end - end diff --git a/test/unit/gateways/iveri_test.rb b/test/unit/gateways/iveri_test.rb index 1269cd1ee53..c193f6c05d7 100644 --- a/test/unit/gateways/iveri_test.rb +++ b/test/unit/gateways/iveri_test.rb @@ -1,6 +1,8 @@ require 'test_helper' class IveriTest < Test::Unit::TestCase + include CommStub + def setup @gateway = IveriGateway.new(app_id: '123', cert_id: '321') @credit_card = credit_card('4242424242424242') @@ -23,6 +25,17 @@ def test_successful_purchase assert response.test? end + def test_successful_purchase_with_iveri_url + @gateway = IveriGateway.new(app_id: '123', cert_id: '321', url_override: 'iveri') + + @gateway.expects(:ssl_post).returns(successful_purchase_response) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal '{F0568958-D10B-4093-A3BF-663168B06140}|{5CEF96FD-960E-4EA5-811F-D02CE6E36A96}|48b63446223ce91451fc3c1641a9ec03', response.authorization + assert response.test? + end + def test_failed_purchase @gateway.expects(:ssl_post).returns(failed_purchase_response) @@ -100,20 +113,19 @@ def test_failed_void end def test_successful_verify - @gateway.expects(:ssl_post).returns(successful_verify_response) - - response = @gateway.verify(@credit_card, @options) + response = stub_comms do + @gateway.verify(@credit_card, @options) + end.respond_with(successful_authorize_response, failed_void_response) assert_success response - assert_equal '{F4337D04-B526-4A7E-A400-2A6DEADDCF57}|{5D5F8BF7-2D9D-42C3-AF32-08C5E62CD45E}|c0006d1d739905afc9e70beaf4194ea3', response.authorization - assert response.test? + assert_equal 'Succeeded', response.message end def test_failed_verify - @gateway.expects(:ssl_post).returns(failed_verify_response) - - response = @gateway.verify(credit_card('2121212121212121'), @options) + response = stub_comms do + @gateway.verify(credit_card('2121212121212121'), @options) + end.respond_with(failed_authorize_response, successful_void_response) assert_failure response - assert_equal '4', response.error_code + assert_equal 'Denied', response.message end def test_successful_verify_credentials @@ -184,370 +196,332 @@ def post_scrubbed end def successful_purchase_response - <<-XML -<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ExecuteResponse xmlns="http://iveri.com/"><ExecuteResult>&lt;V_XML Version="2.0" Direction="Response"&gt; -&lt;Transaction ApplicationID="{D10A603D-4ADE-405B-93F1-826DFC0181E8}" Command="Debit" Mode="Test" RequestID="{F0568958-D10B-4093-A3BF-663168B06140}"&gt; - &lt;Result Status="0" AppServer="105IVERIAPPPR01" DBServer="105IVERIDBPR01" Gateway="Nedbank" AcquirerCode="00" /&gt; - &lt;Amount&gt;100&lt;/Amount&gt; - &lt;AuthorisationCode&gt;537473&lt;/AuthorisationCode&gt; - &lt;Currency&gt;ZAR&lt;/Currency&gt; - &lt;ExpiryDate&gt;092018&lt;/ExpiryDate&gt; - &lt;MerchantReference&gt;48b63446223ce91451fc3c1641a9ec03&lt;/MerchantReference&gt; - &lt;Terminal&gt;Default&lt;/Terminal&gt; - &lt;TransactionIndex&gt;{5CEF96FD-960E-4EA5-811F-D02CE6E36A96}&lt;/TransactionIndex&gt; - &lt;MerchantName&gt;iVeri Payment Technology&lt;/MerchantName&gt; - &lt;MerchantUSN&gt;7771777&lt;/MerchantUSN&gt; - &lt;Acquirer&gt;NBPostilionBICISONBSouthAfrica&lt;/Acquirer&gt; - &lt;AcquirerReference&gt;70417:04077982&lt;/AcquirerReference&gt; - &lt;AcquirerDate&gt;20170417&lt;/AcquirerDate&gt; - &lt;AcquirerTime&gt;190433&lt;/AcquirerTime&gt; - &lt;DisplayAmount&gt;R 1.00&lt;/DisplayAmount&gt; - &lt;BIN&gt;4&lt;/BIN&gt; - &lt;Association&gt;VISA&lt;/Association&gt; - &lt;CardType&gt;Unknown CardType&lt;/CardType&gt; - &lt;Issuer&gt;Unknown&lt;/Issuer&gt; - &lt;Jurisdiction&gt;International&lt;/Jurisdiction&gt; - &lt;PANMode&gt;Keyed,CVV&lt;/PANMode&gt; - &lt;ReconReference&gt;04077982&lt;/ReconReference&gt; - &lt;CardHolderPresence&gt;CardNotPresent&lt;/CardHolderPresence&gt; - &lt;MerchantAddress&gt;MERCHANT ADDRESS&lt;/MerchantAddress&gt; - &lt;MerchantCity&gt;Sandton&lt;/MerchantCity&gt; - &lt;MerchantCountryCode&gt;ZA&lt;/MerchantCountryCode&gt; - &lt;MerchantCountry&gt;South Africa&lt;/MerchantCountry&gt; - &lt;DistributorName&gt;Nedbank&lt;/DistributorName&gt; - &lt;CCNumber&gt;4242........4242&lt;/CCNumber&gt; - &lt;PAN&gt;4242........4242&lt;/PAN&gt; -&lt;/Transaction&gt; -&lt;/V_XML&gt;</ExecuteResult></ExecuteResponse></soap:Body></soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ExecuteResponse xmlns="http://iveri.com/"><ExecuteResult>&lt;V_XML Version="2.0" Direction="Response"&gt; + &lt;Transaction ApplicationID="{D10A603D-4ADE-405B-93F1-826DFC0181E8}" Command="Debit" Mode="Test" RequestID="{F0568958-D10B-4093-A3BF-663168B06140}"&gt; + &lt;Result Status="0" AppServer="105IVERIAPPPR01" DBServer="105IVERIDBPR01" Gateway="Nedbank" AcquirerCode="00" /&gt; + &lt;Amount&gt;100&lt;/Amount&gt; + &lt;AuthorisationCode&gt;537473&lt;/AuthorisationCode&gt; + &lt;Currency&gt;ZAR&lt;/Currency&gt; + &lt;ExpiryDate&gt;092018&lt;/ExpiryDate&gt; + &lt;MerchantReference&gt;48b63446223ce91451fc3c1641a9ec03&lt;/MerchantReference&gt; + &lt;Terminal&gt;Default&lt;/Terminal&gt; + &lt;TransactionIndex&gt;{5CEF96FD-960E-4EA5-811F-D02CE6E36A96}&lt;/TransactionIndex&gt; + &lt;MerchantName&gt;iVeri Payment Technology&lt;/MerchantName&gt; + &lt;MerchantUSN&gt;7771777&lt;/MerchantUSN&gt; + &lt;Acquirer&gt;NBPostilionBICISONBSouthAfrica&lt;/Acquirer&gt; + &lt;AcquirerReference&gt;70417:04077982&lt;/AcquirerReference&gt; + &lt;AcquirerDate&gt;20170417&lt;/AcquirerDate&gt; + &lt;AcquirerTime&gt;190433&lt;/AcquirerTime&gt; + &lt;DisplayAmount&gt;R 1.00&lt;/DisplayAmount&gt; + &lt;BIN&gt;4&lt;/BIN&gt; + &lt;Association&gt;VISA&lt;/Association&gt; + &lt;CardType&gt;Unknown CardType&lt;/CardType&gt; + &lt;Issuer&gt;Unknown&lt;/Issuer&gt; + &lt;Jurisdiction&gt;International&lt;/Jurisdiction&gt; + &lt;PANMode&gt;Keyed,CVV&lt;/PANMode&gt; + &lt;ReconReference&gt;04077982&lt;/ReconReference&gt; + &lt;CardHolderPresence&gt;CardNotPresent&lt;/CardHolderPresence&gt; + &lt;MerchantAddress&gt;MERCHANT ADDRESS&lt;/MerchantAddress&gt; + &lt;MerchantCity&gt;Sandton&lt;/MerchantCity&gt; + &lt;MerchantCountryCode&gt;ZA&lt;/MerchantCountryCode&gt; + &lt;MerchantCountry&gt;South Africa&lt;/MerchantCountry&gt; + &lt;DistributorName&gt;Nedbank&lt;/DistributorName&gt; + &lt;CCNumber&gt;4242........4242&lt;/CCNumber&gt; + &lt;PAN&gt;4242........4242&lt;/PAN&gt; + &lt;/Transaction&gt; + &lt;/V_XML&gt;</ExecuteResult></ExecuteResponse></soap:Body></soap:Envelope> XML end def failed_purchase_response - <<-XML -<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ExecuteResponse xmlns="http://iveri.com/"><ExecuteResult>&lt;V_XML Version="2.0" Direction="Response"&gt; - &lt;Transaction ApplicationID="{D10A603D-4ADE-405B-93F1-826DFC0181E8}" Command="Debit" Mode="Test" RequestID="{B14C3834-72B9-4ACA-B362-B3C9EC96E8C0}"&gt; - &lt;Result Status="-1" Code="4" Description="Denied" Source="NBPostilionBICISONBSouthAfrica" AppServer="105IVERIAPPPR01" DBServer="105IVERIDBPR01" Gateway="Nedbank" AcquirerCode="05" AcquirerDescription="Do not Honour" /&gt; - &lt;Amount&gt;100&lt;/Amount&gt; - &lt;Currency&gt;ZAR&lt;/Currency&gt; - &lt;ExpiryDate&gt;092018&lt;/ExpiryDate&gt; - &lt;MerchantReference&gt;435a5d60b5fe874840c34e2e0504626b&lt;/MerchantReference&gt; - &lt;Terminal&gt;Default&lt;/Terminal&gt; - &lt;TransactionIndex&gt;{B35872A9-39C7-4DB8-9774-A5E34FFA519E}&lt;/TransactionIndex&gt; - &lt;MerchantName&gt;iVeri Payment Technology&lt;/MerchantName&gt; - &lt;MerchantUSN&gt;7771777&lt;/MerchantUSN&gt; - &lt;Acquirer&gt;NBPostilionBICISONBSouthAfrica&lt;/Acquirer&gt; - &lt;AcquirerReference&gt;70417:04077988&lt;/AcquirerReference&gt; - &lt;AcquirerDate&gt;20170417&lt;/AcquirerDate&gt; - &lt;AcquirerTime&gt;192038&lt;/AcquirerTime&gt; - &lt;DisplayAmount&gt;R 1.00&lt;/DisplayAmount&gt; - &lt;BIN&gt;2&lt;/BIN&gt; - &lt;Association&gt;Unknown Association&lt;/Association&gt; - &lt;CardType&gt;Unknown CardType&lt;/CardType&gt; - &lt;Issuer&gt;Unknown&lt;/Issuer&gt; - &lt;Jurisdiction&gt;Local&lt;/Jurisdiction&gt; - &lt;PANMode&gt;Keyed,CVV&lt;/PANMode&gt; - &lt;ReconReference&gt;04077988&lt;/ReconReference&gt; - &lt;CardHolderPresence&gt;CardNotPresent&lt;/CardHolderPresence&gt; - &lt;MerchantAddress&gt;MERCHANT ADDRESS&lt;/MerchantAddress&gt; - &lt;MerchantCity&gt;Sandton&lt;/MerchantCity&gt; - &lt;MerchantCountryCode&gt;ZA&lt;/MerchantCountryCode&gt; - &lt;MerchantCountry&gt;South Africa&lt;/MerchantCountry&gt; - &lt;DistributorName&gt;Nedbank&lt;/DistributorName&gt; - &lt;CCNumber&gt;2121........2121&lt;/CCNumber&gt; - &lt;PAN&gt;2121........2121&lt;/PAN&gt; - &lt;/Transaction&gt; -&lt;/V_XML&gt;</ExecuteResult></ExecuteResponse></soap:Body></soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ExecuteResponse xmlns="http://iveri.com/"><ExecuteResult>&lt;V_XML Version="2.0" Direction="Response"&gt; + &lt;Transaction ApplicationID="{D10A603D-4ADE-405B-93F1-826DFC0181E8}" Command="Debit" Mode="Test" RequestID="{B14C3834-72B9-4ACA-B362-B3C9EC96E8C0}"&gt; + &lt;Result Status="-1" Code="4" Description="Denied" Source="NBPostilionBICISONBSouthAfrica" AppServer="105IVERIAPPPR01" DBServer="105IVERIDBPR01" Gateway="Nedbank" AcquirerCode="05" AcquirerDescription="Do not Honour" /&gt; + &lt;Amount&gt;100&lt;/Amount&gt; + &lt;Currency&gt;ZAR&lt;/Currency&gt; + &lt;ExpiryDate&gt;092018&lt;/ExpiryDate&gt; + &lt;MerchantReference&gt;435a5d60b5fe874840c34e2e0504626b&lt;/MerchantReference&gt; + &lt;Terminal&gt;Default&lt;/Terminal&gt; + &lt;TransactionIndex&gt;{B35872A9-39C7-4DB8-9774-A5E34FFA519E}&lt;/TransactionIndex&gt; + &lt;MerchantName&gt;iVeri Payment Technology&lt;/MerchantName&gt; + &lt;MerchantUSN&gt;7771777&lt;/MerchantUSN&gt; + &lt;Acquirer&gt;NBPostilionBICISONBSouthAfrica&lt;/Acquirer&gt; + &lt;AcquirerReference&gt;70417:04077988&lt;/AcquirerReference&gt; + &lt;AcquirerDate&gt;20170417&lt;/AcquirerDate&gt; + &lt;AcquirerTime&gt;192038&lt;/AcquirerTime&gt; + &lt;DisplayAmount&gt;R 1.00&lt;/DisplayAmount&gt; + &lt;BIN&gt;2&lt;/BIN&gt; + &lt;Association&gt;Unknown Association&lt;/Association&gt; + &lt;CardType&gt;Unknown CardType&lt;/CardType&gt; + &lt;Issuer&gt;Unknown&lt;/Issuer&gt; + &lt;Jurisdiction&gt;Local&lt;/Jurisdiction&gt; + &lt;PANMode&gt;Keyed,CVV&lt;/PANMode&gt; + &lt;ReconReference&gt;04077988&lt;/ReconReference&gt; + &lt;CardHolderPresence&gt;CardNotPresent&lt;/CardHolderPresence&gt; + &lt;MerchantAddress&gt;MERCHANT ADDRESS&lt;/MerchantAddress&gt; + &lt;MerchantCity&gt;Sandton&lt;/MerchantCity&gt; + &lt;MerchantCountryCode&gt;ZA&lt;/MerchantCountryCode&gt; + &lt;MerchantCountry&gt;South Africa&lt;/MerchantCountry&gt; + &lt;DistributorName&gt;Nedbank&lt;/DistributorName&gt; + &lt;CCNumber&gt;2121........2121&lt;/CCNumber&gt; + &lt;PAN&gt;2121........2121&lt;/PAN&gt; + &lt;/Transaction&gt; + &lt;/V_XML&gt;</ExecuteResult></ExecuteResponse></soap:Body></soap:Envelope> XML end def successful_authorize_response - <<-XML -<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ExecuteResponse xmlns="http://iveri.com/"><ExecuteResult>&lt;V_XML Version="2.0" Direction="Response"&gt; - &lt;Transaction ApplicationID="{D10A603D-4ADE-405B-93F1-826DFC0181E8}" Command="Authorisation" Mode="Test" RequestID="{B90D7CDB-C8E8-4477-BDF2-695F28137874}"&gt; - &lt;Result Status="0" AppServer="105IVERIAPPPR01" DBServer="105IVERIDBPR01" Gateway="Nedbank" AcquirerCode="00" /&gt; - &lt;Amount&gt;100&lt;/Amount&gt; - &lt;AuthorisationCode&gt;541267&lt;/AuthorisationCode&gt; - &lt;Currency&gt;ZAR&lt;/Currency&gt; - &lt;ExpiryDate&gt;092018&lt;/ExpiryDate&gt; - &lt;MerchantReference&gt;23b4125c3b8e2777bffee52e196a863b&lt;/MerchantReference&gt; - &lt;Terminal&gt;Default&lt;/Terminal&gt; - &lt;TransactionIndex&gt;{EF0DC64E-2D00-4B6C-BDA0-2AD265391317}&lt;/TransactionIndex&gt; - &lt;MerchantName&gt;iVeri Payment Technology&lt;/MerchantName&gt; - &lt;MerchantUSN&gt;7771777&lt;/MerchantUSN&gt; - &lt;Acquirer&gt;NBPostilionBICISONBSouthAfrica&lt;/Acquirer&gt; - &lt;AcquirerReference&gt;70417:04078057&lt;/AcquirerReference&gt; - &lt;AcquirerDate&gt;20170417&lt;/AcquirerDate&gt; - &lt;AcquirerTime&gt;200747&lt;/AcquirerTime&gt; - &lt;DisplayAmount&gt;R 1.00&lt;/DisplayAmount&gt; - &lt;BIN&gt;4&lt;/BIN&gt; - &lt;Association&gt;VISA&lt;/Association&gt; - &lt;CardType&gt;Unknown CardType&lt;/CardType&gt; - &lt;Issuer&gt;Unknown&lt;/Issuer&gt; - &lt;Jurisdiction&gt;International&lt;/Jurisdiction&gt; - &lt;PANMode&gt;Keyed,CVV&lt;/PANMode&gt; - &lt;ReconReference&gt;04078057&lt;/ReconReference&gt; - &lt;CardHolderPresence&gt;CardNotPresent&lt;/CardHolderPresence&gt; - &lt;MerchantAddress&gt;MERCHANT ADDRESS&lt;/MerchantAddress&gt; - &lt;MerchantCity&gt;Sandton&lt;/MerchantCity&gt; - &lt;MerchantCountryCode&gt;ZA&lt;/MerchantCountryCode&gt; - &lt;MerchantCountry&gt;South Africa&lt;/MerchantCountry&gt; - &lt;DistributorName&gt;Nedbank&lt;/DistributorName&gt; - &lt;CCNumber&gt;4242........4242&lt;/CCNumber&gt; - &lt;PAN&gt;4242........4242&lt;/PAN&gt; - &lt;/Transaction&gt; -&lt;/V_XML&gt;</ExecuteResult></ExecuteResponse></soap:Body></soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ExecuteResponse xmlns="http://iveri.com/"><ExecuteResult>&lt;V_XML Version="2.0" Direction="Response"&gt; + &lt;Transaction ApplicationID="{D10A603D-4ADE-405B-93F1-826DFC0181E8}" Command="Authorisation" Mode="Test" RequestID="{B90D7CDB-C8E8-4477-BDF2-695F28137874}"&gt; + &lt;Result Status="0" AppServer="105IVERIAPPPR01" DBServer="105IVERIDBPR01" Gateway="Nedbank" AcquirerCode="00" /&gt; + &lt;Amount&gt;100&lt;/Amount&gt; + &lt;AuthorisationCode&gt;541267&lt;/AuthorisationCode&gt; + &lt;Currency&gt;ZAR&lt;/Currency&gt; + &lt;ExpiryDate&gt;092018&lt;/ExpiryDate&gt; + &lt;MerchantReference&gt;23b4125c3b8e2777bffee52e196a863b&lt;/MerchantReference&gt; + &lt;Terminal&gt;Default&lt;/Terminal&gt; + &lt;TransactionIndex&gt;{EF0DC64E-2D00-4B6C-BDA0-2AD265391317}&lt;/TransactionIndex&gt; + &lt;MerchantName&gt;iVeri Payment Technology&lt;/MerchantName&gt; + &lt;MerchantUSN&gt;7771777&lt;/MerchantUSN&gt; + &lt;Acquirer&gt;NBPostilionBICISONBSouthAfrica&lt;/Acquirer&gt; + &lt;AcquirerReference&gt;70417:04078057&lt;/AcquirerReference&gt; + &lt;AcquirerDate&gt;20170417&lt;/AcquirerDate&gt; + &lt;AcquirerTime&gt;200747&lt;/AcquirerTime&gt; + &lt;DisplayAmount&gt;R 1.00&lt;/DisplayAmount&gt; + &lt;BIN&gt;4&lt;/BIN&gt; + &lt;Association&gt;VISA&lt;/Association&gt; + &lt;CardType&gt;Unknown CardType&lt;/CardType&gt; + &lt;Issuer&gt;Unknown&lt;/Issuer&gt; + &lt;Jurisdiction&gt;International&lt;/Jurisdiction&gt; + &lt;PANMode&gt;Keyed,CVV&lt;/PANMode&gt; + &lt;ReconReference&gt;04078057&lt;/ReconReference&gt; + &lt;CardHolderPresence&gt;CardNotPresent&lt;/CardHolderPresence&gt; + &lt;MerchantAddress&gt;MERCHANT ADDRESS&lt;/MerchantAddress&gt; + &lt;MerchantCity&gt;Sandton&lt;/MerchantCity&gt; + &lt;MerchantCountryCode&gt;ZA&lt;/MerchantCountryCode&gt; + &lt;MerchantCountry&gt;South Africa&lt;/MerchantCountry&gt; + &lt;DistributorName&gt;Nedbank&lt;/DistributorName&gt; + &lt;CCNumber&gt;4242........4242&lt;/CCNumber&gt; + &lt;PAN&gt;4242........4242&lt;/PAN&gt; + &lt;/Transaction&gt; + &lt;/V_XML&gt;</ExecuteResult></ExecuteResponse></soap:Body></soap:Envelope> XML end def failed_authorize_response - <<-XML -<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ExecuteResponse xmlns="http://iveri.com/"><ExecuteResult>&lt;V_XML Version="2.0" Direction="Response"&gt; - &lt;Transaction ApplicationID="{D10A603D-4ADE-405B-93F1-826DFC0181E8}" Command="Authorisation" Mode="Test" RequestID="{3A1A29BE-288F-4FEE-8C15-B3BB8A207544}"&gt; - &lt;Result Status="-1" Code="4" Description="Denied" Source="NBPostilionBICISONBSouthAfrica" AppServer="105IVERIAPPPR01" DBServer="105IVERIDBPR01" Gateway="Nedbank" AcquirerCode="05" AcquirerDescription="Do not Honour" /&gt; - &lt;Amount&gt;100&lt;/Amount&gt; - &lt;Currency&gt;ZAR&lt;/Currency&gt; - &lt;ExpiryDate&gt;092018&lt;/ExpiryDate&gt; - &lt;MerchantReference&gt;3d12442ea042e78fd33057b7b50c76f7&lt;/MerchantReference&gt; - &lt;Terminal&gt;Default&lt;/Terminal&gt; - &lt;TransactionIndex&gt;{8AC33FB1-0D2E-42C7-A0DB-CF8B20279825}&lt;/TransactionIndex&gt; - &lt;MerchantName&gt;iVeri Payment Technology&lt;/MerchantName&gt; - &lt;MerchantUSN&gt;7771777&lt;/MerchantUSN&gt; - &lt;Acquirer&gt;NBPostilionBICISONBSouthAfrica&lt;/Acquirer&gt; - &lt;AcquirerReference&gt;70417:04078062&lt;/AcquirerReference&gt; - &lt;AcquirerDate&gt;20170417&lt;/AcquirerDate&gt; - &lt;AcquirerTime&gt;202648&lt;/AcquirerTime&gt; - &lt;DisplayAmount&gt;R 1.00&lt;/DisplayAmount&gt; - &lt;BIN&gt;2&lt;/BIN&gt; - &lt;Association&gt;Unknown Association&lt;/Association&gt; - &lt;CardType&gt;Unknown CardType&lt;/CardType&gt; - &lt;Issuer&gt;Unknown&lt;/Issuer&gt; - &lt;Jurisdiction&gt;Local&lt;/Jurisdiction&gt; - &lt;PANMode&gt;Keyed,CVV&lt;/PANMode&gt; - &lt;ReconReference&gt;04078062&lt;/ReconReference&gt; - &lt;CardHolderPresence&gt;CardNotPresent&lt;/CardHolderPresence&gt; - &lt;MerchantAddress&gt;MERCHANT ADDRESS&lt;/MerchantAddress&gt; - &lt;MerchantCity&gt;Sandton&lt;/MerchantCity&gt; - &lt;MerchantCountryCode&gt;ZA&lt;/MerchantCountryCode&gt; - &lt;MerchantCountry&gt;South Africa&lt;/MerchantCountry&gt; - &lt;DistributorName&gt;Nedbank&lt;/DistributorName&gt; - &lt;CCNumber&gt;2121........2121&lt;/CCNumber&gt; - &lt;PAN&gt;2121........2121&lt;/PAN&gt; - &lt;/Transaction&gt; -&lt;/V_XML&gt;</ExecuteResult></ExecuteResponse></soap:Body></soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ExecuteResponse xmlns="http://iveri.com/"><ExecuteResult>&lt;V_XML Version="2.0" Direction="Response"&gt; + &lt;Transaction ApplicationID="{D10A603D-4ADE-405B-93F1-826DFC0181E8}" Command="Authorisation" Mode="Test" RequestID="{3A1A29BE-288F-4FEE-8C15-B3BB8A207544}"&gt; + &lt;Result Status="-1" Code="4" Description="Denied" Source="NBPostilionBICISONBSouthAfrica" AppServer="105IVERIAPPPR01" DBServer="105IVERIDBPR01" Gateway="Nedbank" AcquirerCode="05" AcquirerDescription="Do not Honour" /&gt; + &lt;Amount&gt;100&lt;/Amount&gt; + &lt;Currency&gt;ZAR&lt;/Currency&gt; + &lt;ExpiryDate&gt;092018&lt;/ExpiryDate&gt; + &lt;MerchantReference&gt;3d12442ea042e78fd33057b7b50c76f7&lt;/MerchantReference&gt; + &lt;Terminal&gt;Default&lt;/Terminal&gt; + &lt;TransactionIndex&gt;{8AC33FB1-0D2E-42C7-A0DB-CF8B20279825}&lt;/TransactionIndex&gt; + &lt;MerchantName&gt;iVeri Payment Technology&lt;/MerchantName&gt; + &lt;MerchantUSN&gt;7771777&lt;/MerchantUSN&gt; + &lt;Acquirer&gt;NBPostilionBICISONBSouthAfrica&lt;/Acquirer&gt; + &lt;AcquirerReference&gt;70417:04078062&lt;/AcquirerReference&gt; + &lt;AcquirerDate&gt;20170417&lt;/AcquirerDate&gt; + &lt;AcquirerTime&gt;202648&lt;/AcquirerTime&gt; + &lt;DisplayAmount&gt;R 1.00&lt;/DisplayAmount&gt; + &lt;BIN&gt;2&lt;/BIN&gt; + &lt;Association&gt;Unknown Association&lt;/Association&gt; + &lt;CardType&gt;Unknown CardType&lt;/CardType&gt; + &lt;Issuer&gt;Unknown&lt;/Issuer&gt; + &lt;Jurisdiction&gt;Local&lt;/Jurisdiction&gt; + &lt;PANMode&gt;Keyed,CVV&lt;/PANMode&gt; + &lt;ReconReference&gt;04078062&lt;/ReconReference&gt; + &lt;CardHolderPresence&gt;CardNotPresent&lt;/CardHolderPresence&gt; + &lt;MerchantAddress&gt;MERCHANT ADDRESS&lt;/MerchantAddress&gt; + &lt;MerchantCity&gt;Sandton&lt;/MerchantCity&gt; + &lt;MerchantCountryCode&gt;ZA&lt;/MerchantCountryCode&gt; + &lt;MerchantCountry&gt;South Africa&lt;/MerchantCountry&gt; + &lt;DistributorName&gt;Nedbank&lt;/DistributorName&gt; + &lt;CCNumber&gt;2121........2121&lt;/CCNumber&gt; + &lt;PAN&gt;2121........2121&lt;/PAN&gt; + &lt;/Transaction&gt; + &lt;/V_XML&gt;</ExecuteResult></ExecuteResponse></soap:Body></soap:Envelope> XML end def successful_capture_response - <<-XML -<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ExecuteResponse xmlns="http://iveri.com/"><ExecuteResult>&lt;V_XML Version="2.0" Direction="Response"&gt; - &lt;Transaction ApplicationID="{D10A603D-4ADE-405B-93F1-826DFC0181E8}" Command="Debit" Mode="Test" RequestID="{7C91245F-607D-44AE-8958-C26E447BAEB7}"&gt; - &lt;Result Status="0" AppServer="105IVERIAPPPR02" DBServer="105iveridbpr01" Gateway="Nedbank" AcquirerCode="00" /&gt; - &lt;Amount&gt;100&lt;/Amount&gt; - &lt;AuthorisationCode&gt;541268&lt;/AuthorisationCode&gt; - &lt;Currency&gt;ZAR&lt;/Currency&gt; - &lt;ExpiryDate&gt;092018&lt;/ExpiryDate&gt; - &lt;MerchantReference&gt;23b4125c3b8e2777bffee52e196a863b&lt;/MerchantReference&gt; - &lt;Terminal&gt;Default&lt;/Terminal&gt; - &lt;TransactionIndex&gt;{EF0DC64E-2D00-4B6C-BDA0-2AD265391317}&lt;/TransactionIndex&gt; - &lt;MerchantName&gt;iVeri Payment Technology&lt;/MerchantName&gt; - &lt;MerchantUSN&gt;7771777&lt;/MerchantUSN&gt; - &lt;Acquirer&gt;NBPostilionBICISONBSouthAfrica&lt;/Acquirer&gt; - &lt;AcquirerReference&gt;70417:04078057&lt;/AcquirerReference&gt; - &lt;AcquirerDate&gt;20170417&lt;/AcquirerDate&gt; - &lt;AcquirerTime&gt;200748&lt;/AcquirerTime&gt; - &lt;DisplayAmount&gt;R 1.00&lt;/DisplayAmount&gt; - &lt;BIN&gt;4&lt;/BIN&gt; - &lt;Association&gt;VISA&lt;/Association&gt; - &lt;CardType&gt;Unknown CardType&lt;/CardType&gt; - &lt;Issuer&gt;Unknown&lt;/Issuer&gt; - &lt;Jurisdiction&gt;International&lt;/Jurisdiction&gt; - &lt;PANMode&gt;Keyed,CVV&lt;/PANMode&gt; - &lt;ReconReference&gt;04078057&lt;/ReconReference&gt; - &lt;CardHolderPresence&gt;CardNotPresent&lt;/CardHolderPresence&gt; - &lt;MerchantAddress&gt;MERCHANT ADDRESS&lt;/MerchantAddress&gt; - &lt;MerchantCity&gt;Sandton&lt;/MerchantCity&gt; - &lt;MerchantCountryCode&gt;ZA&lt;/MerchantCountryCode&gt; - &lt;MerchantCountry&gt;South Africa&lt;/MerchantCountry&gt; - &lt;DistributorName&gt;Nedbank&lt;/DistributorName&gt; - &lt;CCNumber&gt;4242........4242&lt;/CCNumber&gt; - &lt;PAN&gt;4242........4242&lt;/PAN&gt; - &lt;/Transaction&gt; -&lt;/V_XML&gt;</ExecuteResult></ExecuteResponse></soap:Body></soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ExecuteResponse xmlns="http://iveri.com/"><ExecuteResult>&lt;V_XML Version="2.0" Direction="Response"&gt; + &lt;Transaction ApplicationID="{D10A603D-4ADE-405B-93F1-826DFC0181E8}" Command="Debit" Mode="Test" RequestID="{7C91245F-607D-44AE-8958-C26E447BAEB7}"&gt; + &lt;Result Status="0" AppServer="105IVERIAPPPR02" DBServer="105iveridbpr01" Gateway="Nedbank" AcquirerCode="00" /&gt; + &lt;Amount&gt;100&lt;/Amount&gt; + &lt;AuthorisationCode&gt;541268&lt;/AuthorisationCode&gt; + &lt;Currency&gt;ZAR&lt;/Currency&gt; + &lt;ExpiryDate&gt;092018&lt;/ExpiryDate&gt; + &lt;MerchantReference&gt;23b4125c3b8e2777bffee52e196a863b&lt;/MerchantReference&gt; + &lt;Terminal&gt;Default&lt;/Terminal&gt; + &lt;TransactionIndex&gt;{EF0DC64E-2D00-4B6C-BDA0-2AD265391317}&lt;/TransactionIndex&gt; + &lt;MerchantName&gt;iVeri Payment Technology&lt;/MerchantName&gt; + &lt;MerchantUSN&gt;7771777&lt;/MerchantUSN&gt; + &lt;Acquirer&gt;NBPostilionBICISONBSouthAfrica&lt;/Acquirer&gt; + &lt;AcquirerReference&gt;70417:04078057&lt;/AcquirerReference&gt; + &lt;AcquirerDate&gt;20170417&lt;/AcquirerDate&gt; + &lt;AcquirerTime&gt;200748&lt;/AcquirerTime&gt; + &lt;DisplayAmount&gt;R 1.00&lt;/DisplayAmount&gt; + &lt;BIN&gt;4&lt;/BIN&gt; + &lt;Association&gt;VISA&lt;/Association&gt; + &lt;CardType&gt;Unknown CardType&lt;/CardType&gt; + &lt;Issuer&gt;Unknown&lt;/Issuer&gt; + &lt;Jurisdiction&gt;International&lt;/Jurisdiction&gt; + &lt;PANMode&gt;Keyed,CVV&lt;/PANMode&gt; + &lt;ReconReference&gt;04078057&lt;/ReconReference&gt; + &lt;CardHolderPresence&gt;CardNotPresent&lt;/CardHolderPresence&gt; + &lt;MerchantAddress&gt;MERCHANT ADDRESS&lt;/MerchantAddress&gt; + &lt;MerchantCity&gt;Sandton&lt;/MerchantCity&gt; + &lt;MerchantCountryCode&gt;ZA&lt;/MerchantCountryCode&gt; + &lt;MerchantCountry&gt;South Africa&lt;/MerchantCountry&gt; + &lt;DistributorName&gt;Nedbank&lt;/DistributorName&gt; + &lt;CCNumber&gt;4242........4242&lt;/CCNumber&gt; + &lt;PAN&gt;4242........4242&lt;/PAN&gt; + &lt;/Transaction&gt; + &lt;/V_XML&gt;</ExecuteResult></ExecuteResponse></soap:Body></soap:Envelope> XML end def failed_capture_response - <<-XML -<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ExecuteResponse xmlns="http://iveri.com/"><ExecuteResult>&lt;V_XML Version="2.0" Direction="Response"&gt; - &lt;Transaction ApplicationID="{D10A603D-4ADE-405B-93F1-826DFC0181E8}" Command="Debit" Mode="Test" RequestID="{9DAAA002-0EF9-46DC-A440-8DCD9E78B36F}"&gt; - &lt;Result Status="-1" Code="14" Description="Missing PAN" Source="NBPostilionBICISONBSouthAfricaTestProvider" AppServer="105IVERIAPPPR02" DBServer="105iveridbpr01" Gateway="Nedbank" AcquirerCode="" AcquirerDescription="" /&gt; - &lt;/Transaction&gt; -&lt;/V_XML&gt;</ExecuteResult></ExecuteResponse></soap:Body></soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ExecuteResponse xmlns="http://iveri.com/"><ExecuteResult>&lt;V_XML Version="2.0" Direction="Response"&gt; + &lt;Transaction ApplicationID="{D10A603D-4ADE-405B-93F1-826DFC0181E8}" Command="Debit" Mode="Test" RequestID="{9DAAA002-0EF9-46DC-A440-8DCD9E78B36F}"&gt; + &lt;Result Status="-1" Code="14" Description="Missing PAN" Source="NBPostilionBICISONBSouthAfricaTestProvider" AppServer="105IVERIAPPPR02" DBServer="105iveridbpr01" Gateway="Nedbank" AcquirerCode="" AcquirerDescription="" /&gt; + &lt;/Transaction&gt; + &lt;/V_XML&gt;</ExecuteResult></ExecuteResponse></soap:Body></soap:Envelope> XML end def successful_refund_response - <<-XML -<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ExecuteResponse xmlns="http://iveri.com/"><ExecuteResult>&lt;V_XML Version="2.0" Direction="Response"&gt; - &lt;Transaction ApplicationID="{D10A603D-4ADE-405B-93F1-826DFC0181E8}" Command="Credit" Mode="Test" RequestID="{097C55B5-D020-40AD-8949-F9F5E4102F1D}"&gt; - &lt;Result Status="0" AppServer="105IVERIAPPPR02" DBServer="105iveridbpr01" Gateway="Nedbank" AcquirerCode="00" /&gt; - &lt;Amount&gt;100&lt;/Amount&gt; - &lt;AuthorisationCode&gt;541996&lt;/AuthorisationCode&gt; - &lt;Currency&gt;ZAR&lt;/Currency&gt; - &lt;ExpiryDate&gt;092018&lt;/ExpiryDate&gt; - &lt;MerchantReference&gt;5be2c040bd46b7eebc70274659779acf&lt;/MerchantReference&gt; - &lt;Terminal&gt;Default&lt;/Terminal&gt; - &lt;TransactionIndex&gt;{D50DB1B4-B6EC-4AF1-AFF7-71C2AA4A957B}&lt;/TransactionIndex&gt; - &lt;MerchantName&gt;iVeri Payment Technology&lt;/MerchantName&gt; - &lt;MerchantUSN&gt;7771777&lt;/MerchantUSN&gt; - &lt;Acquirer&gt;NBPostilionBICISONBSouthAfrica&lt;/Acquirer&gt; - &lt;AcquirerReference&gt;70417:04078059&lt;/AcquirerReference&gt; - &lt;AcquirerDate&gt;20170417&lt;/AcquirerDate&gt; - &lt;AcquirerTime&gt;201956&lt;/AcquirerTime&gt; - &lt;DisplayAmount&gt;R 1.00&lt;/DisplayAmount&gt; - &lt;BIN&gt;4&lt;/BIN&gt; - &lt;Association&gt;VISA&lt;/Association&gt; - &lt;CardType&gt;Unknown CardType&lt;/CardType&gt; - &lt;Issuer&gt;Unknown&lt;/Issuer&gt; - &lt;Jurisdiction&gt;International&lt;/Jurisdiction&gt; - &lt;PANMode /&gt; - &lt;ReconReference&gt;04078059&lt;/ReconReference&gt; - &lt;CardHolderPresence&gt;CardNotPresent&lt;/CardHolderPresence&gt; - &lt;MerchantAddress&gt;MERCHANT ADDRESS&lt;/MerchantAddress&gt; - &lt;MerchantCity&gt;Sandton&lt;/MerchantCity&gt; - &lt;MerchantCountryCode&gt;ZA&lt;/MerchantCountryCode&gt; - &lt;MerchantCountry&gt;South Africa&lt;/MerchantCountry&gt; - &lt;DistributorName&gt;Nedbank&lt;/DistributorName&gt; - &lt;CCNumber&gt;4242........4242&lt;/CCNumber&gt; - &lt;PAN&gt;4242........4242&lt;/PAN&gt; - &lt;/Transaction&gt; -&lt;/V_XML&gt;</ExecuteResult></ExecuteResponse></soap:Body></soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ExecuteResponse xmlns="http://iveri.com/"><ExecuteResult>&lt;V_XML Version="2.0" Direction="Response"&gt; + &lt;Transaction ApplicationID="{D10A603D-4ADE-405B-93F1-826DFC0181E8}" Command="Credit" Mode="Test" RequestID="{097C55B5-D020-40AD-8949-F9F5E4102F1D}"&gt; + &lt;Result Status="0" AppServer="105IVERIAPPPR02" DBServer="105iveridbpr01" Gateway="Nedbank" AcquirerCode="00" /&gt; + &lt;Amount&gt;100&lt;/Amount&gt; + &lt;AuthorisationCode&gt;541996&lt;/AuthorisationCode&gt; + &lt;Currency&gt;ZAR&lt;/Currency&gt; + &lt;ExpiryDate&gt;092018&lt;/ExpiryDate&gt; + &lt;MerchantReference&gt;5be2c040bd46b7eebc70274659779acf&lt;/MerchantReference&gt; + &lt;Terminal&gt;Default&lt;/Terminal&gt; + &lt;TransactionIndex&gt;{D50DB1B4-B6EC-4AF1-AFF7-71C2AA4A957B}&lt;/TransactionIndex&gt; + &lt;MerchantName&gt;iVeri Payment Technology&lt;/MerchantName&gt; + &lt;MerchantUSN&gt;7771777&lt;/MerchantUSN&gt; + &lt;Acquirer&gt;NBPostilionBICISONBSouthAfrica&lt;/Acquirer&gt; + &lt;AcquirerReference&gt;70417:04078059&lt;/AcquirerReference&gt; + &lt;AcquirerDate&gt;20170417&lt;/AcquirerDate&gt; + &lt;AcquirerTime&gt;201956&lt;/AcquirerTime&gt; + &lt;DisplayAmount&gt;R 1.00&lt;/DisplayAmount&gt; + &lt;BIN&gt;4&lt;/BIN&gt; + &lt;Association&gt;VISA&lt;/Association&gt; + &lt;CardType&gt;Unknown CardType&lt;/CardType&gt; + &lt;Issuer&gt;Unknown&lt;/Issuer&gt; + &lt;Jurisdiction&gt;International&lt;/Jurisdiction&gt; + &lt;PANMode /&gt; + &lt;ReconReference&gt;04078059&lt;/ReconReference&gt; + &lt;CardHolderPresence&gt;CardNotPresent&lt;/CardHolderPresence&gt; + &lt;MerchantAddress&gt;MERCHANT ADDRESS&lt;/MerchantAddress&gt; + &lt;MerchantCity&gt;Sandton&lt;/MerchantCity&gt; + &lt;MerchantCountryCode&gt;ZA&lt;/MerchantCountryCode&gt; + &lt;MerchantCountry&gt;South Africa&lt;/MerchantCountry&gt; + &lt;DistributorName&gt;Nedbank&lt;/DistributorName&gt; + &lt;CCNumber&gt;4242........4242&lt;/CCNumber&gt; + &lt;PAN&gt;4242........4242&lt;/PAN&gt; + &lt;/Transaction&gt; + &lt;/V_XML&gt;</ExecuteResult></ExecuteResponse></soap:Body></soap:Envelope> XML end def failed_refund_response - <<-XML -<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ExecuteResponse xmlns="http://iveri.com/"><ExecuteResult>&lt;V_XML Version="2.0" Direction="Response"&gt; - &lt;Transaction ApplicationID="{D10A603D-4ADE-405B-93F1-826DFC0181E8}" Command="Credit" Mode="Test" RequestID="{5097A60A-A112-42F1-9490-FA17A859E7A3}"&gt; - &lt;Result Status="-1" Code="255" Description="Credit is not supported for ApplicationID (D10A603D-4ADE-405B-93F1-826DFC0181E8)" Source="PortalService" AppServer="105IVERIAPPPR02" DBServer="105iveridbpr01" Gateway="Nedbank" AcquirerCode="" AcquirerDescription="" /&gt; - &lt;/Transaction&gt; -&lt;/V_XML&gt;</ExecuteResult></ExecuteResponse></soap:Body></soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ExecuteResponse xmlns="http://iveri.com/"><ExecuteResult>&lt;V_XML Version="2.0" Direction="Response"&gt; + &lt;Transaction ApplicationID="{D10A603D-4ADE-405B-93F1-826DFC0181E8}" Command="Credit" Mode="Test" RequestID="{5097A60A-A112-42F1-9490-FA17A859E7A3}"&gt; + &lt;Result Status="-1" Code="255" Description="Credit is not supported for ApplicationID (D10A603D-4ADE-405B-93F1-826DFC0181E8)" Source="PortalService" AppServer="105IVERIAPPPR02" DBServer="105iveridbpr01" Gateway="Nedbank" AcquirerCode="" AcquirerDescription="" /&gt; + &lt;/Transaction&gt; + &lt;/V_XML&gt;</ExecuteResult></ExecuteResponse></soap:Body></soap:Envelope> XML end def successful_void_response - <<-XML -<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ExecuteResponse xmlns="http://iveri.com/"><ExecuteResult>&lt;V_XML Version="2.0" Direction="Response"&gt; - &lt;Transaction ApplicationID="{D10A603D-4ADE-405B-93F1-826DFC0181E8}" Command="Void" Mode="Test" RequestID="{0A1A3FFF-C2A3-4B91-85FD-10D1C25B765B}"&gt; - &lt;Result Status="0" AppServer="105IVERIAPPPR02" DBServer="105iveridbpr01" Gateway="Nedbank" /&gt; - &lt;OriginalRequestID&gt;{230390C8-4A9E-4426-BDD3-15D072F135FE}&lt;/OriginalRequestID&gt; - &lt;/Transaction&gt; -&lt;/V_XML&gt;</ExecuteResult></ExecuteResponse></soap:Body></soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ExecuteResponse xmlns="http://iveri.com/"><ExecuteResult>&lt;V_XML Version="2.0" Direction="Response"&gt; + &lt;Transaction ApplicationID="{D10A603D-4ADE-405B-93F1-826DFC0181E8}" Command="Void" Mode="Test" RequestID="{0A1A3FFF-C2A3-4B91-85FD-10D1C25B765B}"&gt; + &lt;Result Status="0" AppServer="105IVERIAPPPR02" DBServer="105iveridbpr01" Gateway="Nedbank" /&gt; + &lt;OriginalRequestID&gt;{230390C8-4A9E-4426-BDD3-15D072F135FE}&lt;/OriginalRequestID&gt; + &lt;/Transaction&gt; + &lt;/V_XML&gt;</ExecuteResult></ExecuteResponse></soap:Body></soap:Envelope> XML end def failed_void_response - <<-XML -<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ExecuteResponse xmlns="http://iveri.com/"><ExecuteResult>&lt;V_XML Version="2.0" Direction="Response"&gt; - &lt;Transaction ApplicationID="{D10A603D-4ADE-405B-93F1-826DFC0181E8}" Command="Void" Mode="Test" RequestID="{AE97CCE4-0631-4F08-AB47-9C2698ABEC75}"&gt; - &lt;Result Status="-1" Code="255" Description="Missing OriginalMerchantTrace" Source="NBPostilionBICISONBSouthAfricaTestProvider" AppServer="105IVERIAPPPR01" DBServer="105IVERIDBPR01" Gateway="Nedbank" AcquirerCode="" AcquirerDescription="" /&gt; - &lt;/Transaction&gt; -&lt;/V_XML&gt;</ExecuteResult></ExecuteResponse></soap:Body></soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ExecuteResponse xmlns="http://iveri.com/"><ExecuteResult>&lt;V_XML Version="2.0" Direction="Response"&gt; + &lt;Transaction ApplicationID="{D10A603D-4ADE-405B-93F1-826DFC0181E8}" Command="Void" Mode="Test" RequestID="{AE97CCE4-0631-4F08-AB47-9C2698ABEC75}"&gt; + &lt;Result Status="-1" Code="255" Description="Missing OriginalMerchantTrace" Source="NBPostilionBICISONBSouthAfricaTestProvider" AppServer="105IVERIAPPPR01" DBServer="105IVERIDBPR01" Gateway="Nedbank" AcquirerCode="" AcquirerDescription="" /&gt; + &lt;/Transaction&gt; + &lt;/V_XML&gt;</ExecuteResult></ExecuteResponse></soap:Body></soap:Envelope> XML end def successful_verify_response - <<-XML -<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ExecuteResponse xmlns="http://iveri.com/"><ExecuteResult>&lt;V_XML Version="2.0" Direction="Response"&gt; - &lt;Transaction ApplicationID="{D10A603D-4ADE-405B-93F1-826DFC0181E8}" Command="Authorisation" Mode="Test" RequestID="{F4337D04-B526-4A7E-A400-2A6DEADDCF57}"&gt; - &lt;Result Status="0" AppServer="105IVERIAPPPR01" DBServer="105IVERIDBPR01" Gateway="Nedbank" AcquirerCode="00" /&gt; - &lt;Amount&gt;0&lt;/Amount&gt; - &lt;AuthorisationCode&gt;613755&lt;/AuthorisationCode&gt; - &lt;Currency&gt;ZAR&lt;/Currency&gt; - &lt;ExpiryDate&gt;092018&lt;/ExpiryDate&gt; - &lt;MerchantReference&gt;c0006d1d739905afc9e70beaf4194ea3&lt;/MerchantReference&gt; - &lt;Terminal&gt;Default&lt;/Terminal&gt; - &lt;TransactionIndex&gt;{5D5F8BF7-2D9D-42C3-AF32-08C5E62CD45E}&lt;/TransactionIndex&gt; - &lt;MerchantName&gt;iVeri Payment Technology&lt;/MerchantName&gt; - &lt;MerchantUSN&gt;7771777&lt;/MerchantUSN&gt; - &lt;Acquirer&gt;NBPostilionBICISONBSouthAfrica&lt;/Acquirer&gt; - &lt;AcquirerReference&gt;70418:04078335&lt;/AcquirerReference&gt; - &lt;AcquirerDate&gt;20170418&lt;/AcquirerDate&gt; - &lt;AcquirerTime&gt;161555&lt;/AcquirerTime&gt; - &lt;DisplayAmount&gt;R 0.00&lt;/DisplayAmount&gt; - &lt;BIN&gt;4&lt;/BIN&gt; - &lt;Association&gt;VISA&lt;/Association&gt; - &lt;CardType&gt;Unknown CardType&lt;/CardType&gt; - &lt;Issuer&gt;Unknown&lt;/Issuer&gt; - &lt;Jurisdiction&gt;International&lt;/Jurisdiction&gt; - &lt;PANMode&gt;Keyed,CVV&lt;/PANMode&gt; - &lt;ReconReference&gt;04078335&lt;/ReconReference&gt; - &lt;CardHolderPresence&gt;CardNotPresent&lt;/CardHolderPresence&gt; - &lt;MerchantAddress&gt;MERCHANT ADDRESS&lt;/MerchantAddress&gt; - &lt;MerchantCity&gt;Sandton&lt;/MerchantCity&gt; - &lt;MerchantCountryCode&gt;ZA&lt;/MerchantCountryCode&gt; - &lt;MerchantCountry&gt;South Africa&lt;/MerchantCountry&gt; - &lt;DistributorName&gt;Nedbank&lt;/DistributorName&gt; - &lt;CCNumber&gt;4242........4242&lt;/CCNumber&gt; - &lt;PAN&gt;4242........4242&lt;/PAN&gt; - &lt;/Transaction&gt; -&lt;/V_XML&gt;</ExecuteResult></ExecuteResponse></soap:Body></soap:Envelope> - XML - end - - def failed_verify_response - <<-XML -<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ExecuteResponse xmlns="http://iveri.com/"><ExecuteResult>&lt;V_XML Version="2.0" Direction="Response"&gt; - &lt;Transaction ApplicationID="{D10A603D-4ADE-405B-93F1-826DFC0181E8}" Command="Authorisation" Mode="Test" RequestID="{A700FAE2-2A76-407D-A540-B41668E2B703}"&gt; - &lt;Result Status="-1" Code="4" Description="Denied" Source="NBPostilionBICISONBSouthAfrica" AppServer="105IVERIAPPPR02" DBServer="105iveridbpr01" Gateway="Nedbank" AcquirerCode="05" AcquirerDescription="Do not Honour" /&gt; - &lt;Amount&gt;0&lt;/Amount&gt; - &lt;Currency&gt;ZAR&lt;/Currency&gt; - &lt;ExpiryDate&gt;092018&lt;/ExpiryDate&gt; - &lt;MerchantReference&gt;e955afb03f224284b09ad6ae7e9b4683&lt;/MerchantReference&gt; - &lt;Terminal&gt;Default&lt;/Terminal&gt; - &lt;TransactionIndex&gt;{2A378547-AEA4-48E1-8A3E-29F9BBEA954D}&lt;/TransactionIndex&gt; - &lt;MerchantName&gt;iVeri Payment Technology&lt;/MerchantName&gt; - &lt;MerchantUSN&gt;7771777&lt;/MerchantUSN&gt; - &lt;Acquirer&gt;NBPostilionBICISONBSouthAfrica&lt;/Acquirer&gt; - &lt;AcquirerReference&gt;70418:04078337&lt;/AcquirerReference&gt; - &lt;AcquirerDate&gt;20170418&lt;/AcquirerDate&gt; - &lt;AcquirerTime&gt;161716&lt;/AcquirerTime&gt; - &lt;DisplayAmount&gt;R 0.00&lt;/DisplayAmount&gt; - &lt;BIN&gt;2&lt;/BIN&gt; - &lt;Association&gt;Unknown Association&lt;/Association&gt; - &lt;CardType&gt;Unknown CardType&lt;/CardType&gt; - &lt;Issuer&gt;Unknown&lt;/Issuer&gt; - &lt;Jurisdiction&gt;Local&lt;/Jurisdiction&gt; - &lt;PANMode&gt;Keyed,CVV&lt;/PANMode&gt; - &lt;ReconReference&gt;04078337&lt;/ReconReference&gt; - &lt;CardHolderPresence&gt;CardNotPresent&lt;/CardHolderPresence&gt; - &lt;MerchantAddress&gt;MERCHANT ADDRESS&lt;/MerchantAddress&gt; - &lt;MerchantCity&gt;Sandton&lt;/MerchantCity&gt; - &lt;MerchantCountryCode&gt;ZA&lt;/MerchantCountryCode&gt; - &lt;MerchantCountry&gt;South Africa&lt;/MerchantCountry&gt; - &lt;DistributorName&gt;Nedbank&lt;/DistributorName&gt; - &lt;CCNumber&gt;2121........2121&lt;/CCNumber&gt; - &lt;PAN&gt;2121........2121&lt;/PAN&gt; - &lt;/Transaction&gt; -&lt;/V_XML&gt;</ExecuteResult></ExecuteResponse></soap:Body></soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ExecuteResponse xmlns="http://iveri.com/"><ExecuteResult>&lt;V_XML Version="2.0" Direction="Response"&gt; + &lt;Transaction ApplicationID="{D10A603D-4ADE-405B-93F1-826DFC0181E8}" Command="Authorisation" Mode="Test" RequestID="{F4337D04-B526-4A7E-A400-2A6DEADDCF57}"&gt; + &lt;Result Status="0" AppServer="105IVERIAPPPR01" DBServer="105IVERIDBPR01" Gateway="Nedbank" AcquirerCode="00" /&gt; + &lt;Amount&gt;0&lt;/Amount&gt; + &lt;AuthorisationCode&gt;613755&lt;/AuthorisationCode&gt; + &lt;Currency&gt;ZAR&lt;/Currency&gt; + &lt;ExpiryDate&gt;092018&lt;/ExpiryDate&gt; + &lt;MerchantReference&gt;c0006d1d739905afc9e70beaf4194ea3&lt;/MerchantReference&gt; + &lt;Terminal&gt;Default&lt;/Terminal&gt; + &lt;TransactionIndex&gt;{5D5F8BF7-2D9D-42C3-AF32-08C5E62CD45E}&lt;/TransactionIndex&gt; + &lt;MerchantName&gt;iVeri Payment Technology&lt;/MerchantName&gt; + &lt;MerchantUSN&gt;7771777&lt;/MerchantUSN&gt; + &lt;Acquirer&gt;NBPostilionBICISONBSouthAfrica&lt;/Acquirer&gt; + &lt;AcquirerReference&gt;70418:04078335&lt;/AcquirerReference&gt; + &lt;AcquirerDate&gt;20170418&lt;/AcquirerDate&gt; + &lt;AcquirerTime&gt;161555&lt;/AcquirerTime&gt; + &lt;DisplayAmount&gt;R 0.00&lt;/DisplayAmount&gt; + &lt;BIN&gt;4&lt;/BIN&gt; + &lt;Association&gt;VISA&lt;/Association&gt; + &lt;CardType&gt;Unknown CardType&lt;/CardType&gt; + &lt;Issuer&gt;Unknown&lt;/Issuer&gt; + &lt;Jurisdiction&gt;International&lt;/Jurisdiction&gt; + &lt;PANMode&gt;Keyed,CVV&lt;/PANMode&gt; + &lt;ReconReference&gt;04078335&lt;/ReconReference&gt; + &lt;CardHolderPresence&gt;CardNotPresent&lt;/CardHolderPresence&gt; + &lt;MerchantAddress&gt;MERCHANT ADDRESS&lt;/MerchantAddress&gt; + &lt;MerchantCity&gt;Sandton&lt;/MerchantCity&gt; + &lt;MerchantCountryCode&gt;ZA&lt;/MerchantCountryCode&gt; + &lt;MerchantCountry&gt;South Africa&lt;/MerchantCountry&gt; + &lt;DistributorName&gt;Nedbank&lt;/DistributorName&gt; + &lt;CCNumber&gt;4242........4242&lt;/CCNumber&gt; + &lt;PAN&gt;4242........4242&lt;/PAN&gt; + &lt;/Transaction&gt; + &lt;/V_XML&gt;</ExecuteResult></ExecuteResponse></soap:Body></soap:Envelope> XML end def successful_verify_credentials_response - <<-XML -<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ExecuteResponse xmlns="http://iveri.com/"><ExecuteResult>&lt;V_XML Version="2.0" Direction="Response"&gt; - &lt;Transaction ApplicationID="{D10A603D-4ADE-405B-93F1-826DFC0181E8}" Command="Void" Mode="Test" RequestID="{5ED922D0-92AD-40DF-9019-320591A4BA59}"&gt; - &lt;Result Status="-1" Code="255" Description="Missing OriginalMerchantTrace" Source="NBPostilionBICISONBSouthAfricaTestProvider" AppServer="105IVERIAPPPR01" DBServer="105IVERIDBPR01" Gateway="Nedbank" AcquirerCode="" AcquirerDescription="" /&gt; - &lt;/Transaction&gt; -&lt;/V_XML&gt;</ExecuteResult></ExecuteResponse></soap:Body></soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ExecuteResponse xmlns="http://iveri.com/"><ExecuteResult>&lt;V_XML Version="2.0" Direction="Response"&gt; + &lt;Transaction ApplicationID="{D10A603D-4ADE-405B-93F1-826DFC0181E8}" Command="Void" Mode="Test" RequestID="{5ED922D0-92AD-40DF-9019-320591A4BA59}"&gt; + &lt;Result Status="-1" Code="255" Description="Missing OriginalMerchantTrace" Source="NBPostilionBICISONBSouthAfricaTestProvider" AppServer="105IVERIAPPPR01" DBServer="105IVERIDBPR01" Gateway="Nedbank" AcquirerCode="" AcquirerDescription="" /&gt; + &lt;/Transaction&gt; + &lt;/V_XML&gt;</ExecuteResult></ExecuteResponse></soap:Body></soap:Envelope> XML end def failed_verify_credentials_response - <<-XML -<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ExecuteResponse xmlns="http://iveri.com/"><ExecuteResult>&lt;V_XML Version="2.0" Direction="Response"&gt; - &lt;Result Status="-1" Code="255" Description="The ApplicationID {11111111-1111-1111-1111-111111111111} is not valid for the current CertificateID {11111111-1111-1111-1111-111111111111}" Source="RequestHandler" RequestID="{EE6E5B39-63AD-402C-8331-F25082AD8564}" AppServer="105IVERIAPPPR01" DBServer="105IVERIDBPR01" Gateway="Nedbank" AcquirerCode="" AcquirerDescription="" /&gt; -&lt;/V_XML&gt;</ExecuteResult></ExecuteResponse></soap:Body></soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ExecuteResponse xmlns="http://iveri.com/"><ExecuteResult>&lt;V_XML Version="2.0" Direction="Response"&gt; + &lt;Result Status="-1" Code="255" Description="The ApplicationID {11111111-1111-1111-1111-111111111111} is not valid for the current CertificateID {11111111-1111-1111-1111-111111111111}" Source="RequestHandler" RequestID="{EE6E5B39-63AD-402C-8331-F25082AD8564}" AppServer="105IVERIAPPPR01" DBServer="105IVERIDBPR01" Gateway="Nedbank" AcquirerCode="" AcquirerDescription="" /&gt; + &lt;/V_XML&gt;</ExecuteResult></ExecuteResponse></soap:Body></soap:Envelope> XML end end diff --git a/test/unit/gateways/ixopay_test.rb b/test/unit/gateways/ixopay_test.rb new file mode 100644 index 00000000000..a7f251c0d36 --- /dev/null +++ b/test/unit/gateways/ixopay_test.rb @@ -0,0 +1,673 @@ +require 'test_helper' + +class IxopayTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = IxopayGateway.new( + username: 'username', + password: 'password', + secret: 'secret', + api_key: 'api_key' + ) + + @declined_card = credit_card('4000300011112220') + @credit_card = credit_card + @amount = 100 + + @options = { + order_id: '1', + billing_address: address, + description: 'Store Purchase', + ip: '192.168.1.1' + } + + @extra_data = { extra_data: { customData1: 'some data', customData2: 'Can be anything really' } } + end + + def test_successful_purchase + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<description>.+<\/description>/, data) + end.respond_with(successful_purchase_response) + + assert_success response + assert_equal 'FINISHED', response.message + assert_equal 'b2bef23a30b537b90fbe|20191016-b2bef23a30b537b90fbe', response.authorization + assert response.test? + end + + def test_successful_purchase_with_extra_data + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(@extra_data)) + end.check_request do |_endpoint, data, _headers| + assert_match(/<extraData key="customData1">some data<\/extraData>/, data) + assert_match(/<extraData key="customData2">Can be anything really<\/extraData>/, data) + end.respond_with(successful_purchase_response) + + assert_success response + assert_equal 'FINISHED', response.message + assert_equal 'b2bef23a30b537b90fbe|20191016-b2bef23a30b537b90fbe', response.authorization + assert response.test? + end + + def test_failed_purchase + @gateway.expects(:ssl_post).returns(failed_purchase_response) + + response = @gateway.purchase(@amount, @declined_card, @options) + + assert_failure response + assert_equal 'The transaction was declined', response.message + assert_equal '2003', response.error_code + end + + def test_failed_authentication + @gateway.expects(:ssl_post).raises(mock_response_error) + + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_failure response + assert 'Invalid Signature', response.message + end + + def test_successful_authorize + @gateway.expects(:ssl_post).returns(successful_authorize_response) + + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert match(/<description>.+<\/description>/, data) + end.respond_with(successful_authorize_response) + + assert_success response + assert_equal 'FINISHED', response.message + assert_equal '00eb44f8f0382443cce5|20191028-00eb44f8f0382443cce5', response.authorization + assert response.test? + end + + def test_successful_authorize_with_extra_data + @gateway.expects(:ssl_post).returns(successful_authorize_response) + + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(@extra_data)) + end.check_request do |_endpoint, data, _headers| + assert_match(/<extraData key="customData1">some data<\/extraData>/, data) + assert_match(/<extraData key="customData2">Can be anything really<\/extraData>/, data) + end.respond_with(successful_authorize_response) + + assert_success response + assert_equal 'FINISHED', response.message + assert_equal '00eb44f8f0382443cce5|20191028-00eb44f8f0382443cce5', response.authorization + assert response.test? + end + + def test_failed_authorize + @gateway.expects(:ssl_post).returns(failed_purchase_response) + + response = @gateway.authorize(@amount, @declined_card, @options) + + assert_failure response + assert_equal 'The transaction was declined', response.message + assert_equal '2003', response.error_code + end + + def test_successful_capture + @gateway.expects(:ssl_post).returns(successful_capture_response) + + response = @gateway.capture(@amount, '00eb44f8f0382443cce5|20191028-00eb44f8f0382443cce5') + + assert_success response + assert_equal 'FINISHED', response.message + assert_equal '17dd1e0b09221e9db038|20191031-17dd1e0b09221e9db038', response.authorization + assert response.test? + end + + def test_successful_capture_with_extra_data + @gateway.expects(:ssl_post).returns(successful_capture_response) + + response = stub_comms do + @gateway.capture(@amount, '00eb44f8f0382443cce5|20191028-00eb44f8f0382443cce5', @options.merge(@extra_data)) + end.check_request do |_endpoint, data, _header| + assert_match(/<extraData key="customData1">some data<\/extraData>/, data) + assert_match(/<extraData key="customData2">Can be anything really<\/extraData>/, data) + end.respond_with(successful_capture_response) + + assert_success response + assert_equal 'FINISHED', response.message + assert_equal '17dd1e0b09221e9db038|20191031-17dd1e0b09221e9db038', response.authorization + assert response.test? + end + + def test_failed_capture + @gateway.expects(:ssl_post).returns(failed_capture_response) + + response = @gateway.capture(@amount, nil) + + assert_failure response + assert_equal 'Transaction of type "capture" requires a referenceTransactionId', response.message + assert_equal '9999', response.error_code + end + + def test_successful_refund + @gateway.expects(:ssl_post).returns(successful_refund_response) + response = @gateway.refund(@amount, 'eb2bef23a30b537b90fb|20191016-b2bef23a30b537b90fbe') + + assert_success response + assert_equal 'FINISHED', response.message + end + + def test_successful_refund_with_extra_data + @gateway.expects(:ssl_post).returns(successful_refund_response) + + response = stub_comms do + @gateway.refund(@amount, 'eb2bef23a30b537b90fb|20191016-b2bef23a30b537b90fbe', @options.merge(@extra_data)) + end.check_request do |_endpoint, data, _header| + assert_match(/<extraData key="customData1">some data<\/extraData>/, data) + assert_match(/<extraData key="customData2">Can be anything really<\/extraData>/, data) + end.respond_with(successful_refund_response) + + assert_success response + assert_equal 'FINISHED', response.message + end + + def test_refund_includes_currency_option + options = { currency: 'USD' } + + stub_comms do + @gateway.refund(@amount, 'eb2bef23a30b537b90fb|20191016-b2bef23a30b537b90fbe', options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<currency>USD<\/currency>/, data) + end.respond_with(successful_refund_response) + end + + def test_failed_refund + @gateway.expects(:ssl_post).returns(failed_refund_response) + response = @gateway.refund(@amount, nil) + + assert_failure response + assert_equal 'Transaction of type "refund" requires a referenceTransactionId', response.message + end + + def test_successful_void + @gateway.expects(:ssl_post).returns(successful_void_response) + response = @gateway.void('eb2bef23a30b537b90fb|20191016-b2bef23a30b537b90fbe') + + assert_success response + assert_equal 'FINISHED', response.message + end + + def test_successful_void_with_extra_data + @gateway.expects(:ssl_post).returns(successful_void_response) + response = stub_comms do + @gateway.void('eb2bef23a30b537b90fb|20191016-b2bef23a30b537b90fbe', @options.merge(@extra_data)) + end.check_request do |_endpoint, data, _header| + assert_match(/<extraData key="customData1">some data<\/extraData>/, data) + assert_match(/<extraData key="customData2">Can be anything really<\/extraData>/, data) + end.respond_with(successful_void_response) + + assert_success response + assert_equal 'FINISHED', response.message + end + + def test_failed_void + @gateway.expects(:ssl_post).returns(failed_void_response) + response = @gateway.void(nil) + + assert_failure response + assert_equal 'Transaction of type "void" requires a referenceTransactionId', response.message + end + + def test_successful_verify + @gateway.expects(:ssl_post).times(2).returns(successful_authorize_response, successful_void_response) + response = @gateway.verify(credit_card('4111111111111111'), @options) + + assert_success response + assert_equal 'FINISHED', response.message + end + + def test_successful_verify_with_extra_data + @gateway.expects(:ssl_post).times(2).returns(successful_authorize_response, successful_void_response) + response = stub_comms do + @gateway.verify(credit_card('4111111111111111'), @options.merge(@extra_data)) + end.check_request do |_endpoint, data, _header| + assert_match(/<extraData key="customData1">some data<\/extraData>/, data) + assert_match(/<extraData key="customData2">Can be anything really<\/extraData>/, data) + end.respond_with(successful_authorize_response, successful_void_response) + + assert_success response + assert_equal 'FINISHED', response.message + end + + def test_successful_verify_with_failed_void + @gateway.expects(:ssl_post).times(2).returns(successful_authorize_response, failed_void_response) + + response = @gateway.verify(credit_card('4111111111111111'), @options) + assert_success response + end + + def test_failed_verify + @gateway.expects(:ssl_post).returns(failed_authorize_response) + + response = @gateway.verify(@credit_card, @options) + assert_failure response + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + # Stored Credential Tests + # Ixopay does not pass any parameters for cardholder/merchant initiated. + # Ixopay also doesn't support installment transactions, only recurring + # ("RECURRING") and unscheduled ("CARDONFILE"). + # + # Furthermore, Ixopay is slightly unusual in its application of stored + # credentials in that the gateway does not return a true + # network_transaction_id that can be sent on subsequent transactions. + def test_purchase_stored_credentials_initial + options = @options.merge( + stored_credential: stored_credential(:initial, :recurring) + ) + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<transactionIndicator>INITIAL<\/transactionIndicator>/, data) + end.respond_with(successful_purchase_response) + + assert_success response + assert_equal 'FINISHED', response.message + end + + def test_authorize_stored_credentials_initial + options = @options.merge( + stored_credential: stored_credential(:initial, :unscheduled) + ) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<transactionIndicator>INITIAL<\/transactionIndicator>/, data) + end.respond_with(successful_authorize_response) + + assert_success response + assert_equal 'FINISHED', response.message + end + + def test_purchase_stored_credentials_recurring + options = @options.merge( + stored_credential: stored_credential(:recurring) + ) + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<transactionIndicator>RECURRING<\/transactionIndicator>/, data) + end.respond_with(successful_purchase_response) + + assert_success response + assert_equal 'FINISHED', response.message + end + + def test_authorize_stored_credentials_recurring + options = @options.merge( + stored_credential: stored_credential(:recurring) + ) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<transactionIndicator>RECURRING<\/transactionIndicator>/, data) + end.respond_with(successful_authorize_response) + + assert_success response + assert_equal 'FINISHED', response.message + end + + def test_purchase_stored_credentials_unscheduled + options = @options.merge( + stored_credential: stored_credential(:unscheduled) + ) + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<transactionIndicator>CARDONFILE<\/transactionIndicator>/, data) + end.respond_with(successful_purchase_response) + + assert_success response + assert_equal 'FINISHED', response.message + end + + def test_authorize_stored_credentials_unscheduled + options = @options.merge( + stored_credential: stored_credential(:unscheduled) + ) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<transactionIndicator>CARDONFILE<\/transactionIndicator>/, data) + end.respond_with(successful_authorize_response) + + assert_success response + assert_equal 'FINISHED', response.message + end + + def test_three_decimal_currency_handling + response = stub_comms do + @gateway.authorize(14200, @credit_card, @options.merge(currency: 'KWD')) + end.check_request do |_endpoint, data, _headers| + assert_match(/<amount>14.200<\/amount>/, data) + assert_match(/<currency>KWD<\/currency>/, data) + end.respond_with(successful_authorize_response) + + assert_success response + assert_equal 'FINISHED', response.message + end + + private + + def mock_response_error + mock_response = Net::HTTPUnprocessableEntity.new('1.1', '401', 'Unauthorized') + mock_response.stubs(:body).returns(failed_authentication_response) + + ActiveMerchant::ResponseError.new(mock_response) + end + + def pre_scrubbed + <<-TRANSCRIPT + opening connection to secure.ixopay.com:443... + opened + starting SSL for secure.ixopay.com:443... + SSL established + <- "POST /transaction HTTP/1.1\r\nContent-Type: text/xml; charset=utf-8\r\nAuthorization: Gateway spreedly-integration-1:i8CtuPyY820sX8hvJuRbygSnotj+VibBxqFl9MoFLYdrwC91zxymCv3h72DZBkOYT05P/L1Ig5aQrPf8SdOWtw==\r\nDate: Fri, 18 Oct 2019 19:24:53 GMT\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.ixopay.com\r\nContent-Length: 1717\r\n\r\n" + <- "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<transactionWithCard xmlns=\"http://secure.ixopay.com/Schema/V2/TransactionWithCard\">\n <username>spreedly-dev-api</username>\n <password>834ab26f399def0fea3e444d6cecbf6c61230e09</password>\n <cardData>\n <cardHolder>Longbob Longsen</cardHolder>\n <pan>4111111111111111</pan>\n <cvv>123</cvv>\n <expirationMonth>09</expirationMonth>\n <expirationYear>2020</expirationYear>\n </cardData>\n <debit>\n <transactionId>13454623-e012-4f77-b9e7-c9536964f186</transactionId>\n <customer>\n <firstName>Jim</firstName>\n <lastName>Smith</lastName>\n <billingAddress1>456 My Street</billingAddress1>\n <billingAddress2>Apt 1</billingAddress2>\n <billingCity>Ottawa</billingCity>\n <billingPostcode>K1C2N6</billingPostcode>\n <billingState>ON</billingState>\n <billingCountry>CA</billingCountry>\n <billingPhone>(555)555-5555</billingPhone>\n <shippingFirstName>Jim</shippingFirstName>\n <shippingLastName>Smith</shippingLastName>\n <shippingCompany>Widgets Inc</shippingCompany>\n <shippingAddress1>456 My Street</shippingAddress1>\n <shippingAddress2>Apt 1</shippingAddress2>\n <shippingCity>Ottawa</shippingCity>\n <shippingPostcode>K1C2N6</shippingPostcode>\n <shippingState>ON</shippingState>\n <shippingCountry>CA</shippingCountry>\n <shippingPhone>(555)555-5555</shippingPhone>\n <company>Widgets Inc</company>\n <email>test@example.com</email>\n <ipAddress>192.168.1.1</ipAddress>\n </customer>\n <amount>100</amount>\n <currency>EUR</currency>\n <description>Store Purchase</description>\n <callbackUrl>http://example.com</callbackUrl>\n </debit>\n</transactionWithCard>\n" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Fri, 18 Oct 2019 19:24:55 GMT\r\n" + -> "Content-Type: text/html; charset=UTF-8\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "Set-Cookie: __cfduid=db8efa44225d95d93942c576b8f53feb31571426693; expires=Sat, 17-Oct-20 19:24:53 GMT; path=/; domain=.ixopay.com; HttpOnly\r\n" + -> "5: Content-Type: text/xml; charset=UTF-8\r\n" + -> "Cache-Control: no-cache\r\n" + -> "Strict-Transport-Security: max-age=15552000; includeSubDomains; preload\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "X-Server: vau-prod-webfe-esh-02\r\n" + -> "CF-Cache-Status: DYNAMIC\r\n" + -> "Expect-CT: max-age=604800, report-uri=\"https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct\"\r\n" + -> "Server: cloudflare\r\n" + -> "CF-RAY: 527ce522ab3b9f7c-IAD\r\n" + -> "Content-Encoding: gzip\r\n" + -> "\r\n" + -> "18c\r\n" + reading 396 bytes... + -> "\x1F\x8B\b\x00\x00\x00\x00\x00\x00\x03l\x92Mo\xDB0\f\x86\xEF\xF9\x15\x82\xEF\x8D\xFC\x91\xA6\xC9 \xAB\x87eE\x03\xAC=4\xC3\x80\x1De\x89\x89\x85\xD9\x92AI\x85\xFD\xEF\v\xD9q\x96\xB4\xD3E\xE0\xCB\x87/EB\xEC\xB1o\e\xF2\x0E\xE8\xB45e\x92-\xD3\x84\x80\x91Vis*\x93\xE0\x8Fw\x9B\xE4\x91/\x18\x82\v\x8D'}\xDB\x18W&\xB5\xF7\xDD7J\x1D\xC8\x80\xB0\xD4\xBD\xED\xC4\xB0\x94\xB6\xA5\aYC+\xE8\xEF\x9C\xBE\x8D\x05\t_\x10\xC2\\\x90\x12\x9C\xE3\x1E\x030:G1\x83p\x04\x04#a\xAF\xF8\xAA(\xF2j\x9D\x17b\xBD\x11\x0F\xD5}Q\xACW\x0F\x8C^\x13\xB1\xA2\v(k\xE1b\x98\xA7\xD96K\xB3\xCD\xDD\xFF+\xAF\xC8\xA9\x95\x0Fh~\r\x1D\xF0\xA7\xFD\xEB\xFE\xF0\xFCc\x17\xDD/\xE2h.\x86\x16\x8C\x7F\x01_[\xC5\xBF#(\xED\xA5@\xC5\xE8m\xE6\x9F\xDFNxA\xFC\xD0A\x99\xC8\v\x1E\xC5qrB\xD8\xAD:\x89\x84\xB0X\xC2\xDF\xB5\x13\x8C\xFAs\xF7\t\x17\xA8\x9Em\xA3\x00\xF9OkN\x95\xADH\xBC\x1D\x18F\xAFr3\x0E}\xA7qx\xB1\xC6\xD7<\xDD2z\x1D\xDF2\x7F@ \xCF\xD3<\x9D\xA1Q\x98\x99\xA3F\xE7\x0F\xBA\xDF\xE9\x93\xF6\x8E\xAF\xB2x\x18\xFD$\xCFt#\x9C\x7F\xB2\x01\xCF\xF2\xC4~\x12\xA7\xE9\xE9\xD7\xF1\xE7\xA5_b\xE8=\x8Aq\x8F\x7Fa(\x13):\x1F\x10\xF6*\xE1\xF7J\x88,\xDB\xAC\xB7\xC5\x16\xAA\xF8\xEE3\xC8\x17\xD1$\xFE/\xBE\xF8\x00\x00\x00\xFF\xFF\x03\x00\x0F\x10\x82\b\xC1\x02\x00\x00" + read 396 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + TRANSCRIPT + end + + def post_scrubbed + transcript = <<-TRANSCRIPT + opening connection to secure.ixopay.com:443... + opened + starting SSL for secure.ixopay.com:443... + SSL established + <- "POST /transaction HTTP/1.1\r\nContent-Type: text/xml; charset=utf-8\r\nAuthorization: Gateway [FILTERED]:i8CtuPyY820sX8hvJuRbygSnotj+VibBxqFl9MoFLYdrwC91zxymCv3h72DZBkOYT05P/L1Ig5aQrPf8SdOWtw==\r\nDate: Fri, 18 Oct 2019 19:24:53 GMT\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.ixopay.com\r\nContent-Length: 1717\r\n\r\n" + <- "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<transactionWithCard xmlns=\"http://secure.ixopay.com/Schema/V2/TransactionWithCard\">\n <username>spreedly-dev-api</username>\n <password>[FILTERED]</password>\n <cardData>\n <cardHolder>Longbob Longsen</cardHolder>\n <pan>[FILTERED]</pan>\n <cvv>[FILTERED]</cvv>\n <expirationMonth>09</expirationMonth>\n <expirationYear>2020</expirationYear>\n </cardData>\n <debit>\n <transactionId>13454623-e012-4f77-b9e7-c9536964f186</transactionId>\n <customer>\n <firstName>Jim</firstName>\n <lastName>Smith</lastName>\n <billingAddress1>456 My Street</billingAddress1>\n <billingAddress2>Apt 1</billingAddress2>\n <billingCity>Ottawa</billingCity>\n <billingPostcode>K1C2N6</billingPostcode>\n <billingState>ON</billingState>\n <billingCountry>CA</billingCountry>\n <billingPhone>(555)555-5555</billingPhone>\n <shippingFirstName>Jim</shippingFirstName>\n <shippingLastName>Smith</shippingLastName>\n <shippingCompany>Widgets Inc</shippingCompany>\n <shippingAddress1>456 My Street</shippingAddress1>\n <shippingAddress2>Apt 1</shippingAddress2>\n <shippingCity>Ottawa</shippingCity>\n <shippingPostcode>K1C2N6</shippingPostcode>\n <shippingState>ON</shippingState>\n <shippingCountry>CA</shippingCountry>\n <shippingPhone>(555)555-5555</shippingPhone>\n <company>Widgets Inc</company>\n <email>test@example.com</email>\n <ipAddress>192.168.1.1</ipAddress>\n </customer>\n <amount>100</amount>\n <currency>EUR</currency>\n <description>Store Purchase</description>\n <callbackUrl>http://example.com</callbackUrl>\n </debit>\n</transactionWithCard>\n" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Fri, 18 Oct 2019 19:24:55 GMT\r\n" + -> "Content-Type: text/html; charset=UTF-8\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "Set-Cookie: __cfduid=db8efa44225d95d93942c576b8f53feb31571426693; expires=Sat, 17-Oct-20 19:24:53 GMT; path=/; domain=.ixopay.com; HttpOnly\r\n" + -> "5: Content-Type: text/xml; charset=UTF-8\r\n" + -> "Cache-Control: no-cache\r\n" + -> "Strict-Transport-Security: max-age=15552000; includeSubDomains; preload\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "X-Server: vau-prod-webfe-esh-02\r\n" + -> "CF-Cache-Status: DYNAMIC\r\n" + -> "Expect-CT: max-age=604800, report-uri=\"https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct\"\r\n" + -> "Server: cloudflare\r\n" + -> "CF-RAY: 527ce522ab3b9f7c-IAD\r\n" + -> "Content-Encoding: gzip\r\n" + -> "\r\n" + -> "18c\r\n" + reading 396 bytes... + -> "\x1F\x8B\b\x00\x00\x00\x00\x00\x00\x03l\x92Mo\xDB0\f\x86\xEF\xF9\x15\x82\xEF\x8D\xFC\x91\xA6\xC9 \xAB\x87eE\x03\xAC=4\xC3\x80\x1De\x89\x89\x85\xD9\x92AI\x85\xFD\xEF\v\xD9q\x96\xB4\xD3E\xE0\xCB\x87/EB\xEC\xB1o\e\xF2\x0E\xE8\xB45e\x92-\xD3\x84\x80\x91Vis*\x93\xE0\x8Fw\x9B\xE4\x91/\x18\x82\v\x8D'}\xDB\x18W&\xB5\xF7\xDD7J\x1D\xC8\x80\xB0\xD4\xBD\xED\xC4\xB0\x94\xB6\xA5\aYC+\xE8\xEF\x9C\xBE\x8D\x05\t_\x10\xC2\\\x90\x12\x9C\xE3\x1E\x030:G1\x83p\x04\x04#a\xAF\xF8\xAA(\xF2j\x9D\x17b\xBD\x11\x0F\xD5}Q\xACW\x0F\x8C^\x13\xB1\xA2\v(k\xE1b\x98\xA7\xD96K\xB3\xCD\xDD\xFF+\xAF\xC8\xA9\x95\x0Fh~\r\x1D\xF0\xA7\xFD\xEB\xFE\xF0\xFCc\x17\xDD/\xE2h.\x86\x16\x8C\x7F\x01_[\xC5\xBF#(\xED\xA5@\xC5\xE8m\xE6\x9F\xDFNxA\xFC\xD0A\x99\xC8\v\x1E\xC5qrB\xD8\xAD:\x89\x84\xB0X\xC2\xDF\xB5\x13\x8C\xFAs\xF7\t\x17\xA8\x9Em\xA3\x00\xF9OkN\x95\xADH\xBC\x1D\x18F\xAFr3\x0E}\xA7qx\xB1\xC6\xD7<\xDD2z\x1D\xDF2\x7F@ \xCF\xD3<\x9D\xA1Q\x98\x99\xA3F\xE7\x0F\xBA\xDF\xE9\x93\xF6\x8E\xAF\xB2x\x18\xFD$\xCFt#\x9C\x7F\xB2\x01\xCF\xF2\xC4~\x12\xA7\xE9\xE9\xD7\xF1\xE7\xA5_b\xE8=\x8Aq\x8F\x7Fa(\x13):\x1F\x10\xF6*\xE1\xF7J\x88,\xDB\xAC\xB7\xC5\x16\xAA\xF8\xEE3\xC8\x17\xD1$\xFE/\xBE\xF8\x00\x00\x00\xFF\xFF\x03\x00\x0F\x10\x82\b\xC1\x02\x00\x00" + read 396 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + TRANSCRIPT + + remove_invalid_utf_8_byte_sequences(transcript) + end + + def remove_invalid_utf_8_byte_sequences(text) + text.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '') + end + + def successful_purchase_response + <<-XML + <?xml version="1.0" encoding="utf-8"?> + <result xmlns="http://secure.ixopay.com/Schema/V2/Result"> + <success>true</success> + <referenceId>b2bef23a30b537b90fbe</referenceId> + <purchaseId>20191016-b2bef23a30b537b90fbe</purchaseId> + <returnType>FINISHED</returnType> + <paymentMethod>Creditcard</paymentMethod> + <returnData type="creditcardData"> + <creditcardData> + <type>visa</type> + <cardHolder>Longbob Longsen</cardHolder> + <expiryMonth>09</expiryMonth> + <expiryYear>2020</expiryYear> + <firstSixDigits>411111</firstSixDigits> + <lastFourDigits>1111</lastFourDigits> + </creditcardData> + </returnData> + <extraData key="captureId">5da76cc5ce84b</extraData> + </result> + XML + end + + def failed_purchase_response + <<-XML + <?xml version="1.0" encoding="utf-8"?> + <result xmlns="http://secure.ixopay.com/Schema/V2/Result"> + <success>false</success> + <referenceId>d74211aa7d0ba8294b4d</referenceId> + <purchaseId>20191016-d74211aa7d0ba8294b4d</purchaseId> + <returnType>ERROR</returnType> + <paymentMethod>Creditcard</paymentMethod> + <returnData type="creditcardData"> + <creditcardData> + <type>visa</type> + <cardHolder>Longbob Longsen</cardHolder> + <expiryMonth>09</expiryMonth> + <expiryYear>2020</expiryYear> + <firstSixDigits>400030</firstSixDigits> + <lastFourDigits>2220</lastFourDigits> + </creditcardData> + </returnData> + <errors> + <error> + <message>The transaction was declined</message> + <code>2003</code> + <adapterMessage>Test decline</adapterMessage> + <adapterCode>transaction_declined</adapterCode> + </error> + </errors> + </result> + XML + end + + def failed_authentication_response + <<-XML + <?xml version="1.0" encoding="utf-8"?> + <result xmlns="http://gateway/Schema/V2/TransactionWithCard"> + <success>false</success> + <returnType>ERROR</returnType> + <errors> + <error> + <message>Invalid Signature: Invalid authorization header</message> + <code>1004</code> + </error> + </errors> + </result> + XML + end + + def successful_authorize_response + <<-XML + <?xml version="1.0" encoding="utf-8"?> + <result xmlns="http://secure.ixopay.com/Schema/V2/Result"> + <success>true</success> + <referenceId>00eb44f8f0382443cce5</referenceId> + <purchaseId>20191028-00eb44f8f0382443cce5</purchaseId> + <returnType>FINISHED</returnType> + <paymentMethod>Creditcard</paymentMethod> + <returnData type="creditcardData"> + <creditcardData> + <type>visa</type> + <cardHolder>Longbob Longsen</cardHolder> + <expiryMonth>09</expiryMonth> + <expiryYear>2020</expiryYear> + <firstSixDigits>411111</firstSixDigits> + <lastFourDigits>1111</lastFourDigits> + </creditcardData> + </returnData> + </result> + XML + end + + def failed_authorize_response + <<-XML + <?xml version="1.0" encoding="utf-8"?> + <result xmlns="http://secure.ixopay.com/Schema/V2/Result"> + <success>false</success> + <referenceId>91278c76405116378b85</referenceId> + <purchaseId>20191028-91278c76405116378b85</purchaseId> + <returnType>ERROR</returnType> + <paymentMethod>Creditcard</paymentMethod> + <returnData type="creditcardData"> + <creditcardData> + <type>visa</type> + <cardHolder>Longbob Longsen</cardHolder> + <expiryMonth>09</expiryMonth> + <expiryYear>2020</expiryYear> + <firstSixDigits>400030</firstSixDigits> + <lastFourDigits>2220</lastFourDigits> + </creditcardData> + </returnData> + <errors> + <error> + <message>The transaction was declined</message> + <code>2003</code> + <adapterMessage>Test decline</adapterMessage> + <adapterCode>transaction_declined</adapterCode> + </error> + </errors> + </result> + XML + end + + def successful_capture_response + <<-XML + <?xml version="1.0" encoding="utf-8"?> + <result xmlns="http://secure.ixopay.com/Schema/V2/Result"> + <success>true</success> + <referenceId>17dd1e0b09221e9db038</referenceId> + <purchaseId>20191031-17dd1e0b09221e9db038</purchaseId> + <returnType>FINISHED</returnType> + <paymentMethod>Creditcard</paymentMethod> + <returnData type="creditcardData"> + <creditcardData> + <type>visa</type> + <cardHolder>Longbob Longsen</cardHolder> + <expiryMonth>09</expiryMonth> + <expiryYear>2020</expiryYear> + <firstSixDigits>411111</firstSixDigits> + <lastFourDigits>1111</lastFourDigits> + </creditcardData> + </returnData> + </result> + XML + end + + def failed_capture_response + <<-XML + <?xml version="1.0" encoding="utf-8"?> + <result xmlns="http://secure.ixopay.com/Schema/V2/Result"> + <success>false</success> + <returnType>ERROR</returnType> + <errors> + <error> + <message>Transaction of type "capture" requires a referenceTransactionId</message> + <code>9999</code> + </error> + </errors> + </result> + XML + end + + def successful_refund_response + <<-XML + <result xmlns="http://secure.ixopay.com/Schema/V2/Result"> + <success>true</success> + <referenceId>21c47c977476d5a3b682</referenceId> + <purchaseId>20191028-c9e173c255d14f90816b</purchaseId> + <returnType>FINISHED</returnType> + <paymentMethod>Creditcard</paymentMethod> + </result> + XML + end + + def failed_refund_response + <<-XML + <result xmlns="http://secure.ixopay.com/Schema/V2/Result"> + <success>false</success> + <returnType>ERROR</returnType> + <errors> + <error> + <message>Transaction of type "refund" requires a referenceTransactionId</message> + <code>9999</code> + </error> + </errors> + </result> + XML + end + + def successful_void_response + <<-XML + <result xmlns="http://secure.ixopay.com/Schema/V2/Result"> + <success>true</success> + <referenceId>cb656bd5286e77501b2e</referenceId> + <purchaseId>20191031-b1f9f7991766cf933659</purchaseId> + <returnType>FINISHED</returnType> + <paymentMethod>Creditcard</paymentMethod> + </result> + XML + end + + def failed_void_response + <<-XML + <result xmlns="http://secure.ixopay.com/Schema/V2/Result"> + <success>false</success> + <returnType>ERROR</returnType> + <errors> + <error> + <message>Transaction of type "void" requires a referenceTransactionId</message> + <code>9999</code> + </error> + </errors> + </result> + XML + end +end diff --git a/test/unit/gateways/jetpay_test.rb b/test/unit/gateways/jetpay_test.rb index aafc7966e83..88a58cea6c5 100644 --- a/test/unit/gateways/jetpay_test.rb +++ b/test/unit/gateways/jetpay_test.rb @@ -6,18 +6,18 @@ class JetpayTest < Test::Unit::TestCase def setup Base.mode = :test - @gateway = JetpayGateway.new(:login => 'login') + @gateway = JetpayGateway.new(login: 'login') @credit_card = credit_card @amount = 100 @options = { - :billing_address => address(:country => 'US'), - :shipping_address => address(:country => 'US'), - :email => 'test@test.com', - :ip => '127.0.0.1', - :order_id => '12345', - :tax => 7 + billing_address: address(country: 'US'), + shipping_address: address(country: 'US'), + email: 'test@test.com', + ip: '127.0.0.1', + order_id: '12345', + tax: 7 } end @@ -76,7 +76,7 @@ def test_successful_void def test_successful_credit # no need for csv - card = credit_card('4242424242424242', :verification_value => nil) + card = credit_card('4242424242424242', verification_value: nil) @gateway.expects(:ssl_post).returns(successful_credit_response) @@ -137,12 +137,13 @@ def test_transcript_scrubbing def test_purchase_sends_order_origin @gateway.expects(:ssl_post).with(anything, regexp_matches(/<Origin>RECURRING<\/Origin>/)).returns(successful_purchase_response) - @gateway.purchase(@amount, @credit_card, {:origin => 'RECURRING'}) + @gateway.purchase(@amount, @credit_card, { origin: 'RECURRING' }) end private + def successful_purchase_response - <<-EOF + <<-XML <JetPayResponse> <TransactionID>8afa688fd002821362</TransactionID> <ActionCode>000</ActionCode> @@ -154,21 +155,21 @@ def successful_purchase_response <ZipMatch>Y</ZipMatch> <AVS>Y</AVS> </JetPayResponse> - EOF + XML end def failed_purchase_response - <<-EOF + <<-XML <JetPayResponse> <TransactionID>7605f7c5d6e8f74deb</TransactionID> <ActionCode>005</ActionCode> <ResponseText>DECLINED</ResponseText> </JetPayResponse> - EOF + XML end def successful_authorize_response - <<-EOF + <<-XML <JetPayResponse> <TransactionID>cbf902091334a0b1aa</TransactionID> <ActionCode>000</ActionCode> @@ -180,44 +181,44 @@ def successful_authorize_response <ZipMatch>Y</ZipMatch> <AVS>Y</AVS> </JetPayResponse> - EOF + XML end def successful_capture_response - <<-EOF + <<-XML <JetPayResponse> <TransactionID>010327153017T10018</TransactionID> <ActionCode>000</ActionCode> <Approval>502F6B</Approval> <ResponseText>APPROVED</ResponseText> </JetPayResponse> - EOF + XML end def successful_void_response - <<-EOF + <<-XML <JetPayResponse> <TransactionID>010327153x17T10418</TransactionID> <ActionCode>000</ActionCode> <Approval>502F7B</Approval> <ResponseText>VOID PROCESSED</ResponseText> </JetPayResponse> - EOF + XML end def successful_credit_response - <<-EOF + <<-XML <JetPayResponse> <TransactionID>010327153017T10017</TransactionID> <ActionCode>000</ActionCode> <Approval>002F6B</Approval> <ResponseText>APPROVED</ResponseText> </JetPayResponse> - EOF + XML end def transcript - <<-EOF + <<-XML <TerminalID>TESTTERMINAL</TerminalID> <TransactionType>SALE</TransactionType> <TransactionID>e23c963a1247fd7aad</TransactionID> @@ -226,11 +227,11 @@ def transcript <CardExpYear>16</CardExpYear> <CardName>Longbob Longsen</CardName> <CVV2>123</CVV2> - EOF + XML end def scrubbed_transcript - <<-EOF + <<-XML <TerminalID>TESTTERMINAL</TerminalID> <TransactionType>SALE</TransactionType> <TransactionID>e23c963a1247fd7aad</TransactionID> @@ -239,6 +240,6 @@ def scrubbed_transcript <CardExpYear>16</CardExpYear> <CardName>Longbob Longsen</CardName> <CVV2>[FILTERED]</CVV2> - EOF + XML end end diff --git a/test/unit/gateways/jetpay_v2_test.rb b/test/unit/gateways/jetpay_v2_test.rb index d135ddc7f0a..c7533d18e98 100644 --- a/test/unit/gateways/jetpay_v2_test.rb +++ b/test/unit/gateways/jetpay_v2_test.rb @@ -1,22 +1,21 @@ require 'test_helper' class JetpayV2Test < Test::Unit::TestCase - def setup - @gateway = JetpayV2Gateway.new(:login => 'login') + @gateway = JetpayV2Gateway.new(login: 'login') @credit_card = credit_card @amount = 100 @options = { - :device => 'spreedly', - :application => 'spreedly', - :developer_id => 'GenkID', - :billing_address => address(:country => 'US'), - :shipping_address => address(:country => 'US'), - :email => 'test@test.com', - :ip => '127.0.0.1', - :order_id => '12345' + device: 'spreedly', + application: 'spreedly', + developer_id: 'GenkID', + billing_address: address(country: 'US'), + shipping_address: address(country: 'US'), + email: 'test@test.com', + ip: '127.0.0.1', + order_id: '12345' } end @@ -89,7 +88,7 @@ def test_failed_void end def test_successful_credit - card = credit_card('4242424242424242', :verification_value => nil) + card = credit_card('4242424242424242', verification_value: nil) @gateway.expects(:ssl_post).returns(successful_credit_response) @@ -98,7 +97,7 @@ def test_successful_credit end def test_failed_credit - card = credit_card('2424242424242424', :verification_value => nil) + card = credit_card('2424242424242424', verification_value: nil) @gateway.expects(:ssl_post).returns(failed_credit_response) @@ -133,7 +132,7 @@ def test_successful_verify end def test_failed_verify - card = credit_card('2424242424242424', :verification_value => nil) + card = credit_card('2424242424242424', verification_value: nil) @gateway.expects(:ssl_post).returns(failed_credit_response) @@ -163,19 +162,19 @@ def test_transcript_scrubbing def test_purchase_sends_additional_options @gateway.expects(:ssl_post). - with(anything, regexp_matches(/<TaxAmount ExemptInd=\"false\">777<\/TaxAmount>/)). - with(anything, regexp_matches(/<UDField1>Value1<\/UDField1>/)). - with(anything, regexp_matches(/<UDField2>Value2<\/UDField2>/)). - with(anything, regexp_matches(/<UDField3>Value3<\/UDField3>/)). - returns(successful_purchase_response) + with(anything, regexp_matches(/<TaxAmount ExemptInd=\"false\">777<\/TaxAmount>/)). + with(anything, regexp_matches(/<UDField1>Value1<\/UDField1>/)). + with(anything, regexp_matches(/<UDField2>Value2<\/UDField2>/)). + with(anything, regexp_matches(/<UDField3>Value3<\/UDField3>/)). + returns(successful_purchase_response) - @gateway.purchase(@amount, @credit_card, {:tax => '777', :ud_field_1 => 'Value1', :ud_field_2 => 'Value2', :ud_field_3 => 'Value3'}) + @gateway.purchase(@amount, @credit_card, { tax: '777', ud_field_1: 'Value1', ud_field_2: 'Value2', ud_field_3: 'Value3' }) end private def successful_purchase_response - <<-EOF + <<-XML <JetPayResponse Version="2.2"> <TransactionID>8afa688fd002821362</TransactionID> <ActionCode>000</ActionCode> @@ -187,21 +186,21 @@ def successful_purchase_response <ZipMatch>Y</ZipMatch> <AVS>D</AVS> </JetPayResponse> - EOF + XML end def failed_purchase_response - <<-EOF + <<-XML <JetPayResponse Version="2.2"> <TransactionID>7605f7c5d6e8f74deb</TransactionID> <ActionCode>005</ActionCode> <ResponseText>DECLINED</ResponseText> </JetPayResponse> - EOF + XML end def successful_authorize_response - <<-EOF + <<-XML <JetPayResponse Version="2.2"> <TransactionID>cbf902091334a0b1aa</TransactionID> <ActionCode>000</ActionCode> @@ -213,75 +212,75 @@ def successful_authorize_response <ZipMatch>Y</ZipMatch> <AVS>D</AVS> </JetPayResponse> - EOF + XML end def successful_capture_response - <<-EOF + <<-XML <JetPayResponse Version="2.2"> <TransactionID>010327153017T10018</TransactionID> <ActionCode>000</ActionCode> <Approval>502F6B</Approval> <ResponseText>APPROVED</ResponseText> </JetPayResponse> - EOF + XML end def failed_capture_response - <<-EOF + <<-XML <JetPayResponse Version="2.2"> <TransactionID>010327153017T10018</TransactionID> <ActionCode>025</ActionCode> <Approval>REJECT</Approval> <ResponseText>ED</ResponseText> </JetPayResponse> - EOF + XML end def successful_void_response - <<-EOF + <<-XML <JetPayResponse Version="2.2"> <TransactionID>010327153x17T10418</TransactionID> <ActionCode>000</ActionCode> <Approval>502F7B</Approval> <ResponseText>VOID PROCESSED</ResponseText> </JetPayResponse> - EOF + XML end def failed_void_response - <<-EOF + <<-XML <JetPayResponse Version="2.2"> <TransactionID>010327153x17T10418</TransactionID> <ActionCode>900</ActionCode> <ResponseText>INVALID MESSAGE TYPE</ResponseText> </JetPayResponse> - EOF + XML end def successful_credit_response - <<-EOF + <<-XML <JetPayResponse Version="2.2"> <TransactionID>010327153017T10017</TransactionID> <ActionCode>000</ActionCode> <Approval>002F6B</Approval> <ResponseText>APPROVED</ResponseText> </JetPayResponse> - EOF + XML end def failed_credit_response - <<-EOF + <<-XML <JetPayResponse Version="2.2"> <TransactionID>010327153017T10017</TransactionID> <ActionCode>912</ActionCode> <ResponseText>INVALID CARD NUMBER</ResponseText> </JetPayResponse> - EOF + XML end def transcript - <<-EOF + <<-XML <TerminalID>TESTMCC3136X</TerminalID> <TransactionType>SALE</TransactionType> <TransactionID>e23c963a1247fd7aad</TransactionID> @@ -290,11 +289,11 @@ def transcript <CardExpYear>16</CardExpYear> <CardName>Longbob Longsen</CardName> <CVV2>123</CVV2> - EOF + XML end def scrubbed_transcript - <<-EOF + <<-XML <TerminalID>TESTMCC3136X</TerminalID> <TransactionType>SALE</TransactionType> <TransactionID>e23c963a1247fd7aad</TransactionID> @@ -303,6 +302,6 @@ def scrubbed_transcript <CardExpYear>16</CardExpYear> <CardName>Longbob Longsen</CardName> <CVV2>[FILTERED]</CVV2> - EOF + XML end end diff --git a/test/unit/gateways/komoju_test.rb b/test/unit/gateways/komoju_test.rb index dc67d18686c..deaf75df6f4 100644 --- a/test/unit/gateways/komoju_test.rb +++ b/test/unit/gateways/komoju_test.rb @@ -2,19 +2,19 @@ class KomojuTest < Test::Unit::TestCase def setup - @gateway = KomojuGateway.new(:login => 'login') + @gateway = KomojuGateway.new(login: 'login') @credit_card = credit_card @amount = 100 @options = { - :order_id => '1', - :description => 'Store Purchase', - :tax => '10', - :ip => '192.168.0.1', - :email => 'valid@email.com', - :browser_language => 'en', - :browser_user_agent => 'user_agent' + order_id: '1', + description: 'Store Purchase', + tax: '10', + ip: '192.168.0.1', + email: 'valid@email.com', + browser_language: 'en', + browser_user_agent: 'user_agent' } end @@ -59,7 +59,7 @@ def test_successful_credit_card_refund successful_response = successful_credit_card_refund_response @gateway.expects(:ssl_post).returns(JSON.generate(successful_response)) - response = @gateway.refund(@amount, '7e8c55a54256ce23e387f2838c', @options) + response = @gateway.refund(@amount, '7e8c55a54256ce23e387f2838c', @options) assert_success response assert_equal successful_response['id'], response.authorization diff --git a/test/unit/gateways/kushki_test.rb b/test/unit/gateways/kushki_test.rb index 4020f3cbf6f..8f104d53065 100644 --- a/test/unit/gateways/kushki_test.rb +++ b/test/unit/gateways/kushki_test.rb @@ -28,7 +28,40 @@ def test_successful_purchase_with_options subtotal_iva: '10', iva: '1.54', ice: '3.50' - } + }, + contact_details: { + document_type: 'CC', + document_number: '123456', + email: 'who_dis@monkeys.tv', + first_name: 'Who', + last_name: 'Dis', + second_last_name: 'Buscemi', + phone_number: '+13125556789' + }, + 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 * ( @@ -41,7 +74,15 @@ def test_successful_purchase_with_options @gateway.expects(:ssl_post).returns(successful_charge_response) @gateway.expects(:ssl_post).returns(successful_token_response) - response = @gateway.purchase(amount, @credit_card, options) + response = stub_comms do + @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 assert_equal 'Succeeded', response.message assert_match %r(^\d+$), response.authorization @@ -60,18 +101,16 @@ def test_taxes_are_excluded_when_not_provided } amount = 100 * ( - options[:amount][:subtotal_iva_0].to_f + - options[:amount][:subtotal_iva].to_f + - options[:amount][:iva].to_f + - options[:amount][:ice].to_f + options[:amount][:subtotal_iva_0].to_f + + options[:amount][:subtotal_iva].to_f + + options[:amount][:iva].to_f + + options[:amount][:ice].to_f ) response = stub_comms do @gateway.purchase(amount, @credit_card, options) - end.check_request do |endpoint, data, headers| - if /charges/ =~ endpoint - assert_no_match %r{extraTaxes}, data - end + end.check_request do |endpoint, data, _headers| + assert_no_match %r{extraTaxes}, data if /charges/.match?(endpoint) end.respond_with(successful_charge_response, successful_token_response) assert_success response @@ -93,16 +132,16 @@ def test_partial_taxes_do_not_error } amount = 100 * ( - options[:amount][:subtotal_iva_0].to_f + - options[:amount][:subtotal_iva].to_f + - options[:amount][:iva].to_f + - options[:amount][:ice].to_f + options[:amount][:subtotal_iva_0].to_f + + options[:amount][:subtotal_iva].to_f + + options[:amount][:iva].to_f + + options[:amount][:ice].to_f ) response = stub_comms do @gateway.purchase(amount, @credit_card, options) - end.check_request do |endpoint, data, headers| - if /charges/ =~ endpoint + end.check_request do |endpoint, data, _headers| + if /charges/.match?(endpoint) assert_match %r{extraTaxes}, data assert_no_match %r{propina}, data assert_match %r{iac}, data @@ -112,7 +151,7 @@ def test_partial_taxes_do_not_error assert_success response end - def test_taxes_are_included_when_provided + def test_cop_taxes_are_included_when_provided options = { currency: 'COP', amount: { @@ -130,16 +169,52 @@ def test_taxes_are_included_when_provided } amount = 100 * ( - options[:amount][:subtotal_iva_0].to_f + - options[:amount][:subtotal_iva].to_f + - options[:amount][:iva].to_f + - options[:amount][:ice].to_f + options[:amount][:subtotal_iva_0].to_f + + options[:amount][:subtotal_iva].to_f + + options[:amount][:iva].to_f + + options[:amount][:ice].to_f ) response = stub_comms do @gateway.purchase(amount, @credit_card, options) - end.check_request do |endpoint, data, headers| - if /charges/ =~ endpoint + end.check_request do |endpoint, data, _headers| + if /charges/.match?(endpoint) + assert_match %r{extraTaxes}, data + assert_match %r{propina}, data + end + end.respond_with(successful_charge_response, successful_token_response) + + assert_success response + end + + def test_usd_taxes_are_included_when_provided + options = { + currency: 'USD', + amount: { + subtotal_iva_0: '4.95', + subtotal_iva: '10', + iva: '1.54', + ice: '3.50', + extra_taxes: { + propina: 0.1, + tasa_aeroportuaria: 0.2, + agencia_de_viaje: 0.3, + iac: 0.4 + } + } + } + + amount = 100 * ( + options[:amount][:subtotal_iva_0].to_f + + options[:amount][:subtotal_iva].to_f + + options[:amount][:iva].to_f + + options[:amount][:ice].to_f + ) + + response = stub_comms do + @gateway.purchase(amount, @credit_card, options) + end.check_request do |endpoint, data, _headers| + if /charges/.match?(endpoint) assert_match %r{extraTaxes}, data assert_match %r{propina}, data end @@ -164,6 +239,33 @@ def test_failed_purchase assert_equal '220', response.error_code end + def test_successful_authorize + @gateway.expects(:ssl_post).returns(successful_authorize_response) + @gateway.expects(:ssl_post).returns(successful_token_response) + + response = @gateway.authorize(@amount, @credit_card) + assert_success response + assert_equal 'Succeeded', response.message + assert_match %r(^\d+$), response.authorization + assert response.test? + end + + def test_failed_authorize + options = { + amount: { + subtotal_iva: '200' + } + } + + @gateway.expects(:ssl_post).returns(failed_authorize_response) + @gateway.expects(:ssl_post).returns(successful_token_response) + + response = @gateway.purchase(@amount, @credit_card, options) + assert_failure response + assert_equal 'Monto de la transacción es diferente al monto de la venta inicial', response.message + assert_equal '220', response.error_code + end + def test_successful_refund @gateway.expects(:ssl_post).returns(successful_charge_response) @gateway.expects(:ssl_post).returns(successful_token_response) @@ -193,6 +295,78 @@ 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) + + auth = @gateway.authorize(@amount, @credit_card) + assert_success auth + + @gateway.expects(:ssl_post).returns(successful_capture_response) + + assert capture = @gateway.capture(@amount, auth.authorization) + assert_success capture + assert_equal 'Succeeded', capture.message + end + + def test_failed_capture + @gateway.expects(:ssl_post).returns(successful_authorize_response) + @gateway.expects(:ssl_post).returns(successful_token_response) + + auth = @gateway.authorize(@amount, @credit_card) + assert_success auth + + @gateway.expects(:ssl_post).returns(failed_capture_response) + + assert capture = @gateway.capture(@amount, auth.authorization) + assert_failure capture + assert_equal 'Monto de captura inválido.', capture.message + assert_equal 'K012', capture.error_code + end + def test_successful_void @gateway.expects(:ssl_post).returns(successful_charge_response) @gateway.expects(:ssl_post).returns(successful_token_response) @@ -216,6 +390,23 @@ def test_failed_void assert_equal '205', response.error_code end + def test_successful_purchase_with_full_response_flag + options = { + full_response: 'true' + } + + @gateway.expects(:ssl_post).returns(successful_charge_response) + @gateway.expects(:ssl_post).returns(successful_token_response) + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_includes data, 'fullResponse' + end.respond_with(successful_token_response, successful_charge_response) + + assert_success response + end + def test_scrub assert @gateway.supports_scrubbing? assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed @@ -346,6 +537,39 @@ def failed_charge_response ) end + def successful_authorize_response + %( + { + "ticketNumber":"676185788080292214" + } + ) + end + + def failed_authorize_response + %( + { + "code":"220", + "message":"Monto de la transacción es diferente al monto de la venta inicial" + } + ) + end + + def successful_capture_response + %( + { + "ticketNumber":"911597984059374763" + } + ) + end + + def failed_capture_response + %( + { + "code":"K012","message":"Monto de captura inválido." + } + ) + end + def successful_refund_response %( { diff --git a/test/unit/gateways/latitude19_test.rb b/test/unit/gateways/latitude19_test.rb index 75f7b75cba8..afb7de4214f 100644 --- a/test/unit/gateways/latitude19_test.rb +++ b/test/unit/gateways/latitude19_test.rb @@ -1,6 +1,8 @@ require 'test_helper' class Latitude19Test < Test::Unit::TestCase + include CommStub + def setup @gateway = Latitude19Gateway.new(fixtures(:latitude19)) @@ -29,18 +31,18 @@ def test_successful_purchase # end def test_successful_authorize_and_capture - @gateway.expects(:ssl_post).returns(successful_authorize_response) - @gateway.expects(:ssl_post).returns(successful_token_response) - @gateway.expects(:ssl_post).returns(successful_session_response) - - response = @gateway.authorize(@amount, @credit_card, @options) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.respond_with(successful_session_response, successful_token_response, successful_authorize_response) assert_success response assert_equal 'Approved', response.message assert_match %r(^auth\|\w+$), response.authorization - @gateway.expects(:ssl_post).returns(successful_capture_response) - - capture = @gateway.capture(@amount, response.authorization, @options) + capture = stub_comms do + @gateway.capture(@amount, response.authorization, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/"amount\":\"1.00\"/, data) + end.respond_with(successful_capture_response) assert_success capture assert_equal 'Approved', capture.message end diff --git a/test/unit/gateways/linkpoint_test.rb b/test/unit/gateways/linkpoint_test.rb index 71d5e683317..9d14398a1bb 100644 --- a/test/unit/gateways/linkpoint_test.rb +++ b/test/unit/gateways/linkpoint_test.rb @@ -5,13 +5,13 @@ def setup Base.mode = :test @gateway = LinkpointGateway.new( - :login => 123123, - :pem => 'PEM' + login: 123123, + pem: 'PEM' ) @amount = 100 @credit_card = credit_card('4111111111111111') - @options = { :order_id => 1000, :billing_address => address } + @options = { order_id: 1000, billing_address: address } end def test_instantiating_without_credential_raises @@ -65,7 +65,7 @@ def test_recurring @gateway.expects(:ssl_post).returns(successful_recurring_response) response = assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.recurring(2400, @credit_card, :order_id => 1003, :installments => 12, :startdate => 'immediate', :periodicity => :monthly) + @gateway.recurring(2400, @credit_card, order_id: 1003, installments: 12, startdate: 'immediate', periodicity: :monthly) end assert_success response end @@ -79,12 +79,15 @@ def test_amount_style end def test_purchase_is_valid_xml - @gateway.send(:parameters, 1000, @credit_card, :ordertype => 'SALE', :order_id => 1004, - :billing_address => { - :address1 => '1313 lucky lane', - :city => 'Lost Angeles', - :state => 'CA', - :zip => '90210' + @gateway.send( + :parameters, 1000, @credit_card, + ordertype: 'SALE', + order_id: 1004, + billing_address: { + address1: '1313 lucky lane', + city: 'Lost Angeles', + state: 'CA', + zip: '90210' } ) @@ -93,12 +96,19 @@ def test_purchase_is_valid_xml end def test_recurring_is_valid_xml - @gateway.send(:parameters, 1000, @credit_card, :ordertype => 'SALE', :action => 'SUBMIT', :installments => 12, :startdate => 'immediate', :periodicity => 'monthly', :order_id => 1006, - :billing_address => { - :address1 => '1313 lucky lane', - :city => 'Lost Angeles', - :state => 'CA', - :zip => '90210' + @gateway.send( + :parameters, 1000, @credit_card, + ordertype: 'SALE', + action: 'SUBMIT', + installments: 12, + startdate: 'immediate', + periodicity: 'monthly', + order_id: 1006, + billing_address: { + address1: '1313 lucky lane', + city: 'Lost Angeles', + state: 'CA', + zip: '90210' } ) assert data = @gateway.send(:post_data, @amount, @credit_card, @options) @@ -106,31 +116,61 @@ def test_recurring_is_valid_xml end def test_line_items_are_valid_xml - options = {:ordertype => 'SALE', :action => 'SUBMIT', :installments => 12, :startdate => 'immediate', :periodicity => 'monthly', :order_id => 1006, - :billing_address => { - :address1 => '1313 lucky lane', - :city => 'Lost Angeles', - :state => 'CA', - :zip => '90210' + options = { + ordertype: 'SALE', + action: 'SUBMIT', + installments: 12, + startdate: 'immediate', + periodicity: 'monthly', + order_id: 1006, + billing_address: { + address1: '1313 lucky lane', + city: 'Lost Angeles', + state: 'CA', + zip: '90210' }, - :line_items => [{:id => '123456', :description => 'Logo T-Shirt', :price => - '12.00', :quantity => '1', :options => [{:name => 'Color', :value => - 'Red'}, {:name => 'Size', :value => 'XL'}]},{:id => '111', :description => 'keychain', :price => '3.00', :quantity => '1'}]} - + line_items: [ + { + id: '123456', + description: 'Logo T-Shirt', + price: '12.00', + quantity: '1', + options: [ + { + name: 'Color', + value: 'Red' + }, + { + name: 'Size', + value: 'XL' + } + ] + }, + { + id: '111', + description: 'keychain', + price: '3.00', + quantity: '1' + } + ] + } assert data = @gateway.send(:post_data, @amount, @credit_card, options) assert REXML::Document.new(data) end def test_declined_purchase_is_valid_xml - @gateway = LinkpointGateway.new(:login => 123123, :pem => 'PEM') - - @gateway.send(:parameters, 1000, @credit_card, :ordertype => 'SALE', :order_id => 1005, - :billing_address => { - :address1 => '1313 lucky lane', - :city => 'Lost Angeles', - :state => 'CA', - :zip => '90210' + @gateway = LinkpointGateway.new(login: 123123, pem: 'PEM') + + @gateway.send( + :parameters, 1000, @credit_card, + ordertype: 'SALE', + order_id: 1005, + billing_address: { + address1: '1313 lucky lane', + city: 'Lost Angeles', + state: 'CA', + zip: '90210' } ) @@ -142,9 +182,9 @@ def test_overriding_test_mode Base.mode = :production gateway = LinkpointGateway.new( - :login => 'LOGIN', - :pem => 'PEM', - :test => true + login: 'LOGIN', + pem: 'PEM', + test: true ) assert gateway.test? @@ -154,8 +194,8 @@ def test_using_production_mode Base.mode = :production gateway = LinkpointGateway.new( - :login => 'LOGIN', - :pem => 'PEM' + login: 'LOGIN', + pem: 'PEM' ) assert !gateway.test? @@ -166,7 +206,7 @@ def test_supported_countries end def test_supported_card_types - assert_equal [:visa, :master, :american_express, :discover, :jcb, :diners_club], LinkpointGateway.supported_cardtypes + assert_equal %i[visa master american_express discover jcb diners_club], LinkpointGateway.supported_cardtypes end def test_avs_result @@ -188,6 +228,7 @@ def test_transcript_scrubbing end private + def successful_authorization_response '<r_csp>CSI</r_csp><r_time>Sun Jan 6 21:41:31 2008</r_time><r_ref>0004486182</r_ref><r_error/><r_ordernum>1000</r_ordernum><r_message>APPROVED</r_message><r_code>1234560004486182:NNNM:100018312899:</r_code><r_tdate>1199680890</r_tdate><r_score/><r_authresponse/><r_approved>APPROVED</r_approved><r_avs>NNNM</r_avs>' end @@ -215,5 +256,4 @@ def transcript def scrubbed_transcript '</orderoptions><creditcard><cardnumber>[FILTERED]</cardnumber><cardexpmonth>9</cardexpmonth><cardexpyear>16</cardexpyear><cvmvalue>[FILTERED]</cvmvalue><cvmindicator>provided</cvmindicator></creditcard><billing><name>Jim Smith</name>' end - end diff --git a/test/unit/gateways/litle_test.rb b/test/unit/gateways/litle_test.rb index 59cd7af5969..218fe8d3a67 100644 --- a/test/unit/gateways/litle_test.rb +++ b/test/unit/gateways/litle_test.rb @@ -20,7 +20,8 @@ def setup brand: 'visa', number: '44444444400009', payment_cryptogram: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=' - }) + } + ) @decrypted_android_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new( { source: :android_pay, @@ -29,26 +30,143 @@ def setup brand: 'visa', number: '4457000300000007', payment_cryptogram: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=' - }) + } + ) + @decrypted_google_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new( + { + source: :google_pay, + month: '01', + year: '2021', + brand: 'visa', + number: '4457000300000007', + payment_cryptogram: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=' + } + ) + @decrypted_network_token = NetworkTokenizationCreditCard.new( + { + source: :network_token, + month: '02', + year: '2050', + brand: 'master', + number: '5112010000000000', + payment_cryptogram: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=' + } + ) @amount = 100 @options = {} @check = check( name: 'Tom Black', routing_number: '011075150', account_number: '4099999992', - account_type: 'Checking' + account_type: 'checking' ) @authorize_check = check( name: 'John Smith', routing_number: '011075150', account_number: '1099999999', - account_type: 'Checking' + account_type: nil, + account_holder_type: 'checking' ) + + @long_address = { + address1: '1234 Supercalifragilisticexpialidocious', + address2: 'Unit 6', + city: '‎Lake Chargoggagoggmanchauggagoggchaubunagungamaugg', + state: 'ME', + zip: '09901', + country: 'US' + } end def test_successful_purchase 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) + + assert_success response + assert_equal 'Approved', response.message + assert_equal '100000000000000006;sale;100', response.authorization + assert response.test? + end + + def test_successful_purchase_alternate_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_alternate_response) + + assert_success response + assert_equal '136', response.params['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 + + def test_successful_postlive_url + @gateway = LitleGateway.new( + login: 'login', + password: 'password', + merchant_id: 'merchant_id', + url_override: 'postlive' + ) + + response = stub_comms do + @gateway.purchase(@amount, @credit_card) + end.check_request do |endpoint, _data, _headers| + assert_match(/payments\.vantivpostlive\.com/, endpoint) end.respond_with(successful_purchase_response) assert_success response @@ -60,6 +178,8 @@ def test_successful_purchase def test_successful_purchase_with_echeck response = stub_comms do @gateway.purchase(2004, @check) + end.check_request do |_endpoint, data, _headers| + assert_match(%r(<accType>Checking</accType>), data) end.respond_with(successful_purchase_with_echeck_response) assert_success response @@ -68,6 +188,35 @@ def test_successful_purchase_with_echeck 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(<accType>Checking</accType>), 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_sale_response_duplicate_attribute + dup_response = stub_comms do + @gateway.purchase(@amount, @credit_card) + end.respond_with(duplicate_purchase_response) + + assert_success dup_response + assert_true dup_response.params['duplicate'] + + non_dup_response = stub_comms do + @gateway.purchase(@amount, @credit_card) + end.respond_with(successful_purchase_response) + + assert_success non_dup_response + assert_false non_dup_response.params['duplicate'] + end + def test_failed_purchase response = stub_comms do @gateway.purchase(@amount, @credit_card) @@ -87,7 +236,7 @@ def test_passing_merchant_data ) stub_comms do @gateway.purchase(@amount, @credit_card, options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(%r(<affiliate>some-affiliate</affiliate>), data) assert_match(%r(<campaign>super-awesome-campaign</campaign>), data) assert_match(%r(<merchantGroupingId>brilliant-group</merchantGroupingId>), data) @@ -97,7 +246,7 @@ def test_passing_merchant_data def test_passing_name_on_card stub_comms do @gateway.purchase(@amount, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(%r(<billToAddress>\s*<name>Longbob Longsen<), data) end.respond_with(successful_purchase_response) end @@ -105,15 +254,39 @@ def test_passing_name_on_card def test_passing_order_id stub_comms do @gateway.purchase(@amount, @credit_card, order_id: '774488') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/774488/, data) end.respond_with(successful_purchase_response) end + def test_passing_customer_id_on_purchase + stub_comms do + @gateway.purchase(@amount, @credit_card, customer_id: '8675309') + end.check_request do |_endpoint, data, _headers| + assert_match(%r(customerId=\"8675309\">\n), data) + end.respond_with(successful_purchase_response) + end + + def test_passing_customer_id_on_capture + stub_comms do + @gateway.capture(@amount, @credit_card, customer_id: '8675309') + end.check_request do |_endpoint, data, _headers| + assert_match(%r(customerId=\"8675309\">\n), data) + end.respond_with(successful_capture_response) + end + + def test_passing_customer_id_on_refund + stub_comms do + @gateway.credit(@amount, @credit_card, customer_id: '8675309') + end.check_request do |_endpoint, data, _headers| + assert_match(%r(customerId=\"8675309\">\n), data) + end.respond_with(successful_credit_response) + end + def test_passing_billing_address stub_comms do @gateway.purchase(@amount, @credit_card, billing_address: address) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/<billToAddress>.*Widgets.*456.*Apt 1.*Otta.*ON.*K1C.*CA.*555-5/m, data) end.respond_with(successful_purchase_response) end @@ -121,17 +294,25 @@ def test_passing_billing_address def test_passing_shipping_address stub_comms do @gateway.purchase(@amount, @credit_card, shipping_address: address) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/<shipToAddress>.*Widgets.*456.*Apt 1.*Otta.*ON.*K1C.*CA.*555-5/m, data) end.respond_with(successful_purchase_response) end + def test_truncating_billing_address + stub_comms do + @gateway.purchase(@amount, @credit_card, billing_address: @long_address) + end.check_request do |_endpoint, data, _headers| + refute_match(/<billToAddress>Supercalifragilisticexpialidocious/m, data) + end.respond_with(successful_purchase_response) + end + def test_passing_descriptor stub_comms do @gateway.authorize(@amount, @credit_card, { descriptor_name: 'Name', descriptor_phone: 'Phone' }) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(%r(<customBilling>.*<descriptor>Name<)m, data) assert_match(%r(<customBilling>.*<phone>Phone<)m, data) end.respond_with(successful_authorize_response) @@ -140,23 +321,54 @@ def test_passing_descriptor def test_passing_debt_repayment stub_comms do @gateway.authorize(@amount, @credit_card, { debt_repayment: true }) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(%r(<debtRepayment>true</debtRepayment>), data) end.respond_with(successful_authorize_response) end + def test_fraud_filter_override + stub_comms do + @gateway.authorize(@amount, @credit_card, { fraud_filter_override: true }) + end.check_request do |_endpoint, data, _headers| + assert_match(%r(<fraudFilterOverride>true</fraudFilterOverride>), data) + end.respond_with(successful_authorize_response) + end + def test_passing_payment_cryptogram stub_comms do @gateway.purchase(@amount, @decrypted_apple_pay) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/BwABBJQ1AgAAAAAgJDUCAAAAAAA=/, data) end.respond_with(successful_purchase_response) end + def test_passing_basis_date + stub_comms do + @gateway.purchase(@amount, 'token', { basis_expiration_month: '04', basis_expiration_year: '2027' }) + end.check_request do |_endpoint, data, _headers| + assert_match(/<expDate>0427<\/expDate>/, data) + end.respond_with(successful_purchase_response) + end + + def test_does_not_pass_empty_checknum + check = check( + name: 'Tom Black', + routing_number: '011075150', + account_number: '4099999992', + number: nil, + account_type: 'checking' + ) + stub_comms do + @gateway.purchase(@amount, check) + end.check_request do |_endpoint, data, _headers| + assert_not_match(/<checkNum\/>/m, data) + end.respond_with(successful_purchase_response) + end + def test_add_applepay_order_source stub_comms do @gateway.purchase(@amount, @decrypted_apple_pay) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match '<orderSource>applepay</orderSource>', data end.respond_with(successful_purchase_response) end @@ -164,11 +376,27 @@ def test_add_applepay_order_source def test_add_android_pay_order_source stub_comms do @gateway.purchase(@amount, @decrypted_android_pay) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match '<orderSource>androidpay</orderSource>', data end.respond_with(successful_purchase_response) end + def test_add_google_pay_order_source + stub_comms do + @gateway.purchase(@amount, @decrypted_google_pay) + end.check_request do |_endpoint, data, _headers| + assert_match '<orderSource>androidpay</orderSource>', data + end.respond_with(successful_purchase_response) + end + + def test_add_network_token_order_source + stub_comms do + @gateway.purchase(@amount, @decrypted_network_token) + end.check_request do |_endpoint, data, _headers| + assert_match '<orderSource>ecommerce</orderSource>', data + end.respond_with(successful_purchase_response) + end + def test_successful_authorize_and_capture response = stub_comms do @gateway.authorize(@amount, @credit_card) @@ -181,7 +409,7 @@ def test_successful_authorize_and_capture capture = stub_comms do @gateway.capture(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/100000000000000001/, data) end.respond_with(successful_capture_response) @@ -217,7 +445,7 @@ def test_successful_refund refund = stub_comms do @gateway.refund(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/100000000000000006/, data) end.respond_with(successful_refund_response) @@ -234,6 +462,23 @@ def test_failed_refund assert_equal '360', response.params['response'] end + def test_successful_credit + credit = stub_comms do + @gateway.credit(@amount, @credit_card) + end.respond_with(successful_credit_response) + + assert_success credit + assert_equal 'Approved', credit.message + end + + def test_failed_credit + credit = stub_comms do + @gateway.credit(@amount, @credit_card) + end.respond_with(failed_credit_response) + + assert_failure credit + end + def test_successful_void_of_authorization response = stub_comms do @gateway.authorize(@amount, @credit_card) @@ -244,7 +489,7 @@ def test_successful_void_of_authorization void = stub_comms do @gateway.void(response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/<authReversal.*<litleTxnId>100000000000000001</m, data) end.respond_with(successful_void_of_auth_response) @@ -260,7 +505,7 @@ def test_successful_void_of_other_things void = stub_comms do @gateway.void(refund.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/<void.*<litleTxnId>100000000000000003</m, data) end.respond_with(successful_void_of_other_things_response) @@ -299,7 +544,7 @@ def test_successful_void_of_echeck def test_successful_store response = stub_comms do @gateway.store(@credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/<accountNumber>4242424242424242</, data) end.respond_with(successful_store_response) @@ -354,7 +599,7 @@ def test_add_swipe_data_with_creditcard stub_comms do @gateway.purchase(@amount, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match '<track>Track Data</track>', data assert_match '<orderSource>retail</orderSource>', data assert_match %r{<pos>.+<\/pos>}m, data @@ -364,7 +609,7 @@ def test_add_swipe_data_with_creditcard def test_order_source_with_creditcard_no_track_data stub_comms do @gateway.purchase(@amount, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match '<orderSource>ecommerce</orderSource>', data assert %r{<pos>.+<\/pos>}m !~ data end.respond_with(successful_purchase_response) @@ -373,7 +618,7 @@ def test_order_source_with_creditcard_no_track_data def test_order_source_override stub_comms do @gateway.purchase(@amount, @credit_card, order_source: 'recurring') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match '<orderSource>recurring</orderSource>', data end.respond_with(successful_purchase_response) end @@ -388,6 +633,190 @@ def test_unsuccessful_xml_schema_validation assert_equal '1', response.params['response'] end + def test_stored_credential_cit_card_on_file_initial + options = @options.merge( + stored_credential: { + initial_transaction: true, + reason_type: 'unscheduled', + initiator: 'cardholder', + network_transaction_id: nil + } + ) + + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(%r(<processingType>initialCOF</processingType>), data) + end.respond_with(successful_authorize_stored_credentials) + + assert_success response + end + + def test_stored_credential_cit_card_on_file_used + options = @options.merge( + stored_credential: { + initial_transaction: false, + reason_type: 'unscheduled', + initiator: 'cardholder', + network_transaction_id: + } + ) + + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(%r(<processingType>cardholderInitiatedCOF</processingType>), data) + assert_not_match(%r(<originalNetworkTransactionId>#{network_transaction_id}</originalNetworkTransactionId>), data) + assert_match(%r(<orderSource>ecommerce</orderSource>), data) + end.respond_with(successful_authorize_stored_credentials) + + assert_success response + end + + def test_stored_credential_cit_cof_doesnt_override_order_source + options = @options.merge( + order_source: '3dsAuthenticated', + xid: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=', + cavv: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=', + stored_credential: { + initial_transaction: false, + reason_type: 'unscheduled', + initiator: 'cardholder', + network_transaction_id: + } + ) + + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(%r(<processingType>cardholderInitiatedCOF</processingType>), data) + assert_not_match(%r(<originalNetworkTransactionId>#{network_transaction_id}</originalNetworkTransactionId>), data) + assert_match(%r(<orderSource>ecommerce</orderSource>), data) + end.respond_with(successful_authorize_stored_credentials) + + assert_success response + end + + def test_stored_credential_mit_card_on_file_initial + options = @options.merge( + stored_credential: { + initial_transaction: true, + reason_type: 'unscheduled', + initiator: 'merchant', + network_transaction_id: nil + } + ) + + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(%r(<orderSource>ecommerce</orderSource>), data) + assert_match(%r(<processingType>initialCOF</processingType>), data) + end.respond_with(successful_authorize_stored_credentials) + + assert_success response + end + + def test_stored_credential_mit_card_on_file_used + options = @options.merge( + stored_credential: { + initial_transaction: false, + reason_type: 'unscheduled', + initiator: 'merchant', + network_transaction_id: + } + ) + + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(%r(<processingType>merchantInitiatedCOF</processingType>), data) + assert_match(%r(<originalNetworkTransactionId>#{network_transaction_id}</originalNetworkTransactionId>), data) + assert_match(%r(<orderSource>ecommerce</orderSource>), data) + end.respond_with(successful_authorize_stored_credentials) + + assert_success response + end + + def test_stored_credential_installment_initial + options = @options.merge( + stored_credential: { + initial_transaction: true, + reason_type: 'installment', + initiator: 'merchant', + network_transaction_id: nil + } + ) + + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(%r(<processingType>initialInstallment</processingType>), data) + end.respond_with(successful_authorize_stored_credentials) + + assert_success response + end + + def test_stored_credential_installment_used + options = @options.merge( + stored_credential: { + initial_transaction: false, + reason_type: 'installment', + initiator: 'merchant', + network_transaction_id: + } + ) + + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_not_match(%r(<processingType>), data) + assert_match(%r(<originalNetworkTransactionId>#{network_transaction_id}</originalNetworkTransactionId>), data) + assert_match(%r(<orderSource>installment</orderSource>), data) + end.respond_with(successful_authorize_stored_credentials) + + assert_success response + end + + def test_stored_credential_recurring_initial + options = @options.merge( + stored_credential: { + initial_transaction: true, + reason_type: 'recurring', + initiator: 'merchant', + network_transaction_id: nil + } + ) + + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(%r(<processingType>initialRecurring</processingType>), data) + end.respond_with(successful_authorize_stored_credentials) + + assert_success response + end + + def test_stored_credential_recurring_used + options = @options.merge( + stored_credential: { + initial_transaction: false, + reason_type: 'recurring', + initiator: 'merchant', + network_transaction_id: + } + ) + + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(%r(<originalNetworkTransactionId>#{network_transaction_id}</originalNetworkTransactionId>), data) + assert_match(%r(<orderSource>recurring</orderSource>), data) + end.respond_with(successful_authorize_stored_credentials) + + assert_success response + end + def test_scrub assert_equal @gateway.scrub(pre_scrub), post_scrub end @@ -396,13 +825,54 @@ def test_supports_scrubbing? assert @gateway.supports_scrubbing? end - private - def successful_purchase_response + def network_transaction_id + '63225578415568556365452427825' + end + + def successful_purchase_response(code = '000', message = 'Approved') + %( + <litleOnlineResponse version='8.22' response='0' message='Valid Format' xmlns='http://www.litle.com/schema'> + <saleResponse id='1' reportGroup='Default Report Group' customerId=''> + <litleTxnId>100000000000000006</litleTxnId> + <orderId>1</orderId> + <response>#{code}</response> + <responseTime>2014-03-31T11:34:39</responseTime> + <message>#{message}</message> + <authCode>11111 </authCode> + <fraudResult> + <avsResult>01</avsResult> + <cardValidationResult>M</cardValidationResult> + </fraudResult> + </saleResponse> + </litleOnlineResponse> + ) + end + + def successful_purchase_alternate_response %( <litleOnlineResponse version='8.22' response='0' message='Valid Format' xmlns='http://www.litle.com/schema'> <saleResponse id='1' reportGroup='Default Report Group' customerId=''> + <litleTxnId>100000000000000006</litleTxnId> + <orderId>1</orderId> + <response>136</response> + <responseTime>2014-03-31T11:34:39</responseTime> + <message>Approved</message> + <authCode>11111 </authCode> + <fraudResult> + <avsResult>01</avsResult> + <cardValidationResult>M</cardValidationResult> + </fraudResult> + </saleResponse> + </litleOnlineResponse> + ) + end + + def duplicate_purchase_response + %( + <litleOnlineResponse version='8.22' response='0' message='Valid Format' xmlns='http://www.litle.com/schema'> + <saleResponse id='1' duplicate='true' reportGroup='Default Report Group' customerId=''> <litleTxnId>100000000000000006</litleTxnId> <orderId>1</orderId> <response>000</response> @@ -432,6 +902,64 @@ def successful_purchase_with_echeck_response ) end + def successful_purchase_for_prepaid_cards_141 + %( + <litleOnlineResponse version="9.14" xmlns="http://www.litle.com/schema" response="0" message="Valid Format"> + <saleResponse id="486344231" reportGroup="Report Group" customerId="10000009"> + <litleTxnId>456342657452</litleTxnId> + <orderId>123456</orderId> + <response>141</response> + <responseTime>2024-04-09T19:50:30</responseTime> + <postDate>2024-04-09</postDate> + <message>Consumer non-reloadable prepaid card, Approved</message> + <authCode>382410</authCode> + <fraudResult> + <avsResult>01</avsResult> + <cardValidationResult>M</cardValidationResult> + </fraudResult> + <networkTransactionId>MPMMPMPMPMPU</networkTransactionId> + </saleResponse> + </litleOnlineResponse> + ) + end + + def successful_purchase_for_prepaid_cards_142 + %( + <litleOnlineResponse version="9.14" xmlns="http://www.litle.com/schema" response="0" message="Valid Format"> + <saleResponse id="486344231" reportGroup="Report Group" customerId="10000009"> + <litleTxnId>456342657452</litleTxnId> + <orderId>123456</orderId> + <response>142</response> + <responseTime>2024-04-09T19:50:30</responseTime> + <postDate>2024-04-09</postDate> + <message>Consumer single-use virtual card number, Approved</message> + <authCode>382410</authCode> + <fraudResult> + <avsResult>01</avsResult> + <cardValidationResult>M</cardValidationResult> + </fraudResult> + <networkTransactionId>MPMMPMPMPMPU</networkTransactionId> + </saleResponse> + </litleOnlineResponse> + ) + end + + def successful_authorize_stored_credentials + %( + <litleOnlineResponse xmlns="http://www.litle.com/schema" version="9.14" response="0" message="Valid Format"> + <authorizationResponse id="1" reportGroup="Default Report Group"> + <litleTxnId>991939023768015826</litleTxnId> + <orderId>1</orderId> + <response>000</response> + <message>Approved</message> + <responseTime>2019-02-26T17:45:29.885</responseTime> + <authCode>75045</authCode> + <networkTransactionId>63225578415568556365452427825</networkTransactionId> + </authorizationResponse> + </litleOnlineResponse> + ) + end + def failed_purchase_response %( <litleOnlineResponse version='8.22' response='0' message='Valid Format' xmlns='http://www.litle.com/schema'> @@ -539,6 +1067,28 @@ def failed_refund_response ) end + def successful_credit_response + %( + <?xml version="1.0" encoding="UTF-8" standalone="yes"?> + <litleOnlineResponse version="9.14" response="0" message="Valid Format"> + <creditResponse id="1" reportGroup="Default Report Group"> + <litleTxnId>908410935514139173</litleTxnId> + <orderId>1</orderId> + <response>000</response> + <responseTime>2020-10-30T19:19:38.935</responseTime> + <message>Approved</message> + </creditResponse> + </litleOnlineResponse> + ) + end + + def failed_credit_response + %( + <?xml version="1.0" encoding="UTF-8" standalone="yes"?> + <litleOnlineResponse version="9.14" response="1" message="Error validating xml data against the schema: cvc-minLength-valid: Value '1234567890' with length = '10' is not facet-valid with respect to minLength '13' for type 'ccAccountNumberType'."/> + ) + end + def successful_void_of_auth_response %( <litleOnlineResponse version='8.22' response='0' message='Valid Format' xmlns='http://www.litle.com/schema'> @@ -663,7 +1213,7 @@ def unsuccessful_xml_schema_validation_response end def pre_scrub - <<-pre_scrub + <<-REQUEST opening connection to www.testlitle.com:443... opened starting SSL for www.testlitle.com:443... @@ -689,11 +1239,11 @@ def pre_scrub -> "0\r\n" -> "\r\n" Conn close - pre_scrub + REQUEST end def post_scrub - <<-post_scrub + <<-REQUEST opening connection to www.testlitle.com:443... opened starting SSL for www.testlitle.com:443... @@ -719,7 +1269,6 @@ def post_scrub -> "0\r\n" -> "\r\n" Conn close - post_scrub + REQUEST end - end diff --git a/test/unit/gateways/maxipago_test.rb b/test/unit/gateways/maxipago_test.rb index 04215332445..afac8fc8912 100644 --- a/test/unit/gateways/maxipago_test.rb +++ b/test/unit/gateways/maxipago_test.rb @@ -3,18 +3,18 @@ class MaxipagoTest < Test::Unit::TestCase def setup @gateway = MaxipagoGateway.new( - :login => 'login', - :password => 'password' + login: 'login', + password: 'password' ) @credit_card = credit_card @amount = 100 @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase', - :installments => 3 + order_id: '1', + billing_address: address, + description: 'Store Purchase', + installments: 3 } end @@ -74,7 +74,6 @@ def test_successful_void void = @gateway.void(auth.authorization) assert_success void assert_equal 'VOIDED', void.params['response_message'] - end def test_failed_void diff --git a/test/unit/gateways/mercado_pago_test.rb b/test/unit/gateways/mercado_pago_test.rb index 1f6d043b612..e82522dd08b 100644 --- a/test/unit/gateways/mercado_pago_test.rb +++ b/test/unit/gateways/mercado_pago_test.rb @@ -6,6 +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', + month: 10, + year: 2020, + first_name: 'John', + last_name: 'Smith', + 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', + month: 10, + year: 2020, + first_name: 'John', + last_name: 'Smith', + verification_value: '123' + ) @amount = 100 @options = { @@ -15,10 +39,18 @@ def setup } end + def test_supported_card_types + assert_equal MercadoPagoGateway.supported_cardtypes, %i[visa master american_express elo cabal naranja creditel patagonia_365 tarjeta_sol] + end + def test_successful_purchase - @gateway.expects(:ssl_post).at_most(2).returns(successful_purchase_response) + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_equal true, request['binary_mode'] if /payments/.match?(endpoint) + end.respond_with(successful_purchase_response) - response = @gateway.purchase(@amount, @credit_card, @options) assert_success response assert_equal '4141491|1.0', response.authorization @@ -26,6 +58,39 @@ def test_successful_purchase assert response.test? end + def test_successful_purchase_with_elo + @gateway.expects(:ssl_post).at_most(2).returns(successful_purchase_with_elo_response) + + response = @gateway.purchase(@amount, @elo_credit_card, @options) + assert_success response + + assert_equal '18044843|1.0', response.authorization + assert_equal 'accredited', response.message + assert response.test? + end + + def test_successful_purchase_with_cabal + @gateway.expects(:ssl_post).at_most(2).returns(successful_purchase_with_cabal_response) + + response = @gateway.purchase(@amount, @cabal_credit_card, @options) + assert_success response + + assert_equal '20728968|1.0', response.authorization + assert_equal 'accredited', response.message + assert response.test? + end + + def test_successful_purchase_with_naranja + @gateway.expects(:ssl_post).at_most(2).returns(successful_purchase_with_naranja_response) + + response = @gateway.purchase(@amount, @naranja_credit_card, @options) + assert_success response + + assert_equal '20728968|1.0', response.authorization + assert_equal 'accredited', response.message + assert response.test? + end + def test_failed_purchase @gateway.expects(:ssl_post).at_most(2).returns(failed_purchase_response) @@ -46,6 +111,50 @@ def test_successful_authorize assert response.test? end + def test_successful_authorize_with_capture_option + @gateway.expects(:ssl_post).at_most(2).returns(successful_authorize_with_capture_option_response) + + response = @gateway.authorize(@amount, @credit_card, @options.merge(capture: true)) + assert_success response + + assert_equal '4261941|1.0', response.authorization + assert_equal 'accredited', response.message + assert response.test? + end + + def test_successful_authorize_with_elo + @gateway.expects(:ssl_post).at_most(2).returns(successful_authorize_with_elo_response) + + response = @gateway.authorize(@amount, @elo_credit_card, @options) + assert_success response + + assert_equal '18044850|1.0', response.authorization + assert_equal 'pending_capture', response.message + assert response.test? + end + + def test_successful_authorize_with_cabal + @gateway.expects(:ssl_post).at_most(2).returns(successful_authorize_with_cabal_response) + + response = @gateway.authorize(@amount, @cabal_credit_card, @options) + assert_success response + + assert_equal '20729288|1.0', response.authorization + assert_equal 'pending_capture', response.message + assert response.test? + end + + def test_successful_authorize_with_naranja + @gateway.expects(:ssl_post).at_most(2).returns(successful_authorize_with_naranja_response) + + response = @gateway.authorize(@amount, @naranja_credit_card, @options) + assert_success response + + assert_equal '20729288|1.0', response.authorization + assert_equal 'pending_capture', response.message + assert response.test? + end + def test_failed_authorize @gateway.expects(:ssl_post).at_most(2).returns(failed_authorize_response) @@ -66,6 +175,39 @@ def test_successful_capture assert response.test? end + def test_successful_capture_with_elo + @gateway.expects(:ssl_request).returns(successful_capture_with_elo_response) + + response = @gateway.capture(@amount, 'authorization|amount') + assert_success response + + assert_equal '18044850|1.0', response.authorization + assert_equal 'accredited', response.message + assert response.test? + end + + def test_successful_capture_with_cabal + @gateway.expects(:ssl_request).returns(successful_capture_with_cabal_response) + + response = @gateway.capture(@amount, 'authorization|amount') + assert_success response + + assert_equal '20729288|1.0', response.authorization + assert_equal 'accredited', response.message + assert response.test? + end + + def test_successful_capture_with_naranja + @gateway.expects(:ssl_request).returns(successful_capture_with_naranja_response) + + response = @gateway.capture(@amount, 'authorization|amount') + assert_success response + + assert_equal '20729288|1.0', response.authorization + assert_equal 'accredited', response.message + assert response.test? + end + def test_failed_capture @gateway.expects(:ssl_request).returns(failed_capture_response) @@ -124,6 +266,17 @@ def test_successful_verify assert response.test? end + def test_successful_verify_with_custom_amount + response = stub_comms do + @gateway.verify(@credit_card, @options.merge({ amount: 200 })) + end.check_request do |endpoint, data, _headers| + params = JSON.parse(data) + assert_equal 2.0, params['transaction_amount'] if endpoint.include? 'payment' + end.respond_with(successful_authorize_response) + + assert_success response + end + def test_successful_verify_with_failed_void @gateway.expects(:ssl_request).at_most(3).returns(failed_void_response) @@ -144,71 +297,102 @@ def test_failed_verify assert response.test? end + def test_successful_inquire_with_id + @gateway.expects(:ssl_get).returns(successful_authorize_response) + + response = @gateway.inquire('authorization|amount') + assert_success response + + assert_equal '4261941|', response.authorization + assert_equal 'pending_capture', response.message + assert response.test? + end + + def test_successful_inquire_with_external_reference + @gateway.expects(:ssl_get).returns(successful_search_payments_response) + + response = @gateway.inquire(nil, { external_reference: '1234' }) + assert_success response + + assert_equal '1234', response.params['external_reference'] + assert_equal '1|', response.authorization + assert_equal 'accredited', response.message + assert response.test? + end + def test_scrub assert @gateway.supports_scrubbing? assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed end - def test_sends_american_express_as_amex + def test_does_not_send_brand credit_card = credit_card('378282246310005', brand: 'american_express') response = stub_comms do @gateway.purchase(@amount, credit_card, @options) - end.check_request do |endpoint, data, headers| - if data =~ /"payment_method_id"/ - assert_match(%r(amex), data) - end + end.check_request do |endpoint, data, _headers| + assert_not_match(%r("payment_method_id":"amex"), data) if endpoint =~ /payments/ end.respond_with(successful_purchase_response) assert_success response assert_equal '4141491|1.0', response.authorization end - def test_sends_diners_club_as_diners - credit_card = credit_card('30569309025904', brand: 'diners_club') + def test_sends_payment_method_id + credit_card = credit_card('30569309025904') response = stub_comms do - @gateway.purchase(@amount, credit_card, @options) - end.check_request do |endpoint, data, headers| - if data =~ /"payment_method_id"/ - assert_match(%r(diners), data) - end + @gateway.purchase(@amount, credit_card, @options.merge(payment_method_id: 'diners')) + end.check_request do |endpoint, data, _headers| + assert_match(%r("payment_method_id":"diners"), data) if endpoint =~ /payments/ end.respond_with(successful_purchase_response) assert_success response assert_equal '4141491|1.0', response.authorization end - def test_sends_mastercard_as_master - credit_card = credit_card('5555555555554444', brand: 'master') - + def test_successful_purchase_with_notification_url response = stub_comms do - @gateway.purchase(@amount, credit_card, @options) - end.check_request do |endpoint, data, headers| - if data =~ /"payment_method_id"/ - assert_match(%r(master), data) - end + @gateway.purchase(@amount, credit_card, @options.merge(notification_url: 'www.mercado-pago.com')) + end.check_request do |endpoint, data, _headers| + assert_match(%r("notification_url":"www.mercado-pago.com"), data) if endpoint =~ /payments/ end.respond_with(successful_purchase_response) assert_success response - assert_equal '4141491|1.0', response.authorization end def test_includes_deviceid_header @options[:device_id] = '1a2b3c' - @gateway.expects(:ssl_post).with(anything, anything, headers = {'Content-Type' => 'application/json'}).returns(successful_purchase_response) - @gateway.expects(:ssl_post).with(anything, anything, headers = {'Content-Type' => 'application/json', 'X-Device-Session-ID' => '1a2b3c'}).returns(successful_purchase_response) + @gateway.expects(:ssl_post).with(anything, anything, { 'Content-Type' => 'application/json' }).returns(successful_purchase_response) + @gateway.expects(:ssl_post).with(anything, anything, { 'Content-Type' => 'application/json', 'X-meli-session-id' => '1a2b3c' }).returns(successful_purchase_response) response = @gateway.purchase(@amount, @credit_card, @options) assert_success response end + def test_includes_idempotency_key_header + @options[:idempotency_key] = '12345' + @gateway.expects(:ssl_post).with(anything, anything, { 'Content-Type' => 'application/json' }).returns(successful_purchase_response) + @gateway.expects(:ssl_post).with(anything, anything, { 'Content-Type' => 'application/json', 'X-Idempotency-Key' => '12345' }).returns(successful_purchase_response) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + end + + def test_includes_idempotency_key_header_for_refund + @options[:idempotency_key] = '12345' + @gateway.expects(:ssl_post).with(anything, anything, { 'Content-Type' => 'application/json', 'X-Idempotency-Key' => '12345' }).returns(successful_refund_response) + + response = @gateway.refund(@amount, 'authorization|1.0', @options) + assert_success response + end + def test_includes_additional_data - @options[:additional_info] = {'foo' => 'bar', 'baz' => 'quux'} + @options[:additional_info] = { 'foo' => 'bar', 'baz' => 'quux' } response = stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| - if data =~ /payment_method_id/ + end.check_request do |_endpoint, data, _headers| + if /payment_method_id/.match?(data) assert_match(/"foo":"bar"/, data) assert_match(/"baz":"quux"/, data) end @@ -217,6 +401,160 @@ def test_includes_additional_data assert_success response end + def test_includes_issuer_id + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(issuer_id: '1a2b3c4d')) + end.check_request do |endpoint, data, _headers| + assert_match(%r("issuer_id":"1a2b3c4d"), data) if endpoint =~ /payments/ + end.respond_with(successful_purchase_response) + + assert_success response + assert_equal '4141491|1.0', response.authorization + end + + def test_purchase_includes_taxes_array + taxes_type = 'IVA' + taxes_value = 500.0 + taxes_object = { value: taxes_value, type: taxes_type } + taxes_array = [taxes_object, taxes_object] + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(taxes: taxes_array)) + end.check_request do |endpoint, data, _headers| + single_pattern = "{\"value\":#{taxes_value},\"type\":\"#{taxes_type}\"}" + pattern = "\"taxes\":[#{single_pattern},#{single_pattern}]" + assert_match(pattern, data) if endpoint =~ /payments/ + end.respond_with(successful_purchase_response) + end + + def test_purchase_includes_taxes_hash + taxes_type = 'IVA' + taxes_value = 500.0 + taxes_object = { value: taxes_value, type: taxes_type } + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(taxes: taxes_object)) + end.check_request do |endpoint, data, _headers| + pattern = "\"taxes\":[{\"value\":#{taxes_value},\"type\":\"#{taxes_type}\"}]" + assert_match(pattern, data) if endpoint =~ /payments/ + end.respond_with(successful_purchase_response) + end + + def test_purchase_includes_net_amount + net_amount = 9500 + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(net_amount:)) + end.check_request do |endpoint, data, _headers| + assert_match("\"net_amount\":#{net_amount}", data) if endpoint =~ /payments/ + end.respond_with(successful_purchase_response) + end + + def test_purchase_includes_metadata + key1 = 'value_1' + key2 = 'value_2' + metadata_object = { key_1: key1, key_2: key2 } + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(metadata: metadata_object)) + end.check_request do |endpoint, data, _headers| + assert_match("\"metadata\":{\"key_1\":\"#{key1}\",\"key_2\":\"#{key2}\"}", data) if endpoint =~ /payments/ + end.respond_with(successful_purchase_with_metadata_response) + end + + def test_authorize_includes_taxes_array + taxes_type = 'IVA' + taxes_value = 500.0 + taxes_object = { value: taxes_value, type: taxes_type } + taxes_array = [taxes_object, taxes_object] + + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(taxes: taxes_array)) + end.check_request do |endpoint, data, _headers| + single_pattern = "{\"value\":#{taxes_value},\"type\":\"#{taxes_type}\"}" + pattern = "\"taxes\":[#{single_pattern},#{single_pattern}]" + assert_match(pattern, data) if endpoint =~ /payments/ + end.respond_with(successful_authorize_response) + end + + def test_authorize_includes_taxes_hash + taxes_type = 'IVA' + taxes_value = 500.0 + taxes_object = { value: taxes_value, type: taxes_type } + + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(taxes: taxes_object)) + end.check_request do |endpoint, data, _headers| + pattern = "\"taxes\":[{\"value\":#{taxes_value},\"type\":\"#{taxes_type}\"}]" + assert_match(pattern, data) if endpoint =~ /payments/ + end.respond_with(successful_authorize_response) + end + + def test_authorize_includes_net_amount + net_amount = 9500 + + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(net_amount:)) + end.check_request do |endpoint, data, _headers| + assert_match("\"net_amount\":#{net_amount}", data) if endpoint =~ /payments/ + end.respond_with(successful_authorize_response) + end + + def test_invalid_taxes_type + assert_raises(ArgumentError) do + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(taxes: 10)) + end.respond_with(successful_purchase_response) + end + end + + def test_invalid_taxes_embedded_type + assert_raises(ArgumentError) do + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(taxes: [{ value: 500, type: 'IVA' }, 10])) + end.respond_with(successful_purchase_response) + end + end + + def test_invalid_taxes_shape + assert_raises(ArgumentError) do + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(taxes: [{ amount: 500, type: 'IVA' }])) + end.respond_with(successful_purchase_response) + end + end + + def test_set_binary_mode_to_nil_when_request_is_3ds + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(execute_threed: true)) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_nil request['binary_mode'] if /payments/.match?(endpoint) + end.respond_with(successful_authorize_response) + end + + def test_should_not_include_sponsor_id_when_test_mode_is_enabled + @options[:sponsor_id] = '1234' + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |endpoint, data, _headers| + assert_not_match(%r("sponsor_id":), data) if /payments/.match?(endpoint) + end.respond_with(successful_purchase_response) + end + + def test_should_include_sponsor_id_when_test_mode_is_disabled + @gateway.stubs(test?: false) + @options[:sponsor_id] = '1234' + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_equal '1234', request['sponsor_id'] if /payments/.match?(endpoint) + end.respond_with(successful_purchase_response) + end + private def pre_scrubbed @@ -335,6 +673,24 @@ def successful_purchase_response ) end + def successful_purchase_with_elo_response + %( + {"id":18044843,"date_created":"2019-03-04T09:39:21.000-04:00","date_approved":"2019-03-04T09:39:22.000-04:00","date_last_updated":"2019-03-04T09:39:22.000-04:00","date_of_expiration":null,"money_release_date":"2019-03-18T09:39:22.000-04:00","operation_type":"regular_payment","issuer_id":"687","payment_method_id":"elo","payment_type_id":"credit_card","status":"approved","status_detail":"accredited","currency_id":"BRL","description":"Store Purchase","live_mode":false,"sponsor_id":null,"authorization_code":null,"money_release_schema":null,"taxes_amount":0,"counter_currency":null,"shipping_amount":0,"pos_id":null,"store_id":null,"collector_id":263489584,"payer":{"first_name":"Test","last_name":"Test","email":"test_user_80507629@testuser.com","identification":{"number":"32659430","type":"DNI"},"phone":{"area_code":"01","number":"1111-1111","extension":""},"type":"registered","entity_type":null,"id":"412997143"},"metadata":{},"additional_info":{"payer":{"address":{"zip_code":"K1C2N6","street_name":"456 My Street Apt 1"}},"shipments":{"receiver_address":{"zip_code":"K1C2N6","street_name":"456 My Street Apt 1"}}},"order":{},"external_reference":"6cc551da28909403939d024cd067a65f","transaction_amount":5,"transaction_amount_refunded":0,"coupon_amount":0,"differential_pricing_id":null,"deduction_schema":null,"transaction_details":{"net_received_amount":4.75,"total_paid_amount":5,"overpaid_amount":0,"external_resource_url":null,"installment_amount":5,"financial_institution":null,"payment_method_reference_id":null,"payable_deferral_period":null,"acquirer_reference":null},"fee_details":[{"type":"mercadopago_fee","amount":0.25,"fee_payer":"collector"}],"captured":true,"binary_mode":true,"call_for_authorize_id":null,"statement_descriptor":"MERCADOPAGO","installments":1,"card":{"id":null,"first_six_digits":"506726","last_four_digits":"7446","expiration_month":10,"expiration_year":2020,"date_created":"2019-03-04T09:39:21.000-04:00","date_last_updated":"2019-03-04T09:39:21.000-04:00","cardholder":{"name":"John Smith","identification":{"number":null,"type":null}}},"notification_url":null,"refunds":[],"processing_mode":"aggregator","merchant_account_id":null,"acquirer":null,"merchant_number":null,"acquirer_reconciliation":[]} + ) + end + + def successful_purchase_with_cabal_response + %( + {"id":20728968,"date_created":"2019-08-06T15:38:12.000-04:00","date_approved":"2019-08-06T15:38:12.000-04:00","date_last_updated":"2019-08-06T15:38:12.000-04:00","date_of_expiration":null,"money_release_date":"2019-08-20T15:38:12.000-04:00","operation_type":"regular_payment","issuer_id":"688","payment_method_id":"cabal","payment_type_id":"credit_card","status":"approved","status_detail":"accredited","currency_id":"ARS","description":"Store Purchase","live_mode":false,"sponsor_id":null,"authorization_code":null,"money_release_schema":null,"taxes_amount":0,"counter_currency":null,"shipping_amount":0,"pos_id":null,"store_id":null,"collector_id":388534608,"payer":{"first_name":"Test","last_name":"Test","email":"test_user_80507629@testuser.com","identification":{"number":"32659430","type":"DNI"},"phone":{"area_code":"01","number":"1111-1111","extension":""},"type":"registered","entity_type":null,"id":"388571255"},"metadata":{},"additional_info":{"payer":{"address":{"zip_code":"K1C2N6","street_name":"456 My Street Apt 1"}},"shipments":{"receiver_address":{"zip_code":"K1C2N6","street_name":"456 My Street Apt 1"}}},"order":{},"external_reference":"ab86ce493d1cc1e447877720843812e9","transaction_amount":5,"transaction_amount_refunded":0,"coupon_amount":0,"differential_pricing_id":null,"deduction_schema":null,"transaction_details":{"payment_method_reference_id":null,"net_received_amount":4.79,"total_paid_amount":5,"overpaid_amount":0,"external_resource_url":null,"installment_amount":5,"financial_institution":null,"payable_deferral_period":null,"acquirer_reference":null},"fee_details":[{"type":"mercadopago_fee","amount":0.21,"fee_payer":"collector"}],"captured":true,"binary_mode":true,"call_for_authorize_id":null,"statement_descriptor":"SPREEDLYMLA","installments":1,"card":{"id":null,"first_six_digits":"603522","last_four_digits":"7021","expiration_month":10,"expiration_year":2020,"date_created":"2019-08-06T15:38:12.000-04:00","date_last_updated":"2019-08-06T15:38:12.000-04:00","cardholder":{"name":"John Smith","identification":{"number":null,"type":null}}},"notification_url":null,"refunds":[],"processing_mode":"aggregator","merchant_account_id":null,"acquirer":null,"merchant_number":null,"acquirer_reconciliation":[]} + ) + end + + def successful_purchase_with_naranja_response + %( + {"id":20728968,"date_created":"2019-08-06T15:38:12.000-04:00","date_approved":"2019-08-06T15:38:12.000-04:00","date_last_updated":"2019-08-06T15:38:12.000-04:00","date_of_expiration":null,"money_release_date":"2019-08-20T15:38:12.000-04:00","operation_type":"regular_payment","issuer_id":"688","payment_method_id":"naranja","payment_type_id":"credit_card","status":"approved","status_detail":"accredited","currency_id":"ARS","description":"Store Purchase","live_mode":false,"sponsor_id":null,"authorization_code":null,"money_release_schema":null,"taxes_amount":0,"counter_currency":null,"shipping_amount":0,"pos_id":null,"store_id":null,"collector_id":388534608,"payer":{"first_name":"Test","last_name":"Test","email":"test_user_80507629@testuser.com","identification":{"number":"32659430","type":"DNI"},"phone":{"area_code":"01","number":"1111-1111","extension":""},"type":"registered","entity_type":null,"id":"388571255"},"metadata":{},"additional_info":{"payer":{"address":{"zip_code":"K1C2N6","street_name":"456 My Street Apt 1"}},"shipments":{"receiver_address":{"zip_code":"K1C2N6","street_name":"456 My Street Apt 1"}}},"order":{},"external_reference":"ab86ce493d1cc1e447877720843812e9","transaction_amount":5,"transaction_amount_refunded":0,"coupon_amount":0,"differential_pricing_id":null,"deduction_schema":null,"transaction_details":{"payment_method_reference_id":null,"net_received_amount":4.79,"total_paid_amount":5,"overpaid_amount":0,"external_resource_url":null,"installment_amount":5,"financial_institution":null,"payable_deferral_period":null,"acquirer_reference":null},"fee_details":[{"type":"mercadopago_fee","amount":0.21,"fee_payer":"collector"}],"captured":true,"binary_mode":true,"call_for_authorize_id":null,"statement_descriptor":"SPREEDLYMLA","installments":1,"card":{"id":null,"first_six_digits":"603522","last_four_digits":"7021","expiration_month":10,"expiration_year":2020,"date_created":"2019-08-06T15:38:12.000-04:00","date_last_updated":"2019-08-06T15:38:12.000-04:00","cardholder":{"name":"John Smith","identification":{"number":null,"type":null}}},"notification_url":null,"refunds":[],"processing_mode":"aggregator","merchant_account_id":null,"acquirer":null,"merchant_number":null,"acquirer_reconciliation":[]} + ) + end + def failed_purchase_response %( {"id":4142297,"date_created":"2017-07-06T10:13:32.000-04:00","date_approved":null,"date_last_updated":"2017-07-06T10:13:32.000-04:00","date_of_expiration":null,"money_release_date":null,"operation_type":"regular_payment","issuer_id":"166","payment_method_id":"visa","payment_type_id":"credit_card","status":"rejected","status_detail":"cc_rejected_other_reason","currency_id":"MXN","description":"Store Purchase","live_mode":false,"sponsor_id":null,"authorization_code":null,"related_exchange_rate":null,"collector_id":261735089,"payer":{"type":"guest","id":null,"email":"user@example.com","identification":{"type":null,"number":null},"phone":{"area_code":null,"number":null,"extension":""},"first_name":"First User","last_name":"User","entity_type":null},"metadata":{},"additional_info":{"payer":{"address":{"zip_code":"K1C2N6","street_name":"My Street","street_number":"456"}}},"order":{"type":"mercadopago","id":"830943860538524456"},"external_reference":null,"transaction_amount":5,"transaction_amount_refunded":0,"coupon_amount":0,"differential_pricing_id":null,"deduction_schema":null,"transaction_details":{"net_received_amount":0,"total_paid_amount":5,"overpaid_amount":0,"external_resource_url":null,"installment_amount":5,"financial_institution":null,"payment_method_reference_id":null,"payable_deferral_period":null,"acquirer_reference":null},"fee_details":[],"captured":true,"binary_mode":false,"call_for_authorize_id":null,"statement_descriptor":"WWW.MERCADOPAGO.COM","installments":1,"card":{"id":null,"first_six_digits":"400030","last_four_digits":"2220","expiration_month":9,"expiration_year":2018,"date_created":"2017-07-06T10:13:32.000-04:00","date_last_updated":"2017-07-06T10:13:32.000-04:00","cardholder":{"name":"Longbob Longsen","identification":{"number":null,"type":null}}},"notification_url":null,"refunds":[],"processing_mode":null,"merchant_account_id":null,"acquirer":null,"merchant_number":null} @@ -347,6 +703,30 @@ def successful_authorize_response ) end + def successful_authorize_with_capture_option_response + %( + {"id":4261941,"date_created":"2017-07-13T14:24:46.000-04:00","date_approved":null,"date_last_updated":"2017-07-13T14:24:46.000-04:00","date_of_expiration":null,"money_release_date":null,"operation_type":"regular_payment","issuer_id":"25","payment_method_id":"visa","payment_type_id":"credit_card","status":"authorized","status_detail":"accredited","currency_id":"BRL","description":"Store Purchase","live_mode":false,"sponsor_id":null,"authorization_code":null,"related_exchange_rate":null,"collector_id":263489584,"payer":{"type":"guest","id":null,"email":"user+br@example.com","identification":{"type":null,"number":null},"phone":{"area_code":null,"number":null,"extension":null},"first_name":null,"last_name":null,"entity_type":null},"metadata":{},"additional_info":{"payer":{"address":{"zip_code":"K1C2N6","street_name":"My Street","street_number":"456"}}},"order":{"type":"mercadopago","id":"2294029672081601730"},"external_reference":null,"transaction_amount":5,"transaction_amount_refunded":0,"coupon_amount":0,"differential_pricing_id":null,"deduction_schema":null,"transaction_details":{"net_received_amount":0,"total_paid_amount":5,"overpaid_amount":0,"external_resource_url":null,"installment_amount":5,"financial_institution":null,"payment_method_reference_id":null,"payable_deferral_period":null,"acquirer_reference":null},"fee_details":[],"captured":false,"binary_mode":false,"call_for_authorize_id":null,"statement_descriptor":"WWW.MERCADOPAGO.COM","installments":1,"card":{"id":null,"first_six_digits":"450995","last_four_digits":"3704","expiration_month":9,"expiration_year":2018,"date_created":"2017-07-13T14:24:46.000-04:00","date_last_updated":"2017-07-13T14:24:46.000-04:00","cardholder":{"name":"Longbob Longsen","identification":{"number":null,"type":null}}},"notification_url":null,"refunds":[],"processing_mode":"aggregator","merchant_account_id":null,"acquirer":null,"merchant_number":null} + ) + end + + def successful_authorize_with_elo_response + %( + {"id":18044850,"date_created":"2019-03-04T09:44:37.000-04:00","date_approved":null,"date_last_updated":"2019-03-04T09:44:40.000-04:00","date_of_expiration":null,"money_release_date":null,"operation_type":"regular_payment","issuer_id":"687","payment_method_id":"elo","payment_type_id":"credit_card","status":"authorized","status_detail":"pending_capture","currency_id":"BRL","description":"Store Purchase","live_mode":false,"sponsor_id":null,"authorization_code":null,"money_release_schema":null,"taxes_amount":0,"counter_currency":null,"shipping_amount":0,"pos_id":null,"store_id":null,"collector_id":263489584,"payer":{"first_name":"Test","last_name":"Test","email":"test_user_80507629@testuser.com","identification":{"number":"32659430","type":"DNI"},"phone":{"area_code":"01","number":"1111-1111","extension":""},"type":"registered","entity_type":null,"id":"413001901"},"metadata":{},"additional_info":{"payer":{"address":{"zip_code":"K1C2N6","street_name":"456 My Street Apt 1"}},"shipments":{"receiver_address":{"zip_code":"K1C2N6","street_name":"456 My Street Apt 1"}}},"order":{},"external_reference":"24cd4658b9ea6dbf164f9fb9f67e5e78","transaction_amount":5,"transaction_amount_refunded":0,"coupon_amount":0,"differential_pricing_id":null,"deduction_schema":null,"transaction_details":{"net_received_amount":0,"total_paid_amount":5,"overpaid_amount":0,"external_resource_url":null,"installment_amount":5,"financial_institution":null,"payment_method_reference_id":null,"payable_deferral_period":null,"acquirer_reference":null},"fee_details":[],"captured":false,"binary_mode":true,"call_for_authorize_id":null,"statement_descriptor":"MERCADOPAGO","installments":1,"card":{"id":null,"first_six_digits":"506726","last_four_digits":"7446","expiration_month":10,"expiration_year":2020,"date_created":"2019-03-04T09:44:37.000-04:00","date_last_updated":"2019-03-04T09:44:37.000-04:00","cardholder":{"name":"John Smith","identification":{"number":null,"type":null}}},"notification_url":null,"refunds":[],"processing_mode":"aggregator","merchant_account_id":null,"acquirer":null,"merchant_number":null,"acquirer_reconciliation":[]} + ) + end + + def successful_authorize_with_cabal_response + %( + {"id":20729288,"date_created":"2019-08-06T15:57:47.000-04:00","date_approved":null,"date_last_updated":"2019-08-06T15:57:49.000-04:00","date_of_expiration":null,"money_release_date":null,"operation_type":"regular_payment","issuer_id":"688","payment_method_id":"cabal","payment_type_id":"credit_card","status":"authorized","status_detail":"pending_capture","currency_id":"ARS","description":"Store Purchase","live_mode":false,"sponsor_id":null,"authorization_code":null,"money_release_schema":null,"taxes_amount":0,"counter_currency":null,"shipping_amount":0,"pos_id":null,"store_id":null,"collector_id":388534608,"payer":{"first_name":"Test","last_name":"Test","email":"test_user_80507629@testuser.com","identification":{"number":"32659430","type":"DNI"},"phone":{"area_code":"01","number":"1111-1111","extension":""},"type":"registered","entity_type":null,"id":"388571255"},"metadata":{},"additional_info":{"payer":{"address":{"zip_code":"K1C2N6","street_name":"456 My Street Apt 1"}},"shipments":{"receiver_address":{"zip_code":"K1C2N6","street_name":"456 My Street Apt 1"}}},"order":{},"external_reference":"f70cb796271176441a5077012ff2af2a","transaction_amount":5,"transaction_amount_refunded":0,"coupon_amount":0,"differential_pricing_id":null,"deduction_schema":null,"transaction_details":{"payment_method_reference_id":null,"net_received_amount":0,"total_paid_amount":5,"overpaid_amount":0,"external_resource_url":null,"installment_amount":5,"financial_institution":null,"payable_deferral_period":null,"acquirer_reference":null},"fee_details":[],"captured":false,"binary_mode":true,"call_for_authorize_id":null,"statement_descriptor":"SPREEDLYMLA","installments":1,"card":{"id":null,"first_six_digits":"603522","last_four_digits":"7021","expiration_month":10,"expiration_year":2020,"date_created":"2019-08-06T15:57:47.000-04:00","date_last_updated":"2019-08-06T15:57:47.000-04:00","cardholder":{"name":"John Smith","identification":{"number":null,"type":null}}},"notification_url":null,"refunds":[],"processing_mode":"aggregator","merchant_account_id":null,"acquirer":null,"merchant_number":null,"acquirer_reconciliation":[]} + ) + end + + def successful_authorize_with_naranja_response + %( + {"id":20729288,"date_created":"2019-08-06T15:57:47.000-04:00","date_approved":null,"date_last_updated":"2019-08-06T15:57:49.000-04:00","date_of_expiration":null,"money_release_date":null,"operation_type":"regular_payment","issuer_id":"688","payment_method_id":"naranja","payment_type_id":"credit_card","status":"authorized","status_detail":"pending_capture","currency_id":"ARS","description":"Store Purchase","live_mode":false,"sponsor_id":null,"authorization_code":null,"money_release_schema":null,"taxes_amount":0,"counter_currency":null,"shipping_amount":0,"pos_id":null,"store_id":null,"collector_id":388534608,"payer":{"first_name":"Test","last_name":"Test","email":"test_user_80507629@testuser.com","identification":{"number":"32659430","type":"DNI"},"phone":{"area_code":"01","number":"1111-1111","extension":""},"type":"registered","entity_type":null,"id":"388571255"},"metadata":{},"additional_info":{"payer":{"address":{"zip_code":"K1C2N6","street_name":"456 My Street Apt 1"}},"shipments":{"receiver_address":{"zip_code":"K1C2N6","street_name":"456 My Street Apt 1"}}},"order":{},"external_reference":"f70cb796271176441a5077012ff2af2a","transaction_amount":5,"transaction_amount_refunded":0,"coupon_amount":0,"differential_pricing_id":null,"deduction_schema":null,"transaction_details":{"payment_method_reference_id":null,"net_received_amount":0,"total_paid_amount":5,"overpaid_amount":0,"external_resource_url":null,"installment_amount":5,"financial_institution":null,"payable_deferral_period":null,"acquirer_reference":null},"fee_details":[],"captured":false,"binary_mode":true,"call_for_authorize_id":null,"statement_descriptor":"SPREEDLYMLA","installments":1,"card":{"id":null,"first_six_digits":"603522","last_four_digits":"7021","expiration_month":10,"expiration_year":2020,"date_created":"2019-08-06T15:57:47.000-04:00","date_last_updated":"2019-08-06T15:57:47.000-04:00","cardholder":{"name":"John Smith","identification":{"number":null,"type":null}}},"notification_url":null,"refunds":[],"processing_mode":"aggregator","merchant_account_id":null,"acquirer":null,"merchant_number":null,"acquirer_reconciliation":[]} + ) + end + def failed_authorize_response %( {"id":4261953,"date_created":"2017-07-13T14:25:33.000-04:00","date_approved":null,"date_last_updated":"2017-07-13T14:25:33.000-04:00","date_of_expiration":null,"money_release_date":null,"operation_type":"regular_payment","issuer_id":"25","payment_method_id":"visa","payment_type_id":"credit_card","status":"rejected","status_detail":"cc_rejected_other_reason","currency_id":"BRL","description":"Store Purchase","live_mode":false,"sponsor_id":null,"authorization_code":null,"related_exchange_rate":null,"collector_id":263489584,"payer":{"type":"guest","id":null,"email":"user+br@example.com","identification":{"type":null,"number":null},"phone":{"area_code":null,"number":null,"extension":null},"first_name":null,"last_name":null,"entity_type":null},"metadata":{},"additional_info":{"payer":{"address":{"zip_code":"K1C2N6","street_name":"My Street","street_number":"456"}}},"order":{"type":"mercadopago","id":"7528376941458928221"},"external_reference":null,"transaction_amount":5,"transaction_amount_refunded":0,"coupon_amount":0,"differential_pricing_id":null,"deduction_schema":null,"transaction_details":{"net_received_amount":0,"total_paid_amount":5,"overpaid_amount":0,"external_resource_url":null,"installment_amount":5,"financial_institution":null,"payment_method_reference_id":null,"payable_deferral_period":null,"acquirer_reference":null},"fee_details":[],"captured":false,"binary_mode":false,"call_for_authorize_id":null,"statement_descriptor":"WWW.MERCADOPAGO.COM","installments":1,"card":{"id":null,"first_six_digits":"400030","last_four_digits":"2220","expiration_month":9,"expiration_year":2018,"date_created":"2017-07-13T14:25:33.000-04:00","date_last_updated":"2017-07-13T14:25:33.000-04:00","cardholder":{"name":"Longbob Longsen","identification":{"number":null,"type":null}}},"notification_url":null,"refunds":[],"processing_mode":"aggregator","merchant_account_id":null,"acquirer":null,"merchant_number":null} @@ -359,6 +739,24 @@ def successful_capture_response ) end + def successful_capture_with_elo_response + %( + {"id":18044850,"date_created":"2019-03-04T09:44:37.000-04:00","date_approved":"2019-03-04T09:44:41.000-04:00","date_last_updated":"2019-03-04T09:44:41.000-04:00","date_of_expiration":null,"money_release_date":"2019-03-18T09:44:41.000-04:00","operation_type":"regular_payment","issuer_id":"687","payment_method_id":"elo","payment_type_id":"credit_card","status":"approved","status_detail":"accredited","currency_id":"BRL","description":"Store Purchase","live_mode":false,"sponsor_id":null,"authorization_code":null,"money_release_schema":null,"taxes_amount":0,"counter_currency":null,"shipping_amount":0,"pos_id":null,"store_id":null,"collector_id":263489584,"payer":{"first_name":"Test","last_name":"Test","email":"test_user_80507629@testuser.com","identification":{"number":"32659430","type":"DNI"},"phone":{"area_code":"01","number":"1111-1111","extension":""},"type":"registered","entity_type":null,"id":"413001901"},"metadata":{},"additional_info":{"payer":{"address":{"zip_code":"K1C2N6","street_name":"456 My Street Apt 1"}},"shipments":{"receiver_address":{"zip_code":"K1C2N6","street_name":"456 My Street Apt 1"}}},"order":{},"external_reference":"24cd4658b9ea6dbf164f9fb9f67e5e78","transaction_amount":5,"transaction_amount_refunded":0,"coupon_amount":0,"differential_pricing_id":null,"deduction_schema":null,"transaction_details":{"net_received_amount":4.75,"total_paid_amount":5,"overpaid_amount":0,"external_resource_url":null,"installment_amount":5,"financial_institution":null,"payment_method_reference_id":null,"payable_deferral_period":null,"acquirer_reference":null},"fee_details":[{"type":"mercadopago_fee","amount":0.25,"fee_payer":"collector"}],"captured":true,"binary_mode":true,"call_for_authorize_id":null,"statement_descriptor":"MERCADOPAGO","installments":1,"card":{"id":null,"first_six_digits":"506726","last_four_digits":"7446","expiration_month":10,"expiration_year":2020,"date_created":"2019-03-04T09:44:37.000-04:00","date_last_updated":"2019-03-04T09:44:37.000-04:00","cardholder":{"name":"John Smith","identification":{"number":null,"type":null}}},"notification_url":null,"refunds":[],"processing_mode":"aggregator","merchant_account_id":null,"acquirer":null,"merchant_number":null,"acquirer_reconciliation":[]} + ) + end + + def successful_capture_with_cabal_response + %( + {"id":20729288,"date_created":"2019-08-06T15:57:47.000-04:00","date_approved":"2019-08-06T15:57:49.000-04:00","date_last_updated":"2019-08-06T15:57:49.000-04:00","date_of_expiration":null,"money_release_date":"2019-08-20T15:57:49.000-04:00","operation_type":"regular_payment","issuer_id":"688","payment_method_id":"cabal","payment_type_id":"credit_card","status":"approved","status_detail":"accredited","currency_id":"ARS","description":"Store Purchase","live_mode":false,"sponsor_id":null,"authorization_code":null,"money_release_schema":null,"taxes_amount":0,"counter_currency":null,"shipping_amount":0,"pos_id":null,"store_id":null,"collector_id":388534608,"payer":{"first_name":"Test","last_name":"Test","email":"test_user_80507629@testuser.com","identification":{"number":"32659430","type":"DNI"},"phone":{"area_code":"01","number":"1111-1111","extension":""},"type":"registered","entity_type":null,"id":"388571255"},"metadata":{},"additional_info":{"payer":{"address":{"zip_code":"K1C2N6","street_name":"456 My Street Apt 1"}},"shipments":{"receiver_address":{"zip_code":"K1C2N6","street_name":"456 My Street Apt 1"}}},"order":{},"external_reference":"f70cb796271176441a5077012ff2af2a","transaction_amount":5,"transaction_amount_refunded":0,"coupon_amount":0,"differential_pricing_id":null,"deduction_schema":null,"transaction_details":{"payment_method_reference_id":null,"net_received_amount":4.79,"total_paid_amount":5,"overpaid_amount":0,"external_resource_url":null,"installment_amount":5,"financial_institution":null,"payable_deferral_period":null,"acquirer_reference":null},"fee_details":[{"type":"mercadopago_fee","amount":0.21,"fee_payer":"collector"}],"captured":true,"binary_mode":true,"call_for_authorize_id":null,"statement_descriptor":"SPREEDLYMLA","installments":1,"card":{"id":null,"first_six_digits":"603522","last_four_digits":"7021","expiration_month":10,"expiration_year":2020,"date_created":"2019-08-06T15:57:47.000-04:00","date_last_updated":"2019-08-06T15:57:47.000-04:00","cardholder":{"name":"John Smith","identification":{"number":null,"type":null}}},"notification_url":null,"refunds":[],"processing_mode":"aggregator","merchant_account_id":null,"acquirer":null,"merchant_number":null,"acquirer_reconciliation":[]} + ) + end + + def successful_capture_with_naranja_response + %( + {"id":20729288,"date_created":"2019-08-06T15:57:47.000-04:00","date_approved":"2019-08-06T15:57:49.000-04:00","date_last_updated":"2019-08-06T15:57:49.000-04:00","date_of_expiration":null,"money_release_date":"2019-08-20T15:57:49.000-04:00","operation_type":"regular_payment","issuer_id":"688","payment_method_id":"naranja","payment_type_id":"credit_card","status":"approved","status_detail":"accredited","currency_id":"ARS","description":"Store Purchase","live_mode":false,"sponsor_id":null,"authorization_code":null,"money_release_schema":null,"taxes_amount":0,"counter_currency":null,"shipping_amount":0,"pos_id":null,"store_id":null,"collector_id":388534608,"payer":{"first_name":"Test","last_name":"Test","email":"test_user_80507629@testuser.com","identification":{"number":"32659430","type":"DNI"},"phone":{"area_code":"01","number":"1111-1111","extension":""},"type":"registered","entity_type":null,"id":"388571255"},"metadata":{},"additional_info":{"payer":{"address":{"zip_code":"K1C2N6","street_name":"456 My Street Apt 1"}},"shipments":{"receiver_address":{"zip_code":"K1C2N6","street_name":"456 My Street Apt 1"}}},"order":{},"external_reference":"f70cb796271176441a5077012ff2af2a","transaction_amount":5,"transaction_amount_refunded":0,"coupon_amount":0,"differential_pricing_id":null,"deduction_schema":null,"transaction_details":{"payment_method_reference_id":null,"net_received_amount":4.79,"total_paid_amount":5,"overpaid_amount":0,"external_resource_url":null,"installment_amount":5,"financial_institution":null,"payable_deferral_period":null,"acquirer_reference":null},"fee_details":[{"type":"mercadopago_fee","amount":0.21,"fee_payer":"collector"}],"captured":true,"binary_mode":true,"call_for_authorize_id":null,"statement_descriptor":"SPREEDLYMLA","installments":1,"card":{"id":null,"first_six_digits":"603522","last_four_digits":"7021","expiration_month":10,"expiration_year":2020,"date_created":"2019-08-06T15:57:47.000-04:00","date_last_updated":"2019-08-06T15:57:47.000-04:00","cardholder":{"name":"John Smith","identification":{"number":null,"type":null}}},"notification_url":null,"refunds":[],"processing_mode":"aggregator","merchant_account_id":null,"acquirer":null,"merchant_number":null,"acquirer_reconciliation":[]} + ) + end + def failed_capture_response %( {"message":"Method not allowed","error":"method_not_allowed","status":405,"cause":[{"code":"Method not allowed","description":"Method not allowed","data":null}]} @@ -388,4 +786,16 @@ def failed_void_response {"message":"Method not allowed","error":"method_not_allowed","status":405,"cause":[{"code":"Method not allowed","description":"Method not allowed","data":null}]} ) end + + def successful_purchase_with_metadata_response + %( + {"id":4141491,"date_created":"2017-07-06T09:49:35.000-04:00","date_approved":"2017-07-06T09:49:35.000-04:00","date_last_updated":"2017-07-06T09:49:35.000-04:00","date_of_expiration":null,"money_release_date":"2017-07-18T09:49:35.000-04:00","operation_type":"regular_payment","issuer_id":"166","payment_method_id":"visa","payment_type_id":"credit_card","status":"approved","status_detail":"accredited","currency_id":"MXN","description":"Store Purchase","live_mode":false,"sponsor_id":null,"authorization_code":null,"related_exchange_rate":null,"collector_id":261735089,"payer":{"type":"guest","id":null,"email":"user@example.com","identification":{"type":null,"number":null},"phone":{"area_code":null,"number":null,"extension":""},"first_name":"First User","last_name":"User","entity_type":null},"metadata":{"key_1":"value_1","key_2":"value_2","key_3":{"nested_key_1":"value_3"}},"additional_info":{"payer":{"address":{"zip_code":"K1C2N6","street_name":"My Street","street_number":"456"}}},"order":{"type":"mercadopago","id":"2326513804447055222"},"external_reference":null,"transaction_amount":5,"transaction_amount_refunded":0,"coupon_amount":0,"differential_pricing_id":null,"deduction_schema":null,"transaction_details":{"net_received_amount":0.14,"total_paid_amount":5,"overpaid_amount":0,"external_resource_url":null,"installment_amount":5,"financial_institution":null,"payment_method_reference_id":null,"payable_deferral_period":null,"acquirer_reference":null},"fee_details":[{"type":"mercadopago_fee","amount":4.86,"fee_payer":"collector"}],"captured":true,"binary_mode":false,"call_for_authorize_id":null,"statement_descriptor":"WWW.MERCADOPAGO.COM","installments":1,"card":{"id":null,"first_six_digits":"450995","last_four_digits":"3704","expiration_month":9,"expiration_year":2018,"date_created":"2017-07-06T09:49:35.000-04:00","date_last_updated":"2017-07-06T09:49:35.000-04:00","cardholder":{"name":"Longbob Longsen","identification":{"number":null,"type":null}}},"notification_url":null,"refunds":[],"processing_mode":null,"merchant_account_id":null,"acquirer":null,"merchant_number":null} + ) + end + + def successful_search_payments_response + %( + [{"paging":{"total":17493,"limit":30,"offset":0},"results":[{"id":1,"date_created":"2017-08-31T11:26:38.000Z","date_approved":"2017-08-31T11:26:38.000Z","date_last_updated":"2017-08-31T11:26:38.000Z","money_release_date":"2017-09-14T11:26:38.000Z","payment_method_id":"account_money","payment_type_id":"credit_card","status":"approved","status_detail":"accredited","currency_id":"BRL","description":"PagoPizza","collector_id":2,"payer":{"id":123,"email":"afriend@gmail.com","identification":{"type":"DNI","number":12345678},"type":"customer"},"metadata":{},"additional_info":{},"external_reference":"1234","transaction_amount":250,"transaction_amount_refunded":0,"coupon_amount":0,"transaction_details":{"net_received_amount":250,"total_paid_amount":250,"overpaid_amount":0,"installment_amount":250},"installments":1,"card":{}}]}] + ) + end end diff --git a/test/unit/gateways/merchant_e_solutions_test.rb b/test/unit/gateways/merchant_e_solutions_test.rb index 26b932d25a0..228ac4e1305 100644 --- a/test/unit/gateways/merchant_e_solutions_test.rb +++ b/test/unit/gateways/merchant_e_solutions_test.rb @@ -7,17 +7,27 @@ def setup Base.mode = :test @gateway = MerchantESolutionsGateway.new( - :login => 'login', - :password => 'password' - ) + login: 'login', + password: 'password' + ) @credit_card = credit_card @amount = 100 @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase' + order_id: '1', + billing_address: address, + description: 'Store Purchase' + } + + @stored_credential_options = { + moto_ecommerce_ind: '7', + client_reference_number: '345892', + recurring_pmt_num: 11, + recurring_pmt_count: 10, + card_on_file: 'Y', + cit_mit_indicator: 'C101', + account_data_source: 'Y' } end @@ -30,6 +40,20 @@ def test_successful_purchase assert response.test? end + def test_successful_purchase_with_stored_credentials + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @stored_credential_options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/moto_ecommerce_ind=7/, data) + assert_match(/client_reference_number=345892/, data) + assert_match(/recurring_pmt_num=11/, data) + assert_match(/recurring_pmt_count=10/, data) + assert_match(/card_on_file=Y/, data) + assert_match(/cit_mit_indicator=C101/, data) + assert_match(/account_data_source=Y/, data) + end.respond_with(successful_purchase_response) + end + def test_unsuccessful_purchase @gateway.expects(:ssl_post).returns(failed_purchase_response) assert response = @gateway.purchase(@amount, @credit_card, @options) @@ -38,11 +62,11 @@ def test_unsuccessful_purchase end def test_purchase_with_long_order_id_truncates_id - options = {order_id: 'thisislongerthan17characters'} + options = { order_id: 'thisislongerthan17characters' } @gateway.expects(:ssl_post).with( anything, all_of( - includes('invoice_number=thisislongerthan1'), + includes('invoice_number=thisislongerthan1') ) ).returns(successful_purchase_response) assert response = @gateway.purchase(@amount, @credit_card, options) @@ -84,14 +108,6 @@ def test_void assert response.test? end - def test_store - @gateway.expects(:ssl_post).returns(successful_store_response) - assert response = @gateway.store(@credit_card) - assert response.success? - assert_equal 'ae641b57b19b3bb89faab44191479872', response.authorization - assert response.test? - end - def test_unstore @gateway.expects(:ssl_post).returns(successful_unstore_response) assert response = @gateway.unstore('ae641b57b19b3bb89faab44191479872') @@ -100,6 +116,18 @@ def test_unstore assert response.test? end + def test_successful_verify + response = stub_comms(@gateway, :ssl_request) do + @gateway.verify(@credit_card, { store_card: 'y' }) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/transaction_type=A/, data) + assert_match(/store_card=y/, data) + assert_match(/card_number=#{@credit_card.number}/, data) + end.respond_with(successful_verify_response) + assert_success response + assert_equal 'Card Ok', response.message + end + def test_successful_avs_check @gateway.expects(:ssl_post).returns(successful_purchase_response + '&avs_result=Y') assert response = @gateway.purchase(@amount, @credit_card, @options) @@ -122,7 +150,7 @@ def test_unsuccessful_avs_check_with_bad_zip @gateway.expects(:ssl_post).returns(successful_purchase_response + '&avs_result=A') assert response = @gateway.purchase(@amount, @credit_card, @options) assert_equal response.avs_result['code'], 'A' - assert_equal response.avs_result['message'], 'Street address matches, but 5-digit and 9-digit postal code do not match.' + assert_equal response.avs_result['message'], 'Street address matches, but postal code does not match.' assert_equal response.avs_result['street_match'], 'Y' assert_equal response.avs_result['postal_match'], 'N' end @@ -143,8 +171,8 @@ def test_unsuccessful_cvv_check def test_visa_3dsecure_params_submitted stub_comms(@gateway, :ssl_request) do - @gateway.purchase(@amount, @credit_card, @options.merge({:xid => '1', :cavv => '2'})) - end.check_request do |method, endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, @options.merge({ xid: '1', cavv: '2' })) + end.check_request do |_method, _endpoint, data, _headers| assert_match(/xid=1/, data) assert_match(/cavv=2/, data) end.respond_with(successful_purchase_response) @@ -152,8 +180,8 @@ def test_visa_3dsecure_params_submitted def test_mastercard_3dsecure_params_submitted stub_comms(@gateway, :ssl_request) do - @gateway.purchase(@amount, @credit_card, @options.merge({:ucaf_collection_ind => '1', :ucaf_auth_data => '2'})) - end.check_request do |method, endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, @options.merge({ ucaf_collection_ind: '1', ucaf_auth_data: '2' })) + end.check_request do |_method, _endpoint, data, _headers| assert_match(/ucaf_collection_ind=1/, data) assert_match(/ucaf_auth_data=2/, data) end.respond_with(successful_purchase_response) @@ -164,7 +192,7 @@ def test_supported_countries end def test_supported_card_types - assert_equal [:visa, :master, :american_express, :discover, :jcb], MerchantESolutionsGateway.supported_cardtypes + assert_equal %i[visa master american_express discover jcb], MerchantESolutionsGateway.supported_cardtypes end def test_scrub @@ -202,6 +230,10 @@ def successful_unstore_response 'transaction_id=d79410c91b4b31ba99f5a90558565df9&error_code=000&auth_response_text=Stored Card Data Deleted' end + def successful_verify_response + 'transaction_id=a5ef059bff7a3f75ac2398eea4cc73cd&error_code=085&auth_response_text=Card Ok&avs_result=0&cvv2_result=M&auth_code=T1933H' + end + def failed_purchase_response 'transaction_id=error&error_code=101&auth_response_text=Invalid%20I%20or%20Key%20Incomplete%20Request' end diff --git a/test/unit/gateways/merchant_one_test.rb b/test/unit/gateways/merchant_one_test.rb index 16e3b879473..78189ec91cb 100644 --- a/test/unit/gateways/merchant_one_test.rb +++ b/test/unit/gateways/merchant_one_test.rb @@ -1,23 +1,22 @@ require 'test_helper' class MerchantOneTest < Test::Unit::TestCase - def setup @gateway = MerchantOneGateway.new(fixtures(:merchant_one)) @credit_card = credit_card @amount = 1000 @options = { - :order_id => '1', - :description => 'Store Purchase', - :billing_address => { - :name =>'Jim Smith', - :address1 =>'1234 My Street', - :address2 =>'Apt 1', - :city =>'Tampa', - :state =>'FL', - :zip =>'33603', - :country =>'US', - :phone =>'(813)421-4331' + order_id: '1', + description: 'Store Purchase', + billing_address: { + name: 'Jim Smith', + address1: '1234 My Street', + address2: 'Apt 1', + city: 'Tampa', + state: 'FL', + zip: '33603', + country: 'US', + phone: '(813)421-4331' } } end diff --git a/test/unit/gateways/merchant_partners_test.rb b/test/unit/gateways/merchant_partners_test.rb index 77930e9830d..d86a9697e0e 100644 --- a/test/unit/gateways/merchant_partners_test.rb +++ b/test/unit/gateways/merchant_partners_test.rb @@ -19,7 +19,7 @@ def setup def test_successful_purchase response = stub_comms do @gateway.purchase(@amount, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_not_nil root = doc.at_xpath(@request_root) assert_equal @gateway.options[:account_id], root.at_xpath('//acctid').content @@ -51,7 +51,7 @@ def test_failed_purchase def test_successful_authorize_and_capture response = stub_comms do @gateway.authorize(@amount, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_not_nil root = doc.at_xpath(@request_root) assert_equal @gateway.options[:account_id], root.at_xpath('//acctid').content @@ -70,7 +70,7 @@ def test_successful_authorize_and_capture capture = stub_comms do @gateway.capture(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_not_nil root = doc.at_xpath(@request_root) assert_equal @gateway.options[:account_id], root.at_xpath('//acctid').content @@ -115,7 +115,7 @@ def test_successful_void void = stub_comms do @gateway.void(response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_not_nil root = doc.at_xpath(@request_root) assert_equal @gateway.options[:account_id], root.at_xpath('//acctid').content @@ -145,7 +145,7 @@ def test_successful_refund refund = stub_comms do @gateway.refund(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_not_nil root = doc.at_xpath(@request_root) assert_equal @gateway.options[:account_id], root.at_xpath('//acctid').content @@ -170,7 +170,7 @@ def test_failed_refund def test_successful_credit response = stub_comms do @gateway.credit(@amount, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_not_nil root = doc.at_xpath(@request_root) assert_equal @gateway.options[:account_id], root.at_xpath('//acctid').content @@ -217,7 +217,7 @@ def test_failed_verify def test_successful_store response = stub_comms do @gateway.store(@credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_not_nil root = doc.at_xpath(@request_root) assert_equal @gateway.options[:account_id], root.at_xpath('//acctid').content @@ -247,7 +247,7 @@ def test_successful_stored_purchase purchase = stub_comms do @gateway.purchase(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_not_nil root = doc.at_xpath(@request_root) assert_equal @gateway.options[:account_id], root.at_xpath('//acctid').content @@ -275,7 +275,7 @@ def test_successful_stored_credit credit = stub_comms do @gateway.credit(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| parse(data) do |doc| assert_not_nil root = doc.at_xpath(@request_root) assert_equal @gateway.options[:account_id], root.at_xpath('//acctid').content @@ -628,7 +628,6 @@ def failed_credit_response ) end - def successful_store_response %(<?xml version="1.0"?><interface_driver> <trans_catalog> diff --git a/test/unit/gateways/merchant_ware_test.rb b/test/unit/gateways/merchant_ware_test.rb index bfaf52f2462..a63e8259543 100644 --- a/test/unit/gateways/merchant_ware_test.rb +++ b/test/unit/gateways/merchant_ware_test.rb @@ -5,17 +5,17 @@ class MerchantWareTest < Test::Unit::TestCase def setup @gateway = MerchantWareGateway.new( - :login => 'login', - :password => 'password', - :name => 'name' - ) + login: 'login', + password: 'password', + name: 'name' + ) @credit_card = credit_card @amount = 100 @options = { - :order_id => '1', - :billing_address => address + order_id: '1', + billing_address: address } end @@ -32,8 +32,8 @@ def test_successful_authorization end def test_soap_fault_during_authorization - response_500 = stub(:code => '500', :message => 'Internal Server Error', :body => fault_authorization_response) - @gateway.expects(:ssl_post).raises(ActiveMerchant::ResponseError.new(response_500)) + response500 = stub(code: '500', message: 'Internal Server Error', body: fault_authorization_response) + @gateway.expects(:ssl_post).raises(ActiveMerchant::ResponseError.new(response500)) assert response = @gateway.authorize(@amount, @credit_card, @options) assert_instance_of Response, response @@ -42,8 +42,8 @@ def test_soap_fault_during_authorization assert_nil response.authorization assert_equal 'Server was unable to process request. ---> strPAN should be at least 13 to at most 19 characters in size. Parameter name: strPAN', response.message - assert_equal response_500.code, response.params['http_code'] - assert_equal response_500.message, response.params['http_message'] + assert_equal response500.code, response.params['http_code'] + assert_equal response500.message, response.params['http_message'] end def test_failed_authorization @@ -110,10 +110,10 @@ def test_cvv_result def test_add_swipe_data_with_creditcard @credit_card.track_data = 'Track Data' - options = {:order_id => '1'} + options = { order_id: '1' } stub_comms do @gateway.authorize(@amount, @credit_card, options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match '<trackData>Track Data</trackData>', data end.respond_with(successful_authorization_response) end @@ -121,100 +121,99 @@ def test_add_swipe_data_with_creditcard private def successful_authorization_response - <<-XML -<?xml version="1.0" encoding="utf-8"?> -<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <soap:Body> - <IssueKeyedPreAuthResponse xmlns="http://merchantwarehouse.com/MerchantWARE/Client/TransactionRetail"> - <IssueKeyedPreAuthResult> - <ReferenceID>4706382</ReferenceID> - <OrderNumber>1</OrderNumber> - <TXDate>7/3/2009 2:05:04 AM</TXDate> - <ApprovalStatus>APPROVED</ApprovalStatus> - <AuthCode>VI0100</AuthCode> - <CardHolder>Longbob Longsen</CardHolder> - <Amount>1.00</Amount> - <Type>5</Type> - <CardNumber>************4242</CardNumber> - <CardType>0</CardType> - <AVSResponse>N</AVSResponse> - <CVResponse>M</CVResponse> - <POSEntryType>1</POSEntryType> - </IssueKeyedPreAuthResult> - </IssueKeyedPreAuthResponse> - </soap:Body> -</soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?> + <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> + <soap:Body> + <IssueKeyedPreAuthResponse xmlns="http://merchantwarehouse.com/MerchantWARE/Client/TransactionRetail"> + <IssueKeyedPreAuthResult> + <ReferenceID>4706382</ReferenceID> + <OrderNumber>1</OrderNumber> + <TXDate>7/3/2009 2:05:04 AM</TXDate> + <ApprovalStatus>APPROVED</ApprovalStatus> + <AuthCode>VI0100</AuthCode> + <CardHolder>Longbob Longsen</CardHolder> + <Amount>1.00</Amount> + <Type>5</Type> + <CardNumber>************4242</CardNumber> + <CardType>0</CardType> + <AVSResponse>N</AVSResponse> + <CVResponse>M</CVResponse> + <POSEntryType>1</POSEntryType> + </IssueKeyedPreAuthResult> + </IssueKeyedPreAuthResponse> + </soap:Body> + </soap:Envelope> XML end def fault_authorization_response - <<-XML -<?xml version="1.0" encoding="utf-8"?> -<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <soap:Body> - <soap:Fault> - <faultcode>soap:Server</faultcode> - <faultstring>Server was unable to process request. ---&gt; strPAN should be at least 13 to at most 19 characters in size. -Parameter name: strPAN</faultstring> - <detail/> - </soap:Fault> - </soap:Body> -</soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?> + <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> + <soap:Body> + <soap:Fault> + <faultcode>soap:Server</faultcode> + <faultstring>Server was unable to process request. ---&gt; strPAN should be at least 13 to at most 19 characters in size. + Parameter name: strPAN</faultstring> + <detail/> + </soap:Fault> + </soap:Body> + </soap:Envelope> XML end def failed_authorization_response - <<-XML -<?xml version="1.0" encoding="utf-8"?> -<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <soap:Body> - <IssueKeyedPreAuthResponse xmlns="http://merchantwarehouse.com/MerchantWARE/Client/TransactionRetail"> - <IssueKeyedPreAuthResult> - <ReferenceID/> - <OrderNumber>1</OrderNumber> - <TXDate>7/3/2009 3:04:33 AM</TXDate> - <ApprovalStatus>FAILED;1014;transaction type not supported by version</ApprovalStatus> - <AuthCode/> - <CardHolder>Longbob Longsen</CardHolder> - <Amount>1.00</Amount> - <Type>5</Type> - <CardNumber>*********0123</CardNumber> - <CardType>0</CardType> - <AVSResponse/> - <CVResponse/> - <POSEntryType>1</POSEntryType> - </IssueKeyedPreAuthResult> - </IssueKeyedPreAuthResponse> - </soap:Body> -</soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?> + <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> + <soap:Body> + <IssueKeyedPreAuthResponse xmlns="http://merchantwarehouse.com/MerchantWARE/Client/TransactionRetail"> + <IssueKeyedPreAuthResult> + <ReferenceID/> + <OrderNumber>1</OrderNumber> + <TXDate>7/3/2009 3:04:33 AM</TXDate> + <ApprovalStatus>FAILED;1014;transaction type not supported by version</ApprovalStatus> + <AuthCode/> + <CardHolder>Longbob Longsen</CardHolder> + <Amount>1.00</Amount> + <Type>5</Type> + <CardNumber>*********0123</CardNumber> + <CardType>0</CardType> + <AVSResponse/> + <CVResponse/> + <POSEntryType>1</POSEntryType> + </IssueKeyedPreAuthResult> + </IssueKeyedPreAuthResponse> + </soap:Body> + </soap:Envelope> XML end def failed_void_response - <<-XML -<?xml version="1.0" encoding="utf-8"?> -<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <soap:Body> - <VoidPreAuthorizationResponse xmlns="http://merchantwarehouse.com/MerchantWARE/Client/TransactionRetail"> - <VoidPreAuthorizationResult> - <ReferenceID>4707277</ReferenceID> - <OrderNumber/> - <TXDate>7/3/2009 3:28:38 AM</TXDate> - <ApprovalStatus>DECLINED;1012;decline</ApprovalStatus> - <AuthCode/> - <CardHolder/> - <Amount/> - <Type>3</Type> - <CardNumber/> - <CardType>0</CardType> - <AVSResponse/> - <CVResponse/> - <POSEntryType>0</POSEntryType> - </VoidPreAuthorizationResult> - </VoidPreAuthorizationResponse> - </soap:Body> -</soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?> + <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> + <soap:Body> + <VoidPreAuthorizationResponse xmlns="http://merchantwarehouse.com/MerchantWARE/Client/TransactionRetail"> + <VoidPreAuthorizationResult> + <ReferenceID>4707277</ReferenceID> + <OrderNumber/> + <TXDate>7/3/2009 3:28:38 AM</TXDate> + <ApprovalStatus>DECLINED;1012;decline</ApprovalStatus> + <AuthCode/> + <CardHolder/> + <Amount/> + <Type>3</Type> + <CardNumber/> + <CardType>0</CardType> + <AVSResponse/> + <CVResponse/> + <POSEntryType>0</POSEntryType> + </VoidPreAuthorizationResult> + </VoidPreAuthorizationResponse> + </soap:Body> + </soap:Envelope> XML end - end diff --git a/test/unit/gateways/merchant_ware_version_four_test.rb b/test/unit/gateways/merchant_ware_version_four_test.rb index 4119f8f63d9..b51f07a34f7 100644 --- a/test/unit/gateways/merchant_ware_version_four_test.rb +++ b/test/unit/gateways/merchant_ware_version_four_test.rb @@ -3,18 +3,18 @@ class MerchantWareVersionFourTest < Test::Unit::TestCase def setup @gateway = MerchantWareVersionFourGateway.new( - :login => 'login', - :password => 'password', - :name => 'name' - ) + login: 'login', + password: 'password', + name: 'name' + ) @credit_card = credit_card @authorization = '1236564' @amount = 100 @options = { - :order_id => '1', - :billing_address => address + order_id: '1', + billing_address: address } end @@ -31,8 +31,8 @@ def test_successful_authorization end def test_soap_fault_during_authorization - response_400 = stub(:code => '400', :message => 'Bad Request', :body => failed_authorize_response) - @gateway.expects(:ssl_post).raises(ActiveMerchant::ResponseError.new(response_400)) + response400 = stub(code: '400', message: 'Bad Request', body: failed_authorize_response) + @gateway.expects(:ssl_post).raises(ActiveMerchant::ResponseError.new(response400)) assert response = @gateway.authorize(@amount, @credit_card, @options) assert_instance_of Response, response @@ -41,8 +41,8 @@ def test_soap_fault_during_authorization assert_nil response.authorization assert_equal 'amount cannot be null. Parameter name: amount', response.message - assert_equal response_400.code, response.params['http_code'] - assert_equal response_400.message, response.params['http_message'] + assert_equal response400.code, response.params['http_code'] + assert_equal response400.message, response.params['http_message'] end def test_failed_authorization @@ -161,212 +161,212 @@ def post_scrubbed end def successful_authorize_response - <<-XML -<?xml version="1.0" encoding="utf-8"?> -<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/"> - <soap:Body> - <PreAuthorizationKeyedResponse xmlns="http://schemas.merchantwarehouse.com/merchantware/40/Credit/"> - <PreAuthorizationKeyedResult> - <Amount>1.00</Amount> - <ApprovalStatus>APPROVED</ApprovalStatus> - <AuthorizationCode>MC0110</AuthorizationCode> - <AvsResponse>N</AvsResponse> - <Cardholder></Cardholder> - <CardNumber></CardNumber> - <CardType>0</CardType> - <CvResponse>M</CvResponse> - <EntryMode>0</EntryMode> - <ErrorMessage></ErrorMessage> - <ExtraData></ExtraData> - <InvoiceNumber></InvoiceNumber> - <Token>1236564</Token> - <TransactionDate>10/10/2008 1:13:55 PM</TransactionDate> - <TransactionType>7</TransactionType> - </PreAuthorizationKeyedResult> - </PreAuthorizationKeyedResponse> - </soap:Body> -</soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?> + <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/"> + <soap:Body> + <PreAuthorizationKeyedResponse xmlns="http://schemas.merchantwarehouse.com/merchantware/40/Credit/"> + <PreAuthorizationKeyedResult> + <Amount>1.00</Amount> + <ApprovalStatus>APPROVED</ApprovalStatus> + <AuthorizationCode>MC0110</AuthorizationCode> + <AvsResponse>N</AvsResponse> + <Cardholder></Cardholder> + <CardNumber></CardNumber> + <CardType>0</CardType> + <CvResponse>M</CvResponse> + <EntryMode>0</EntryMode> + <ErrorMessage></ErrorMessage> + <ExtraData></ExtraData> + <InvoiceNumber></InvoiceNumber> + <Token>1236564</Token> + <TransactionDate>10/10/2008 1:13:55 PM</TransactionDate> + <TransactionType>7</TransactionType> + </PreAuthorizationKeyedResult> + </PreAuthorizationKeyedResponse> + </soap:Body> + </soap:Envelope> XML end def failed_authorize_response - <<-XML -<?xml version="1.0" encoding="utf-8"?> -<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <soap:Body> - <PreAuthorizationKeyedResponse xmlns="http://schemas.merchantwarehouse.com/merchantware/40/Credit/"> - <PreAuthorizationKeyedResult> - <Amount /> - <ApprovalStatus /> - <AuthorizationCode /> - <AvsResponse /> - <Cardholder /> - <CardNumber /> - <CardType>0</CardType> - <CvResponse /> - <EntryMode>0</EntryMode> - <ErrorMessage>amount cannot be null. Parameter name: amount</ErrorMessage> - <ExtraData /> - <InvoiceNumber /> - <Token /> - <TransactionDate /> - <TransactionType>0</TransactionType> - </PreAuthorizationKeyedResult> - </PreAuthorizationKeyedResponse> - </soap:Body> -</soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?> + <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:xsd="http://www.w3.org/2001/XMLSchema"> + <soap:Body> + <PreAuthorizationKeyedResponse xmlns="http://schemas.merchantwarehouse.com/merchantware/40/Credit/"> + <PreAuthorizationKeyedResult> + <Amount /> + <ApprovalStatus /> + <AuthorizationCode /> + <AvsResponse /> + <Cardholder /> + <CardNumber /> + <CardType>0</CardType> + <CvResponse /> + <EntryMode>0</EntryMode> + <ErrorMessage>amount cannot be null. Parameter name: amount</ErrorMessage> + <ExtraData /> + <InvoiceNumber /> + <Token /> + <TransactionDate /> + <TransactionType>0</TransactionType> + </PreAuthorizationKeyedResult> + </PreAuthorizationKeyedResponse> + </soap:Body> + </soap:Envelope> XML end def failed_authorization_response - <<-XML -<?xml version="1.0" encoding="utf-8"?> -<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <soap:Body> - <PreAuthorizationKeyedResponse xmlns="http://schemas.merchantwarehouse.com/merchantware/40/Credit/"> - <PreAuthorizationKeyedResult> - <Amount>1.00</Amount> - <ApprovalStatus>DECLINED;1024;invalid exp date</ApprovalStatus> - <AuthorizationCode /> - <AvsResponse /> - <Cardholder>Visa Test Card</Cardholder> - <CardNumber>************0019</CardNumber> - <CardType>4</CardType> - <CvResponse /> - <EntryMode>1</EntryMode> - <ErrorMessage /> - <ExtraData /> - <InvoiceNumber>TT0017</InvoiceNumber> - <Token /> - <TransactionDate>5/15/2013 8:47:14 AM</TransactionDate> - <TransactionType>5</TransactionType> - </PreAuthorizationKeyedResult> - </PreAuthorizationKeyedResponse> - </soap:Body> -</soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?> + <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> + <soap:Body> + <PreAuthorizationKeyedResponse xmlns="http://schemas.merchantwarehouse.com/merchantware/40/Credit/"> + <PreAuthorizationKeyedResult> + <Amount>1.00</Amount> + <ApprovalStatus>DECLINED;1024;invalid exp date</ApprovalStatus> + <AuthorizationCode /> + <AvsResponse /> + <Cardholder>Visa Test Card</Cardholder> + <CardNumber>************0019</CardNumber> + <CardType>4</CardType> + <CvResponse /> + <EntryMode>1</EntryMode> + <ErrorMessage /> + <ExtraData /> + <InvoiceNumber>TT0017</InvoiceNumber> + <Token /> + <TransactionDate>5/15/2013 8:47:14 AM</TransactionDate> + <TransactionType>5</TransactionType> + </PreAuthorizationKeyedResult> + </PreAuthorizationKeyedResponse> + </soap:Body> + </soap:Envelope> XML end def successful_void_response - <<-XML -<?xml version="1.0" encoding="utf-8"?> -<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <soap:Body> - <VoidResponse xmlns="http://schemas.merchantwarehouse.com/merchantware/40/Credit/"> - <VoidResult> - <Amount /> - <ApprovalStatus>APPROVED</ApprovalStatus> - <AuthorizationCode>VOID</AuthorizationCode> - <AvsResponse /> - <Cardholder /> - <CardNumber /> - <CardType>0</CardType> - <CvResponse /> - <EntryMode>0</EntryMode> - <ErrorMessage /> - <ExtraData /> - <InvoiceNumber /> - <Token>266783537</Token> - <TransactionDate>7/9/2015 3:13:51 PM</TransactionDate> - <TransactionType>3</TransactionType> - </VoidResult> - </VoidResponse> - </soap:Body> -</soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?> + <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> + <soap:Body> + <VoidResponse xmlns="http://schemas.merchantwarehouse.com/merchantware/40/Credit/"> + <VoidResult> + <Amount /> + <ApprovalStatus>APPROVED</ApprovalStatus> + <AuthorizationCode>VOID</AuthorizationCode> + <AvsResponse /> + <Cardholder /> + <CardNumber /> + <CardType>0</CardType> + <CvResponse /> + <EntryMode>0</EntryMode> + <ErrorMessage /> + <ExtraData /> + <InvoiceNumber /> + <Token>266783537</Token> + <TransactionDate>7/9/2015 3:13:51 PM</TransactionDate> + <TransactionType>3</TransactionType> + </VoidResult> + </VoidResponse> + </soap:Body> + </soap:Envelope> XML end def failed_void_response - <<-XML -<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <soap:Body> - <VoidResponse xmlns="http://schemas.merchantwarehouse.com/merchantware/40/Credit/"> - <VoidResult> - <Amount /> - <ApprovalStatus>DECLINED;1019;original transaction id not found</ApprovalStatus> - <AuthorizationCode /> - <AvsResponse /> - <Cardholder /> - <CardNumber /> - <CardType>0</CardType> - <CvResponse /> - <EntryMode>0</EntryMode> - <ErrorMessage /> - <ExtraData /> - <InvoiceNumber /> - <Token /> - <TransactionDate>5/15/2013 9:37:04 AM</TransactionDate> - <TransactionType>3</TransactionType> - </VoidResult> - </VoidResponse> - </soap:Body> -</soap:Envelope> + <<~XML + <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> + <soap:Body> + <VoidResponse xmlns="http://schemas.merchantwarehouse.com/merchantware/40/Credit/"> + <VoidResult> + <Amount /> + <ApprovalStatus>DECLINED;1019;original transaction id not found</ApprovalStatus> + <AuthorizationCode /> + <AvsResponse /> + <Cardholder /> + <CardNumber /> + <CardType>0</CardType> + <CvResponse /> + <EntryMode>0</EntryMode> + <ErrorMessage /> + <ExtraData /> + <InvoiceNumber /> + <Token /> + <TransactionDate>5/15/2013 9:37:04 AM</TransactionDate> + <TransactionType>3</TransactionType> + </VoidResult> + </VoidResponse> + </soap:Body> + </soap:Envelope> XML end def successful_purchase_using_prior_transaction_response - <<-XML -<?xml version="1.0" encoding="utf-8"?> -<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/"> - <soap:Body> - <RepeatSaleResponse xmlns="http://schemas.merchantwarehouse.com/merchantware/40/Credit/"> - <RepeatSaleResult> - <Amount>5.00</Amount> - <ApprovalStatus>APPROVED</ApprovalStatus> - <AuthorizationCode>MC0110</AuthorizationCode> - <AvsResponse></AvsResponse> - <Cardholder></Cardholder> - <CardNumber></CardNumber> - <CardType>0</CardType> - <CvResponse></CvResponse> - <EntryMode>0</EntryMode> - <ErrorMessage></ErrorMessage> - <ExtraData></ExtraData> - <InvoiceNumber></InvoiceNumber> - <Token>1236564</Token> - <TransactionDate>10/10/2008 1:13:55 PM</TransactionDate> - <TransactionType>7</TransactionType> - </RepeatSaleResult> - </RepeatSaleResponse> - </soap:Body> -</soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?> + <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/"> + <soap:Body> + <RepeatSaleResponse xmlns="http://schemas.merchantwarehouse.com/merchantware/40/Credit/"> + <RepeatSaleResult> + <Amount>5.00</Amount> + <ApprovalStatus>APPROVED</ApprovalStatus> + <AuthorizationCode>MC0110</AuthorizationCode> + <AvsResponse></AvsResponse> + <Cardholder></Cardholder> + <CardNumber></CardNumber> + <CardType>0</CardType> + <CvResponse></CvResponse> + <EntryMode>0</EntryMode> + <ErrorMessage></ErrorMessage> + <ExtraData></ExtraData> + <InvoiceNumber></InvoiceNumber> + <Token>1236564</Token> + <TransactionDate>10/10/2008 1:13:55 PM</TransactionDate> + <TransactionType>7</TransactionType> + </RepeatSaleResult> + </RepeatSaleResponse> + </soap:Body> + </soap:Envelope> XML end def invalid_credit_card_number_response - <<-XML -<?xml version="1.0" encoding="utf-8"?> -<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <soap:Body> - <PreAuthorizationKeyedResponse xmlns="http://schemas.merchantwarehouse.com/merchantware/40/Credit/"> - <PreAuthorizationKeyedResult> - <Amount /> - <ApprovalStatus /> - <AuthorizationCode /> - <AvsResponse /> - <Cardholder /> - <CardNumber /> - <CardType>0</CardType> - <CvResponse /> - <EntryMode>0</EntryMode> - <ErrorMessage>Invalid card number.</ErrorMessage> - <ExtraData /> - <InvoiceNumber /> - <Token /> - <TransactionDate /> - <TransactionType>0</TransactionType> - </PreAuthorizationKeyedResult> - </PreAuthorizationKeyedResponse> - </soap:Body> -</soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?> + <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> + <soap:Body> + <PreAuthorizationKeyedResponse xmlns="http://schemas.merchantwarehouse.com/merchantware/40/Credit/"> + <PreAuthorizationKeyedResult> + <Amount /> + <ApprovalStatus /> + <AuthorizationCode /> + <AvsResponse /> + <Cardholder /> + <CardNumber /> + <CardType>0</CardType> + <CvResponse /> + <EntryMode>0</EntryMode> + <ErrorMessage>Invalid card number.</ErrorMessage> + <ExtraData /> + <InvoiceNumber /> + <Token /> + <TransactionDate /> + <TransactionType>0</TransactionType> + </PreAuthorizationKeyedResult> + </PreAuthorizationKeyedResponse> + </soap:Body> + </soap:Envelope> XML end end diff --git a/test/unit/gateways/merchant_warrior_test.rb b/test/unit/gateways/merchant_warrior_test.rb index 8eb0e1e85f2..0dd24550ed8 100644 --- a/test/unit/gateways/merchant_warrior_test.rb +++ b/test/unit/gateways/merchant_warrior_test.rb @@ -5,10 +5,10 @@ class MerchantWarriorTest < Test::Unit::TestCase def setup @gateway = MerchantWarriorGateway.new( - :merchant_uuid => '4e922de8c2a4c', - :api_key => 'g6jrxa9o', - :api_passphrase => 'vp4ujoem' - ) + merchant_uuid: '4e922de8c2a4c', + api_key: 'g6jrxa9o', + api_passphrase: 'vp4ujoem' + ) @credit_card = credit_card @success_amount = 10000 @@ -16,11 +16,41 @@ def setup @failure_amount = 10033 @options = { - :address => address, - :transaction_product => 'TestProduct' + address:, + transaction_product: 'TestProduct', + email: 'user@aol.com', + ip: '1.2.3.4', + store_id: 'My Store' + } + @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 + @gateway.expects(:ssl_post).returns(successful_authorize_response) + + assert response = @gateway.authorize(@success_amount, @credit_card, @options) + assert_success response + assert_equal 'Transaction approved', response.message + assert response.test? + assert_equal '1336-20be3569-b600-11e6-b9c3-005056b209e0', response.authorization + end + + def test_failed_authorize + @gateway.expects(:ssl_post).returns(nil) + + assert response = @gateway.authorize(@success_amount, @credit_card, @options) + assert_failure response + assert_equal 'Invalid gateway response', response.message + assert response.test? + end + def test_successful_purchase @gateway.expects(:ssl_post).returns(successful_purchase_response) @@ -61,13 +91,33 @@ def test_failed_refund assert_nil response.authorization end + def test_successful_void + @gateway.expects(:ssl_post).returns(successful_refund_response) + + assert response = @gateway.void(@transaction_id, amount: @success_amount) + assert_success response + assert_equal 'Transaction approved', response.message + assert response.test? + assert_equal '30-d4d19f4-db17-11df-9322-0022198101cd', response.authorization + end + + def test_failed_void + @gateway.expects(:ssl_post).returns(failed_refund_response) + + assert response = @gateway.void(@transaction_id, amount: @success_amount) + assert_failure response + assert_equal 'MW -016:transactionID has already been reversed', response.message + assert response.test? + assert_nil response.authorization + end + def test_successful_store @credit_card.month = '2' @credit_card.year = '2005' store = stub_comms do @gateway.store(@credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/cardExpiryMonth=02\b/, data) assert_match(/cardExpiryYear=05\b/, data) end.respond_with(successful_store_response) @@ -84,7 +134,7 @@ def test_scrub_name stub_comms do @gateway.purchase(@success_amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/customerName=Ren\+\+Stimpy/, data) assert_match(/paymentCardName=Chars\+Merchant-Warrior\+Dont\+Like\+\+More\.\+\+Here/, data) end.respond_with(successful_purchase_response) @@ -98,14 +148,45 @@ def test_address state: 'NY', country: 'US', zip: '11111', - phone: '555-1212', - email: 'user@aol.com', - ip: '1.2.3.4' + phone: '555-1212' } stub_comms do @gateway.purchase(@success_amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| + assert_match(/customerName=Bat\+Man/, data) + assert_match(/customerCountry=US/, data) + assert_match(/customerState=NY/, data) + assert_match(/customerCity=Brooklyn/, data) + assert_match(/customerAddress=123\+Main/, data) + assert_match(/customerPostCode=11111/, data) + assert_match(/customerIP=1.2.3.4/, data) + assert_match(/customerPhone=555-1212/, data) + assert_match(/customerEmail=user%40aol.com/, data) + assert_match(/storeID=My\+Store/, data) + end.respond_with(successful_purchase_response) + end + + def test_address_with_phone_number + options = { + address: { + name: 'Bat Man', + address1: '123 Main', + city: 'Brooklyn', + state: 'NY', + country: 'US', + zip: '11111', + phone_number: '555-1212' + }, + transaction_product: 'TestProduct', + email: 'user@aol.com', + ip: '1.2.3.4', + store_id: 'My Store' + } + + stub_comms do + @gateway.purchase(@success_amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| assert_match(/customerName=Bat\+Man/, data) assert_match(/customerCountry=US/, data) assert_match(/customerState=NY/, data) @@ -115,6 +196,7 @@ def test_address assert_match(/customerIP=1.2.3.4/, data) assert_match(/customerPhone=555-1212/, data) assert_match(/customerEmail=user%40aol.com/, data) + assert_match(/storeID=My\+Store/, data) end.respond_with(successful_purchase_response) end @@ -126,7 +208,7 @@ def test_address_without_state stub_comms do @gateway.purchase(@success_amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/customerState=N%2FA/, data) end.respond_with(successful_purchase_response) end @@ -134,99 +216,293 @@ def test_address_without_state def test_orderid_truncated stub_comms do @gateway.purchase(@success_amount, @credit_card, order_id: 'ThisIsQuiteALongDescriptionWithLotsOfChars') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/transactionProduct=ThisIsQuiteALongDescriptionWithLot&/, data) end.respond_with(successful_purchase_response) end + def test_authorize_recurring_flag_absent + stub_comms do + @gateway.authorize(@success_amount, @credit_card) + end.check_request do |_endpoint, data, _headers| + assert_not_match(/recurringFlag&/, data) + end.respond_with(successful_authorize_response) + end + + def test_authorize_recurring_flag_present + recurring_flag = 1 + + stub_comms do + @gateway.authorize(@success_amount, @credit_card, recurring_flag:) + end.check_request do |_endpoint, data, _headers| + assert_match(/recurringFlag=#{recurring_flag}&/, data) + end.respond_with(successful_authorize_response) + end + + def test_purchase_recurring_flag_absent + stub_comms do + @gateway.purchase(@success_amount, @credit_card) + end.check_request do |_endpoint, data, _headers| + assert_not_match(/recurringFlag&/, data) + end.respond_with(successful_purchase_response) + end + + def test_purchase_recurring_flag_present + recurring_flag = 1 + + stub_comms do + @gateway.purchase(@success_amount, @credit_card, recurring_flag:) + end.check_request do |_endpoint, data, _headers| + assert_match(/recurringFlag=#{recurring_flag}&/, data) + end.respond_with(successful_purchase_response) + end + + def test_authorize_with_soft_descriptor_absent + stub_comms do + @gateway.authorize(@success_amount, @credit_card) + end.check_request do |_endpoint, data, _headers| + assert_not_match(/descriptorName&/, data) + assert_not_match(/descriptorCity&/, data) + assert_not_match(/descriptorState&/, data) + end.respond_with(successful_authorize_response) + end + + def test_authorize_with_soft_descriptor_present + stub_comms do + @gateway.authorize(@success_amount, @credit_card, soft_descriptor_options) + end.check_request do |_endpoint, data, _headers| + assert_match(/descriptorName=FOO%2ATest&/, data) + assert_match(/descriptorCity=Melbourne&/, data) + assert_match(/descriptorState=VIC&/, data) + end.respond_with(successful_authorize_response) + end + + def test_purchase_with_soft_descriptor_absent + stub_comms do + @gateway.purchase(@success_amount, @credit_card) + end.check_request do |_endpoint, data, _headers| + assert_not_match(/descriptorName&/, data) + assert_not_match(/descriptorCity&/, data) + assert_not_match(/descriptorState&/, data) + end.respond_with(successful_purchase_response) + end + + def test_purchase_with_soft_descriptor_present + stub_comms do + @gateway.purchase(@success_amount, @credit_card, soft_descriptor_options) + end.check_request do |_endpoint, data, _headers| + assert_match(/descriptorName=FOO%2ATest&/, data) + assert_match(/descriptorCity=Melbourne&/, data) + assert_match(/descriptorState=VIC&/, data) + end.respond_with(successful_purchase_response) + end + + def test_capture_with_soft_descriptor_absent + stub_comms do + @gateway.capture(@success_amount, @credit_card) + end.check_request do |_endpoint, data, _headers| + assert_not_match(/descriptorName&/, data) + assert_not_match(/descriptorCity&/, data) + assert_not_match(/descriptorState&/, data) + end.respond_with(successful_capture_response) + end + + def test_capture_with_soft_descriptor_present + stub_comms do + @gateway.capture(@success_amount, @credit_card, soft_descriptor_options) + end.check_request do |_endpoint, data, _headers| + assert_match(/descriptorName=FOO%2ATest&/, data) + assert_match(/descriptorCity=Melbourne&/, data) + assert_match(/descriptorState=VIC&/, data) + end.respond_with(successful_capture_response) + end + + def test_refund_with_soft_descriptor_absent + stub_comms do + @gateway.refund(@success_amount, @credit_card) + end.check_request do |_endpoint, data, _headers| + assert_not_match(/descriptorName&/, data) + assert_not_match(/descriptorCity&/, data) + assert_not_match(/descriptorState&/, data) + end.respond_with(successful_refund_response) + end + + def test_refund_with_soft_descriptor_present + stub_comms do + @gateway.refund(@success_amount, @credit_card, soft_descriptor_options) + end.check_request do |_endpoint, data, _headers| + assert_match(/descriptorName=FOO%2ATest&/, data) + assert_match(/descriptorCity=Melbourne&/, data) + assert_match(/descriptorState=VIC&/, data) + end.respond_with(successful_refund_response) + end + def test_scrub assert @gateway.supports_scrubbing? 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 - <<-XML -<?xml version="1.0" encoding="UTF-8"?> -<mwResponse> - <responseCode>0</responseCode> - <responseMessage>Transaction approved</responseMessage> - <transactionID>30-98a79008-dae8-11df-9322-0022198101cd</transactionID> - <authCode>44639</authCode> - <authMessage>Approved</authMessage> - <authResponseCode>0</authResponseCode> - <authSettledDate>2010-10-19</authSettledDate> - <custom1></custom1> - <custom2></custom2> - <custom3></custom3> - <customHash>c0aca5a0d9573322c79cc323d6cc8050</customHash> -</mwResponse> + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <mwResponse> + <responseCode>0</responseCode> + <responseMessage>Transaction approved</responseMessage> + <transactionID>30-98a79008-dae8-11df-9322-0022198101cd</transactionID> + <authCode>44639</authCode> + <authMessage>Approved</authMessage> + <authResponseCode>0</authResponseCode> + <authSettledDate>2010-10-19</authSettledDate> + <custom1></custom1> + <custom2></custom2> + <custom3></custom3> + <customHash>c0aca5a0d9573322c79cc323d6cc8050</customHash> + </mwResponse> XML end def failed_purchase_response - <<-XML -<?xml version="1.0" encoding="UTF-8"?> -<mwResponse> - <responseCode>4</responseCode> - <responseMessage>Card has expired</responseMessage> - <transactionID>30-69433444-af1-11df-9322-0022198101cd</transactionID> - <authCode>44657</authCode> - <authMessage>Expired+Card</authMessage> - <authResponseCode>4</authResponseCode> - <authSettledDate>2010-10-19</authSettledDate> - <custom1></custom1> - <custom2></custom2> - <custom3></custom3> - <customHash>c0aca5a0d9573322c79cc323d6cc8050</customHash> -</mwResponse> + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <mwResponse> + <responseCode>4</responseCode> + <responseMessage>Card has expired</responseMessage> + <transactionID>30-69433444-af1-11df-9322-0022198101cd</transactionID> + <authCode>44657</authCode> + <authMessage>Expired+Card</authMessage> + <authResponseCode>4</authResponseCode> + <authSettledDate>2010-10-19</authSettledDate> + <custom1></custom1> + <custom2></custom2> + <custom3></custom3> + <customHash>c0aca5a0d9573322c79cc323d6cc8050</customHash> + </mwResponse> XML end def successful_refund_response - <<-XML -<?xml version="1.0" encoding="UTF-8"?> -<mwResponse> - <responseCode>0</responseCode> - <responseMessage>Transaction approved</responseMessage> - <transactionID>30-d4d19f4-db17-11df-9322-0022198101cd</transactionID> - <authCode>44751</authCode> - <authMessage>Approved</authMessage> - <authResponseCode>0</authResponseCode> - <authSettledDate>2010-10-19</authSettledDate> -</mwResponse> + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <mwResponse> + <responseCode>0</responseCode> + <responseMessage>Transaction approved</responseMessage> + <transactionID>30-d4d19f4-db17-11df-9322-0022198101cd</transactionID> + <authCode>44751</authCode> + <authMessage>Approved</authMessage> + <authResponseCode>0</authResponseCode> + <authSettledDate>2010-10-19</authSettledDate> + </mwResponse> XML end def failed_refund_response - <<-XML -<?xml version="1.0" encoding="UTF-8"?> - <mwResponse> - <responseCode>-2</responseCode> - <responseMessage>MW -016:transactionID has already been reversed</responseMessage> -</mwResponse> + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <mwResponse> + <responseCode>-2</responseCode> + <responseMessage>MW -016:transactionID has already been reversed</responseMessage> + </mwResponse> XML end def successful_store_response - <<-XML -<?xml version="1.0" encoding="UTF-8"?> -<mwResponse> - <responseCode>0</responseCode> - <responseMessage>Operation successful</responseMessage> - <cardID>KOCI10023982</cardID> - <cardKey>s5KQIxsZuiyvs3Sc</cardKey> - <ivrCardID>10023982</ivrCardID> -</mwResponse> + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <mwResponse> + <responseCode>0</responseCode> + <responseMessage>Operation successful</responseMessage> + <cardID>KOCI10023982</cardID> + <cardKey>s5KQIxsZuiyvs3Sc</cardKey> + <ivrCardID>10023982</ivrCardID> + </mwResponse> + XML + end + + def successful_authorize_response + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <mwResponse> + <responseCode>0</responseCode> + <responseMessage>Transaction approved</responseMessage> + <transactionID>1336-20be3569-b600-11e6-b9c3-005056b209e0</transactionID> + <transactionReferenceID>12345</transactionReferenceID> + <authCode>731357421</authCode> + <receiptNo>731357421</receiptNo> + <authMessage>Honour with identification</authMessage> + <authResponseCode>08</authResponseCode> + <authSettledDate>2016-11-29</authSettledDate> + <paymentCardNumber>512345XXXXXX2346</paymentCardNumber> + <transactionAmount>1.00</transactionAmount> + <cardType>mc</cardType> + <cardExpiryMonth>05</cardExpiryMonth> + <cardExpiryYear>21</cardExpiryYear> + <custom1/> + <custom2/> + <custom3/> + <customHash>65b172551b7d3a0706c0ce5330c98470</customHash> + </mwResponse> + XML + end + + def successful_capture_response + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <mwResponse> + <responseCode>0</responseCode> + <responseMessage>Transaction approved</responseMessage> + <transactionID>1336-fe4d3be6-b604-11e6-b9c3-005056b209e0</transactionID> + <authCode>731357526</authCode> + <receiptNo>731357526</receiptNo> + <authMessage>Approved or completed successfully</authMessage> + <authResponseCode>00</authResponseCode> + <authSettledDate>2016-11-30</authSettledDate> + </mwResponse> XML end def pre_scrubbed - %q(transactionAmount=1.00&transactionCurrency=AUD&hash=adb50f6ff360f861e6f525e8daae76b5&transactionProduct=98fc25d40a47f3d24da460c0ca307c&customerName=Longbob+Longsen&customerCountry=AU&customerState=Queensland&customerCity=Brisbane&customerAddress=123+test+st&customerPostCode=4000&customerIP=&customerPhone=&customerEmail=&paymentCardNumber=5123456789012346&paymentCardName=Longbob+Longsen&paymentCardExpiry=0520&paymentCardCSC=123&merchantUUID=51f7da294af8f&apiKey=nooudtd0&method=processCard) + 'transactionAmount=1.00&transactionCurrency=AUD&hash=adb50f6ff360f861e6f525e8daae76b5&transactionProduct=98fc25d40a47f3d24da460c0ca307c&customerName=Longbob+Longsen&customerCountry=AU&customerState=Queensland&customerCity=Brisbane&customerAddress=123+test+st&customerPostCode=4000&customerIP=&customerPhone=&customerEmail=&paymentCardNumber=5123456789012346&paymentCardName=Longbob+Longsen&paymentCardExpiry=0520&paymentCardCSC=123&merchantUUID=51f7da294af8f&apiKey=nooudtd0&method=processCard' end def post_scrubbed - %q(transactionAmount=1.00&transactionCurrency=AUD&hash=adb50f6ff360f861e6f525e8daae76b5&transactionProduct=98fc25d40a47f3d24da460c0ca307c&customerName=Longbob+Longsen&customerCountry=AU&customerState=Queensland&customerCity=Brisbane&customerAddress=123+test+st&customerPostCode=4000&customerIP=&customerPhone=&customerEmail=&paymentCardNumber=[FILTERED]&paymentCardName=Longbob+Longsen&paymentCardExpiry=0520&paymentCardCSC=[FILTERED]&merchantUUID=51f7da294af8f&apiKey=[FILTERED]&method=processCard) + 'transactionAmount=1.00&transactionCurrency=AUD&hash=adb50f6ff360f861e6f525e8daae76b5&transactionProduct=98fc25d40a47f3d24da460c0ca307c&customerName=Longbob+Longsen&customerCountry=AU&customerState=Queensland&customerCity=Brisbane&customerAddress=123+test+st&customerPostCode=4000&customerIP=&customerPhone=&customerEmail=&paymentCardNumber=[FILTERED]&paymentCardName=Longbob+Longsen&paymentCardExpiry=0520&paymentCardCSC=[FILTERED]&merchantUUID=51f7da294af8f&apiKey=[FILTERED]&method=processCard' + end + + def soft_descriptor_options + { + descriptor_name: 'FOO*Test', + descriptor_city: 'Melbourne', + descriptor_state: 'VIC' + } end end diff --git a/test/unit/gateways/mercury_test.rb b/test/unit/gateways/mercury_test.rb index e7d298f52b4..9dd62ea082b 100644 --- a/test/unit/gateways/mercury_test.rb +++ b/test/unit/gateways/mercury_test.rb @@ -9,18 +9,18 @@ def setup @gateway = MercuryGateway.new(fixtures(:mercury)) @amount = 100 - @credit_card = credit_card('5499990123456781', :brand => 'master') + @credit_card = credit_card('5499990123456781', brand: 'master') @declined_card = credit_card('4000300011112220') @options = { - :order_id => 'c111111111.1' + order_id: 'c111111111.1' } end def test_successful_purchase response = stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/InvoiceNo>c111111111.1</, data) assert_match(/Frequency>OneTime/, data) assert_match(/RecordNo>RecordNumberRequested/, data) @@ -36,7 +36,7 @@ def test_successful_purchase def test_successful_purchase_with_allow_partial_auth response = stub_comms do @gateway.purchase(@amount, @credit_card, @options.merge(allow_partial_auth: true)) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/PartialAuth>Allow</, data) end.respond_with(successful_purchase_response) @@ -68,7 +68,7 @@ def test_card_present_with_track_1_data @credit_card.track_data = track_data response = stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/<Track1>#{Regexp.escape(track_data)}<\/Track1>/, data) end.respond_with(successful_purchase_response) @@ -82,7 +82,7 @@ def test_card_present_with_track_2_data @credit_card.track_data = track_data response = stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/<Track2>#{Regexp.escape(stripped_track_data)}<\/Track2>/, data) end.respond_with(successful_purchase_response) @@ -92,11 +92,11 @@ def test_card_present_with_track_2_data def test_card_present_with_max_length_track_1_data track_data = '%B373953192351004^CARDUSER/JOHN^200910100000019301000000877000000930001234567?' - stripped_data = 'B373953192351004^CARDUSER/JOHN^200910100000019301000000877000000930001234567' + stripped_data = 'B373953192351004^CARDUSER/JOHN^200910100000019301000000877000000930001234567' @credit_card.track_data = track_data response = stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/<Track1>#{Regexp.escape(stripped_data)}<\/Track1>/, data) end.respond_with(successful_purchase_response) @@ -109,7 +109,7 @@ def test_card_present_with_invalid_data @credit_card.track_data = track_data response = stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/<Track1>#{Regexp.escape(track_data)}<\/Track1>/, data) end.respond_with(successful_purchase_response) @@ -125,88 +125,57 @@ def test_transcript_scrubbing private def successful_purchase_response - <<-RESPONSE -<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><CreditTransactionResponse xmlns="http://www.mercurypay.com"><CreditTransactionResult><?xml version="1.0"?> -<RStream> - <CmdResponse> - <ResponseOrigin>Processor</ResponseOrigin> - <DSIXReturnCode>000000</DSIXReturnCode> - <CmdStatus>Approved</CmdStatus> - <TextResponse>AP*</TextResponse> - <UserTraceData></UserTraceData> - </CmdResponse> - <TranResponse> - <MerchantID>595901</MerchantID> - <AcctNo>5499990123456781</AcctNo> - <ExpDate>0813</ExpDate> - <CardType>M/C</CardType> - <TranCode>Sale</TranCode> - <AuthCode>000011</AuthCode> - <CaptureStatus>Captured</CaptureStatus> - <RefNo>0194</RefNo> - <InvoiceNo>1</InvoiceNo> - <AVSResult>Y</AVSResult> - <CVVResult>M</CVVResult> - <OperatorID>999</OperatorID> - <Memo>LM Integration (Ruby)</Memo> - <Amount> - <Purchase>1.00</Purchase> - <Authorize>1.00</Authorize> - </Amount> - <AcqRefData>KbMCC0742510421 </AcqRefData> - <ProcessData>|17|410100700000</ProcessData> - </TranResponse> -</RStream> -</CreditTransactionResult></CreditTransactionResponse></soap:Body></soap:Envelope> + <<~RESPONSE + <?xml version=\"1.0\" encoding=\"utf-8\"?><soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"><soap:Body><CreditTransactionResponse xmlns=\"http://www.mercurypay.com\"><CreditTransactionResult><?xml version=\"1.0\"?>\r\n<RStream>\r\n\t<CmdResponse>\r\n\t\t<ResponseOrigin>Processor</ResponseOrigin>\r\n\t\t<DSIXReturnCode>000000</DSIXReturnCode>\r\n\t\t<CmdStatus>Approved</CmdStatus>\r\n\t\t<TextResponse>AP</TextResponse>\r\n\t\t<UserTraceData></UserTraceData>\r\n\t</CmdResponse>\r\n\t<TranResponse>\r\n\t\t<MerchantID>595901</MerchantID>\r\n\t\t<AcctNo>5499990123456781</AcctNo>\r\n\t\t<ExpDate>0813</ExpDate>\r\n\t\t<CardType>M/C</CardType>\r\n\t\t<TranCode>Sale</TranCode>\r\n\t\t<AuthCode>000011</AuthCode>\r\n\t\t<CaptureStatus>Captured</CaptureStatus>\r\n\t\t<RefNo>0194</RefNo>\r\n\t\t<InvoiceNo>1</InvoiceNo>\r\n\t\t<AVSResult>Y</AVSResult>\r\n\t\t<CVVResult>M</CVVResult>\r\n\t\t<OperatorID>999</OperatorID>\r\n\t\t<Memo>LM Integration (Ruby)</Memo>\r\n\t\t<Amount>\r\n\t\t\t<Purchase>1.00</Purchase>\r\n\t\t\t<Authorize>1.00</Authorize>\r\n\t\t</Amount>\r\n\t\t<AcqRefData>KbMCC0742510421 </AcqRefData>\r\n\t\t<ProcessData>|17|410100700000</ProcessData>\r\n\t</TranResponse>\r\n</RStream>\r\n</CreditTransactionResult></CreditTransactionResponse></soap:Body></soap:Envelope> RESPONSE end def failed_purchase_response - <<-RESPONSE -<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><CreditTransactionResponse xmlns="http://www.mercurypay.com"><CreditTransactionResult><?xml version="1.0"?> -<RStream> - <CmdResponse> - <ResponseOrigin>Server</ResponseOrigin> - <DSIXReturnCode>000000</DSIXReturnCode> - <CmdStatus>Error</CmdStatus> - <TextResponse>No Live Cards on Test Merchant ID Allowed.</TextResponse> - <UserTraceData></UserTraceData> - </CmdResponse> -</RStream> -</CreditTransactionResult></CreditTransactionResponse></soap:Body></soap:Envelope> + <<~RESPONSE + <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><CreditTransactionResponse xmlns="http://www.mercurypay.com"><CreditTransactionResult><?xml version="1.0"?> + <RStream> + <CmdResponse> + <ResponseOrigin>Server</ResponseOrigin> + <DSIXReturnCode>000000</DSIXReturnCode> + <CmdStatus>Error</CmdStatus> + <TextResponse>No Live Cards on Test Merchant ID Allowed.</TextResponse> + <UserTraceData></UserTraceData> + </CmdResponse> + </RStream> + </CreditTransactionResult></CreditTransactionResponse></soap:Body></soap:Envelope> RESPONSE end def successful_refund_response - <<-RESPONSE -<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><CreditTransactionResponse xmlns="http://www.mercurypay.com"><CreditTransactionResult><?xml version="1.0"?> -<RStream> - <CmdResponse> - <ResponseOrigin>Processor</ResponseOrigin> - <DSIXReturnCode>000000</DSIXReturnCode> - <CmdStatus>Approved</CmdStatus> - <TextResponse>AP</TextResponse> - <UserTraceData></UserTraceData> - </CmdResponse> - <TranResponse> - <MerchantID>595901</MerchantID> - <AcctNo>5499990123456781</AcctNo> - <ExpDate>0813</ExpDate> - <CardType>M/C</CardType> - <TranCode>VoidSale</TranCode> - <AuthCode>VOIDED</AuthCode> - <CaptureStatus>Captured</CaptureStatus> - <RefNo>0568</RefNo> - <InvoiceNo>123</InvoiceNo> - <OperatorID>999</OperatorID> - <Amount> - <Purchase>1.00</Purchase> - <Authorize>1.00</Authorize> - </Amount> - <AcqRefData>K</AcqRefData> - </TranResponse> -</RStream> -</CreditTransactionResult></CreditTransactionResponse></soap:Body></soap:Envelope> + <<~RESPONSE + <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><CreditTransactionResponse xmlns="http://www.mercurypay.com"><CreditTransactionResult><?xml version="1.0"?> + <RStream> + <CmdResponse> + <ResponseOrigin>Processor</ResponseOrigin> + <DSIXReturnCode>000000</DSIXReturnCode> + <CmdStatus>Approved</CmdStatus> + <TextResponse>AP</TextResponse> + <UserTraceData></UserTraceData> + </CmdResponse> + <TranResponse> + <MerchantID>595901</MerchantID> + <AcctNo>5499990123456781</AcctNo> + <ExpDate>0813</ExpDate> + <CardType>M/C</CardType> + <TranCode>VoidSale</TranCode> + <AuthCode>VOIDED</AuthCode> + <CaptureStatus>Captured</CaptureStatus> + <RefNo>0568</RefNo> + <InvoiceNo>123</InvoiceNo> + <OperatorID>999</OperatorID> + <Amount> + <Purchase>1.00</Purchase> + <Authorize>1.00</Authorize> + </Amount> + <AcqRefData>K</AcqRefData> + </TranResponse> + </RStream> + </CreditTransactionResult></CreditTransactionResponse></soap:Body></soap:Envelope> RESPONSE end diff --git a/test/unit/gateways/metrics_global_test.rb b/test/unit/gateways/metrics_global_test.rb index e16afd5dd8f..b8c2d7072bc 100644 --- a/test/unit/gateways/metrics_global_test.rb +++ b/test/unit/gateways/metrics_global_test.rb @@ -1,15 +1,16 @@ require 'test_helper' +require 'active_support/core_ext/kernel/singleton_class' class MetricsGlobalTest < Test::Unit::TestCase include CommStub def setup @gateway = ActiveMerchant::Billing::MetricsGlobalGateway.new( - :login => 'X', - :password => 'Y' + login: 'X', + password: 'Y' ) @amount = 100 - @credit_card = credit_card('4111111111111111', :verification_value => '999') + @credit_card = credit_card('4111111111111111', verification_value: '999') @subscription_id = '100748' @subscription_status = 'active' end @@ -44,9 +45,9 @@ def test_failed_authorization def test_add_address_outsite_north_america result = {} - @gateway.send(:add_address, result, :billing_address => {:address1 => '164 Waverley Street', :country => 'DE', :state => ''} ) + @gateway.send(:add_address, result, billing_address: { address1: '164 Waverley Street', country: 'DE', state: '' }) - assert_equal ['address', 'city', 'company', 'country', 'phone', 'state', 'zip'], result.stringify_keys.keys.sort + assert_equal %w[address city company country phone state zip], result.stringify_keys.keys.sort assert_equal 'n/a', result[:state] assert_equal '164 Waverley Street', result[:address] assert_equal 'DE', result[:country] @@ -55,24 +56,23 @@ def test_add_address_outsite_north_america def test_add_address result = {} - @gateway.send(:add_address, result, :billing_address => {:address1 => '164 Waverley Street', :country => 'US', :state => 'CO'} ) + @gateway.send(:add_address, result, billing_address: { address1: '164 Waverley Street', country: 'US', state: 'CO' }) - assert_equal ['address', 'city', 'company', 'country', 'phone', 'state', 'zip'], result.stringify_keys.keys.sort + assert_equal %w[address city company country phone state zip], result.stringify_keys.keys.sort assert_equal 'CO', result[:state] assert_equal '164 Waverley Street', result[:address] assert_equal 'US', result[:country] - end def test_add_invoice result = {} - @gateway.send(:add_invoice, result, :order_id => '#1001') + @gateway.send(:add_invoice, result, order_id: '#1001') assert_equal '#1001', result[:invoice_num] end def test_add_description result = {} - @gateway.send(:add_invoice, result, :description => 'My Purchase is great') + @gateway.send(:add_invoice, result, description: 'My Purchase is great') assert_equal 'My Purchase is great', result[:description] end @@ -91,17 +91,17 @@ def test_add_duplicate_window_with_duplicate_window end def test_purchase_is_valid_csv - params = { :amount => '1.01' } + params = { amount: '1.01' } - @gateway.send(:add_creditcard, params, @credit_card) + @gateway.send(:add_creditcard, params, @credit_card) - assert data = @gateway.send(:post_data, 'AUTH_ONLY', params) - assert_equal post_data_fixture.size, data.size + assert data = @gateway.send(:post_data, 'AUTH_ONLY', params) + assert_equal post_data_fixture.size, data.size end def test_purchase_meets_minimum_requirements params = { - :amount => '1.01', + amount: '1.01' } @gateway.send(:add_creditcard, params, @credit_card) @@ -114,15 +114,15 @@ def test_purchase_meets_minimum_requirements def test_successful_refund @gateway.expects(:ssl_post).returns(successful_purchase_response) - assert response = @gateway.refund(@amount, '123456789', :card_number => @credit_card.number) + assert response = @gateway.refund(@amount, '123456789', card_number: @credit_card.number) assert_success response assert_equal 'This transaction has been approved', response.message 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') - end.check_request do |endpoint, data, headers| + @gateway.refund(50, '123456789', card_number: @credit_card.number, first_name: 'Bob', last_name: 'Smith', zip: '12345') + end.check_request do |_endpoint, data, _headers| assert_match(/x_first_name=Bob/, data) assert_match(/x_last_name=Smith/, data) assert_match(/x_zip=12345/, data) @@ -133,7 +133,7 @@ def test_refund_passing_extra_info def test_failed_refund @gateway.expects(:ssl_post).returns(failed_refund_response) - assert response = @gateway.refund(@amount, '123456789', :card_number => @credit_card.number) + assert response = @gateway.refund(@amount, '123456789', card_number: @credit_card.number) assert_failure response assert_equal 'The referenced transaction does not meet the criteria for issuing a credit', response.message end @@ -141,7 +141,7 @@ def test_failed_refund def test_deprecated_credit @gateway.expects(:ssl_post).returns(successful_purchase_response) assert_deprecation_warning(Gateway::CREDIT_DEPRECATION_MESSAGE) do - assert response = @gateway.credit(@amount, '123456789', :card_number => @credit_card.number) + assert response = @gateway.credit(@amount, '123456789', card_number: @credit_card.number) assert_success response assert_equal 'This transaction has been approved', response.message end @@ -152,7 +152,7 @@ def test_supported_countries end def test_supported_card_types - assert_equal [:visa, :master, :american_express, :discover, :diners_club, :jcb], MetricsGlobalGateway.supported_cardtypes + assert_equal %i[visa master american_express discover diners_club jcb], MetricsGlobalGateway.supported_cardtypes end def test_failure_without_response_reason_text @@ -189,16 +189,16 @@ def test_message_from public :message_from } result = { - :response_code => 2, - :card_code => 'N', - :avs_result_code => 'A', - :response_reason_code => '27', - :response_reason_text => 'Failure.', + response_code: 2, + card_code: 'N', + avs_result_code: 'A', + response_reason_code: '27', + response_reason_text: 'Failure.' } assert_equal 'CVV does not match', @gateway.message_from(result) result[:card_code] = 'M' - assert_equal 'Street address matches, but 5-digit and 9-digit postal code do not match.', @gateway.message_from(result) + assert_equal 'Street address matches, but postal code does not match.', @gateway.message_from(result) result[:response_reason_code] = '22' assert_equal 'Failure', @gateway.message_from(result) diff --git a/test/unit/gateways/micropayment_test.rb b/test/unit/gateways/micropayment_test.rb index 5e63d8f0fa3..ca773b448ec 100644 --- a/test/unit/gateways/micropayment_test.rb +++ b/test/unit/gateways/micropayment_test.rb @@ -15,7 +15,7 @@ def setup def test_successful_purchase response = stub_comms do @gateway.purchase(@amount, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/accessKey=key/, data) assert_match(/number=#{@credit_card.number}/, data) assert_match(/cvc2=#{@credit_card.verification_value}/, data) @@ -40,7 +40,7 @@ def test_failed_purchase def test_successful_authorize_and_capture response = stub_comms do @gateway.authorize(@amount, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/accessKey=key/, data) assert_match(/number=#{@credit_card.number}/, data) assert_match(/cvc2=#{@credit_card.verification_value}/, data) @@ -52,7 +52,7 @@ def test_successful_authorize_and_capture capture = stub_comms do @gateway.capture(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/accessKey=key/, data) assert_match(/transactionId=www.spreedly.com-IDhngtaj81a1/, data) assert_match(/sessionId=CC747358d9598614c3ba1e9a7b82a28318cd81bc/, data) @@ -89,7 +89,7 @@ def test_successful_void void = stub_comms do @gateway.void(response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/accessKey=key/, data) assert_match(/transactionId=www.spreedly.com-IDhngtaj81a1/, data) assert_match(/sessionId=CC747358d9598614c3ba1e9a7b82a28318cd81bc/, data) @@ -115,7 +115,7 @@ def test_successful_refund refund = stub_comms do @gateway.refund(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/accessKey=key/, data) assert_match(/transactionId=www.spreedly.com-IDhm7nyju168/, data) assert_match(/sessionId=CCadc2b593ca98bfd730c383582de00faed995b0/, data) diff --git a/test/unit/gateways/migs_test.rb b/test/unit/gateways/migs_test.rb index 807a7def5b0..8f5eb3dbe6e 100644 --- a/test/unit/gateways/migs_test.rb +++ b/test/unit/gateways/migs_test.rb @@ -3,18 +3,21 @@ class MigsTest < Test::Unit::TestCase def setup @gateway = MigsGateway.new( - :login => 'login', - :password => 'password', - :secure_hash => '76AF3392002D202A60D0AB5F9D81653C' - ) - + login: 'login', + password: 'password', + secure_hash: '76AF3392002D202A60D0AB5F9D81653C', + advanced_login: 'advlogin', + advanced_password: 'advpass' + ) @credit_card = credit_card @amount = 100 + @authorization = '2070000742' + @tx_source = 'MOTO' @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase' + order_id: '1', + billing_address: address, + description: 'Store Purchase' } end @@ -41,9 +44,9 @@ def test_unsuccessful_request def test_secure_hash params = { - :MerchantId => 'MER123', - :OrderInfo => 'A48cvE28', - :Amount => 2995 + MerchantId: 'MER123', + OrderInfo: 'A48cvE28', + Amount: 2995 } ordered_values = 'vpc_Amount=2995&vpc_MerchantId=MER123&vpc_OrderInfo=A48cvE28' @gateway.send(:add_secure_hash, params) @@ -63,10 +66,10 @@ def test_purchase_offsite_response assert_success response tampered_response1 = response_params.gsub('20DE', '20DF') - assert_raise(SecurityError){@gateway.purchase_offsite_response(tampered_response1)} + assert_raise(SecurityError) { @gateway.purchase_offsite_response(tampered_response1) } tampered_response2 = response_params.gsub('Locale=en', 'Locale=es') - assert_raise(SecurityError){@gateway.purchase_offsite_response(tampered_response2)} + assert_raise(SecurityError) { @gateway.purchase_offsite_response(tampered_response2) } end def test_scrub @@ -74,77 +77,101 @@ def test_scrub assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed end + def test_purchase_passes_tx_source + expect_commit_with_tx_source + @gateway.purchase(@amount, @credit_card, @options.merge(tx_source: @tx_source)) + end + + def test_capture_passes_tx_source + expect_commit_with_tx_source + @gateway.capture(@amount, @authorization, @options.merge(tx_source: @tx_source)) + end + + def test_refund_passes_tx_source + expect_commit_with_tx_source + @gateway.refund(@amount, @authorization, @options.merge(tx_source: @tx_source)) + end + + def test_void_passes_tx_source + expect_commit_with_tx_source + @gateway.void(@authorization, @options.merge(tx_source: @tx_source)) + end + private # Place raw successful response from gateway here def successful_purchase_response build_response( - :TxnResponseCode => '0', - :TransactionNo => '123456' + TxnResponseCode: '0', + TransactionNo: '123456' ) end # Place raw failed response from gateway here def failed_purchase_response build_response( - :TxnResponseCode => '3', - :TransactionNo => '654321' + TxnResponseCode: '3', + TransactionNo: '654321' ) end def build_response(options) - options.collect { |key, value| "vpc_#{key}=#{CGI.escape(value.to_s)}"}.join('&') + options.collect { |key, value| "vpc_#{key}=#{CGI.escape(value.to_s)}" }.join('&') + end + + def expect_commit_with_tx_source + @gateway.expects(:commit).with(has_entries(TxSource: @tx_source)) end def pre_scrubbed - <<-EOS -opening connection to migs.mastercard.com.au:443... -opened -starting SSL for migs.mastercard.com.au:443... -SSL established -<- "POST /vpcdps 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: migs.mastercard.com.au\r\nContent-Length: 354\r\n\r\n" -<- "vpc_Amount=100&vpc_Currency=SAR&vpc_OrderInfo=1&vpc_CardNum=4987654321098769&vpc_CardSecurityCode=123&vpc_CardExp=2105&vpc_Version=1&vpc_Merchant=TESTH-STATION&vpc_AccessCode=F1CE6F32&vpc_Command=pay&vpc_MerchTxnRef=84c1f31ded35dea26ac297fd7ba092da&vpc_SecureHash=CD1B2B8BC325C6C8FC1A041AD6AC90821984277113DF708B16B37809E7B0EC33&vpc_SecureHashType=SHA256&vpc_VerType=3DS&vpc_3DSXID=YzRjZWRjODY4MmY2NGQ3ZTgzNDQ&vpc_VerToken=AAACAgeVABgnAggAQ5UAAAAAAAA&vpc_3DSenrolled=Y&vpc_3DSECI=05&3DSstatus=Y" --> "HTTP/1.1 200 OK\r\n" --> "Date: Tue, 13 Feb 2018 19:02:18 GMT\r\n" --> "Expires: Sun, 15 Jul 1990 00:00:00 GMT\r\n" --> "Pragma: no-cache\r\n" --> "Cache-Control: no-cache\r\n" --> "Content-Length: 595\r\n" --> "P3P: CP=\"NOI DSP COR CURa ADMa TA1a OUR BUS IND UNI COM NAV INT\"\r\n" --> "Content-Type: text/plain;charset=iso-8859-1\r\n" --> "Connection: close\r\n" --> "Set-Cookie: TS01c4b9ca=01fb8d8de2ba6ffaf7439497dd78d9b3348c82bcf24d4619e65a406161e57276b6b293e77732a293be63bf750213e588797bc86f05; Path=/; Secure; HTTPOnly\r\n" --> "\r\n" -reading 595 bytes... --> "vpc_AVSResultCode=Unsupported&vpc_AcqAVSRespCode=Unsupported&vpc_AcqCSCRespCode=Unsupported&vpc_AcqResponseCode=00&vpc_Amount=100&vpc_AuthorizeId=239491&vpc_BatchNo=20180214&vpc_CSCResultCode=Unsupported&vpc_Card=VC&vpc_Command=pay&vpc_Currency=SAR&vpc_Locale=en_SA&vpc_MerchTxnRef=84c1f31ded35dea26ac297fd7ba092da&vpc_Merchant=TESTH-STATION&vpc_Message=Approved&vpc_OrderInfo=1&vpc_ReceiptNo=804506239491&vpc_RiskOverallResult=ACC&vpc_SecureHash=99993E000461810D9F71B1A4FC5EA2D68DF6BA1F7EBA6A9FC544DA035627C03C&vpc_SecureHashType=SHA256&vpc_TransactionNo=372&vpc_TxnResponseCode=0&vpc_Version=1&vpc_VerType=3DS&vpc_3DSXID=YzRjZWRjODY4MmY2NGQ3ZTgzNDQ&vpc_VerToken=AAACAgeVABgnAggAQ5UAAAAAAAA&vpc_3DSenrolled=Y&vpc_3DSECI=05&3DSstatus=Y" -read 595 bytes -Conn close - EOS + <<~REQUEST + opening connection to migs.mastercard.com.au:443... + opened + starting SSL for migs.mastercard.com.au:443... + SSL established + <- "POST /vpcdps 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: migs.mastercard.com.au\r\nContent-Length: 354\r\n\r\n" + <- "vpc_Amount=100&vpc_Currency=SAR&vpc_OrderInfo=1&vpc_CardNum=4987654321098769&vpc_CardSecurityCode=123&vpc_CardExp=2105&vpc_Version=1&vpc_Merchant=TESTH-STATION&vpc_AccessCode=F1CE6F32&vpc_Command=pay&vpc_MerchTxnRef=84c1f31ded35dea26ac297fd7ba092da&vpc_SecureHash=CD1B2B8BC325C6C8FC1A041AD6AC90821984277113DF708B16B37809E7B0EC33&vpc_SecureHashType=SHA256&vpc_VerType=3DS&vpc_3DSXID=YzRjZWRjODY4MmY2NGQ3ZTgzNDQ&vpc_VerToken=AAACAgeVABgnAggAQ5UAAAAAAAA&vpc_3DSenrolled=Y&vpc_3DSECI=05&3DSstatus=Y" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Tue, 13 Feb 2018 19:02:18 GMT\r\n" + -> "Expires: Sun, 15 Jul 1990 00:00:00 GMT\r\n" + -> "Pragma: no-cache\r\n" + -> "Cache-Control: no-cache\r\n" + -> "Content-Length: 595\r\n" + -> "P3P: CP=\"NOI DSP COR CURa ADMa TA1a OUR BUS IND UNI COM NAV INT\"\r\n" + -> "Content-Type: text/plain;charset=iso-8859-1\r\n" + -> "Connection: close\r\n" + -> "Set-Cookie: TS01c4b9ca=01fb8d8de2ba6ffaf7439497dd78d9b3348c82bcf24d4619e65a406161e57276b6b293e77732a293be63bf750213e588797bc86f05; Path=/; Secure; HTTPOnly\r\n" + -> "\r\n" + reading 595 bytes... + -> "vpc_AVSResultCode=Unsupported&vpc_AcqAVSRespCode=Unsupported&vpc_AcqCSCRespCode=Unsupported&vpc_AcqResponseCode=00&vpc_Amount=100&vpc_AuthorizeId=239491&vpc_BatchNo=20180214&vpc_CSCResultCode=Unsupported&vpc_Card=VC&vpc_Command=pay&vpc_Currency=SAR&vpc_Locale=en_SA&vpc_MerchTxnRef=84c1f31ded35dea26ac297fd7ba092da&vpc_Merchant=TESTH-STATION&vpc_Message=Approved&vpc_OrderInfo=1&vpc_ReceiptNo=804506239491&vpc_RiskOverallResult=ACC&vpc_SecureHash=99993E000461810D9F71B1A4FC5EA2D68DF6BA1F7EBA6A9FC544DA035627C03C&vpc_SecureHashType=SHA256&vpc_TransactionNo=372&vpc_TxnResponseCode=0&vpc_Version=1&vpc_VerType=3DS&vpc_3DSXID=YzRjZWRjODY4MmY2NGQ3ZTgzNDQ&vpc_VerToken=AAACAgeVABgnAggAQ5UAAAAAAAA&vpc_3DSenrolled=Y&vpc_3DSECI=05&3DSstatus=Y" + read 595 bytes + Conn close + REQUEST end def post_scrubbed - <<-EOS -opening connection to migs.mastercard.com.au:443... -opened -starting SSL for migs.mastercard.com.au:443... -SSL established -<- "POST /vpcdps 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: migs.mastercard.com.au\r\nContent-Length: 354\r\n\r\n" -<- "vpc_Amount=100&vpc_Currency=SAR&vpc_OrderInfo=1&vpc_CardNum=[FILTERED]&vpc_CardSecurityCode=[FILTERED]&vpc_CardExp=2105&vpc_Version=1&vpc_Merchant=TESTH-STATION&vpc_AccessCode=[FILTERED]&vpc_Command=pay&vpc_MerchTxnRef=84c1f31ded35dea26ac297fd7ba092da&vpc_SecureHash=CD1B2B8BC325C6C8FC1A041AD6AC90821984277113DF708B16B37809E7B0EC33&vpc_SecureHashType=SHA256&vpc_VerType=3DS&vpc_3DSXID=[FILTERED]&vpc_VerToken=[FILTERED]&vpc_3DSenrolled=Y&vpc_3DSECI=05&3DSstatus=Y" --> "HTTP/1.1 200 OK\r\n" --> "Date: Tue, 13 Feb 2018 19:02:18 GMT\r\n" --> "Expires: Sun, 15 Jul 1990 00:00:00 GMT\r\n" --> "Pragma: no-cache\r\n" --> "Cache-Control: no-cache\r\n" --> "Content-Length: 595\r\n" --> "P3P: CP=\"NOI DSP COR CURa ADMa TA1a OUR BUS IND UNI COM NAV INT\"\r\n" --> "Content-Type: text/plain;charset=iso-8859-1\r\n" --> "Connection: close\r\n" --> "Set-Cookie: TS01c4b9ca=01fb8d8de2ba6ffaf7439497dd78d9b3348c82bcf24d4619e65a406161e57276b6b293e77732a293be63bf750213e588797bc86f05; Path=/; Secure; HTTPOnly\r\n" --> "\r\n" -reading 595 bytes... --> "vpc_AVSResultCode=Unsupported&vpc_AcqAVSRespCode=Unsupported&vpc_AcqCSCRespCode=Unsupported&vpc_AcqResponseCode=00&vpc_Amount=100&vpc_AuthorizeId=239491&vpc_BatchNo=20180214&vpc_CSCResultCode=Unsupported&vpc_Card=VC&vpc_Command=pay&vpc_Currency=SAR&vpc_Locale=en_SA&vpc_MerchTxnRef=84c1f31ded35dea26ac297fd7ba092da&vpc_Merchant=TESTH-STATION&vpc_Message=Approved&vpc_OrderInfo=1&vpc_ReceiptNo=804506239491&vpc_RiskOverallResult=ACC&vpc_SecureHash=99993E000461810D9F71B1A4FC5EA2D68DF6BA1F7EBA6A9FC544DA035627C03C&vpc_SecureHashType=SHA256&vpc_TransactionNo=372&vpc_TxnResponseCode=0&vpc_Version=1&vpc_VerType=3DS&vpc_3DSXID=[FILTERED]&vpc_VerToken=[FILTERED]&vpc_3DSenrolled=Y&vpc_3DSECI=05&3DSstatus=Y" -read 595 bytes -Conn close - EOS + <<~REQUEST + opening connection to migs.mastercard.com.au:443... + opened + starting SSL for migs.mastercard.com.au:443... + SSL established + <- "POST /vpcdps 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: migs.mastercard.com.au\r\nContent-Length: 354\r\n\r\n" + <- "vpc_Amount=100&vpc_Currency=SAR&vpc_OrderInfo=1&vpc_CardNum=[FILTERED]&vpc_CardSecurityCode=[FILTERED]&vpc_CardExp=2105&vpc_Version=1&vpc_Merchant=TESTH-STATION&vpc_AccessCode=[FILTERED]&vpc_Command=pay&vpc_MerchTxnRef=84c1f31ded35dea26ac297fd7ba092da&vpc_SecureHash=CD1B2B8BC325C6C8FC1A041AD6AC90821984277113DF708B16B37809E7B0EC33&vpc_SecureHashType=SHA256&vpc_VerType=3DS&vpc_3DSXID=[FILTERED]&vpc_VerToken=[FILTERED]&vpc_3DSenrolled=Y&vpc_3DSECI=05&3DSstatus=Y" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Tue, 13 Feb 2018 19:02:18 GMT\r\n" + -> "Expires: Sun, 15 Jul 1990 00:00:00 GMT\r\n" + -> "Pragma: no-cache\r\n" + -> "Cache-Control: no-cache\r\n" + -> "Content-Length: 595\r\n" + -> "P3P: CP=\"NOI DSP COR CURa ADMa TA1a OUR BUS IND UNI COM NAV INT\"\r\n" + -> "Content-Type: text/plain;charset=iso-8859-1\r\n" + -> "Connection: close\r\n" + -> "Set-Cookie: TS01c4b9ca=01fb8d8de2ba6ffaf7439497dd78d9b3348c82bcf24d4619e65a406161e57276b6b293e77732a293be63bf750213e588797bc86f05; Path=/; Secure; HTTPOnly\r\n" + -> "\r\n" + reading 595 bytes... + -> "vpc_AVSResultCode=Unsupported&vpc_AcqAVSRespCode=Unsupported&vpc_AcqCSCRespCode=Unsupported&vpc_AcqResponseCode=00&vpc_Amount=100&vpc_AuthorizeId=239491&vpc_BatchNo=20180214&vpc_CSCResultCode=Unsupported&vpc_Card=VC&vpc_Command=pay&vpc_Currency=SAR&vpc_Locale=en_SA&vpc_MerchTxnRef=84c1f31ded35dea26ac297fd7ba092da&vpc_Merchant=TESTH-STATION&vpc_Message=Approved&vpc_OrderInfo=1&vpc_ReceiptNo=804506239491&vpc_RiskOverallResult=ACC&vpc_SecureHash=99993E000461810D9F71B1A4FC5EA2D68DF6BA1F7EBA6A9FC544DA035627C03C&vpc_SecureHashType=SHA256&vpc_TransactionNo=372&vpc_TxnResponseCode=0&vpc_Version=1&vpc_VerType=3DS&vpc_3DSXID=[FILTERED]&vpc_VerToken=[FILTERED]&vpc_3DSenrolled=Y&vpc_3DSECI=05&3DSstatus=Y" + read 595 bytes + Conn close + REQUEST end end diff --git a/test/unit/gateways/mit_test.rb b/test/unit/gateways/mit_test.rb new file mode 100644 index 00000000000..4ae7b4932be --- /dev/null +++ b/test/unit/gateways/mit_test.rb @@ -0,0 +1,268 @@ +require 'test_helper' + +class MitTest < Test::Unit::TestCase + def setup + @credentials = { + commerce_id: '147', + user: 'IVCA33721', + api_key: 'IGECPJ0QOJJCEHUI', + key_session: 'CB0DC4887DD1D5CEA205E66EE934E430' + } + @gateway = MitGateway.new(@credentials) + + @credit_card = ActiveMerchant::Billing::CreditCard.new( + number: '4000000000000002', + verification_value: '183', + month: '01', + year: '2024', + first_name: 'Pedro', + last_name: 'Flores Valdes' + ) + + @amount = 100 + + @options = { + order_id: '7111', + transaction_id: '7111', + billing_address: address, + description: 'Store Purchase' + } + end + + def test_successful_purchase + @gateway.expects(:ssl_post).returns(successful_authorize_response) + auth_response = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth_response + assert_equal 'approved', auth_response.message + + @gateway.expects(:ssl_post).returns(successful_capture_response) + response = @gateway.capture(@amount, @credit_card, @options) + assert_success response + + assert_equal 'approved', response.message + assert response.test? + end + + def test_failed_purchase + @gateway.expects(:ssl_post).returns(failed_capture_response) + response = @gateway.capture(@amount, @credit_card, @options) + + assert_not_equal 'approved', response.message + assert response.test? + 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 response.test? + end + + def test_failed_authorize + @gateway.expects(:ssl_post).returns(failed_authorize_response) + response = @gateway.authorize(@amount, @credit_card, @options) + + assert_not_equal 'approved', response.message + assert response.test? + end + + def test_successful_capture + @gateway.expects(:ssl_post).returns(successful_capture_response) + response = @gateway.capture(@amount, @credit_card, @options) + assert_success response + + assert_equal 'approved', response.message + assert response.test? + end + + def test_failed_capture + @gateway.expects(:ssl_post).returns(failed_capture_response) + response = @gateway.capture(@amount, @credit_card, @options) + + assert_not_equal 'approved', response.message + assert response.test? + end + + def test_successful_refund + @gateway.expects(:ssl_post).returns(successful_refund_response) + response = @gateway.refund(@amount, 'testauthorization', @options) + assert_success response + + assert_equal 'approved', response.message + assert response.test? + end + + def test_failed_refund + @gateway.expects(:ssl_post).returns(failed_refund_response) + response = @gateway.refund(@amount, 'authorizationtest', @options) + assert_failure response + assert_not_equal 'approved', response.message + end + + def test_transcript_scrubbing + assert @gateway.supports_scrubbing? + assert_equal post_scrubbed, @gateway.scrub(pre_scrubbed) + end + + private + + def successful_purchase_response + %( + 2Nvuvo/fotnYnFVFHpNVPAW+U9oJVZ0eiB6jFFPixkAJi9rciTa1YKd2qPl+YybJHQaLgdITH3L+3M1N3xYObd03vZnKdTin3fXFE6B+0jcrjhMuYq1h8TP7tgfYheFVHQY6Kur/N606pCG9NAwQZ2WbpZ3Vf6byfVo7euVCRF8B95zx9ZyAbsohxrXEQpWHqd09z6SduCG2CTQG+ZfXveoJfAroOLpiRoF6KqOsprnxXP6ikhE454PAvAz4WROY51AGtPi35egb80OF69fiHg== + ) + end + + def failed_purchase_response + %{ + VXlw5DmHlP8LGpDmxSsdabtCw5yycFl3Jq3QYcRKbdYWYXKZR4Rv+gEZDqJH1e9Uk9FW43CtJKu4et+x8Wskc3VTOsD1BrNrOgv8EguZ+MhUQsnWeUWwqEGZd9rO4B51qS7Pb69SJb4PWsyOB0fMUBctiduyGF5kaxA2ieoLA9eGfxmoIsfBptpax37PdsaxTTHbHNQXiRkg1c9f9nyAbBzPQFD/Xuf7OOjhbECXq5Ev1OIxT97PqxVh5RQX+KIQ6gZUFVkwWDaiQh/c7KGIgI3UtXCEFxZtxTvNP81l9p6FgZRDfAcYRZfEOLE8LcqtMpW6p6GTsW6EfnvEaZxzy89xFv+RXuYdns1suxYOPb0= + } + end + + def successful_authorize_response + %( + SQOZIaVhhKGAdneX1QpUuA7ZfKNkx1vq39s8o+yLsl2kbMznbkA32/Z5ovA3leZNMHShEqJPAh4AK3BC0qiL7xETKNFv1BozHaLtZlvaPhPKMCrNeWkAdqesNpD0SvSvT7XZRarWRjcMnwGP9zSvuHqz3kaASZt7Oagh+FCssjZvXUoic7XV7owZEkEAvYiXlTfmd6sv0WYbUknMI9igr2MSe6rNBarIAscnhGJF/yW+ng0wR1pGnvtXJqlYbaTYx7urZEPP6GDfO2BeHkkMT46graqjNnQhsPLr2/Nfe6g= + ) + end + + def failed_authorize_response + %{ + VXlw5DmHlP8LGpDmxSsdabtCw5yycFl3Jq3QYcRKbdYWYXKZR4Rv+gEZDqJH1e9Uk9FW43CtJKu4et+x8Wskc3VTOsD1BrNrOgv8EguZ+MhUQsnWeUWwqEGZd9rO4B51qS7Pb69SJb4PWsyOB0fMUBctiduyGF5kaxA2ieoLA9eGfxmoIsfBptpax37PdsaxTTHbHNQXiRkg1c9f9nyAbBzPQFD/Xuf7OOjhbECXq5Ev1OIxT97PqxVh5RQX+KIQ6gZUFVkwWDaiQh/c7KGIgI3UtXCEFxZtxTvNP81l9p6FgZRDfAcYRZfEOLE8LcqtMpW6p6GTsW6EfnvEaZxzy89xFv+RXuYdns1suxYOPb0= + } + end + + def successful_capture_response + %( + 2Nvuvo/fotnYnFVFHpNVPAW+U9oJVZ0eiB6jFFPixkAJi9rciTa1YKd2qPl+YybJHQaLgdITH3L+3M1N3xYObd03vZnKdTin3fXFE6B+0jcrjhMuYq1h8TP7tgfYheFVHQY6Kur/N606pCG9NAwQZ2WbpZ3Vf6byfVo7euVCRF8B95zx9ZyAbsohxrXEQpWHqd09z6SduCG2CTQG+ZfXveoJfAroOLpiRoF6KqOsprnxXP6ikhE454PAvAz4WROY51AGtPi35egb80OF69fiHg== + ) + end + + def failed_capture_response + %{ + VXlw5DmHlP8LGpDmxSsdabtCw5yycFl3Jq3QYcRKbdYWYXKZR4Rv+gEZDqJH1e9Uk9FW43CtJKu4et+x8Wskc3VTOsD1BrNrOgv8EguZ+MhUQsnWeUWwqEGZd9rO4B51qS7Pb69SJb4PWsyOB0fMUBctiduyGF5kaxA2ieoLA9eGfxmoIsfBptpax37PdsaxTTHbHNQXiRkg1c9f9nyAbBzPQFD/Xuf7OOjhbECXq5Ev1OIxT97PqxVh5RQX+KIQ6gZUFVkwWDaiQh/c7KGIgI3UtXCEFxZtxTvNP81l9p6FgZRDfAcYRZfEOLE8LcqtMpW6p6GTsW6EfnvEaZxzy89xFv+RXuYdns1suxYOPb0= + } + end + + def successful_refund_response + %{ + yn3RJK3KwXedXShm/1DaCED1QA6lpFzVGORcTfHCviFTwSUxGduuhCZEWPTaiksvCpTMwMFBrdQO/2THtJ/+GH2+1vIdV5QYFbLU4QCD5G33Le1x1WAU72e8o7arPdBYapZqhiIjz1NwEPOnir2XGV1AXNAuj8OjDj+YQ42cH+iUxWYU6ROaVUhApWqgVhAWz9pZqPTeDssj3dzO/iAM9z7mlhxYnqDlHdWpPpNFdk34jPb//+xCfg13HLdplqBaeDPVuWaRiEG/pc3ttETjYw== + } + end + + def failed_refund_response + %{ + i+HTzdwnXqLEh9EPAyP54p6DyHOeKlt0lZqdbNy5paxwAAUSTciZzUGgFb8t8eXakdlZXWtlFHLBIRuiUFUyLZSB/btqldzuQPc+I8dEsz5F5yL4DdI/FFtAChYEHoumAvrth9uiBeEyGoAKL9etHOTPed2RFCcZYpsA8Gc3P1LterAeZwWX91LS0PzL6mKcsSUkkLCeT2UBJCg+N7a7ipop+U9jGsGBzKMhpZH6DyjZleBfh7j8ICbwMNClI8ixSYDQvmE5/fP7AZtL4oszdVAnlALhjL0Ld1MBLmeiTIiGkycZB0dKbrN5fAS0/mpbOm64wSF3ZAM/geKaXA6jmQ== + } + end + + def successful_void_response + %{ + yn3RJK3KwXedXShm/1DaCED1QA6lpFzVGORcTfHCviFTwSUxGduuhCZEWPTaiksvCpTMwMFBrdQO/2THtJ/+GH2+1vIdV5QYFbLU4QCD5G33Le1x1WAU72e8o7arPdBYapZqhiIjz1NwEPOnir2XGV1AXNAuj8OjDj+YQ42cH+iUxWYU6ROaVUhApWqgVhAWz9pZqPTeDssj3dzO/iAM9z7mlhxYnqDlHdWpPpNFdk34jPb//+xCfg13HLdplqBaeDPVuWaRiEG/pc3ttETjYw== + } + end + + def failed_void_response + %{ + i+HTzdwnXqLEh9EPAyP54p6DyHOeKlt0lZqdbNy5paxwAAUSTciZzUGgFb8t8eXakdlZXWtlFHLBIRuiUFUyLZSB/btqldzuQPc+I8dEsz5F5yL4DdI/FFtAChYEHoumAvrth9uiBeEyGoAKL9etHOTPed2RFCcZYpsA8Gc3P1LterAeZwWX91LS0PzL6mKcsSUkkLCeT2UBJCg+N7a7ipop+U9jGsGBzKMhpZH6DyjZleBfh7j8ICbwMNClI8ixSYDQvmE5/fP7AZtL4oszdVAnlALhjL0Ld1MBLmeiTIiGkycZB0dKbrN5fAS0/mpbOm64wSF3ZAM/geKaXA6jmQ== + } + 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\":\"<authorization>1aUSihtRXgd+1nycRfVWgv0JDZsGLsrpsNkahpkx4jmnBRRAPPao+zJYqsN4xrGMIeVdJ3Y5LlQYXg5qu8O7iZmDPTqWbyKmsurCxJidr6AkFszwvRfugElyb5sAYpUcrnFSpVUgz2NGcIuMRalr0irf7q30+TzbLRHQc1Z5QTe6am3ndO8aSKKLwYYmfHcO8E/+dPiCsSP09P2heNqpMbf5IKdSwGCVS1Rtpcoijl3wXB8zgeBZ1PXHAmmkC1/CWRs/fh1qmvYFzb8YAiRy5q80Tyq09IaeSpQ1ydq3r95QBSJy6H4gz2OV/v2xdm1A63XEh2+6N6p2XDyzGWQrxKE41wmqRCxie7qY2xqdv4S8Cl8ldSMEpZY46A68hKIN6zrj6eMWxauwdi6ZkZfMDuh9Pn9x5gwwgfElLopIpR8fejB6G4hAQHtq2jhn5D4ccmAqNxkrB4w5k+zc53Rupk2u3MDp5T5sRkqvNyIN2kCE6i0DD9HlqkCjWV+bG9WcUiO4D7m5fWRE5f9OQ2XjeA==</authorization><dataID>IVCA33721</dataID>\"}" + -> "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\":\"<capture>Z6l24tZG2YfTOQTne8NVygr/YeuVRNya8ZUCM5NvRgOEL/Mt8PO0voNnspoiFSg+RVamC4V2BipmU3spPVBg6Dr0xMpPL7ryVB9mlM4PokUdHkZTjXJHbbr1GWdyEPMYYSH0f+M1qUDO57EyUuZv8o6QSv+a/tuOrrBwsHI8cnsv+y9qt5L9LuGRMeBYvZkkK+xw53eDqYsJGoCvpk/pljCCkGU7Q/sKsLOx0MT6dA/BLVGrGeo8ngO+W/cnOigGfIZJSPFTcrUKI/Q7AsHuP+3lG6q9VAri9UJZXm5pWOg=</capture><dataID>IVCA33721</dataID>\"}" + -> "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\":\"<authorization>{\"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]\"}</authorization><dataID>IVCA33721</dataID>\"}" + -> "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\":\"<capture>{\"operation\":\"Capture\",\"commerce_id\":\"147\",\"user\":\"IVCA33721\",\"apikey\":\"[FILTERED]\",\"testMode\":\"YES\",\"transaction_id\":\"721\",\"amount\":\"11.15\",\"key_session\":\"[FILTERED]\"}</capture><dataID>IVCA33721</dataID>\"}" + -> "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/modern_payments_cim_test.rb b/test/unit/gateways/modern_payments_cim_test.rb index 0e6aa87a998..e126c3a4a54 100644 --- a/test/unit/gateways/modern_payments_cim_test.rb +++ b/test/unit/gateways/modern_payments_cim_test.rb @@ -5,17 +5,17 @@ def setup Base.mode = :test @gateway = ModernPaymentsCimGateway.new( - :login => 'login', - :password => 'password' - ) + login: 'login', + password: 'password' + ) @credit_card = credit_card @amount = 100 @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase' + order_id: '1', + billing_address: address, + description: 'Store Purchase' } end @@ -83,80 +83,80 @@ def test_soap_fault_response private def successful_create_customer_response - <<-XML -<?xml version="1.0" encoding="utf-8"?> -<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <soap:Body> - <CreateCustomerResponse xmlns="http://secure.modpay.com:81/ws/"> - <CreateCustomerResult>6677348</CreateCustomerResult> - </CreateCustomerResponse> - </soap:Body> -</soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?> + <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> + <soap:Body> + <CreateCustomerResponse xmlns="http://secure.modpay.com:81/ws/"> + <CreateCustomerResult>6677348</CreateCustomerResult> + </CreateCustomerResponse> + </soap:Body> + </soap:Envelope> XML end def successful_modify_customer_credit_card_response - <<-XML -<?xml version="1.0" encoding="utf-8"?> -<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <soap:Body> - <ModifyCustomerCreditCardResponse xmlns="http://secure.modpay.com:81/ws/"> - <ModifyCustomerCreditCardResult>6677757</ModifyCustomerCreditCardResult> - </ModifyCustomerCreditCardResponse> - </soap:Body> -</soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?> + <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> + <soap:Body> + <ModifyCustomerCreditCardResponse xmlns="http://secure.modpay.com:81/ws/"> + <ModifyCustomerCreditCardResult>6677757</ModifyCustomerCreditCardResult> + </ModifyCustomerCreditCardResponse> + </soap:Body> + </soap:Envelope> XML end def unsuccessful_credit_card_authorization_response - <<-XML -<?xml version="1.0" encoding="utf-8"?> -<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <soap:Body> - <AuthorizeCreditCardPaymentResponse xmlns="https://secure.modpay.com/netservices/test/"> - <AuthorizeCreditCardPaymentResult> - <transId>999</transId> - <authCode/> - <avsCode/> - <transCode/> - <authString>RESPONSECODE=D,AUTHCODE=,DECLINEREASON.1.TAG=,DECLINEREASON.1.ERRORCLASS=card declined,DECLINEREASON.1.PARAM1=05:DECLINE,DECLINEREASON.1.PARAM2=The authorization is declined,DECLINEREASON.1.MESSAGE=Card was declined: The authorization is declined,AVSDATA</authString> - <messageText>RESPONSECODE=D,AUTHCODE=,DECLINEREASON.1.TAG=,DECLINEREASON.1.ERRORCLASS=card declined,DECLINEREASON.1.PARAM1=05:DECLINE,DECLINEREASON.1.PARAM2=The authorization is declined,DECLINEREASON.1.MESSAGE=Card was declined: The authorization is declined,AVSDATA</messageText> - <approved>false</approved> - </AuthorizeCreditCardPaymentResult> - </AuthorizeCreditCardPaymentResponse> - </soap:Body> -</soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?> + <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> + <soap:Body> + <AuthorizeCreditCardPaymentResponse xmlns="https://secure.modpay.com/netservices/test/"> + <AuthorizeCreditCardPaymentResult> + <transId>999</transId> + <authCode/> + <avsCode/> + <transCode/> + <authString>RESPONSECODE=D,AUTHCODE=,DECLINEREASON.1.TAG=,DECLINEREASON.1.ERRORCLASS=card declined,DECLINEREASON.1.PARAM1=05:DECLINE,DECLINEREASON.1.PARAM2=The authorization is declined,DECLINEREASON.1.MESSAGE=Card was declined: The authorization is declined,AVSDATA</authString> + <messageText>RESPONSECODE=D,AUTHCODE=,DECLINEREASON.1.TAG=,DECLINEREASON.1.ERRORCLASS=card declined,DECLINEREASON.1.PARAM1=05:DECLINE,DECLINEREASON.1.PARAM2=The authorization is declined,DECLINEREASON.1.MESSAGE=Card was declined: The authorization is declined,AVSDATA</messageText> + <approved>false</approved> + </AuthorizeCreditCardPaymentResult> + </AuthorizeCreditCardPaymentResponse> + </soap:Body> + </soap:Envelope> XML end def soap_fault_response - <<-XML -<?xml version="1.0" encoding="utf-8"?> -<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <soap:Body> - <soap:Fault> - <faultcode>soap:Client</faultcode> - <faultstring>System.Web.Services.Protocols.SoapException: Server did not recognize the value of HTTP Header SOAPAction: h heheheh http://secure.modpay.com:81/ws/CreateCustomer. - at System.Web.Services.Protocols.Soap11ServerProtocolHelper.RouteRequest() - at System.Web.Services.Protocols.SoapServerProtocol.RouteRequest(SoapServerMessage message) - at System.Web.Services.Protocols.SoapServerProtocol.Initialize() - at System.Web.Services.Protocols.ServerProtocolFactory.Create(Type type, HttpContext context, HttpRequest request, HttpResponse response, Boolean&amp; abortProcessing)</faultstring> - <detail/> - </soap:Fault> - </soap:Body> -</soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?> + <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> + <soap:Body> + <soap:Fault> + <faultcode>soap:Client</faultcode> + <faultstring>System.Web.Services.Protocols.SoapException: Server did not recognize the value of HTTP Header SOAPAction: h heheheh http://secure.modpay.com:81/ws/CreateCustomer. + at System.Web.Services.Protocols.Soap11ServerProtocolHelper.RouteRequest() + at System.Web.Services.Protocols.SoapServerProtocol.RouteRequest(SoapServerMessage message) + at System.Web.Services.Protocols.SoapServerProtocol.Initialize() + at System.Web.Services.Protocols.ServerProtocolFactory.Create(Type type, HttpContext context, HttpRequest request, HttpResponse response, Boolean&amp; abortProcessing)</faultstring> + <detail/> + </soap:Fault> + </soap:Body> + </soap:Envelope> XML end def successful_authorization_response - <<-XML -<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><AuthorizeCreditCardPaymentResponse xmlns="https://secure.modpay.com/ws/"><AuthorizeCreditCardPaymentResult><transId>18713505</transId><authCode>020411</authCode><avsCode>Z</avsCode><transCode>C00 17093294 -</transCode><authString>RESPONSECODE=A -AUTHCODE=020411 -DECLINEREASON= -AVSDATA=Z -TRANSID=C00 17093294 -</authString><messageText>Approved</messageText><approved>true</approved></AuthorizeCreditCardPaymentResult></AuthorizeCreditCardPaymentResponse></soap:Body></soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><AuthorizeCreditCardPaymentResponse xmlns="https://secure.modpay.com/ws/"><AuthorizeCreditCardPaymentResult><transId>18713505</transId><authCode>020411</authCode><avsCode>Z</avsCode><transCode>C00 17093294 + </transCode><authString>RESPONSECODE=A + AUTHCODE=020411 + DECLINEREASON= + AVSDATA=Z + TRANSID=C00 17093294 + </authString><messageText>Approved</messageText><approved>true</approved></AuthorizeCreditCardPaymentResult></AuthorizeCreditCardPaymentResponse></soap:Body></soap:Envelope> XML end end diff --git a/test/unit/gateways/moka_test.rb b/test/unit/gateways/moka_test.rb new file mode 100644 index 00000000000..dc5bbc04fb7 --- /dev/null +++ b/test/unit/gateways/moka_test.rb @@ -0,0 +1,356 @@ +require 'test_helper' + +class MokaTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = MokaGateway.new(dealer_code: '123', username: 'username', password: 'password') + @credit_card = credit_card + @amount = 100 + + @options = { + description: 'Store Purchase' + } + end + + def test_successful_purchase + @gateway.expects(:ssl_post).returns(successful_response) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + + assert_equal 'Test-9732c2ce-08d9-4ff6-a89f-bd3fa345811c', response.authorization + assert response.test? + end + + def test_failed_purchase_with_top_level_error + @gateway.expects(:ssl_post).returns(failed_response_with_top_level_error) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_equal 'PaymentDealer.DoDirectPayment.InvalidRequest', response.error_code + assert_equal 'PaymentDealer.DoDirectPayment.InvalidRequest', response.message + end + + def test_failed_purchase_with_nested_error + @gateway.expects(:ssl_post).returns(failed_response_with_nested_error) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_failure response + + assert_equal 'General error', response.error_code + assert_equal 'Genel Hata(Geçersiz kart numarası)', 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_equal 1, JSON.parse(data)['PaymentDealerRequest']['IsPreAuth'] + end.respond_with(successful_response) + assert_success response + + assert_equal 'Test-9732c2ce-08d9-4ff6-a89f-bd3fa345811c', response.authorization + assert response.test? + end + + def test_failed_authorize + @gateway.expects(:ssl_post).returns(failed_response_with_top_level_error) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + end + + def test_successful_capture + @gateway.expects(:ssl_post).returns(successful_response) + + response = @gateway.capture(@amount, 'Test-9732c2ce-08d9-4ff6-a89f-bd3fa345811c', @options) + assert_success response + end + + def test_failed_capture + @gateway.expects(:ssl_post).returns(failed_capture_response) + + response = @gateway.capture(@amount, 'wrong-authorization', @options) + assert_failure response + assert_equal 'PaymentDealer.DoCapture.PaymentNotFound', response.error_code + end + + def test_successful_refund + @gateway.expects(:ssl_post).returns(successful_refund_response) + + response = @gateway.refund(0, 'Test-9732c2ce-08d9-4ff6-a89f-bd3fa345811c') + assert_success response + end + + def test_successful_partial_refund + stub_comms do + @gateway.refund(50, 'Test-9732c2ce-08d9-4ff6-a89f-bd3fa345811c') + end.check_request do |_endpoint, data, _headers| + assert_equal '0.50', JSON.parse(data)['PaymentDealerRequest']['Amount'] + end.respond_with(successful_refund_response) + end + + def test_failed_refund + @gateway.expects(:ssl_post).returns(failed_refund_response) + + response = @gateway.refund(0, '') + assert_failure response + assert_equal 'PaymentDealer.DoCreateRefundRequest.OtherTrxCodeOrVirtualPosOrderIdMustGiven', response.error_code + end + + def test_successful_void + @gateway.expects(:ssl_post).returns(successful_response) + + response = @gateway.void('Test-9732c2ce-08d9-4ff6-a89f-bd3fa345811c') + assert_success response + end + + def test_failed_void + @gateway.expects(:ssl_post).returns(failed_void_response) + + response = @gateway.void('') + assert_failure response + assert_equal 'PaymentDealer.DoVoid.InvalidRequest', response.error_code + end + + def test_successful_verify + response = stub_comms do + @gateway.verify(@credit_card) + end.respond_with(successful_response) + assert_success response + assert_equal 'Test-9732c2ce-08d9-4ff6-a89f-bd3fa345811c', response.authorization + assert_equal 'Success', response.message + end + + def test_failed_verify + response = stub_comms do + @gateway.verify(@credit_card) + end.respond_with(failed_response_with_top_level_error) + assert_failure response + assert_equal 'PaymentDealer.DoDirectPayment.InvalidRequest', response.message + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + def test_buyer_information_is_passed + options = @options.merge({ + billing_address: address, + email: 'safiye.ali@example.com' + }) + + stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + buyer_info = JSON.parse(data)['PaymentDealerRequest']['BuyerInformation'] + assert_equal buyer_info['BuyerFullName'], [@credit_card.first_name, @credit_card.last_name].join(' ') + assert_equal buyer_info['BuyerEmail'], 'safiye.ali@example.com' + assert_equal buyer_info['BuyerAddress'], options[:billing_address][:address1] + assert_equal buyer_info['BuyerGsmNumber'], options[:billing_address][:phone] + end.respond_with(successful_response) + end + + def test_basket_product_is_passed + options = @options.merge({ + basket_product: [ + { + product_id: 333, + product_code: '0173', + unit_price: 19900, + quantity: 1 + }, + { + product_id: 281, + product_code: '38', + unit_price: 5000, + quantity: 1 + } + ] + }) + + stub_comms do + @gateway.authorize(24900, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + basket = JSON.parse(data)['PaymentDealerRequest']['BasketProduct'] + basket.each_with_index do |product, i| + assert_equal product['ProductId'], options[:basket_product][i][:product_id] + assert_equal product['ProductCode'], options[:basket_product][i][:product_code] + assert_equal product['UnitPrice'], (sprintf '%.2f', options[:basket_product][i][:unit_price] / 100) + assert_equal product['Quantity'], options[:basket_product][i][:quantity] + end + end.respond_with(successful_response) + end + + def test_additional_auth_purchase_fields_are_passed + options = @options.merge({ + description: 'custom purchase', + installment_number: 12, + sub_merchant_name: 'testco', + is_pool_payment: 1 + }) + stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + response = JSON.parse(data) + assert_equal response['PaymentDealerRequest']['Description'], 'custom purchase' + assert_equal response['PaymentDealerRequest']['InstallmentNumber'], 12 + assert_equal response['SubMerchantName'], 'testco' + assert_equal response['IsPoolPayment'], 1 + end.respond_with(successful_response) + end + + private + + def pre_scrubbed + <<-PRE_SCRUBBED + opening connection to service.testmoka.com:443... + opened + starting SSL for service.testmoka.com:443... + SSL established + <- "POST /PaymentDealer/DoDirectPayment 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: service.testmoka.com\r\nContent-Length: 443\r\n\r\n" + <- "{\"PaymentDealerRequest\":{\"Amount\":\"1.00\",\"Currency\":\"TL\",\"CardHolderFullName\":\"Longbob Longsen\",\"CardNumber\":\"5269111122223332\",\"ExpMonth\":10,\"ExpYear\":2024,\"CvcNumber\":\"123\",\"IsPreAuth\":0,\"BuyerInformation\":{\"BuyerFullName\":\"Longbob Longsen\"}},\"PaymentDealerAuthentication\":{\"DealerCode\":\"1731\",\"Username\":\"TestMoka2\",\"Password\":\"HYSYHDS8DU8HU\",\"CheckKey\":\"1c1cccfe19b782415c207f1d66f97889cf11ed6d1e1ad6f585e5fe70b6f5da90\"},\"IsPoolPayment\":0}" + -> "HTTP/1.1 200 OK\r\n" + -> "Cache-Control: no-cache\r\n" + -> "Pragma: no-cache\r\n" + -> "Content-Type: application/json; charset=utf-8\r\n" + -> "Expires: -1\r\n" + -> "Server: Microsoft-IIS/10.0\r\n" + -> "X-AspNet-Version: 4.0.30319\r\n" + -> "X-Powered-By: ASP.NET\r\n" + -> "Access-Control-Allow-Origin: *\r\n" + -> "Access-Control-Allow-Headers: *\r\n" + -> "Date: Mon, 16 Aug 2021 20:33:17 GMT\r\n" + -> "Connection: close\r\n" + -> "Content-Length: 188\r\n" + -> "\r\n" + reading 188 bytes... + -> "{\"Data\":{\"IsSuccessful\":true,\"ResultCode\":\"\",\"ResultMessage\":\"\",\"VirtualPosOrderId\":\"Test-e8345c66-b614-4490-83ce-7be510f22312\"},\"ResultCode\":\"Success\",\"ResultMessage\":\"\",\"Exception\":null}" + read 188 bytes + Conn close + PRE_SCRUBBED + end + + def post_scrubbed + <<-POST_SCRUBBED + opening connection to service.testmoka.com:443... + opened + starting SSL for service.testmoka.com:443... + SSL established + <- "POST /PaymentDealer/DoDirectPayment 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: service.testmoka.com\r\nContent-Length: 443\r\n\r\n" + <- "{\"PaymentDealerRequest\":{\"Amount\":\"1.00\",\"Currency\":\"TL\",\"CardHolderFullName\":\"Longbob Longsen\",\"CardNumber\":\"[FILTERED]\",\"ExpMonth\":10,\"ExpYear\":2024,\"CvcNumber\":\"[FILTERED]\",\"IsPreAuth\":0,\"BuyerInformation\":{\"BuyerFullName\":\"Longbob Longsen\"}},\"PaymentDealerAuthentication\":{\"DealerCode\":\"[FILTERED]\",\"Username\":\"[FILTERED]\",\"Password\":\"[FILTERED]\",\"CheckKey\":\"[FILTERED]\"},\"IsPoolPayment\":0}" + -> "HTTP/1.1 200 OK\r\n" + -> "Cache-Control: no-cache\r\n" + -> "Pragma: no-cache\r\n" + -> "Content-Type: application/json; charset=utf-8\r\n" + -> "Expires: -1\r\n" + -> "Server: Microsoft-IIS/10.0\r\n" + -> "X-AspNet-Version: 4.0.30319\r\n" + -> "X-Powered-By: ASP.NET\r\n" + -> "Access-Control-Allow-Origin: *\r\n" + -> "Access-Control-Allow-Headers: *\r\n" + -> "Date: Mon, 16 Aug 2021 20:33:17 GMT\r\n" + -> "Connection: close\r\n" + -> "Content-Length: 188\r\n" + -> "\r\n" + reading 188 bytes... + -> "{\"Data\":{\"IsSuccessful\":true,\"ResultCode\":\"\",\"ResultMessage\":\"\",\"VirtualPosOrderId\":\"Test-e8345c66-b614-4490-83ce-7be510f22312\"},\"ResultCode\":\"Success\",\"ResultMessage\":\"\",\"Exception\":null}" + read 188 bytes + Conn close + POST_SCRUBBED + end + + def successful_response + <<-RESPONSE + { + "Data": { + "IsSuccessful": true, + "ResultCode": "", + "ResultMessage": "", + "VirtualPosOrderId": "Test-9732c2ce-08d9-4ff6-a89f-bd3fa345811c" + }, + "ResultCode": "Success", + "ResultMessage": "", + "Exception": null + } + RESPONSE + end + + def successful_refund_response + <<-RESPONSE + { + "Data": { + "IsSuccessful": true, + "ResultCode": "", + "ResultMessage": "", + "RefundRequestId": 2320 + }, + "ResultCode": "Success", + "ResultMessage": "", + "Exception": null + } + RESPONSE + end + + def failed_response_with_top_level_error + <<-RESPONSE + { + "Data": null, + "ResultCode": "PaymentDealer.DoDirectPayment.InvalidRequest", + "ResultMessage": "", + "Exception": null + } + RESPONSE + end + + def failed_response_with_nested_error + <<-RESPONSE + { + "Data": { + "IsSuccessful": false, + "ResultCode": "000", + "ResultMessage": "Genel Hata(Geçersiz kart numarası)", + "VirtualPosOrderId": "" + }, + "ResultCode": "Success", + "ResultMessage": "", + "Exception": null + } + RESPONSE + end + + def failed_capture_response + <<-RESPONSE + { + "Data": null, + "ResultCode": "PaymentDealer.DoCapture.PaymentNotFound", + "ResultMessage": "", + "Exception": null + } + RESPONSE + end + + def failed_refund_response + <<-RESPONSE + { + "Data": null, + "ResultCode": "PaymentDealer.DoCreateRefundRequest.OtherTrxCodeOrVirtualPosOrderIdMustGiven", + "ResultMessage": "", + "Exception": null + } + RESPONSE + end + + def failed_void_response + <<-RESPONSE + { + "Data": null, + "ResultCode": "PaymentDealer.DoVoid.InvalidRequest", + "ResultMessage": "", + "Exception": null + } + RESPONSE + end +end diff --git a/test/unit/gateways/monei_test.rb b/test/unit/gateways/monei_test.rb index 27b70aff1bd..3c1152a7107 100755 --- a/test/unit/gateways/monei_test.rb +++ b/test/unit/gateways/monei_test.rb @@ -5,10 +5,7 @@ class MoneiTest < Test::Unit::TestCase def setup @gateway = MoneiGateway.new( - :sender_id => 'mother', - :channel_id => 'there is no other', - :login => 'like mother', - :pwd => 'so treat Her right' + fixtures(:monei) ) @credit_card = credit_card @@ -27,7 +24,7 @@ def test_successful_purchase response = @gateway.purchase(@amount, @credit_card, @options) assert_success response - assert_equal '8a829449488d79090148996c441551fb', response.authorization + assert_equal '067574158f1f42499c31404752d52d06', response.authorization assert response.test? end @@ -130,290 +127,292 @@ def test_failed_verify assert_failure response end + def test_3ds_request + authentication_eci = '05' + authentication_cavv = 'AAACAgSRBklmQCFgMpEGAAAAAAA=' + authentication_xid = 'CAACCVVUlwCXUyhQNlSXAAAAAAA=' + + three_d_secure_options = { + eci: authentication_eci, + cavv: authentication_cavv, + xid: authentication_xid + } + options = @options.merge!({ + three_d_secure: three_d_secure_options + }) + stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/\"eci\":\"#{authentication_eci}\"/, data) + assert_match(/\"cavv\":\"#{authentication_cavv}\"/, data) + assert_match(/\"xid\":\"#{authentication_xid}\"/, data) + end.respond_with(successful_purchase_response) + end + + def test_sending_cardholder_name + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_equal @credit_card.name, JSON.parse(data)['paymentMethod']['card']['cardholderName'] + end.respond_with(successful_purchase_response) + end + + def test_sending_browser_info + ip = '77.110.174.153' + user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36' + lang = 'en' + + @options.merge!({ + ip:, + user_agent:, + lang: + }) + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + parsed = JSON.parse(data)['sessionDetails'] + assert_equal ip, parsed['ip'] + assert_equal user_agent, parsed['userAgent'] + assert_equal lang, parsed['lang'] + end.respond_with(successful_purchase_response) + end + + def test_scrub + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + def test_scrubs_auth_data + assert_equal @gateway.scrub(pre_scrubbed_with_auth), post_scrubbed_with_auth + end + + def test_supports_scrubbing? + assert @gateway.supports_scrubbing? + end + private def successful_purchase_response - return <<-XML -<?xml version="1.0" encoding="UTF-8"?> -<Response version="1.0"> - <Transaction mode="CONNECTOR_TEST" channel="8a82941746287806014698a0e3240575" response="SYNC"> - <Identification> - <ShortID>7621.0198.1858</ShortID> - <UniqueID>8a829449488d79090148996c441551fb</UniqueID> - <TransactionID>1</TransactionID> - </Identification> - <Payment code="CC.DB"> - <Clearing> - <Amount>1.00</Amount> - <Currency>EUR</Currency> - <Descriptor>7621.0198.1858 DEFAULT Store Purchase</Descriptor> - <FxRate>1.0</FxRate> - <FxSource>INTERN</FxSource> - <FxDate>2014-09-21 18:14:42</FxDate> - </Clearing> - </Payment> - <Processing code="CC.DB.90.00"> - <Timestamp>2014-09-21 18:14:42</Timestamp> - <Result>ACK</Result> - <Status code="90">NEW</Status> - <Reason code="00">Successful Processing</Reason> - <Return code="000.100.112">Request successfully processed in 'Merchant in Connector Test Mode'</Return> - <Risk score="0" /> - </Processing> - </Transaction> -</Response> - XML + <<-RESPONSE + { + "id": "067574158f1f42499c31404752d52d06", + "amount": 110, + "currency": "EUR", + "orderId": "1", + "accountId": "00000000-aaaa-bbbb-cccc-dddd123456789", + "status": "SUCCEEDED", + "statusMessage": "Transaction Approved", + "signature": "3dc52e4dbcc15cee5bb03cb7e3ab90708bf8b8a21818c0262ac05ec0c01780d0" + } + RESPONSE end def failed_purchase_response - <<-XML -<?xml version="1.0" encoding="UTF-8"?> -<Response version="1.0"> - <Transaction mode="CONNECTOR_TEST" channel="8a82943746287806014628a0e3240575" response="SYNC"> - <Identification> - <ShortID>9086.6774.0834</ShortID> - <UniqueID>8a82944a488d36c101489972b0ee6ace</UniqueID> - <TransactionID>1</TransactionID> - </Identification> - <Payment code="CC.DB" /> - <Processing code="CC.DB.70.40"> - <Timestamp>2014-09-21 18:21:43</Timestamp> - <Result>NOK</Result> - <Status code="70">REJECTED_VALIDATION</Status> - <Reason code="40">Account Validation</Reason> - <Return code="100.100.700">invalid cc number/brand combination</Return> - </Processing> - </Transaction> -</Response> - XML + <<-RESPONSE + { + "status": "error", + "message": "Card number declined by processor" + } + RESPONSE end def successful_authorize_response - <<-XML -<?xml version="1.0" encoding="UTF-8"?> -<Response version="1.0"> - <Transaction mode="CONNECTOR_TEST" channel="8a82941746487806014628a0e3240575" response="SYNC"> - <Identification> - <ShortID>6853.2944.1442</ShortID> - <UniqueID>8a82944a488d36c101489976f0cc6b1c</UniqueID> - <TransactionID>1</TransactionID> - </Identification> - <Payment code="CC.PA"> - <Clearing> - <Amount>1.00</Amount> - <Currency>EUR</Currency> - <Descriptor>6853.2944.1442 DEFAULT Store Purchase</Descriptor> - <FxRate>1.0</FxRate> - <FxSource>INTERN</FxSource> - <FxDate>2014-09-21 18:26:22</FxDate> - </Clearing> - </Payment> - <Processing code="CC.PA.90.00"> - <Timestamp>2014-09-21 18:26:22</Timestamp> - <Result>ACK</Result> - <Status code="90">NEW</Status> - <Reason code="00">Successful Processing</Reason> - <Return code="000.100.112">Request successfully processed in 'Merchant in Connector Test Mode'</Return> - <Risk score="0" /> - </Processing> - </Transaction> -</Response> - XML + <<-RESPONSE + { + "id": "067574158f1f42499c31404752d52d06", + "amount": 110, + "currency": "EUR", + "orderId": "1", + "accountId": "00000000-aaaa-bbbb-cccc-dddd123456789", + "status": "AUTHORIZED", + "statusMessage": "Transaction Approved", + "signature": "3dc52e4dbcc15cee5bb03cb7e3ab90708bf8b8a21818c0262ac05ec0c01780d0" + } + RESPONSE end def failed_authorize_response - <<-XML -<?xml version="1.0" encoding="UTF-8"?> -<Response version="1.0"> - <Transaction mode="CONNECTOR_TEST" channel="8a82941746287806014628a0e3240575" response="SYNC"> - <Identification> - <ShortID>4727.2856.0290</ShortID> - <UniqueID>8a829449488d79090148998943a853f6</UniqueID> - <TransactionID>1</TransactionID> - </Identification> - <Payment code="CC.PA" /> - <Processing code="CC.PA.70.40"> - <Timestamp>2014-09-21 18:46:22</Timestamp> - <Result>NOK</Result> - <Status code="70">REJECTED_VALIDATION</Status> - <Reason code="40">Account Validation</Reason> - <Return code="100.100.700">invalid cc number/brand combination</Return> - </Processing> - </Transaction> -</Response> - XML + <<-RESPONSE + { + "status": "error", + "message": "Card number declined by processor" + } + RESPONSE end def successful_capture_response - <<-XML -<?xml version="1.0" encoding="UTF-8"?> -<Response version="1.0"> - <Transaction mode="CONNECTOR_TEST" channel="8a82941746287806014628a0e3240575" response="SYNC"> - <Identification> - <ShortID>1269.8369.2962</ShortID> - <UniqueID>8a82944a488d36c10148998d9b316cc6</UniqueID> - <TransactionID /> - <ReferenceID>8a829449488d79090148998d97f05439</ReferenceID> - </Identification> - <Payment code="CC.CP"> - <Clearing> - <Amount>1.00</Amount> - <Currency>EUR</Currency> - <Descriptor>1269.8369.2962 DEFAULT Store Purchase</Descriptor> - <FxRate>1.0</FxRate> - <FxSource>INTERN</FxSource> - <FxDate>2014-09-21 18:51:07</FxDate> - </Clearing> - </Payment> - <Processing code="CC.CP.90.00"> - <Timestamp>2014-09-21 18:51:07</Timestamp> - <Result>ACK</Result> - <Status code="90">NEW</Status> - <Reason code="00">Successful Processing</Reason> - <Return code="000.100.112">Request successfully processed in 'Merchant in Connector Test Mode'</Return> - <Risk score="0" /> - </Processing> - </Transaction> -</Response> - XML + <<-RESPONSE + { + "id": "067574158f1f42499c31404752d52d06", + "amount": 110, + "currency": "EUR", + "orderId": "1", + "accountId": "00000000-aaaa-bbbb-cccc-dddd123456789", + "status": "SUCCEEDED", + "statusMessage": "Transaction Approved", + "signature": "3dc52e4dbcc15cee5bb03cb7e3ab90708bf8b8a21818c0262ac05ec0c01780d0" + } + RESPONSE end def failed_capture_response - <<-XML -<?xml version="1.0" encoding="UTF-8"?> -<Response version="1.0"> - <Transaction mode="CONNECTOR_TEST" channel="8a82941746287806014628a0e3240575" response="SYNC"> - <Identification> - <ShortID>0239.0447.7858</ShortID> - <UniqueID>8a82944a488d36c10148998fc4b66cfc</UniqueID> - <TransactionID /> - <ReferenceID /> - </Identification> - <Payment code="CC.CP" /> - <Processing code="CC.CP.70.20"> - <Timestamp>2014-09-21 18:53:29</Timestamp> - <Result>NOK</Result> - <Status code="70">REJECTED_VALIDATION</Status> - <Reason code="20">Format Error</Reason> - <Return code="200.100.302">invalid Request/Transaction/Payment/Presentation tag (not present or [partially] empty)</Return> - </Processing> - </Transaction> -</Response> - XML + <<-RESPONSE + { + "status": "error", + "message": "Card number declined by processor" + } + RESPONSE end def successful_refund_response - <<-XML -<?xml version="1.0" encoding="UTF-8"?> -<Response version="1.0"> - <Transaction mode="CONNECTOR_TEST" channel="8a82941746287806014628a0e3240575" response="SYNC"> - <Identification> - <ShortID>3009.2986.8450</ShortID> - <UniqueID>8a829449488d790901489992a493546f</UniqueID> - <TransactionID /> - <ReferenceID>8a82944a488d36c101489992a10f6d21</ReferenceID> - </Identification> - <Payment code="CC.RF"> - <Clearing> - <Amount>1.00</Amount> - <Currency>EUR</Currency> - <Descriptor>3009.2986.8450 DEFAULT Store Purchase</Descriptor> - <FxRate>1.0</FxRate> - <FxSource>INTERN</FxSource> - <FxDate>2014-09-21 18:56:37</FxDate> - </Clearing> - </Payment> - <Processing code="CC.RF.90.00"> - <Timestamp>2014-09-21 18:56:37</Timestamp> - <Result>ACK</Result> - <Status code="90">NEW</Status> - <Reason code="00">Successful Processing</Reason> - <Return code="000.100.112">Request successfully processed in 'Merchant in Connector Test Mode'</Return> - </Processing> - </Transaction> -</Response> - XML + <<-RESPONSE + { + "id": "067574158f1f42499c31404752d52d06", + "amount": 110, + "currency": "EUR", + "orderId": "1", + "accountId": "00000000-aaaa-bbbb-cccc-dddd123456789", + "status": "REFUNDED", + "statusMessage": "Transaction Approved", + "signature": "3dc52e4dbcc15cee5bb03cb7e3ab90708bf8b8a21818c0262ac05ec0c01780d0" + } + RESPONSE end def failed_refund_response - <<-XML -<?xml version="1.0" encoding="UTF-8"?> -<Response version="1.0"> - <Transaction mode="CONNECTOR_TEST" channel="8a82941746287806014628a0e3240575" response="SYNC"> - <Identification> - <ShortID>5070.8829.8658</ShortID> - <UniqueID>8a829449488d790901489994b2c65481</UniqueID> - <TransactionID /> - <ReferenceID /> - </Identification> - <Payment code="CC.RF" /> - <Processing code="CC.RF.70.20"> - <Timestamp>2014-09-21 18:58:52</Timestamp> - <Result>NOK</Result> - <Status code="70">REJECTED_VALIDATION</Status> - <Reason code="20">Format Error</Reason> - <Return code="200.100.302">invalid Request/Transaction/Payment/Presentation tag (not present or [partially] empty)</Return> - </Processing> - </Transaction> -</Response> - XML + <<-RESPONSE + { + "status": "error", + "message": "Card number declined by processor" + } + RESPONSE end def successful_void_response - <<-XML -<?xml version="1.0" encoding="UTF-8"?> -<Response version="1.0"> - <Transaction mode="CONNECTOR_TEST" channel="8a82941746287806014628a0e3240575" response="SYNC"> - <Identification> - <ShortID>4587.6991.6578</ShortID> - <UniqueID>8a82944a488d36c1014899957fff6d49</UniqueID> - <TransactionID /> - <ReferenceID>8a829449488d7909014899957cb45486</ReferenceID> - </Identification> - <Payment code="CC.RV"> - <Clearing> - <Amount>1.00</Amount> - <Currency>EUR</Currency> - <Descriptor>4587.6991.6578 DEFAULT Store Purchase</Descriptor> - <FxRate>1.0</FxRate> - <FxSource>INTERN</FxSource> - <FxDate>2014-09-21 18:59:44</FxDate> - </Clearing> - </Payment> - <Processing code="CC.RV.90.00"> - <Timestamp>2014-09-21 18:59:44</Timestamp> - <Result>ACK</Result> - <Status code="90">NEW</Status> - <Reason code="00">Successful Processing</Reason> - <Return code="000.100.112">Request successfully processed in 'Merchant in Connector Test Mode'</Return> - <Risk score="0" /> - </Processing> - </Transaction> -</Response> - XML + <<-RESPONSE + { + "id": "067574158f1f42499c31404752d52d06", + "amount": 110, + "currency": "EUR", + "orderId": "1", + "accountId": "00000000-aaaa-bbbb-cccc-dddd123456789", + "status": "CANCELED", + "statusMessage": "Transaction Approved", + "signature": "3dc52e4dbcc15cee5bb03cb7e3ab90708bf8b8a21818c0262ac05ec0c01780d0" + } + RESPONSE end def failed_void_response - <<-XML -<Response version="1.0"> - <Transaction mode="CONNECTOR_TEST" channel="8a82941746287806014628a0e3240575" response="SYNC"> - <Identification> - <ShortID>5843.9770.9986</ShortID> - <UniqueID>8a829449488d7909014899965cd354b6</UniqueID> - <TransactionID /> - <ReferenceID /> - </Identification> - <Payment code="CC.RV" /> - <Processing code="CC.RV.70.30"> - <Timestamp>2014-09-21 19:00:41</Timestamp> - <Result>NOK</Result> - <Status code="70">REJECTED_VALIDATION</Status> - <Reason code="30">Reference Error</Reason> - <Return code="700.400.530">reversal needs at least one successful transaction of type (CP or DB or RB or PA)</Return> - <Risk score="0" /> - </Processing> - </Transaction> -</Response> - XML + <<-RESPONSE + { + "status": "error", + "message": "Card number declined by processor" + } + RESPONSE + end + + def pre_scrubbed + <<-PRE_SCRUBBED + <- "POST /v1/payments HTTP/1.1\r\nContent-Type: application/json;charset=UTF-8\r\nAuthorization: pk_test_3cb2d54b7ee145fa92d683c01816ad15\r\nUser-Agent: MONEI/Shopify/0.1.0\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nHost: api.monei.com\r\nContent-Length: 443\r\n\r\n" + <- "{\"livemode\":\"false\",\"orderId\":\"66e0d04361fb7b401bec3b078744c21e\",\"transactionType\":\"AUTH\",\"description\":\"Store Purchase\",\"amount\":100,\"currency\":\"EUR\",\"paymentMethod\":{\"card\":{\"number\":\"5453010000059675\",\"expMonth\":\"12\",\"expYear\":\"34\",\"cvc\":\"123\"}},\"customer\":{\"email\":\"support@monei.com\",\"name\":\"Jim Smith\"},\"billingDetails\":{\"address\":{\"line1\":\"456 My Street\",\"city\":\"Ottawa\",\"state\":\"ON\",\"zip\":\"K1C2N6\",\"country\":\"CA\"}},\"sessionDetails\":{}}" + -> "HTTP/1.1 200 OK\r\n" + -> "Content-Type: application/json\r\n" + -> "Content-Length: 1069\r\n" + -> "Connection: close\r\n" + -> "Date: Mon, 05 Jul 2021 15:59:36 GMT\r\n" + -> "x-amzn-RequestId: 75b637ff-f230-4522-b6c5-bc5b95495a55\r\n" + -> "Access-Control-Allow-Origin: *\r\n" + -> "x-amz-apigw-id: CAPf6EPXjoEFdxA=\r\n" + -> "X-Amzn-Trace-Id: Root=1-60e32c65-625fbe465afdff1666fa4da9;Sampled=0\r\n" + -> "Access-Control-Allow-Credentials: true\r\n" + -> "X-Cache: Miss from cloudfront\r\n" + -> "Via: 1.1 508a0f3451a34ff5e6a3963c94ef304d.cloudfront.net (CloudFront)\r\n" + -> "X-Amz-Cf-Pop: MAD51-C3\r\n" + -> "X-Amz-Cf-Id: SH_5SGGltcCwOgNwn4cnuZAYCa8__JZuUe5lj_Dnvkhigu2yB8M-SQ==\r\n" + -> "\r\n" + reading 1069 bytes... + -> "{\"id\":\"cdc503654e76e29051bce6054e4b4d47dfb63edc\",\"amount\":100,\"currency\":\"EUR\",\"orderId\":\"66e0d04361fb7b401bec3b078744c21e\",\"description\":\"Store Purchase\",\"accountId\":\"00000000-aaaa-bbbb-cccc-dddd123456789\",\"authorizationCode\":\"++++++\",\"livemode\":false,\"status\":\"FAILED\",\"statusCode\":\"E501\",\"statusMessage\":\"Card rejected: invalid card number\",\"customer\":{\"name\":\"Jim Smith\",\"email\":\"support@monei.com\"},\"billingDetails\":{\"address\":{\"zip\":\"K1C2N6\",\"country\":\"CA\",\"state\":\"ON\",\"city\":\"Ottawa\",\"line1\":\"456 My Street\"}},\"sessionDetails\":{\"deviceType\":\"desktop\"},\"traceDetails\":{\"deviceType\":\"desktop\",\"sourceVersion\":\"0.1.0\",\"countryCode\":\"ES\",\"ip\":\"217.61.227.107\",\"userAgent\":\"MONEI/Shopify/0.1.0\",\"source\":\"MONEI/Shopify\",\"lang\":\"en\"},\"createdAt\":1625500773,\"updatedAt\":1625500776,\"paymentMethod\":{\"method\":\"card\",\"card\":{\"country\":\"US\",\"last4\":\"9675\",\"threeDSecure\":false,\"expiration\":2048544000,\"type\":\"credit\",\"brand\":\"mastercard\"}},\"nextAction\":{\"type\":\"COMPLETE\",\"redirectUrl\":\"https://secure.monei.com/payments/cdc503654e76e29051bce6054e4b4d47dfb63edc/receipt\"}}" + read 1069 bytes + Conn close + PRE_SCRUBBED + end + + def pre_scrubbed_with_auth + <<-PRE_SCRUBBED_WITH_AUTH + <- "POST /v1/payments HTTP/1.1\r\nContent-Type: application/json;charset=UTF-8\r\nAuthorization: pk_test_3cb2d54b7ee145fa92d683c01816ad15\r\nUser-Agent: MONEI/Shopify/0.1.0\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nHost: api.monei.com\r\nContent-Length: 1063\r\n\r\n" + <- "{\"livemode\":\"false\",\"orderId\":\"851925032d391d67e3fbf70b06aa182d\",\"transactionType\":\"SALE\",\"description\":\"Store Purchase\",\"amount\":100,\"currency\":\"EUR\",\"paymentMethod\":{\"card\":{\"number\":\"4444444444444406\",\"expMonth\":\"12\",\"expYear\":\"34\",\"cvc\":\"123\",\"auth\":{\"threeDSVersion\":null,\"eci\":\"05\",\"cavv\":\"AAACAgSRBklmQCFgMpEGAAAAAAA=\",\"dsTransID\":\"7eac9571-3533-4c38-addd-00cf34af6a52\",\"directoryResponse\":null,\"authenticationResponse\":null,\"notificationUrl\":\"https://example.com/notification\"}}},\"customer\":{\"email\":\"support@monei.com\",\"name\":\"Jim Smith\"},\"billingDetails\":{\"address\":{\"line1\":\"456 My Street\",\"city\":\"Ottawa\",\"state\":\"ON\",\"zip\":\"K1C2N6\",\"country\":\"CA\"}},\"sessionDetails\":{\"ip\":\"77.110.174.153\",\"userAgent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36\",\"browserAccept\":\"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8,application/json\",\"browserColorDepth\":\"100\",\"lang\":\"US\",\"browserScreenHeight\":\"1000\",\"browserScreenWidth\":\"500\",\"browserTimezoneOffset\":\"-120\"}}" + -> "HTTP/1.1 200 OK\r\n" + -> "Content-Type: application/json\r\n" + -> "Content-Length: 253\r\n" + -> "Connection: close\r\n" + -> "Date: Mon, 05 Jul 2021 15:59:59 GMT\r\n" + -> "x-amzn-RequestId: ac5a5ec8-6dd4-4254-a28a-8e9fa652ba90\r\n" + -> "Access-Control-Allow-Origin: *\r\n" + -> "x-amz-apigw-id: CAPj7FFfDoEFXOQ=\r\n" + -> "X-Amzn-Trace-Id: Root=1-60e32c7f-690a46280dc8da307f679795;Sampled=0\r\n" + -> "Access-Control-Allow-Credentials: true\r\n" + -> "X-Cache: Miss from cloudfront\r\n" + -> "Via: 1.1 1868e2f5b79bbf25cd21cd4b652be313.cloudfront.net (CloudFront)\r\n" + -> "X-Amz-Cf-Pop: MAD51-C3\r\n" + -> "X-Amz-Cf-Id: RVunC63Qvaswh2fcVB5n0p0BB_1zxbMOx68nuq5m6GKhWUFPpfAgVQ==\r\n" + -> "\r\n" + reading 253 bytes... + -> "{\"id\":\"e1310ab50f7cf1dcf87f1ae75b2ed0fbd2a4d05f\",\"amount\":100,\"currency\":\"EUR\",\"orderId\":\"851925032d391d67e3fbf70b06aa182d\",\"accountId\":\"00000000-aaaa-bbbb-cccc-dddd123456789\",\"liveMode\":false,\"status\":\"SUCCEEDED\",\"statusMessage\":\"Transaction Approved\"}" + read 253 bytes + Conn close + PRE_SCRUBBED_WITH_AUTH + end + + def post_scrubbed + <<-POST_SCRUBBED + <- "POST /v1/payments HTTP/1.1\r\nContent-Type: application/json;charset=UTF-8\r\n\Authorization: [FILTERED]\r\nUser-Agent: MONEI/Shopify/0.1.0\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nHost: api.monei.com\r\nContent-Length: 443\r\n\r\n" + <- "{\"livemode\":\"false\",\"orderId\":\"66e0d04361fb7b401bec3b078744c21e\",\"transactionType\":\"AUTH\",\"description\":\"Store Purchase\",\"amount\":100,\"currency\":\"EUR\",\"paymentMethod\":{\"card\":{\"number\":\"[FILTERED]\",\"expMonth\":\"12\",\"expYear\":\"34\",\"cvc\":\"[FILTERED]\"}},\"customer\":{\"email\":\"support@monei.com\",\"name\":\"Jim Smith\"},\"billingDetails\":{\"address\":{\"line1\":\"456 My Street\",\"city\":\"Ottawa\",\"state\":\"ON\",\"zip\":\"K1C2N6\",\"country\":\"CA\"}},\"sessionDetails\":{}}" + -> "HTTP/1.1 200 OK\r\n" + -> "Content-Type: application/json\r\n" + -> "Content-Length: 1069\r\n" + -> "Connection: close\r\n" + -> "Date: Mon, 05 Jul 2021 15:59:36 GMT\r\n" + -> "x-amzn-RequestId: 75b637ff-f230-4522-b6c5-bc5b95495a55\r\n" + -> "Access-Control-Allow-Origin: *\r\n" + -> "x-amz-apigw-id: CAPf6EPXjoEFdxA=\r\n" + -> "X-Amzn-Trace-Id: Root=1-60e32c65-625fbe465afdff1666fa4da9;Sampled=0\r\n" + -> "Access-Control-Allow-Credentials: true\r\n" + -> "X-Cache: Miss from cloudfront\r\n" + -> "Via: 1.1 508a0f3451a34ff5e6a3963c94ef304d.cloudfront.net (CloudFront)\r\n" + -> "X-Amz-Cf-Pop: MAD51-C3\r\n" + -> "X-Amz-Cf-Id: SH_5SGGltcCwOgNwn4cnuZAYCa8__JZuUe5lj_Dnvkhigu2yB8M-SQ==\r\n" + -> "\r\n" + reading 1069 bytes... + -> "{\"id\":\"cdc503654e76e29051bce6054e4b4d47dfb63edc\",\"amount\":100,\"currency\":\"EUR\",\"orderId\":\"66e0d04361fb7b401bec3b078744c21e\",\"description\":\"Store Purchase\",\"accountId\":\"00000000-aaaa-bbbb-cccc-dddd123456789\",\"authorizationCode\":\"++++++\",\"livemode\":false,\"status\":\"FAILED\",\"statusCode\":\"E501\",\"statusMessage\":\"Card rejected: invalid card number\",\"customer\":{\"name\":\"Jim Smith\",\"email\":\"support@monei.com\"},\"billingDetails\":{\"address\":{\"zip\":\"K1C2N6\",\"country\":\"CA\",\"state\":\"ON\",\"city\":\"Ottawa\",\"line1\":\"456 My Street\"}},\"sessionDetails\":{\"deviceType\":\"desktop\"},\"traceDetails\":{\"deviceType\":\"desktop\",\"sourceVersion\":\"0.1.0\",\"countryCode\":\"ES\",\"ip\":\"217.61.227.107\",\"userAgent\":\"MONEI/Shopify/0.1.0\",\"source\":\"MONEI/Shopify\",\"lang\":\"en\"},\"createdAt\":1625500773,\"updatedAt\":1625500776,\"paymentMethod\":{\"method\":\"card\",\"card\":{\"country\":\"US\",\"last4\":\"9675\",\"threeDSecure\":false,\"expiration\":2048544000,\"type\":\"credit\",\"brand\":\"mastercard\"}},\"nextAction\":{\"type\":\"COMPLETE\",\"redirectUrl\":\"https://secure.monei.com/payments/cdc503654e76e29051bce6054e4b4d47dfb63edc/receipt\"}}" + read 1069 bytes + Conn close + POST_SCRUBBED + end + + def post_scrubbed_with_auth + <<-POST_SCRUBBED_WITH_AUTH + <- "POST /v1/payments HTTP/1.1\r\nContent-Type: application/json;charset=UTF-8\r\nAuthorization: [FILTERED]\r\nUser-Agent: MONEI/Shopify/0.1.0\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nHost: api.monei.com\r\nContent-Length: 1063\r\n\r\n" + <- "{\"livemode\":\"false\",\"orderId\":\"851925032d391d67e3fbf70b06aa182d\",\"transactionType\":\"SALE\",\"description\":\"Store Purchase\",\"amount\":100,\"currency\":\"EUR\",\"paymentMethod\":{\"card\":{\"number\":\"[FILTERED]\",\"expMonth\":\"12\",\"expYear\":\"34\",\"cvc\":\"[FILTERED]\",\"auth\":{\"threeDSVersion\":null,\"eci\":\"05\",\"cavv\":\"[FILTERED]\",\"dsTransID\":\"7eac9571-3533-4c38-addd-00cf34af6a52\",\"directoryResponse\":null,\"authenticationResponse\":null,\"notificationUrl\":\"https://example.com/notification\"}}},\"customer\":{\"email\":\"support@monei.com\",\"name\":\"Jim Smith\"},\"billingDetails\":{\"address\":{\"line1\":\"456 My Street\",\"city\":\"Ottawa\",\"state\":\"ON\",\"zip\":\"K1C2N6\",\"country\":\"CA\"}},\"sessionDetails\":{\"ip\":\"77.110.174.153\",\"userAgent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36\",\"browserAccept\":\"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8,application/json\",\"browserColorDepth\":\"100\",\"lang\":\"US\",\"browserScreenHeight\":\"1000\",\"browserScreenWidth\":\"500\",\"browserTimezoneOffset\":\"-120\"}}" + -> "HTTP/1.1 200 OK\r\n" + -> "Content-Type: application/json\r\n" + -> "Content-Length: 253\r\n" + -> "Connection: close\r\n" + -> "Date: Mon, 05 Jul 2021 15:59:59 GMT\r\n" + -> "x-amzn-RequestId: ac5a5ec8-6dd4-4254-a28a-8e9fa652ba90\r\n" + -> "Access-Control-Allow-Origin: *\r\n" + -> "x-amz-apigw-id: CAPj7FFfDoEFXOQ=\r\n" + -> "X-Amzn-Trace-Id: Root=1-60e32c7f-690a46280dc8da307f679795;Sampled=0\r\n" + -> "Access-Control-Allow-Credentials: true\r\n" + -> "X-Cache: Miss from cloudfront\r\n" + -> "Via: 1.1 1868e2f5b79bbf25cd21cd4b652be313.cloudfront.net (CloudFront)\r\n" + -> "X-Amz-Cf-Pop: MAD51-C3\r\n" + -> "X-Amz-Cf-Id: RVunC63Qvaswh2fcVB5n0p0BB_1zxbMOx68nuq5m6GKhWUFPpfAgVQ==\r\n" + -> "\r\n" + reading 253 bytes... + -> "{\"id\":\"e1310ab50f7cf1dcf87f1ae75b2ed0fbd2a4d05f\",\"amount\":100,\"currency\":\"EUR\",\"orderId\":\"851925032d391d67e3fbf70b06aa182d\",\"accountId\":\"00000000-aaaa-bbbb-cccc-dddd123456789\",\"liveMode\":false,\"status\":\"SUCCEEDED\",\"statusMessage\":\"Transaction Approved\"}" + read 253 bytes + Conn close + POST_SCRUBBED_WITH_AUTH end end diff --git a/test/unit/gateways/moneris_test.rb b/test/unit/gateways/moneris_test.rb index 084bc5c84ca..3963fad83ee 100644 --- a/test/unit/gateways/moneris_test.rb +++ b/test/unit/gateways/moneris_test.rb @@ -7,18 +7,23 @@ def setup Base.mode = :test @gateway = MonerisGateway.new( - :login => 'store1', - :password => 'yesguy' + login: 'store3', + password: 'yesguy' ) @amount = 100 @credit_card = credit_card('4242424242424242') - @options = { :order_id => '1', :customer => '1', :billing_address => address} + + # https://developer.moneris.com/livedemo/3ds2/reference/guide/php + @fully_authenticated_eci = '02' + @no_liability_shift_eci = 7 + + @options = { order_id: '1', customer: '1', billing_address: address } end def test_default_options assert_equal 7, @gateway.options[:crypt_type] - assert_equal 'store1', @gateway.options[:login] + assert_equal 'store3', @gateway.options[:login] assert_equal 'yesguy', @gateway.options[:password] end @@ -30,9 +35,125 @@ def test_successful_purchase assert_equal '58-0_3;1026.1', response.authorization end + def test_successful_mpi_cavv_purchase + 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(/<ds_trans_id>12345<\/ds_trans_id>/, data) + assert_match(/<threeds_server_trans_id>d0f461f8-960f-40c9-a323-4e43a4e16aaa<\/threeds_server_trans_id>/, data) + assert_match(/<threeds_version>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(/<cust_id>test1234<\/cust_id>/, data) + end.respond_with(successful_cavv_purchase_response) + + assert_success response + assert_equal '69785-0_98;a131684dbecc1d89d9927c539ed3791b', response.authorization + end + + def test_failed_mpi_cavv_purchase + 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(/<ds_trans_id>12345<\/ds_trans_id>/, data) + assert_match(/<threeds_server_trans_id>d0f461f8-960f-40c9-a323-4e43a4e16aaa<\/threeds_server_trans_id>/, data) + assert_match(/<threeds_version>2<\/threeds_version>/, data) + assert_match(/<crypt_type>2<\/crypt_type>/, data) + end.respond_with(failed_cavv_purchase_response) + + assert_failure response + assert_equal '69785-0_98;a131684dbecc1d89d9927c539ed3791b', response.authorization + end + + def test_successful_first_purchase_with_credential_on_file + gateway = MonerisGateway.new( + login: 'store3', + password: 'yesguy' + ) + gateway.expects(:ssl_post).returns(successful_first_cof_purchase_response) + assert response = gateway.purchase( + @amount, + @credit_card, + @options.merge( + issuer_id: '', + payment_indicator: 'C', + payment_information: '0' + ) + ) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + assert_not_empty response.params['issuer_id'] + end + + def test_successful_subsequent_purchase_with_credential_on_file + gateway = MonerisGateway.new( + login: 'store3', + password: 'yesguy' + ) + gateway.expects(:ssl_post).returns(successful_first_cof_authorize_response) + assert response = gateway.authorize( + @amount, + @credit_card, + @options.merge( + issuer_id: '', + payment_indicator: 'C', + payment_information: '0' + ) + ) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + + gateway.expects(:ssl_post).returns(successful_subsequent_cof_purchase_response) + + assert response2 = gateway.purchase( + @amount, + @credit_card, + @options.merge( + order_id: response.authorization, + issuer_id: response.params['issuer_id'], + payment_indicator: 'U', + payment_information: '2' + ) + ) + assert_success response2 + assert_equal 'Approved', response2.message + assert_false response2.authorization.blank? + end + 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 ) @@ -63,60 +184,61 @@ def test_refund end def test_amount_style - assert_equal '10.34', @gateway.send(:amount, 1034) + assert_equal '10.34', @gateway.send(:amount, 1034) - assert_raise(ArgumentError) do - @gateway.send(:amount, '10.34') - end + assert_raise(ArgumentError) do + @gateway.send(:amount, '10.34') + end end def test_preauth_is_valid_xml - params = { - :order_id => 'order1', - :amount => '1.01', - :pan => '4242424242424242', - :expdate => '0303', - :crypt_type => 7, - } - - assert data = @gateway.send(:post_data, 'preauth', params) - assert REXML::Document.new(data) - assert_equal xml_capture_fixture.size, data.size + params = { + order_id: 'order1', + amount: '1.01', + pan: '4242424242424242', + expdate: '0303', + crypt_type: 7 + } + + assert data = @gateway.send(:post_data, 'preauth', params) + assert REXML::Document.new(data) + assert_equal xml_capture_fixture.size, data.size end def test_purchase_is_valid_xml - params = { - :order_id => 'order1', - :amount => '1.01', - :pan => '4242424242424242', - :expdate => '0303', - :crypt_type => 7, - } - - assert data = @gateway.send(:post_data, 'purchase', params) - assert REXML::Document.new(data) - assert_equal xml_purchase_fixture.size, data.size + params = { + order_id: 'order1', + amount: '1.01', + pan: '4242424242424242', + expdate: '0303', + crypt_type: 7 + } + + assert data = @gateway.send(:post_data, 'purchase', params) + assert REXML::Document.new(data) + assert_equal xml_purchase_fixture.size, data.size end def test_capture_is_valid_xml - params = { - :order_id => 'order1', - :amount => '1.01', - :pan => '4242424242424242', - :expdate => '0303', - :crypt_type => 7, - } - - assert data = @gateway.send(:post_data, 'preauth', params) - assert REXML::Document.new(data) - assert_equal xml_capture_fixture.size, data.size + params = { + order_id: 'order1', + amount: '1.01', + pan: '4242424242424242', + expdate: '0303', + crypt_type: 7 + } + + assert data = @gateway.send(:post_data, 'preauth', params) + assert REXML::Document.new(data) + assert_equal xml_capture_fixture.size, data.size end def test_successful_verify - response = stub_comms do - @gateway.verify(@credit_card, @options) - end.respond_with(successful_authorize_response, failed_void_response) + @gateway.expects(:ssl_post).returns(successful_verify_response) + + assert response = @gateway.verify(@credit_card, @options) assert_success response + assert_equal '125-0_14;93565164-01571', response.authorization assert_equal 'Approved', response.message end @@ -125,7 +247,7 @@ def test_supported_countries end def test_supported_card_types - assert_equal [:visa, :master, :american_express, :diners_club, :discover], MonerisGateway.supported_cardtypes + assert_equal %i[visa master american_express diners_club discover], MonerisGateway.supported_cardtypes end def test_should_raise_error_if_transaction_param_empty_on_credit_request @@ -143,6 +265,15 @@ def test_successful_store @data_key = response.params['data_key'] end + def test_successful_store_with_duration + @gateway.expects(:ssl_post).returns(successful_store_with_duration_response) + assert response = @gateway.store(@credit_card, duration: 600) + assert_success response + assert_equal 'Successfully registered cc details', response.message + assert response.params['data_key'].present? + @data_key = response.params['data_key'] + end + def test_successful_unstore @gateway.expects(:ssl_post).returns(successful_unstore_response) test_successful_store @@ -164,7 +295,7 @@ def test_update def test_successful_purchase_with_vault @gateway.expects(:ssl_post).returns(successful_purchase_response) test_successful_store - assert response = @gateway.purchase(100, @data_key, {:order_id => generate_unique_id, :customer => generate_unique_id}) + assert response = @gateway.purchase(100, @data_key, { order_id: generate_unique_id, customer: generate_unique_id }) assert_success response assert_equal 'Approved', response.message assert response.authorization.present? @@ -172,7 +303,8 @@ 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 ) @@ -184,7 +316,7 @@ def test_successful_authorize_with_network_tokenization def test_successful_authorization_with_vault @gateway.expects(:ssl_post).returns(successful_purchase_response) test_successful_store - assert response = @gateway.authorize(100, @data_key, {:order_id => generate_unique_id, :customer => generate_unique_id}) + assert response = @gateway.authorize(100, @data_key, { order_id: generate_unique_id, customer: generate_unique_id }) assert_success response assert_equal 'Approved', response.message assert response.authorization.present? @@ -203,7 +335,7 @@ def test_cvv_enabled_and_provided @credit_card.verification_value = '452' stub_comms(gateway) do gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(%r{cvd_indicator>1<}, data) assert_match(%r{cvd_value>452<}, data) end.respond_with(successful_purchase_response) @@ -215,7 +347,7 @@ def test_cvv_enabled_but_not_provided @credit_card.verification_value = '' stub_comms(gateway) do gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(%r{cvd_indicator>0<}, data) assert_no_match(%r{cvd_value>}, data) end.respond_with(successful_purchase_response) @@ -225,7 +357,7 @@ def test_cvv_disabled_and_provided @credit_card.verification_value = '452' stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_no_match(%r{cvd_value>}, data) assert_no_match(%r{cvd_indicator>}, data) end.respond_with(successful_purchase_response) @@ -235,7 +367,7 @@ def test_cvv_disabled_but_not_provided @credit_card.verification_value = '' stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_no_match(%r{cvd_value>}, data) assert_no_match(%r{cvd_indicator>}, data) end.respond_with(successful_purchase_response) @@ -246,8 +378,8 @@ def test_avs_enabled_and_provided billing_address = address(address1: '1234 Anystreet', address2: '') stub_comms(gateway) do - gateway.purchase(@amount, @credit_card, billing_address: billing_address, order_id: '1') - end.check_request do |endpoint, data, headers| + gateway.purchase(@amount, @credit_card, billing_address:, order_id: '1') + end.check_request do |_endpoint, data, _headers| assert_match(%r{avs_street_number>1234<}, data) assert_match(%r{avs_street_name>Anystreet<}, data) assert_match(%r{avs_zipcode>#{billing_address[:zip]}<}, data) @@ -259,7 +391,7 @@ def test_avs_enabled_but_not_provided stub_comms(gateway) do gateway.purchase(@amount, @credit_card, @options.tap { |x| x.delete(:billing_address) }) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_no_match(%r{avs_street_number>}, data) assert_no_match(%r{avs_street_name>}, data) assert_no_match(%r{avs_zipcode>}, data) @@ -269,8 +401,8 @@ def test_avs_enabled_but_not_provided def test_avs_disabled_and_provided billing_address = address(address1: '1234 Anystreet', address2: '') stub_comms do - @gateway.purchase(@amount, @credit_card, billing_address: billing_address, order_id: '1') - end.check_request do |endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, billing_address:, order_id: '1') + end.check_request do |_endpoint, data, _headers| assert_no_match(%r{avs_street_number>}, data) assert_no_match(%r{avs_street_name>}, data) assert_no_match(%r{avs_zipcode>}, data) @@ -280,7 +412,7 @@ def test_avs_disabled_and_provided def test_avs_disabled_and_not_provided stub_comms do @gateway.purchase(@amount, @credit_card, @options.tap { |x| x.delete(:billing_address) }) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_no_match(%r{avs_street_number>}, data) assert_no_match(%r{avs_street_name>}, data) assert_no_match(%r{avs_zipcode>}, data) @@ -292,7 +424,7 @@ def test_avs_result_valid_with_address assert response = @gateway.purchase(100, @credit_card, @options) assert_equal(response.avs_result, { 'code' => 'A', - 'message' => 'Street address matches, but 5-digit and 9-digit postal code do not match.', + 'message' => 'Street address matches, but postal code does not match.', 'street_match' => 'Y', 'postal_match' => 'N' }) @@ -301,7 +433,7 @@ def test_avs_result_valid_with_address def test_customer_can_be_specified stub_comms do @gateway.purchase(@amount, @credit_card, order_id: '3', customer: 'Joe Jones') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(%r{cust_id>Joe Jones}, data) end.respond_with(successful_purchase_response) end @@ -309,7 +441,7 @@ def test_customer_can_be_specified def test_customer_not_specified_card_name_used stub_comms do @gateway.purchase(@amount, @credit_card, order_id: '3') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(%r{cust_id>Longbob Longsen}, data) end.respond_with(successful_purchase_response) end @@ -319,7 +451,7 @@ def test_add_swipe_data_with_creditcard stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match '<pos_code>00</pos_code>', data assert_match '<track2>Track Data</track2>', data end.respond_with(successful_purchase_response) @@ -333,58 +465,384 @@ def test_supports_scrubbing? assert @gateway.supports_scrubbing? end + def test_stored_credential_recurring_cit_initial + options = stored_credential_options(:cardholder, :recurring, :initial) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<issuer_id><\/issuer_id>/, data) + assert_match(/<payment_indicator>C<\/payment_indicator>/, data) + assert_match(/<payment_information>0<\/payment_information>/, data) + end.respond_with(successful_first_cof_authorize_response) + + assert_success response + end + + def test_stored_credential_recurring_cit_used + options = stored_credential_options(:cardholder, :recurring, id: 'abc123') + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<issuer_id>abc123<\/issuer_id>/, data) + assert_match(/<payment_indicator>Z<\/payment_indicator>/, data) + assert_match(/<payment_information>2<\/payment_information>/, data) + end.respond_with(successful_first_cof_authorize_response) + + assert_success response + end + + def test_stored_credential_recurring_mit_initial + options = stored_credential_options(:merchant, :recurring, :initial) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<issuer_id><\/issuer_id>/, data) + assert_match(/<payment_indicator>R<\/payment_indicator>/, data) + assert_match(/<payment_information>0<\/payment_information>/, data) + end.respond_with(successful_first_cof_authorize_response) + + assert_success response + end + + def test_stored_credential_recurring_mit_used + options = stored_credential_options(:merchant, :recurring, id: 'abc123') + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<issuer_id>abc123<\/issuer_id>/, data) + assert_match(/<payment_indicator>R<\/payment_indicator>/, data) + assert_match(/<payment_information>2<\/payment_information>/, data) + end.respond_with(successful_first_cof_authorize_response) + + assert_success response + end + + def test_stored_credential_installment_cit_initial + options = stored_credential_options(:cardholder, :installment, :initial) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<issuer_id><\/issuer_id>/, data) + assert_match(/<payment_indicator>C<\/payment_indicator>/, data) + assert_match(/<payment_information>0<\/payment_information>/, data) + end.respond_with(successful_first_cof_authorize_response) + + assert_success response + end + + def test_stored_credential_installment_cit_used + options = stored_credential_options(:cardholder, :installment, id: 'abc123') + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<issuer_id>abc123<\/issuer_id>/, data) + assert_match(/<payment_indicator>Z<\/payment_indicator>/, data) + assert_match(/<payment_information>2<\/payment_information>/, data) + end.respond_with(successful_first_cof_authorize_response) + + assert_success response + end + + def test_stored_credential_installment_mit_initial + options = stored_credential_options(:merchant, :installment, :initial) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<issuer_id><\/issuer_id>/, data) + assert_match(/<payment_indicator>R<\/payment_indicator>/, data) + assert_match(/<payment_information>0<\/payment_information>/, data) + end.respond_with(successful_first_cof_authorize_response) + + assert_success response + end + + def test_stored_credential_installment_mit_used + options = stored_credential_options(:merchant, :installment, id: 'abc123') + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<issuer_id>abc123<\/issuer_id>/, data) + assert_match(/<payment_indicator>R<\/payment_indicator>/, data) + assert_match(/<payment_information>2<\/payment_information>/, data) + end.respond_with(successful_first_cof_authorize_response) + + assert_success response + end + + def test_stored_credential_unscheduled_cit_initial + options = stored_credential_options(:cardholder, :unscheduled, :initial) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<issuer_id><\/issuer_id>/, data) + assert_match(/<payment_indicator>C<\/payment_indicator>/, data) + assert_match(/<payment_information>0<\/payment_information>/, data) + end.respond_with(successful_first_cof_authorize_response) + + assert_success response + end + + def test_stored_credential_unscheduled_cit_used + options = stored_credential_options(:cardholder, :unscheduled, id: 'abc123') + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<issuer_id>abc123<\/issuer_id>/, data) + assert_match(/<payment_indicator>Z<\/payment_indicator>/, data) + assert_match(/<payment_information>2<\/payment_information>/, data) + end.respond_with(successful_first_cof_authorize_response) + + assert_success response + end + + def test_stored_credential_unscheduled_mit_initial + options = stored_credential_options(:merchant, :unscheduled, :initial) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<issuer_id><\/issuer_id>/, data) + assert_match(/<payment_indicator>C<\/payment_indicator>/, data) + assert_match(/<payment_information>0<\/payment_information>/, data) + end.respond_with(successful_first_cof_authorize_response) + + assert_success response + end + + def test_stored_credential_unscheduled_mit_used + options = stored_credential_options(:merchant, :unscheduled, id: 'abc123') + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<issuer_id>abc123<\/issuer_id>/, data) + assert_match(/<payment_indicator>U<\/payment_indicator>/, data) + assert_match(/<payment_information>2<\/payment_information>/, data) + end.respond_with(successful_first_cof_authorize_response) + + assert_success response + end + + def test_add_cof_overrides_stored_credential_option + options = stored_credential_options(:merchant, :unscheduled, id: 'abc123').merge(issuer_id: 'xyz987', payment_indicator: 'R', payment_information: '0') + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<issuer_id>xyz987<\/issuer_id>/, data) + assert_match(/<payment_indicator>R<\/payment_indicator>/, data) + assert_match(/<payment_information>0<\/payment_information>/, data) + end.respond_with(successful_first_cof_authorize_response) + + assert_success response + end + private + def stored_credential_options(*args, id: nil) + { + order_id: '#1001', + description: 'AM test', + currency: 'CAD', + customer: '123', + stored_credential: stored_credential(*args, id:), + issuer_id: '' + } + end + def successful_purchase_response - <<-RESPONSE -<?xml version="1.0"?> -<response> - <receipt> - <ReceiptId>1026.1</ReceiptId> - <ReferenceNum>661221050010170010</ReferenceNum> - <ResponseCode>027</ResponseCode> - <ISO>01</ISO> - <AuthCode>013511</AuthCode> - <TransTime>18:41:13</TransTime> - <TransDate>2008-01-05</TransDate> - <TransType>00</TransType> - <Complete>true</Complete> - <Message>APPROVED * =</Message> - <TransAmount>1.00</TransAmount> - <CardType>V</CardType> - <TransID>58-0_3</TransID> - <TimedOut>false</TimedOut> - </receipt> -</response> + <<~RESPONSE + <?xml version="1.0"?> + <response> + <receipt> + <ReceiptId>1026.1</ReceiptId> + <ReferenceNum>661221050010170010</ReferenceNum> + <ResponseCode>027</ResponseCode> + <ISO>01</ISO> + <AuthCode>013511</AuthCode> + <TransTime>18:41:13</TransTime> + <TransDate>2008-01-05</TransDate> + <TransType>00</TransType> + <Complete>true</Complete> + <Message>APPROVED * =</Message> + <TransAmount>1.00</TransAmount> + <CardType>V</CardType> + <TransID>58-0_3</TransID> + <TimedOut>false</TimedOut> + </receipt> + </response> + + RESPONSE + end + + def successful_cavv_purchase_response + <<~RESPONSE + <?xml version="1.0" standalone="yes"?> + <response> + <receipt> + <ReceiptId>a131684dbecc1d89d9927c539ed3791b</ReceiptId> + <ReferenceNum>660148420010137130</ReferenceNum> + <ResponseCode>027</ResponseCode> + <ISO>01</ISO> + <AuthCode>655371</AuthCode> + <TransTime>18:26:32</TransTime> + <TransDate>2022-03-25</TransDate> + <TransType>00</TransType> + <Complete>true</Complete> + <Message>APPROVED * =</Message> + <TransAmount>1.00</TransAmount> + <CardType>V</CardType> + <TransID>69785-0_98</TransID> + <TimedOut>false</TimedOut> + <BankTotals>null</BankTotals> + <Ticket>null</Ticket> + <CorporateCard>false</CorporateCard> + <CavvResultCode>2</CavvResultCode> + <IsVisaDebit>false</IsVisaDebit> + </receipt> + </response> + + RESPONSE + end + + def failed_cavv_purchase_response + # this response is exactly the same as successful_cavv_purchase_response MINUS the CavvResultCode + <<~RESPONSE + <?xml version="1.0" standalone="yes"?> + <response> + <receipt> + <ReceiptId>a131684dbecc1d89d9927c539ed3791b</ReceiptId> + <ReferenceNum>660148420010137130</ReferenceNum> + <ResponseCode>027</ResponseCode> + <ISO>01</ISO> + <AuthCode>655371</AuthCode> + <TransTime>18:26:32</TransTime> + <TransDate>2022-03-25</TransDate> + <TransType>00</TransType> + <Complete>true</Complete> + <Message>APPROVED * =</Message> + <TransAmount>1.00</TransAmount> + <CardType>V</CardType> + <TransID>69785-0_98</TransID> + <TimedOut>false</TimedOut> + <BankTotals>null</BankTotals> + <Ticket>null</Ticket> + <CorporateCard>false</CorporateCard> + <IsVisaDebit>false</IsVisaDebit> + </receipt> + </response> + + RESPONSE + end + + def successful_first_cof_purchase_response + <<~RESPONSE + <?xml version=\"1.0\" standalone=\"yes\"?> + <?xml version=“1.0” standalone=“yes”?> + <response> + <receipt> + <ReceiptId>a33ba7edd448b91ef8d2f85fea614b8d</ReceiptId> + <ReferenceNum>660114080015099160</ReferenceNum> + <ResponseCode>027</ResponseCode> + <ISO>01</ISO> + <AuthCode>822665</AuthCode> + <TransTime>07:43:28</TransTime> + <TransDate>2018-11-11</TransDate> + <TransType>00</TransType> + <Complete>true</Complete> + <Message>APPROVED * =</Message> + <TransAmount>1.00</TransAmount> + <CardType>V</CardType> + <TransID>799655-0_11</TransID> + <TimedOut>false</TimedOut> + <BankTotals>null</BankTotals> + <Ticket>null</Ticket> + <IssuerId>355689484440192</IssuerId> + <IsVisaDebit>false</IsVisaDebit> + </receipt> + </response> + RESPONSE + end + + def successful_first_cof_authorize_response + <<~RESPONSE + <?xml version=\"1.0\" standalone=\"yes\"?> + <response> + <receipt> + <ReceiptId>8dbc28468af2007779bbede7ec1bab6c</ReceiptId> + <ReferenceNum>660109300018229130</ReferenceNum> + <ResponseCode>027</ResponseCode> + <ISO>01</ISO> + <AuthCode>718280</AuthCode> + <TransTime>07:50:53</TransTime> + <TransDate>2018-11-11</TransDate> + <TransType>01</TransType> + <Complete>true</Complete> + <Message>APPROVED * =</Message> + <TransAmount>1.00</TransAmount> + <CardType>V</CardType> + <TransID>830724-0_11</TransID> + <TimedOut>false</TimedOut> + <BankTotals>null</BankTotals> + <Ticket>null</Ticket> + <MessageId>1A8315282537312</MessageId> + <IssuerId>550923784451193</IssuerId> + <IsVisaDebit>false</IsVisaDebit> + </receipt> + </response> + RESPONSE + end + def successful_subsequent_cof_purchase_response + <<~RESPONSE + <?xml version="1.0" standalone="yes"?> + <response> + <receipt> + <ReceiptId>830724-0_11;8dbc28468af2007779bbede7ec1bab6c</ReceiptId> + <ReferenceNum>660109490014038930</ReferenceNum> + <ResponseCode>027</ResponseCode> + <ISO>01</ISO> + <AuthCode>111234</AuthCode> + <TransTime>07:50:54</TransTime> + <TransDate>2018-11-11</TransDate> + <TransType>00</TransType> + <Complete>true</Complete> + <Message>APPROVED * =</Message> + <TransAmount>1.00</TransAmount> + <CardType>V</CardType> + <TransID>455422-0_11</TransID> + <TimedOut>false</TimedOut> + <BankTotals>null</BankTotals> + <Ticket>null</Ticket> + <IssuerId>762097792112819</IssuerId> + <IsVisaDebit>false</IsVisaDebit> + </receipt> + </response> RESPONSE end def successful_purchase_network_tokenization - <<-RESPONSE -<?xml version="1.0"?> -<response> - <receipt> - <ReceiptId>0bbb277b543a17b6781243889a689573</ReceiptId> - <ReferenceNum>660110910011133780</ReferenceNum> - <ResponseCode>027</ResponseCode> - <ISO>01</ISO> - <AuthCode>368269</AuthCode> - <TransTime>22:54:10</TransTime> - <TransDate>2015-07-05</TransDate> - <TransType>00</TransType> - <Complete>true</Complete> - <Message>APPROVED * =</Message> - <TransAmount>1.00</TransAmount> - <CardType>V</CardType> - <TransID>101965-0_10</TransID> - <TimedOut>false</TimedOut> - <BankTotals>null</BankTotals> - <Ticket>null</Ticket> - <CorporateCard>false</CorporateCard> - <IsVisaDebit>false</IsVisaDebit> - </receipt> -</response> + <<~RESPONSE + <?xml version="1.0"?> + <response> + <receipt> + <ReceiptId>0bbb277b543a17b6781243889a689573</ReceiptId> + <ReferenceNum>660110910011133780</ReferenceNum> + <ResponseCode>027</ResponseCode> + <ISO>01</ISO> + <AuthCode>368269</AuthCode> + <TransTime>22:54:10</TransTime> + <TransDate>2015-07-05</TransDate> + <TransType>00</TransType> + <Complete>true</Complete> + <Message>APPROVED * =</Message> + <TransAmount>1.00</TransAmount> + <CardType>V</CardType> + <TransID>101965-0_10</TransID> + <TimedOut>false</TimedOut> + <BankTotals>null</BankTotals> + <Ticket>null</Ticket> + <CorporateCard>false</CorporateCard> + <IsVisaDebit>false</IsVisaDebit> + </receipt> + </response> RESPONSE end @@ -419,128 +877,163 @@ def successful_authorize_response end def successful_authorization_network_tokenization - <<-RESPONSE -<?xml version="1.0"?> -<response> - <receipt> - <ReceiptId>d88d9f5f3472898832c54d6b5572757e</ReceiptId> - <ReferenceNum>660110910011139740</ReferenceNum> - <ResponseCode>027</ResponseCode> - <ISO>01</ISO> - <AuthCode>873534</AuthCode> - <TransTime>09:31:41</TransTime> - <TransDate>2015-07-09</TransDate> - <TransType>01</TransType> - <Complete>true</Complete> - <Message>APPROVED * =</Message> - <TransAmount>1.00</TransAmount> - <CardType>V</CardType> - <TransID>109232-0_10</TransID> - <TimedOut>false</TimedOut> - <BankTotals>null</BankTotals> - <Ticket>null</Ticket> - <CorporateCard>false</CorporateCard> - <IsVisaDebit>false</IsVisaDebit> - </receipt> -</response> + <<~RESPONSE + <?xml version="1.0"?> + <response> + <receipt> + <ReceiptId>d88d9f5f3472898832c54d6b5572757e</ReceiptId> + <ReferenceNum>660110910011139740</ReferenceNum> + <ResponseCode>027</ResponseCode> + <ISO>01</ISO> + <AuthCode>873534</AuthCode> + <TransTime>09:31:41</TransTime> + <TransDate>2015-07-09</TransDate> + <TransType>01</TransType> + <Complete>true</Complete> + <Message>APPROVED * =</Message> + <TransAmount>1.00</TransAmount> + <CardType>V</CardType> + <TransID>109232-0_10</TransID> + <TimedOut>false</TimedOut> + <BankTotals>null</BankTotals> + <Ticket>null</Ticket> + <CorporateCard>false</CorporateCard> + <IsVisaDebit>false</IsVisaDebit> + </receipt> + </response> RESPONSE end def successful_purchase_response_with_avs_result - <<-RESPONSE -<?xml version="1.0"?> -<response> - <receipt> - <ReceiptId>9c7189ec64b58f541335be1ca6294d09</ReceiptId> - <ReferenceNum>660110910011136190</ReferenceNum> - <ResponseCode>027</ResponseCode> - <ISO>01</ISO> - <AuthCode>115497</AuthCode> - <TransTime>15:20:51</TransTime> - <TransDate>2014-06-18</TransDate> - <TransType>00</TransType> - <Complete>true</Complete><Message>APPROVED * =</Message> - <TransAmount>10.10</TransAmount> - <CardType>V</CardType> - <TransID>491573-0_9</TransID> - <TimedOut>false</TimedOut> - <BankTotals>null</BankTotals> - <Ticket>null</Ticket> - <CorporateCard>false</CorporateCard> - <AvsResultCode>A</AvsResultCode> - <ITDResponse>null</ITDResponse> - <IsVisaDebit>false</IsVisaDebit> - </receipt> -</response> + <<~RESPONSE + <?xml version="1.0"?> + <response> + <receipt> + <ReceiptId>9c7189ec64b58f541335be1ca6294d09</ReceiptId> + <ReferenceNum>660110910011136190</ReferenceNum> + <ResponseCode>027</ResponseCode> + <ISO>01</ISO> + <AuthCode>115497</AuthCode> + <TransTime>15:20:51</TransTime> + <TransDate>2014-06-18</TransDate> + <TransType>00</TransType> + <Complete>true</Complete><Message>APPROVED * =</Message> + <TransAmount>10.10</TransAmount> + <CardType>V</CardType> + <TransID>491573-0_9</TransID> + <TimedOut>false</TimedOut> + <BankTotals>null</BankTotals> + <Ticket>null</Ticket> + <CorporateCard>false</CorporateCard> + <AvsResultCode>A</AvsResultCode> + <ITDResponse>null</ITDResponse> + <IsVisaDebit>false</IsVisaDebit> + </receipt> + </response> RESPONSE end def failed_purchase_response - <<-RESPONSE -<?xml version="1.0"?> -<response> - <receipt> - <ReceiptId>1026.1</ReceiptId> - <ReferenceNum>661221050010170010</ReferenceNum> - <ResponseCode>481</ResponseCode> - <ISO>01</ISO> - <AuthCode>013511</AuthCode> - <TransTime>18:41:13</TransTime> - <TransDate>2008-01-05</TransDate> - <TransType>00</TransType> - <Complete>true</Complete> - <Message>DECLINED * =</Message> - <TransAmount>1.00</TransAmount> - <CardType>V</CardType> - <TransID>97-2-0</TransID> - <TimedOut>false</TimedOut> - </receipt> -</response> + <<~RESPONSE + <?xml version="1.0"?> + <response> + <receipt> + <ReceiptId>1026.1</ReceiptId> + <ReferenceNum>661221050010170010</ReferenceNum> + <ResponseCode>481</ResponseCode> + <ISO>01</ISO> + <AuthCode>013511</AuthCode> + <TransTime>18:41:13</TransTime> + <TransDate>2008-01-05</TransDate> + <TransType>00</TransType> + <Complete>true</Complete> + <Message>DECLINED * =</Message> + <TransAmount>1.00</TransAmount> + <CardType>V</CardType> + <TransID>97-2-0</TransID> + <TimedOut>false</TimedOut> + </receipt> + </response> RESPONSE end def successful_store_response - <<-RESPONSE -<?xml version="1.0"?> -<response> - <receipt> - <DataKey>1234567890</DataKey> - <ResponseCode>027</ResponseCode> - <Complete>true</Complete> - <Message>Successfully registered cc details * =</Message> - </receipt> -</response> + <<~RESPONSE + <?xml version="1.0"?> + <response> + <receipt> + <DataKey>1234567890</DataKey> + <ResponseCode>027</ResponseCode> + <Complete>true</Complete> + <Message>Successfully registered cc details * =</Message> + </receipt> + </response> + RESPONSE + end + + def successful_store_with_duration_response + <<~RESPONSE + <?xml version="1.0"?> + <response> + <receipt> + <DataKey>1234567890</DataKey> + <ReceiptId>null</ReceiptId> + <ReferenceNum>null</ReferenceNum> + <ResponseCode>001</ResponseCode> + <ISO>null</ISO> + <AuthCode>null</AuthCode> + <Message>Successfully registered CC details.</Message> + <TransType>null</TransType> + <Complete>true</Complete> + <TransAmount>null</TransAmount> + <CardType>null</CardType> + <TransID>null</TransID> + <TimedOut>false</TimedOut> + <CorporateCard>null</CorporateCard> + <RecurSuccess>null</RecurSuccess> + <AvsResultCode>null</AvsResultCode> + <CvdResultCode>null</CvdResultCode> + <ResSuccess>true</ResSuccess> + <PaymentType>cc</PaymentType> + <IsVisaDebit>null</IsVisaDebit> + <ResolveData> + <anc1/> + <masked_pan>4242***4242</masked_pan> + <expdate>2010</expdate> + </ResolveData> + </receipt> + </response> RESPONSE end def successful_unstore_response - <<-RESPONSE -<?xml version="1.0"?> -<response> - <receipt> - <DataKey>1234567890</DataKey> - <ResponseCode>027</ResponseCode> - <Complete>true</Complete> - <Message>Successfully deleted cc details * =</Message> - </receipt> -</response> + <<~RESPONSE + <?xml version="1.0"?> + <response> + <receipt> + <DataKey>1234567890</DataKey> + <ResponseCode>027</ResponseCode> + <Complete>true</Complete> + <Message>Successfully deleted cc details * =</Message> + </receipt> + </response> RESPONSE end def successful_update_response - <<-RESPONSE -<?xml version="1.0"?> -<response> - <receipt> - <DataKey>1234567890</DataKey> - <ResponseCode>027</ResponseCode> - <Complete>true</Complete> - <Message>Successfully updated cc details * =</Message> - </receipt> -</response> + <<~RESPONSE + <?xml version="1.0"?> + <response> + <receipt> + <DataKey>1234567890</DataKey> + <ResponseCode>027</ResponseCode> + <Complete>true</Complete> + <Message>Successfully updated cc details * =</Message> + </receipt> + </response> RESPONSE end @@ -571,16 +1064,47 @@ def failed_void_response RESPONSE end + def successful_verify_response + <<-RESPONSE + <?xml version="1.0" standalone="yes"?> + <response> + <receipt> + <ReceiptId>93565164-01571</ReceiptId> + <ReferenceNum>660158360010251110</ReferenceNum> + <ResponseCode>027</ResponseCode> + <ISO>01</ISO> + <AuthCode>000000</AuthCode> + <TransTime>16:06:11</TransTime> + <TransDate>2019-11-04</TransDate> + <TransType>06</TransType> + <Complete>true</Complete> + <Message>APPROVED * =</Message> + <TransAmount>0.00</TransAmount> + <CardType>V</CardType> + <TransID>125-0_14</TransID> + <TimedOut>false</TimedOut> + <BankTotals>null</BankTotals> + <Ticket>null</Ticket> + <AvsResultCode>null</AvsResultCode> + <ITDResponse>null</ITDResponse> + <CvdResultCode>1M</CvdResultCode> + <CavvResultCode>2</CavvResultCode> + <IsVisaDebit>false</IsVisaDebit> + </receipt> + </response> + RESPONSE + end + def xml_purchase_fixture - '<request><store_id>store1</store_id><api_token>yesguy</api_token><purchase><amount>1.01</amount><pan>4242424242424242</pan><expdate>0303</expdate><crypt_type>7</crypt_type><order_id>order1</order_id></purchase></request>' + '<request><store_id>store1</store_id><api_token>yesguy</api_token><purchase><amount>1.01</amount><pan>4242424242424242</pan><expdate>0303</expdate><crypt_type>7</crypt_type><order_id>order1</order_id></purchase></request>' end def xml_capture_fixture - '<request><store_id>store1</store_id><api_token>yesguy</api_token><preauth><amount>1.01</amount><pan>4242424242424242</pan><expdate>0303</expdate><crypt_type>7</crypt_type><order_id>order1</order_id></preauth></request>' + '<request><store_id>store1</store_id><api_token>yesguy</api_token><preauth><amount>1.01</amount><pan>4242424242424242</pan><expdate>0303</expdate><crypt_type>7</crypt_type><order_id>order1</order_id></preauth></request>' end def pre_scrub - <<-pre_scrub + <<-REQUEST opening connection to esqa.moneris.com:443... opened starting SSL for esqa.moneris.com:443... @@ -604,11 +1128,11 @@ def pre_scrub -> "0\r\n" -> "\r\n" Conn close - pre_scrub + REQUEST end def post_scrub - <<-post_scrub + <<-REQUEST opening connection to esqa.moneris.com:443... opened starting SSL for esqa.moneris.com:443... @@ -632,6 +1156,6 @@ def post_scrub -> "0\r\n" -> "\r\n" Conn close - post_scrub + REQUEST end end diff --git a/test/unit/gateways/moneris_us_test.rb b/test/unit/gateways/moneris_us_test.rb deleted file mode 100644 index 9d358635147..00000000000 --- a/test/unit/gateways/moneris_us_test.rb +++ /dev/null @@ -1,652 +0,0 @@ -require 'test_helper' - -class MonerisUsTest < Test::Unit::TestCase - include CommStub - - def setup - Base.mode = :test - - @gateway = MonerisUsGateway.new( - :login => 'monusqa002', - :password => 'qatoken' - ) - - @amount = 100 - @credit_card = credit_card('4242424242424242') - @check = check({ - routing_number: '011000015', - account_number: '1234455', - number: 123 - }) - @options = { :order_id => '1', :billing_address => address } - end - - def test_default_options - assert_equal 7, @gateway.options[:crypt_type] - assert_equal 'monusqa002', @gateway.options[:login] - assert_equal 'qatoken', @gateway.options[:password] - end - - def test_successful_purchase - @gateway.expects(:ssl_post).returns(successful_purchase_response) - - assert response = @gateway.authorize(100, @credit_card, @options) - assert_success response - assert_equal '58-0_3;1026.1', response.authorization - end - - def test_failed_purchase - @gateway.expects(:ssl_post).returns(failed_purchase_response) - - assert response = @gateway.authorize(100, @credit_card, @options) - assert_failure response - end - - def test_successful_echeck_purchase - @gateway.expects(:ssl_post).returns(successful_echeck_purchase_response) - - assert response = @gateway.authorize(100, @check, @options) - assert_success response - assert_equal '1522-0_25;cb80f38f44af2168fd9033cdf2d0d4c0', response.authorization - end - - def test_deprecated_credit - @gateway.expects(:ssl_post).with(anything, regexp_matches(/txn_number>123<\//), anything).returns('') - @gateway.expects(:parse).returns({}) - assert_deprecation_warning(Gateway::CREDIT_DEPRECATION_MESSAGE) do - @gateway.credit(@amount, '123;456', @options) - end - end - - def test_refund - @gateway.expects(:ssl_post).with(anything, regexp_matches(/txn_number>123<\//), anything).returns('') - @gateway.expects(:parse).returns({}) - @gateway.refund(@amount, '123;456', @options) - end - - def test_successful_verify - response = stub_comms do - @gateway.verify(@credit_card, @options) - end.respond_with(successful_authorize_response, successful_capture_response) - assert_success response - assert_equal '830337-0_25;d315c7a28623dec77dc136450692d2dd', response.authorization - end - - def test_successful_verify_and_failed_void - response = stub_comms do - @gateway.verify(@credit_card, @options) - end.respond_with(successful_authorize_response, failed_capture_response) - assert_success response - assert_equal '830337-0_25;d315c7a28623dec77dc136450692d2dd', response.authorization - assert_equal 'Approved', response.message - end - - def test_failed_verify - response = stub_comms do - @gateway.verify(@credit_card, @options) - end.respond_with(failed_authorize_response, successful_capture_response) - assert_failure response - assert_equal 'Declined', response.message - end - - def test_amount_style - assert_equal '10.34', @gateway.send(:amount, 1034) - - assert_raise(ArgumentError) do - @gateway.send(:amount, '10.34') - end - end - - def test_preauth_is_valid_xml - - params = { - :order_id => 'order1', - :amount => '1.01', - :pan => '4242424242424242', - :expdate => '0303', - :crypt_type => 7, - } - - assert data = @gateway.send(:post_data, 'us_preauth', params) - assert REXML::Document.new(data) - assert_equal xml_capture_fixture.size, data.size - end - - def test_purchase_is_valid_xml - - params = { - :order_id => 'order1', - :amount => '1.01', - :pan => '4242424242424242', - :expdate => '0303', - :crypt_type => 7, - } - - assert data = @gateway.send(:post_data, 'us_purchase', params) - assert REXML::Document.new(data) - assert_equal xml_purchase_fixture.size, data.size - end - - def test_capture_is_valid_xml - - params = { - :order_id => 'order1', - :amount => '1.01', - :pan => '4242424242424242', - :expdate => '0303', - :crypt_type => 7, - } - - assert data = @gateway.send(:post_data, 'us_preauth', params) - assert REXML::Document.new(data) - assert_equal xml_capture_fixture.size, data.size - end - - def test_supported_countries - assert_equal ['US'], MonerisUsGateway.supported_countries - end - - def test_supported_card_types - assert_equal [:visa, :master, :american_express, :diners_club, :discover], MonerisUsGateway.supported_cardtypes - end - - def test_should_raise_error_if_transaction_param_empty_on_credit_request - [nil, '', '1234'].each do |invalid_transaction_param| - assert_raise(ArgumentError) { @gateway.void(invalid_transaction_param) } - end - end - - def test_successful_store - @gateway.expects(:ssl_post).returns(successful_store_response) - assert response = @gateway.store(@credit_card) - assert_success response - assert_equal 'Successfully registered cc details', response.message - assert response.params['data_key'].present? - @data_key = response.params['data_key'] - end - - def test_successful_unstore - @gateway.expects(:ssl_post).returns(successful_unstore_response) - test_successful_store - assert response = @gateway.unstore(@data_key) - assert_success response - assert_equal 'Successfully deleted cc details', response.message - assert response.params['data_key'].present? - end - - def test_update - @gateway.expects(:ssl_post).returns(successful_update_response) - test_successful_store - assert response = @gateway.update(@data_key, @credit_card) - assert_success response - assert_equal 'Successfully updated cc details', response.message - assert response.params['data_key'].present? - end - - def test_successful_purchase_with_vault - @gateway.expects(:ssl_post).returns(successful_purchase_response) - test_successful_store - assert response = @gateway.purchase(100, @data_key, {:order_id => generate_unique_id, :customer => generate_unique_id}) - assert_success response - assert_equal 'Approved', response.message - assert response.authorization.present? - end - - def test_successful_authorization_with_vault - @gateway.expects(:ssl_post).returns(successful_purchase_response) - test_successful_store - assert response = @gateway.authorize(100, @data_key, {:order_id => generate_unique_id, :customer => generate_unique_id}) - assert_success response - assert_equal 'Approved', response.message - assert response.authorization.present? - end - - def test_failed_authorization_with_vault - @gateway.expects(:ssl_post).returns(failed_purchase_response) - test_successful_store - assert response = @gateway.authorize(100, @data_key, @options) - assert_failure response - end - - def test_cvv_enabled_and_provided - gateway = MonerisGateway.new(login: 'store1', password: 'yesguy', cvv_enabled: true) - - @credit_card.verification_value = '452' - stub_comms(gateway) do - gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| - assert_match(%r{cvd_indicator>1<}, data) - assert_match(%r{cvd_value>452<}, data) - end.respond_with(successful_purchase_response) - end - - def test_cvv_enabled_but_not_provided - gateway = MonerisGateway.new(login: 'store1', password: 'yesguy', cvv_enabled: true) - - @credit_card.verification_value = '' - stub_comms(gateway) do - gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| - assert_match(%r{cvd_indicator>0<}, data) - assert_no_match(%r{cvd_value>}, data) - end.respond_with(successful_purchase_response) - end - - def test_cvv_disabled_and_provided - @credit_card.verification_value = '452' - stub_comms do - @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| - assert_no_match(%r{cvd_value>}, data) - assert_no_match(%r{cvd_indicator>}, data) - end.respond_with(successful_purchase_response) - end - - def test_cvv_disabled_but_not_provided - @credit_card.verification_value = '' - stub_comms do - @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| - assert_no_match(%r{cvd_value>}, data) - assert_no_match(%r{cvd_indicator>}, data) - end.respond_with(successful_purchase_response) - end - - def test_avs_enabled_and_provided - gateway = MonerisGateway.new(login: 'store1', password: 'yesguy', avs_enabled: true) - - billing_address = address(address1: '1234 Anystreet', address2: '') - stub_comms(gateway) do - gateway.purchase(@amount, @credit_card, billing_address: billing_address, order_id: '1') - end.check_request do |endpoint, data, headers| - assert_match(%r{avs_street_number>1234<}, data) - assert_match(%r{avs_street_name>Anystreet<}, data) - assert_match(%r{avs_zipcode>#{billing_address[:zip]}<}, data) - end.respond_with(successful_purchase_response_with_avs_result) - end - - def test_avs_enabled_but_not_provided - gateway = MonerisGateway.new(login: 'store1', password: 'yesguy', avs_enabled: true) - - stub_comms(gateway) do - gateway.purchase(@amount, @credit_card, @options.tap { |x| x.delete(:billing_address) }) - end.check_request do |endpoint, data, headers| - assert_no_match(%r{avs_street_number>}, data) - assert_no_match(%r{avs_street_name>}, data) - assert_no_match(%r{avs_zipcode>}, data) - end.respond_with(successful_purchase_response) - end - - def test_avs_disabled_and_provided - billing_address = address(address1: '1234 Anystreet', address2: '') - stub_comms do - @gateway.purchase(@amount, @credit_card, billing_address: billing_address, order_id: '1') - end.check_request do |endpoint, data, headers| - assert_no_match(%r{avs_street_number>}, data) - assert_no_match(%r{avs_street_name>}, data) - assert_no_match(%r{avs_zipcode>}, data) - end.respond_with(successful_purchase_response_with_avs_result) - end - - def test_avs_disabled_and_not_provided - stub_comms do - @gateway.purchase(@amount, @credit_card, @options.tap { |x| x.delete(:billing_address) }) - end.check_request do |endpoint, data, headers| - assert_no_match(%r{avs_street_number>}, data) - assert_no_match(%r{avs_street_name>}, data) - assert_no_match(%r{avs_zipcode>}, data) - end.respond_with(successful_purchase_response) - end - - def test_avs_result_valid_with_address - @gateway.expects(:ssl_post).returns(successful_purchase_response_with_avs_result) - assert response = @gateway.purchase(100, @credit_card, @options) - assert_equal(response.avs_result, { - 'code' => 'A', - 'message' => 'Street address matches, but 5-digit and 9-digit postal code do not match.', - 'street_match' => 'Y', - 'postal_match' => 'N' - }) - end - - def test_customer_can_be_specified - stub_comms do - @gateway.purchase(@amount, @credit_card, order_id: '3', customer: 'Joe Jones') - end.check_request do |endpoint, data, headers| - assert_match(%r{cust_id>Joe Jones}, data) - end.respond_with(successful_purchase_response) - end - - def test_customer_not_specified_card_name_used - stub_comms do - @gateway.purchase(@amount, @credit_card, order_id: '3') - end.check_request do |endpoint, data, headers| - assert_match(%r{cust_id>Longbob Longsen}, data) - end.respond_with(successful_purchase_response) - end - - def test_transcript_scrubbing - assert @gateway.supports_scrubbing? - assert_equal @gateway.scrub(pre_scrub), post_scrub - end - - private - - def successful_purchase_response - <<-RESPONSE - <?xml version="1.0"?> - <response> - <receipt> - <ReceiptId>1026.1</ReceiptId> - <ReferenceNum>661221050010170010</ReferenceNum> - <ResponseCode>027</ResponseCode> - <ISO>01</ISO> - <AuthCode>013511</AuthCode> - <TransTime>18:41:13</TransTime> - <TransDate>2008-01-05</TransDate> - <TransType>00</TransType> - <Complete>true</Complete> - <Message>APPROVED * =</Message> - <TransAmount>1.00</TransAmount> - <CardType>V</CardType> - <TransID>58-0_3</TransID> - <TimedOut>false</TimedOut> - </receipt> - </response> - RESPONSE - end - - def successful_purchase_response_with_avs_result - <<-RESPONSE - <?xml version="1.0"?> - <response> - <receipt> - <ReceiptId>9c7189ec64b58f541335be1ca6294d09</ReceiptId> - <ReferenceNum>660110910011136190</ReferenceNum> - <ResponseCode>027</ResponseCode> - <ISO>01</ISO> - <AuthCode>115497</AuthCode> - <TransTime>15:20:51</TransTime> - <TransDate>2014-06-18</TransDate> - <TransType>00</TransType> - <Complete>true</Complete><Message>APPROVED * =</Message> - <TransAmount>10.10</TransAmount> - <CardType>V</CardType> - <TransID>491573-0_9</TransID> - <TimedOut>false</TimedOut> - <BankTotals>null</BankTotals> - <Ticket>null</Ticket> - <CorporateCard>false</CorporateCard> - <AvsResultCode>A</AvsResultCode> - <ITDResponse>null</ITDResponse> - <IsVisaDebit>false</IsVisaDebit> - </receipt> - </response> - RESPONSE - end - - def failed_purchase_response - <<-RESPONSE - <?xml version="1.0"?> - <response> - <receipt> - <ReceiptId>1026.1</ReceiptId> - <ReferenceNum>661221050010170010</ReferenceNum> - <ResponseCode>481</ResponseCode> - <ISO>01</ISO> - <AuthCode>013511</AuthCode> - <TransTime>18:41:13</TransTime> - <TransDate>2008-01-05</TransDate> - <TransType>00</TransType> - <Complete>true</Complete> - <Message>DECLINED * =</Message> - <TransAmount>1.00</TransAmount> - <CardType>V</CardType> - <TransID>97-2-0</TransID> - <TimedOut>false</TimedOut> - </receipt> - </response> - RESPONSE - end - - def successful_authorize_response - <<-RESPONSE - <?xml version="1.0" encoding="UTF-8"?> - <response> - <receipt> - <ReceiptId>d315c7a28623dec77dc136450692d2dd</ReceiptId> - <ReferenceNum>640000030011763320</ReferenceNum> - <ResponseCode>001</ResponseCode> - <ISO>00</ISO> - <AuthCode>372611</AuthCode> - <TransTime>09:08:58</TransTime> - <TransDate>2015-04-21</TransDate> - <TransType>01</TransType> - <Complete>true</Complete> - <Message>APPROVED*</Message> - <TransAmount>1.00</TransAmount> - <CardType>V</CardType> - <TransID>830337-0_25</TransID> - <TimedOut>false</TimedOut> - <BankTotals>null</BankTotals> - <Ticket>null</Ticket> - <CorporateCard>false</CorporateCard> - <CardLevelResult>A</CardLevelResult> - <CavvResultCode /> - </receipt> - </response> - RESPONSE - end - - def failed_authorize_response - <<-RESPONSE - <?xml version="1.0" encoding="UTF-8"?> - <response> - <receipt> - <ReceiptId>1fa06a83bbd1285ccfa1312835d5aa8d</ReceiptId> - <ReferenceNum>640020510015803330</ReferenceNum> - <ResponseCode>481</ResponseCode> - <ISO>05</ISO> - <AuthCode>242724</AuthCode> - <TransTime>09:12:31</TransTime> - <TransDate>2015-04-21</TransDate> - <TransType>01</TransType> - <Complete>true</Complete> - <Message>DECLINED*</Message> - <TransAmount>1.05</TransAmount> - <CardType>V</CardType> - <TransID>118187-0_25</TransID> - <TimedOut>false</TimedOut> - <BankTotals>null</BankTotals> - <Ticket>null</Ticket> - <CorporateCard>false</CorporateCard> - <CardLevelResult>A</CardLevelResult> - <CavvResultCode /> - </receipt> - </response> - RESPONSE - end - - def successful_capture_response - <<-RESPONSE - <?xml version="1.0" encoding="UTF-8"?> - <response> - <receipt> - <ReceiptId>3a7150ceb7026fccc380743ea3f47fdf</ReceiptId> - <ReferenceNum>640000030011763340</ReferenceNum> - <ResponseCode>001</ResponseCode> - <ISO>00</ISO> - <AuthCode>224958</AuthCode> - <TransTime>09:13:45</TransTime> - <TransDate>2015-04-21</TransDate> - <TransType>02</TransType> - <Complete>true</Complete> - <Message>APPROVED*</Message> - <TransAmount>0.00</TransAmount> - <CardType>V</CardType> - <TransID>830339-1_25</TransID> - <TimedOut>false</TimedOut> - <BankTotals>null</BankTotals> - <Ticket>null</Ticket> - <CorporateCard>false</CorporateCard> - <CardLevelResult>A</CardLevelResult> - </receipt> - </response> - RESPONSE - end - - def failed_capture_response - <<-RESPONSE - <?xml version="1.0" encoding="UTF-8"?> - <response> - <receipt> - <ReceiptId>3a7150ceb7026fccc380743ea3f47fdf</ReceiptId> - <ReferenceNum>640000030011763350</ReferenceNum> - <ResponseCode>476</ResponseCode> - <ISO>12</ISO> - <AuthCode>224958</AuthCode> - <TransTime>09:13:46</TransTime> - <TransDate>2015-04-21</TransDate> - <TransType>02</TransType> - <Complete>true</Complete> - <Message>DECLINED*</Message> - <TransAmount>0.00</TransAmount> - <CardType>V</CardType> - <TransID>830340-2_25</TransID> - <TimedOut>false</TimedOut> - <BankTotals>null</BankTotals> - <Ticket>null</Ticket> - <CorporateCard>false</CorporateCard> - </receipt> - </response> - RESPONSE - end - - def successful_store_response - <<-RESPONSE - <?xml version="1.0"?> - <response> - <receipt> - <DataKey>1234567890</DataKey> - <ResponseCode>027</ResponseCode> - <Complete>true</Complete> - <Message>Successfully registered cc details * =</Message> - </receipt> - </response> - RESPONSE - end - - def successful_unstore_response - <<-RESPONSE - <?xml version="1.0"?> - <response> - <receipt> - <DataKey>1234567890</DataKey> - <ResponseCode>027</ResponseCode> - <Complete>true</Complete> - <Message>Successfully deleted cc details * =</Message> - </receipt> - </response> - RESPONSE - end - - def successful_update_response - <<-RESPONSE - <?xml version="1.0"?> - <response> - <receipt> - <DataKey>1234567890</DataKey> - <ResponseCode>027</ResponseCode> - <Complete>true</Complete> - <Message>Successfully updated cc details * =</Message> - </receipt> - </response> - RESPONSE - end - - def successful_echeck_purchase_response - <<-RESPONSE - <?xml version="1.0"?> - <response> - <receipt> - <ReceiptId>cb80f38f44af2168fd9033cdf2d0d4c0</ReceiptId> - <ReferenceNum>001000040010015220</ReferenceNum> - <ResponseCode>005</ResponseCode> - <ISO>01</ISO> - <AuthCode></AuthCode> - <TransTime>08:23:37</TransTime> - <TransDate>2018-06-18</TransDate> - <TransType>00</TransType> - <Complete>true</Complete> - <Message>REGISTERED * =</Message> - <TransAmount>1.0</TransAmount> - <CardType>CQ</CardType> - <TransID>1522-0_25</TransID> - <TimedOut>false</TimedOut> - <BankTotals>null</BankTotals> - <Ticket>null</Ticket> - <CorporateCard>false</CorporateCard> - <MessageId>null</MessageId> - <RecurSuccess>true</RecurSuccess> - </receipt> - </response> - RESPONSE - end - - def xml_purchase_fixture - '<request><store_id>monusqa002</store_id><api_token>qatoken</api_token><us_purchase><amount>1.01</amount><pan>4242424242424242</pan><expdate>0303</expdate><crypt_type>7</crypt_type><order_id>order1</order_id></us_purchase></request>' - end - - def xml_capture_fixture - '<request><store_id>monusqa002</store_id><api_token>qatoken</api_token><us_preauth><amount>1.01</amount><pan>4242424242424242</pan><expdate>0303</expdate><crypt_type>7</crypt_type><order_id>order1</order_id></us_preauth></request>' - end - - def pre_scrub - %q{ -opening connection to esplusqa.moneris.com:443... -opened -starting SSL for esplusqa.moneris.com:443... -SSL established -<- "POST /gateway_us/servlet/MpgRequest 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: esplusqa.moneris.com\r\nContent-Length: 291\r\n\r\n" -<- "<request><store_id>monusqa002</store_id><api_token>qatoken</api_token><us_purchase><order_id>cec9ca34132f0945446589e36fff9cce</order_id><cust_id>Longbob Longsen</cust_id><amount>1.00</amount><pan>4242424242424242</pan><expdate>1909</expdate><crypt_type>7</crypt_type></us_purchase></request>" --> "HTTP/1.1 200 OK\r\n" --> "Date: Mon, 08 Jan 2018 19:20:26 GMT\r\n" --> "Content-Length: 659\r\n" --> "X-UA-Compatible: IE=Edge\r\n" --> "Connection: close\r\n" --> "Content-Type: text/html; charset=UTF-8\r\n" --> "Set-Cookie: TS01d02998=01649737b16d4ca54c296a0a369f4e549e4191e85d8d022d01468e559975e945b419002a42; Path=/\r\n" --> "Set-Cookie: TS01d02998_28=01e24a44e55591744bc115f421ddccd549b1655d75bda586c8ea625670efaa4432f67c8b7e06e7af82c70ef3ac4f46d7435664f2ac; Path=/\r\n" --> "\r\n" -reading 659 bytes... --> "<?xml version=\"1.0\" standalone=\"yes\"?><response><receipt><ReceiptId>cec9ca34132f0945446589e36fff9cce</ReceiptId><ReferenceNum>640000030013630190</ReferenceNum><ResponseCode>001</ResponseCode><ISO>00</ISO><AuthCode>827125</AuthCode><TransTime>13:20:24</TransTime><TransDate>2018-01-08</TransDate><TransType>00</TransType><Complete>true</Complete><Message>APPROVED*</Message><TransAmount>1.00</TransAmount><CardType>V</CardType><TransID>113295-0_25</TransID><TimedOut>false</TimedOut><BankTotals>null</BankTotals><Ticket>null</Ticket><CorporateCard>false</CorporateCard><CardLevelResult>A</CardLevelResult><CavvResultCode> </CavvResultCode></receipt></response>" -read 659 bytes -Conn close - } - end - - def post_scrub - %q{ -opening connection to esplusqa.moneris.com:443... -opened -starting SSL for esplusqa.moneris.com:443... -SSL established -<- "POST /gateway_us/servlet/MpgRequest 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: esplusqa.moneris.com\r\nContent-Length: 291\r\n\r\n" -<- "<request><store_id>monusqa002</store_id><api_token>[FILTERED]</api_token><us_purchase><order_id>cec9ca34132f0945446589e36fff9cce</order_id><cust_id>Longbob Longsen</cust_id><amount>1.00</amount><pan>[FILTERED]</pan><expdate>1909</expdate><crypt_type>7</crypt_type></us_purchase></request>" --> "HTTP/1.1 200 OK\r\n" --> "Date: Mon, 08 Jan 2018 19:20:26 GMT\r\n" --> "Content-Length: 659\r\n" --> "X-UA-Compatible: IE=Edge\r\n" --> "Connection: close\r\n" --> "Content-Type: text/html; charset=UTF-8\r\n" --> "Set-Cookie: TS01d02998=01649737b16d4ca54c296a0a369f4e549e4191e85d8d022d01468e559975e945b419002a42; Path=/\r\n" --> "Set-Cookie: TS01d02998_28=01e24a44e55591744bc115f421ddccd549b1655d75bda586c8ea625670efaa4432f67c8b7e06e7af82c70ef3ac4f46d7435664f2ac; Path=/\r\n" --> "\r\n" -reading 659 bytes... --> "<?xml version=\"1.0\" standalone=\"yes\"?><response><receipt><ReceiptId>cec9ca34132f0945446589e36fff9cce</ReceiptId><ReferenceNum>640000030013630190</ReferenceNum><ResponseCode>001</ResponseCode><ISO>00</ISO><AuthCode>827125</AuthCode><TransTime>13:20:24</TransTime><TransDate>2018-01-08</TransDate><TransType>00</TransType><Complete>true</Complete><Message>APPROVED*</Message><TransAmount>1.00</TransAmount><CardType>V</CardType><TransID>113295-0_25</TransID><TimedOut>false</TimedOut><BankTotals>null</BankTotals><Ticket>null</Ticket><CorporateCard>false</CorporateCard><CardLevelResult>A</CardLevelResult><CavvResultCode> </CavvResultCode></receipt></response>" -read 659 bytes -Conn close - } - end - -end diff --git a/test/unit/gateways/money_movers_test.rb b/test/unit/gateways/money_movers_test.rb index de2c1820701..6bd903e88c9 100644 --- a/test/unit/gateways/money_movers_test.rb +++ b/test/unit/gateways/money_movers_test.rb @@ -3,8 +3,8 @@ class MoneyMoversTest < Test::Unit::TestCase def setup @gateway = MoneyMoversGateway.new( - :login => 'demo', - :password => 'password' + login: 'demo', + password: 'password' ) @credit_card = credit_card('4111111111111111') @@ -12,9 +12,9 @@ def setup @amount = 100 @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase' + order_id: '1', + billing_address: address, + description: 'Store Purchase' } end @@ -44,8 +44,8 @@ def test_unsuccessful_request def test_add_address result = {} - @gateway.send(:add_address, result, :billing_address => {:address1 => '1 Main St.', :address2 => 'apt 13', :country => 'US', :state => 'MI', :phone => '1234567890'} ) - assert_equal ['address1', 'address2', 'city', 'company', 'country', 'phone', 'state', 'zip'], result.stringify_keys.keys.sort + @gateway.send(:add_address, result, billing_address: { address1: '1 Main St.', address2: 'apt 13', country: 'US', state: 'MI', phone: '1234567890' }) + assert_equal %w[address1 address2 city company country phone state zip], result.stringify_keys.keys.sort assert_equal 'MI', result[:state] assert_equal '1 Main St.', result[:address1] assert_equal 'apt 13', result[:address2] @@ -54,13 +54,13 @@ def test_add_address def test_add_invoice result = {} - @gateway.send(:add_invoice, result, :order_id => '#1001', :description => 'This is a great order') + @gateway.send(:add_invoice, result, order_id: '#1001', description: 'This is a great order') assert_equal '#1001', result[:orderid] assert_equal 'This is a great order', result[:orderdescription] end def test_purchase_is_valid_csv - params = {:amount => @amount} + params = { amount: @amount } @gateway.send(:add_creditcard, params, @credit_card) assert data = @gateway.send(:post_data, 'auth', params) @@ -68,7 +68,7 @@ def test_purchase_is_valid_csv end def test_purchase_meets_minimum_requirements - params = {:amount => @amount} + params = { amount: @amount } @gateway.send(:add_creditcard, params, @credit_card) assert data = @gateway.send(:post_data, 'auth', params) minimum_requirements.each do |key| @@ -77,8 +77,8 @@ def test_purchase_meets_minimum_requirements end def test_expdate_formatting - assert_equal '0909', @gateway.send(:expdate, credit_card('4111111111111111', :month => '9', :year => '2009')) - assert_equal '0711', @gateway.send(:expdate, credit_card('4111111111111111', :month => '7', :year => '2011')) + assert_equal '0909', @gateway.send(:expdate, credit_card('4111111111111111', month: '9', year: '2009')) + assert_equal '0711', @gateway.send(:expdate, credit_card('4111111111111111', month: '7', year: '2011')) end def test_supported_countries @@ -86,7 +86,7 @@ def test_supported_countries end def test_supported_card_types - assert_equal @gateway.supported_cardtypes, [:visa, :master, :american_express, :discover] + assert_equal @gateway.supported_cardtypes, %i[visa master american_express discover] end def test_avs_result diff --git a/test/unit/gateways/mundipagg_test.rb b/test/unit/gateways/mundipagg_test.rb index f8fe08779cb..c1236a4fdc6 100644 --- a/test/unit/gateways/mundipagg_test.rb +++ b/test/unit/gateways/mundipagg_test.rb @@ -3,32 +3,89 @@ class MundipaggTest < Test::Unit::TestCase include CommStub def setup - @gateway = MundipaggGateway.new(api_key: 'my_api_key') @credit_card = credit_card + + @alelo_card = credit_card( + '5067700000000028', + { + month: 10, + year: 2032, + first_name: 'John', + last_name: 'Smith', + verification_value: '737', + brand: 'alelo' + } + ) + + @alelo_visa_card = credit_card( + '4025880000000010', + { + month: 10, + year: 2032, + first_name: 'John', + last_name: 'Smith', + verification_value: '737', + brand: 'alelo' + } + ) + + @gateway = MundipaggGateway.new(api_key: 'my_api_key') @amount = 100 @options = { + gateway_affiliation_id: 'abc123', order_id: '1', billing_address: address, description: 'Store Purchase' } + + @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' + }, + address: { + street: 'Malibu Point', + number: '10880', + complement: 'A', + neighborhood: 'Central Malibu', + city: 'Malibu', + state: 'CA', + country: 'US', + zip_code: '24210-460' + } + } + } + + @gateway_response_error = 'Esta loja n??o possui um meio de pagamento configurado para a bandeira VR' + @acquirer_message = 'Simulator|Transação de simulada negada por falta de crédito, utilizado para realizar simulação de autorização parcial.' end def test_successful_purchase - @gateway.expects(:ssl_post).returns(successful_purchase_response) + test_successful_purchase_with(@credit_card) + end - response = @gateway.purchase(@amount, @credit_card, @options) - assert_success response + def test_successful_purchase_with_alelo_card + test_successful_purchase_with(@alelo_card) + end - assert_equal 'ch_90Vjq8TrwfP74XJO', response.authorization - assert response.test? + def test_successful_purchase_with_alelo_number_beginning_with_4 + test_successful_purchase_with(@alelo_visa_card) end def test_successful_purchase_with_holder_document - @options.merge!(holder_document: 'a1b2c3d4') + @options[:holder_document] = 'a1b2c3d4' response = stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/a1b2c3d4/, data) end.respond_with(successful_purchase_response) @@ -36,11 +93,82 @@ def test_successful_purchase_with_holder_document assert response.test? end + def test_successful_purchase_with_authorization_secret_key + options = { + gateway_affiliation_id: 'abc123', + order_id: '1', + billing_address: address, + description: 'Store Purchase', + authorization_secret_key: 'secret_token' + } + basic_token = Base64.strict_encode64("#{options[:authorization_secret_key]}:") + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, _data, headers| + assert_match(basic_token, headers['Authorization']) + end.respond_with(successful_purchase_response) + + assert_success response + assert_equal 'Simulator|Transação de simulação autorizada com sucesso', response.message + assert response.test? + end + + def test_api_key_in_headers + basic_token = Base64.strict_encode64("#{@gateway.options[:api_key]}:") + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, _data, headers| + assert_match(basic_token, headers['Authorization']) + end.respond_with(successful_purchase_response) + + assert_success response + assert_equal 'Simulator|Transação de simulação autorizada com sucesso', response.message + assert response.test? + end + + def test_failed_with_voucher + @gateway.expects(:ssl_post).returns(failed_voucher_response) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_equal 'General Failure', response.message + assert_equal '500', response.params['last_transaction']['gateway_response']['code'] + end + + def test_successful_purchase_with_submerchant + options = @options.update(@submerchant_options) + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/44444/, data) + assert_match(/5555555/, data) + assert_match(/code2/, data) + assert_match(/Sub Tony Stark/, data) + assert_match(/123456789/, data) + assert_match(/individual/, data) + assert_match(/55/, data) + assert_match(/000000000/, data) + assert_match(/21/, data) + assert_match(/Malibu Point/, data) + assert_match(/10880/, data) + assert_match(/A/, data) + assert_match(/Central Malibu/, data) + assert_match(/Malibu/, data) + assert_match(/CA/, data) + assert_match(/US/, data) + assert_match(/24210-460/, data) + end.respond_with(successful_purchase_response) + + assert_success response + assert_equal 'Simulator|Transação de simulação autorizada com sucesso', response.message + assert response.test? + end + def test_billing_not_sent @options.delete(:billing_address) stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| refute data['billing_address'] end.respond_with(successful_purchase_response) end @@ -50,7 +178,35 @@ def test_failed_purchase response = @gateway.purchase(@amount, @credit_card, @options) assert_failure response - assert_equal Gateway::STANDARD_ERROR_CODE[:processing_error], response.error_code + assert_equal @acquirer_message, response.message + assert_equal '92', response.error_code + end + + def test_failed_purchase_with_top_level_errors + @gateway.expects(:ssl_post).raises(mock_response_error) + + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_invalid_parameter_errors(response) + end + + def test_failed_purchase_with_gateway_response_errors + @gateway.expects(:ssl_post).returns(failed_response_with_gateway_response_errors) + + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + assert_equal @gateway_response_error, response.message + end + + def test_failed_purchase_with_acquirer_return_code + @gateway.expects(:ssl_post).returns(failed_response_with_acquirer_return_code) + + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_failure response + assert_equal 'VR|', response.message + assert_equal '14', response.error_code end def test_successful_authorize @@ -63,12 +219,55 @@ def test_successful_authorize assert response.test? end + def test_successful_authorize_with_partially_missing_address + shipping_address = { + country: 'BR', + address1: 'Foster St.' + } + + @gateway.expects(:ssl_post).returns(successful_authorize_response) + response = @gateway.authorize(@amount, @credit_card, @options.merge(shipping_address:)) + assert_success response + + assert_equal 'ch_gm5wrlGMI2Fb0x6K', response.authorization + assert response.test? + end + + def test_successful_authorize_with_submerchant + options = @options.update(@submerchant_options) + + @gateway.expects(:ssl_post).returns(successful_authorize_response) + response = @gateway.authorize(@amount, @credit_card, options) + assert_success response + + assert_equal 'ch_gm5wrlGMI2Fb0x6K', response.authorization + assert response.test? + 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 Gateway::STANDARD_ERROR_CODE[:processing_error], response.error_code + assert_equal @acquirer_message, response.message + assert_equal '92', response.error_code + end + + def test_failed_authorize_with_top_level_errors + @gateway.expects(:ssl_post).raises(mock_response_error) + + response = @gateway.authorize(@amount, @credit_card, @options) + + assert_invalid_parameter_errors(response) + end + + def test_failed_authorize_with_gateway_response_errors + @gateway.expects(:ssl_post).returns(failed_response_with_gateway_response_errors) + + response = @gateway.authorize(@amount, @credit_card, @options) + + assert_success response + assert_equal @gateway_response_error, response.message end def test_successful_capture @@ -154,6 +353,37 @@ def test_sucessful_store assert response.test? end + def test_failed_store_with_top_level_errors + @gateway.expects(:ssl_post).times(2).raises(mock_response_error) + + response = @gateway.store(@credit_card, @options) + + assert_invalid_parameter_errors(response) + end + + def test_failed_store_with_gateway_response_errors + @gateway.expects(:ssl_post).times(2).returns(failed_response_with_gateway_response_errors) + + response = @gateway.store(@credit_card, @options) + + assert_success response + assert_equal @gateway_response_error, response.message + end + + def test_gateway_id_fallback + @gateway = MundipaggGateway.new(api_key: 'my_api_key', gateway_id: 'abcd123') + options = { + order_id: '1', + billing_address: address, + description: 'Store Purchase' + } + stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/"gateway_affiliation_id":"abcd123"/, data) + end.respond_with(successful_purchase_response) + end + def test_scrub assert @gateway.supports_scrubbing? assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed @@ -161,6 +391,32 @@ def test_scrub private + def test_successful_purchase_with(card) + @gateway.expects(:ssl_post).returns(successful_purchase_response) + + response = @gateway.purchase(@amount, card, @options) + assert_success response + + assert_equal 'ch_90Vjq8TrwfP74XJO', response.authorization + assert response.test? + end + + def mock_response_error + mock_response = Net::HTTPUnprocessableEntity.new('1.1', '422', 'Unprocessable Entity') + mock_response.stubs(:body).returns(failed_response_with_top_level_errors) + + ActiveMerchant::ResponseError.new(mock_response) + end + + def assert_invalid_parameter_errors(response) + assert_failure response + + assert_equal( + 'Invalid parameters; The request is invalid. | The field neighborhood must be a string with a maximum length of 64. | The field line_1 must be a string with a maximum length of 256.', + response.message + ) + end + def pre_scrubbed %q( opening connection to api.mundipagg.com:443... @@ -285,6 +541,82 @@ def successful_purchase_response ) end + def failed_voucher_response + %( + { + "id": "FILTERED", + "code": "FILTERED", + "amount": 300, + "status": "processing", + "currency": "BRL", + "payment_method": "voucher", + "created_at": "2021-09-20T13:40:04Z", + "updated_at": "2021-09-20T13:40:04Z", + "customer": { + "id": "FILTERED", + "name": "FILTERED", + "email": "FILTERED", + "delinquent": false, + "created_at": "2021-09-20T13:40:04Z", + "updated_at": "2021-09-20T13:40:04Z", + "phones": {} + }, + "last_transaction": { + "id": "FILTERED", + "transaction_type": "voucher", + "amount": 300, + "status": "with_error", + "success": false, + "operation_type": "auth_and_capture", + "card": { + "id": "FILTERED", + "first_six_digits": "FILTERED", + "last_four_digits": "FILTERED", + "brand": "Sodexo", + "holder_name": "FILTERED", + "holder_document": "FILTERED", + "exp_month": 5, + "exp_year": 2030, + "status": "active", + "type": "voucher", + "created_at": "2021-09-20T13:40:04Z", + "updated_at": "2021-09-20T13:40:04Z", + "billing_address": { + "street": "FILTERED", + "number": "00", + "zip_code": "FILTERED", + "neighborhood": "FILTERED", + "city": "FILTERED", + "state": "FILTERED", + "country": "FILTERED", + "line_1": "FILTERED" + }, + "customer": { + "id": "FILTERED", + "name": "FILTERED", + "email": "FILTERED", + "delinquent": false, + "created_at": "2021-09-20T13:40:04Z", + "updated_at": "2021-09-20T13:40:04Z", + "phones": {} + } + }, + "created_at": "2021-09-20T13:40:04Z", + "updated_at": "2021-09-20T13:40:04Z", + "gateway_response": { + "code": "500", + "errors": [ + { + "message": "General Failure" + } + ] + }, + "antifraud_response": {} + } + } + ) + end + def failed_purchase_response %( { @@ -352,6 +684,203 @@ def failed_purchase_response ) end + def failed_response_with_top_level_errors + %( + { + "message": "The request is invalid.", + "errors": { + "charge.payment.credit_card.card.billing_address.neighborhood": [ + "The field neighborhood must be a string with a maximum length of 64." + ], + "charge.payment.credit_card.card.billing_address.line_1": [ + "The field line_1 must be a string with a maximum length of 256." + ] + }, + "request": { + "currency": "USD", + "amount": 100, + "customer": { + "name": "Longbob Longsen", + "phones": {}, + "metadata": {} + }, + "payment": { + "gateway_affiliation_id": "d76dffc8-c3e5-4d80-b9ee-dc8fb6c56c83", + "payment_method": "credit_card", + "credit_card": { + "installments": 1, + "capture": true, + "card": { + "last_four_digits": "2224", + "brand": "Visa", + "holder_name": "Longbob Longsen", + "exp_month": 9, + "exp_year": 2020, + "billing_address": { + "street": "My Street", + "number": "456", + "zip_code": "K1C2N6", + "neighborhood": "Sesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame Street", + "city": "Ottawa", + "state": "ON", + "country": "CA", + "line_1": "456, My Street, Sesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame StreetSesame Street" + }, + "options": {} + } + }, + "metadata": { + "mundipagg_payment_method_code": "1" + } + } + } + } + ) + end + + def failed_response_with_gateway_response_errors + %( + { + "id": "ch_90Vjq8TrwfP74XJO", + "code": "ME0KIN4A0O", + "gateway_id": "162bead8-23a0-4708-b687-078a69a1aa7c", + "amount": 100, + "paid_amount": 100, + "status": "paid", + "currency": "USD", + "payment_method": "credit_card", + "paid_at": "2018-02-01T18:41:05Z", + "created_at": "2018-02-01T18:41:04Z", + "updated_at": "2018-02-01T18:41:04Z", + "customer": { + "id": "cus_VxJX2NmTqyUnXgL9", + "name": "Longbob Longsen", + "email": "", + "delinquent": false, + "created_at": "2018-02-01T18:41:04Z", + "updated_at": "2018-02-01T18:41:04Z", + "phones": {} + }, + "last_transaction": { + "id": "tran_JNzjzadcVZHlG8K2", + "transaction_type": "credit_card", + "gateway_id": "c579c8fa-53d7-41a8-b4cc-a03c712ebbb7", + "amount": 100, + "status": "captured", + "success": true, + "installments": 1, + "operation_type": "auth_and_capture", + "card": { + "id": "card_pD02Q6WtOTB7a3kE", + "first_six_digits": "400010", + "last_four_digits": "2224", + "brand": "Visa", + "holder_name": "Longbob Longsen", + "exp_month": 9, + "exp_year": 2019, + "status": "active", + "created_at": "2018-02-01T18:41:04Z", + "updated_at": "2018-02-01T18:41:04Z", + "billing_address": { + "street": "My Street", + "number": "456", + "zip_code": "K1C2N6", + "neighborhood": "Sesame Street", + "city": "Ottawa", + "state": "ON", + "country": "CA", + "line_1": "456, My Street, Sesame Street" + }, + "type": "credit" + }, + "created_at": "2018-02-01T18:41:04Z", + "updated_at": "2018-02-01T18:41:04Z", + "gateway_response": { + "code": "400", + "errors": [ + { + "message": "Esta loja n??o possui um meio de pagamento configurado para a bandeira VR" + } + ] + } + } + } + ) + end + + def failed_response_with_acquirer_return_code + %( + { + "id": "ch_9qY3lpeCJyTe2Gxz", + "code": "3Y4ZFENCK4", + "gateway_id": "db9a46cb-2c59-4663-a658-e7817302d97c", + "amount": 2946, + "status": "failed", + "currency": "BRL", + "payment_method": "credit_card", + "created_at": "2019-11-15T16:21:58Z", + "updated_at": "2019-11-15T16:21:59Z", + "customer": { + "id": "cus_KD14bY1F51UR1GrX", + "name": "JOSE NETO", + "email": "jose_bar@uol.com.br", + "delinquent": false, + "created_at": "2019-11-15T16:21:58Z", + "updated_at": "2019-11-15T16:21:58Z", + "phones": {} + }, + "last_transaction": { + "id": "tran_P2zwvPztdVCg6pvA", + "transaction_type": "credit_card", + "gateway_id": "174a1d12-cbea-4c09-a27a-23bbad992cc9", + "amount": 2946, + "status": "not_authorized", + "success": false, + "installments": 1, + "acquirer_name": "vr", + "acquirer_affiliation_code": "", + "acquirer_tid": "28128131916", + "acquirer_nsu": "281281", + "acquirer_message": "VR|", + "acquirer_return_code": "14", + "operation_type": "auth_and_capture", + "card": { + "id": "card_V2pQo2IbjtPqaXRZ", + "first_six_digits": "627416", + "last_four_digits": "7116", + "brand": "VR", + "holder_name": "JOSE NETO", + "holder_document": "27207590822", + "exp_month": 8, + "exp_year": 2029, + "status": "active", + "type": "voucher", + "created_at": "2019-11-15T16:21:58Z", + "updated_at": "2019-11-15T16:21:58Z", + "billing_address": { + "street": "R.Dr.Eduardo de Souza Aranha,", + "number": "67", + "zip_code": "04530030", + "neighborhood": "Av Das Nacoes Unidas 6873", + "city": "Sao Paulo", + "state": "SP", + "country": "BR", + "line_1": "67, R.Dr.Eduardo de Souza Aranha,, Av Das Nacoes Unidas 6873" + } + }, + "created_at": "2019-11-15T16:21:58Z", + "updated_at": "2019-11-15T16:21:58Z", + "gateway_response": { + "code": "201", + "errors": [] + }, + "antifraud_response": {}, + "metadata": {} + } + } + ) + end + def successful_authorize_response %( { @@ -712,7 +1241,7 @@ def successful_void_response def failed_void_response '{"message": "Charge not found."}' end - + def successful_verify_response %( { diff --git a/test/unit/gateways/nab_transact_test.rb b/test/unit/gateways/nab_transact_test.rb index 3e7dca2eedb..c1630729559 100644 --- a/test/unit/gateways/nab_transact_test.rb +++ b/test/unit/gateways/nab_transact_test.rb @@ -5,16 +5,16 @@ class NabTransactTest < Test::Unit::TestCase def setup @gateway = NabTransactGateway.new( - :login => 'login', - :password => 'password' - ) + login: 'login', + password: 'password' + ) @credit_card = credit_card @amount = 200 @options = { - :order_id => '1', - :billing_address => address, - :description => 'Test NAB Purchase' + order_id: '1', + billing_address: address, + description: 'Test NAB Purchase' } end @@ -33,7 +33,7 @@ def test_successful_purchase_with_merchant_descriptor name, location = 'Active Merchant', 'USA' response = assert_metadata(name, location) do - response = @gateway.purchase(@amount, @credit_card, @options.merge(:merchant_name => name, :merchant_location => location)) + response = @gateway.purchase(@amount, @credit_card, @options.merge(merchant_name: name, merchant_location: location)) end assert response @@ -52,7 +52,7 @@ def test_successful_authorize_with_merchant_descriptor name, location = 'Active Merchant', 'USA' response = assert_metadata(name, location) do - response = @gateway.authorize(@amount, @credit_card, @options.merge(:merchant_name => name, :merchant_location => location)) + response = @gateway.authorize(@amount, @credit_card, @options.merge(merchant_name: name, merchant_location: location)) end assert response @@ -71,7 +71,7 @@ def test_successful_capture_with_merchant_descriptor name, location = 'Active Merchant', 'USA' response = assert_metadata(name, location) do - response = @gateway.capture(@amount, '009887*test*009887*200', @options.merge(:merchant_name => name, :merchant_location => location)) + response = @gateway.capture(@amount, '009887*test*009887*200', @options.merge(merchant_name: name, merchant_location: location)) end assert response @@ -103,19 +103,19 @@ def test_supported_countries end def test_supported_card_types - assert_equal [:visa, :master, :american_express, :diners_club, :jcb], NabTransactGateway.supported_cardtypes + assert_equal %i[visa master american_express diners_club jcb], NabTransactGateway.supported_cardtypes end def test_successful_refund @gateway.expects(:ssl_post).with(&check_transaction_type(:refund)).returns(successful_refund_response) - assert_success @gateway.refund(@amount, '009887', {:order_id => '1'}) + assert_success @gateway.refund(@amount, '009887', { order_id: '1' }) end def test_successful_refund_with_merchant_descriptor name, location = 'Active Merchant', 'USA' response = assert_metadata(name, location) do - response = @gateway.refund(@amount, '009887', {:order_id => '1', :merchant_name => name, :merchant_location => location}) + response = @gateway.refund(@amount, '009887', { order_id: '1', merchant_name: name, merchant_location: location }) end assert response @@ -125,13 +125,13 @@ def test_successful_refund_with_merchant_descriptor def test_successful_credit @gateway.expects(:ssl_post).with(&check_transaction_type(:unmatched_refund)).returns(successful_refund_response) - assert_success @gateway.credit(@amount, @credit_card, {:order_id => '1'}) + assert_success @gateway.credit(@amount, @credit_card, { order_id: '1' }) end def test_failed_refund @gateway.expects(:ssl_post).with(&check_transaction_type(:refund)).returns(failed_refund_response) - response = @gateway.refund(@amount, '009887', {:order_id => '1'}) + response = @gateway.refund(@amount, '009887', { order_id: '1' }) assert_failure response assert_equal 'Only $1.00 available for refund', response.message end @@ -139,7 +139,7 @@ def test_failed_refund def test_request_timeout_default stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/<timeoutValue>60/, data) end.respond_with(successful_purchase_response) end @@ -148,7 +148,7 @@ def test_override_request_timeout gateway = NabTransactGateway.new(login: 'login', password: 'password', request_timeout: 44) stub_comms(gateway, :ssl_request) do gateway.purchase(@amount, @credit_card, @options) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/<timeoutValue>44/, data) end.respond_with(successful_purchase_response) end @@ -156,7 +156,7 @@ def test_override_request_timeout def test_nonfractional_currencies stub_comms(@gateway, :ssl_request) do @gateway.authorize(10000, @credit_card, @options.merge(currency: 'JPY')) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/<amount>100<\/amount>/, data) end.respond_with(successful_authorize_response) end @@ -169,68 +169,66 @@ def test_scrub private def pre_scrubbed - <<-'PRE_SCRUBBED' -opening connection to transact.nab.com.au:443... -opened -starting SSL for transact.nab.com.au:443... -SSL established -<- "POST /test/xmlapi/payment 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: transact.nab.com.au\r\nContent-Length: 715\r\n\r\n" -<- "<?xml version=\"1.0\" encoding=\"UTF-8\"?><NABTransactMessage><MessageInfo><messageID>6673348a21d79657983ab247b2483e</messageID><messageTimestamp>20151212075932886818+000</messageTimestamp><timeoutValue>60</timeoutValue><apiVersion>xml-4.2</apiVersion></MessageInfo><MerchantInfo><merchantID>XYZ0010</merchantID><password>abcd1234</password></MerchantInfo><RequestType>Payment</RequestType><Payment><TxnList count=\"1\"><Txn ID=\"1\"><txnType>0</txnType><txnSource>23</txnSource><amount>200</amount><currency>AUD</currency><purchaseOrderNo></purchaseOrderNo><CreditCardInfo><cardNumber>4444333322221111</cardNumber><expiryDate>05/17</expiryDate><cvv>111</cvv></CreditCardInfo></Txn></TxnList></Payment></NABTransactMessage>" --> "HTTP/1.1 200 OK\r\n" --> "Date: Sat, 12 Dec 2015 07:59:34 GMT\r\n" --> "Server: Apache-Coyote/1.1\r\n" --> "Content-Type: text/xml;charset=ISO-8859-1\r\n" --> "Content-Length: 920\r\n" --> "Connection: close\r\n" --> "\r\n" -reading 920 bytes... --> "" --> "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?><NABTransactMessage><MessageInfo><messageID>6673348a21d79657983ab247b2483e</messageID><messageTimestamp>20151212185934964000+660</messageTimestamp><apiVersion>xml-4.2</apiVersion></MessageInfo><RequestType>Payment</RequestType><MerchantInfo><merchantID>XYZ0010</merchantID></MerchantInfo><Status><statusCode>000</statusCode><statusDescription>Normal</statusDescription></Status><Payment><TxnList count=\"1\"><Txn ID=\"1\"><txnType>0</txnType><txnSource>23</txnSource><amount>200</amount><currency>AUD</currency><purchaseOrderNo/><approved>No</approved><responseCode>103</responseCode><responseText>Invalid Purchase Order Number</responseText><settlementDate/><txnID/><authID/><CreditCardInfo><pan>444433...111</pan><expiryDate>05/17</expiryDate><cardType>6</cardType><cardDescription>Visa</cardDescription></CreditCardInfo></Txn></TxnList></Payment></NABTransactMessage>" -read 920 bytes -Conn close + <<~'PRE_SCRUBBED' + opening connection to transact.nab.com.au:443... + opened + starting SSL for transact.nab.com.au:443... + SSL established + <- "POST /test/xmlapi/payment 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: transact.nab.com.au\r\nContent-Length: 715\r\n\r\n" + <- "<?xml version=\"1.0\" encoding=\"UTF-8\"?><NABTransactMessage><MessageInfo><messageID>6673348a21d79657983ab247b2483e</messageID><messageTimestamp>20151212075932886818+000</messageTimestamp><timeoutValue>60</timeoutValue><apiVersion>xml-4.2</apiVersion></MessageInfo><MerchantInfo><merchantID>XYZ0010</merchantID><password>abcd1234</password></MerchantInfo><RequestType>Payment</RequestType><Payment><TxnList count=\"1\"><Txn ID=\"1\"><txnType>0</txnType><txnSource>23</txnSource><amount>200</amount><currency>AUD</currency><purchaseOrderNo></purchaseOrderNo><CreditCardInfo><cardNumber>4444333322221111</cardNumber><expiryDate>05/17</expiryDate><cvv>111</cvv></CreditCardInfo></Txn></TxnList></Payment></NABTransactMessage>" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Sat, 12 Dec 2015 07:59:34 GMT\r\n" + -> "Server: Apache-Coyote/1.1\r\n" + -> "Content-Type: text/xml;charset=ISO-8859-1\r\n" + -> "Content-Length: 920\r\n" + -> "Connection: close\r\n" + -> "\r\n" + reading 920 bytes... + -> "" + -> "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?><NABTransactMessage><MessageInfo><messageID>6673348a21d79657983ab247b2483e</messageID><messageTimestamp>20151212185934964000+660</messageTimestamp><apiVersion>xml-4.2</apiVersion></MessageInfo><RequestType>Payment</RequestType><MerchantInfo><merchantID>XYZ0010</merchantID></MerchantInfo><Status><statusCode>000</statusCode><statusDescription>Normal</statusDescription></Status><Payment><TxnList count=\"1\"><Txn ID=\"1\"><txnType>0</txnType><txnSource>23</txnSource><amount>200</amount><currency>AUD</currency><purchaseOrderNo/><approved>No</approved><responseCode>103</responseCode><responseText>Invalid Purchase Order Number</responseText><settlementDate/><txnID/><authID/><CreditCardInfo><pan>444433...111</pan><expiryDate>05/17</expiryDate><cardType>6</cardType><cardDescription>Visa</cardDescription></CreditCardInfo></Txn></TxnList></Payment></NABTransactMessage>" + read 920 bytes + Conn close PRE_SCRUBBED end def post_scrubbed - <<-'POST_SCRUBBED' -opening connection to transact.nab.com.au:443... -opened -starting SSL for transact.nab.com.au:443... -SSL established -<- "POST /test/xmlapi/payment 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: transact.nab.com.au\r\nContent-Length: 715\r\n\r\n" -<- "<?xml version=\"1.0\" encoding=\"UTF-8\"?><NABTransactMessage><MessageInfo><messageID>6673348a21d79657983ab247b2483e</messageID><messageTimestamp>20151212075932886818+000</messageTimestamp><timeoutValue>60</timeoutValue><apiVersion>xml-4.2</apiVersion></MessageInfo><MerchantInfo><merchantID>XYZ0010</merchantID><password>[FILTERED]</password></MerchantInfo><RequestType>Payment</RequestType><Payment><TxnList count=\"1\"><Txn ID=\"1\"><txnType>0</txnType><txnSource>23</txnSource><amount>200</amount><currency>AUD</currency><purchaseOrderNo></purchaseOrderNo><CreditCardInfo><cardNumber>[FILTERED]</cardNumber><expiryDate>05/17</expiryDate><cvv>[FILTERED]</cvv></CreditCardInfo></Txn></TxnList></Payment></NABTransactMessage>" --> "HTTP/1.1 200 OK\r\n" --> "Date: Sat, 12 Dec 2015 07:59:34 GMT\r\n" --> "Server: Apache-Coyote/1.1\r\n" --> "Content-Type: text/xml;charset=ISO-8859-1\r\n" --> "Content-Length: 920\r\n" --> "Connection: close\r\n" --> "\r\n" -reading 920 bytes... --> "" --> "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?><NABTransactMessage><MessageInfo><messageID>6673348a21d79657983ab247b2483e</messageID><messageTimestamp>20151212185934964000+660</messageTimestamp><apiVersion>xml-4.2</apiVersion></MessageInfo><RequestType>Payment</RequestType><MerchantInfo><merchantID>XYZ0010</merchantID></MerchantInfo><Status><statusCode>000</statusCode><statusDescription>Normal</statusDescription></Status><Payment><TxnList count=\"1\"><Txn ID=\"1\"><txnType>0</txnType><txnSource>23</txnSource><amount>200</amount><currency>AUD</currency><purchaseOrderNo/><approved>No</approved><responseCode>103</responseCode><responseText>Invalid Purchase Order Number</responseText><settlementDate/><txnID/><authID/><CreditCardInfo><pan>444433...111</pan><expiryDate>05/17</expiryDate><cardType>6</cardType><cardDescription>Visa</cardDescription></CreditCardInfo></Txn></TxnList></Payment></NABTransactMessage>" -read 920 bytes -Conn close + <<~'POST_SCRUBBED' + opening connection to transact.nab.com.au:443... + opened + starting SSL for transact.nab.com.au:443... + SSL established + <- "POST /test/xmlapi/payment 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: transact.nab.com.au\r\nContent-Length: 715\r\n\r\n" + <- "<?xml version=\"1.0\" encoding=\"UTF-8\"?><NABTransactMessage><MessageInfo><messageID>6673348a21d79657983ab247b2483e</messageID><messageTimestamp>20151212075932886818+000</messageTimestamp><timeoutValue>60</timeoutValue><apiVersion>xml-4.2</apiVersion></MessageInfo><MerchantInfo><merchantID>XYZ0010</merchantID><password>[FILTERED]</password></MerchantInfo><RequestType>Payment</RequestType><Payment><TxnList count=\"1\"><Txn ID=\"1\"><txnType>0</txnType><txnSource>23</txnSource><amount>200</amount><currency>AUD</currency><purchaseOrderNo></purchaseOrderNo><CreditCardInfo><cardNumber>[FILTERED]</cardNumber><expiryDate>05/17</expiryDate><cvv>[FILTERED]</cvv></CreditCardInfo></Txn></TxnList></Payment></NABTransactMessage>" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Sat, 12 Dec 2015 07:59:34 GMT\r\n" + -> "Server: Apache-Coyote/1.1\r\n" + -> "Content-Type: text/xml;charset=ISO-8859-1\r\n" + -> "Content-Length: 920\r\n" + -> "Connection: close\r\n" + -> "\r\n" + reading 920 bytes... + -> "" + -> "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?><NABTransactMessage><MessageInfo><messageID>6673348a21d79657983ab247b2483e</messageID><messageTimestamp>20151212185934964000+660</messageTimestamp><apiVersion>xml-4.2</apiVersion></MessageInfo><RequestType>Payment</RequestType><MerchantInfo><merchantID>XYZ0010</merchantID></MerchantInfo><Status><statusCode>000</statusCode><statusDescription>Normal</statusDescription></Status><Payment><TxnList count=\"1\"><Txn ID=\"1\"><txnType>0</txnType><txnSource>23</txnSource><amount>200</amount><currency>AUD</currency><purchaseOrderNo/><approved>No</approved><responseCode>103</responseCode><responseText>Invalid Purchase Order Number</responseText><settlementDate/><txnID/><authID/><CreditCardInfo><pan>444433...111</pan><expiryDate>05/17</expiryDate><cardType>6</cardType><cardDescription>Visa</cardDescription></CreditCardInfo></Txn></TxnList></Payment></NABTransactMessage>" + read 920 bytes + Conn close POST_SCRUBBED end def check_transaction_type(type) - Proc.new do |endpoint, data, headers| + Proc.new do |_endpoint, data, _headers| request_hash = Hash.from_xml(data) request_hash['NABTransactMessage']['Payment']['TxnList']['Txn']['txnType'] == NabTransactGateway::TRANSACTIONS[type].to_s end end def valid_metadata(name, location) - return <<-XML.gsub(/^\s{4}/,'').gsub(/\n/, '') + return <<-XML.gsub(/^\s{4}/, '').delete("\n") <metadata><meta name="ca_name" value="#{name}"/><meta name="ca_location" value="#{location}"/></metadata> XML end - def assert_metadata(name, location, &block) - stub_comms(@gateway, :ssl_request) do - block.call - end.check_request do |method, endpoint, data, headers| + def assert_metadata(name, location, &) + stub_comms(@gateway, :ssl_request, &).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) @@ -241,7 +239,7 @@ def failed_login_response end def successful_purchase_response - <<-XML.gsub(/^\s{4}/,'') + <<-XML.gsub(/^\s{4}/, '') <?xml version="1.0" encoding="UTF-8"?> <NABTransactMessage> <MessageInfo> @@ -284,7 +282,7 @@ def successful_purchase_response end def failed_purchase_response - <<-XML.gsub(/^\s{4}/,'') + <<-XML.gsub(/^\s{4}/, '') <?xml version="1.0" encoding="UTF-8"?> <NABTransactMessage> <MessageInfo> @@ -327,7 +325,7 @@ def failed_purchase_response end def successful_authorize_response - <<-XML.gsub(/^\s{4}/,'') + <<-XML.gsub(/^\s{4}/, '') <?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?> <NABTransactMessage> <MessageInfo> @@ -372,7 +370,7 @@ def successful_authorize_response end def successful_refund_response - <<-XML.gsub(/^\s{4}/,'') + <<-XML.gsub(/^\s{4}/, '') <?xml version="1.0" encoding="UTF-8"?> <NABTransactMessage> <MessageInfo> @@ -415,7 +413,7 @@ def successful_refund_response end def failed_refund_response - <<-XML.gsub(/^\s{4}/,'') + <<-XML.gsub(/^\s{4}/, '') <?xml version="1.0" encoding="UTF-8"?> <NABTransactMessage> <MessageInfo> @@ -456,5 +454,4 @@ def failed_refund_response </NABTransactMessage> XML end - end diff --git a/test/unit/gateways/ncr_secure_pay_test.rb b/test/unit/gateways/ncr_secure_pay_test.rb index f1b8de1eccd..6dee2375635 100644 --- a/test/unit/gateways/ncr_secure_pay_test.rb +++ b/test/unit/gateways/ncr_secure_pay_test.rb @@ -16,10 +16,9 @@ def setup end def test_successful_purchase - response = stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/\<username\>login\<\/username\>/, data) assert_match(/\<password\>password\<\/password\>/, data) assert_match(/\<action\>sale\<\/action\>/, data) @@ -42,10 +41,9 @@ def test_failed_purchase end def test_successful_authorize - response = stub_comms do @gateway.authorize(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/\<username\>login\<\/username\>/, data) assert_match(/\<password\>password\<\/password\>/, data) assert_match(/\<action\>preauth\<\/action\>/, data) @@ -67,10 +65,9 @@ def test_failed_authorize end def test_successful_capture - response = stub_comms do @gateway.capture(@amount, '12345', @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/\<username\>login\<\/username\>/, data) assert_match(/\<password\>password\<\/password\>/, data) assert_match(/\<action\>preauthcomplete\<\/action\>/, data) @@ -91,10 +88,9 @@ def test_failed_capture end def test_successful_refund - response = stub_comms do @gateway.refund(@amount, '12345', @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/\<username\>login\<\/username\>/, data) assert_match(/\<password\>password\<\/password\>/, data) assert_match(/\<action\>credit\<\/action\>/, data) @@ -115,10 +111,9 @@ def test_failed_refund end def test_successful_void - response = stub_comms do @gateway.void('12345', @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/\<username\>login\<\/username\>/, data) assert_match(/\<password\>password\<\/password\>/, data) assert_match(/\<action\>void\<\/action\>/, data) @@ -138,7 +133,6 @@ def test_failed_void end def test_successful_verify - response = stub_comms do @gateway.verify(@credit_card, @options) end.respond_with(successful_authorize_response, successful_void_response) @@ -148,7 +142,6 @@ def test_successful_verify end def test_successful_verify_with_failed_void - response = stub_comms do @gateway.verify(@credit_card, @options) end.respond_with(successful_authorize_response, failed_void_response) @@ -158,7 +151,6 @@ def test_successful_verify_with_failed_void end def test_failed_verify - response = stub_comms do @gateway.verify(@credit_card, @options) end.respond_with(failed_authorize_response, failed_void_response) diff --git a/test/unit/gateways/net_registry_test.rb b/test/unit/gateways/net_registry_test.rb index e478e8d6151..40d0daba5fa 100644 --- a/test/unit/gateways/net_registry_test.rb +++ b/test/unit/gateways/net_registry_test.rb @@ -3,15 +3,15 @@ class NetRegistryTest < Test::Unit::TestCase def setup @gateway = NetRegistryGateway.new( - :login => 'X', - :password => 'Y' + login: 'X', + password: 'Y' ) @amount = 100 @credit_card = credit_card @options = { - :order_id => '1', - :billing_address => address + order_id: '1', + billing_address: address } end @@ -67,7 +67,7 @@ def test_successful_authorization_and_capture assert_success response assert_match %r{\A\d{6}\z}, response.authorization - response = @gateway.capture(@amount, response.authorization, :credit_card => @credit_card) + response = @gateway.capture(@amount, response.authorization, credit_card: @credit_card) assert_success response end @@ -96,7 +96,7 @@ def test_purchase_with_invalid_month end def test_bad_login - gateway = NetRegistryGateway.new(:login => 'bad-login', :password => 'bad-login') + gateway = NetRegistryGateway.new(login: 'bad-login', password: 'bad-login') gateway.stubs(:ssl_post).returns(bad_login_response) response = gateway.purchase(@amount, @credit_card, @options) @@ -105,320 +105,321 @@ def test_bad_login end private + def successful_purchase_response - <<-RESPONSE -approved -00015X000000 -Transaction No: 00000000 ------------------------- -MERCHANTNAME -LOCATION AU - -MERCH ID 10000000 -TERM ID Y0TR00 -COUNTRY CODE AU -16/07/07 18:59 -RRN 00015X000000 -VISA -411111-111 -CREDIT A/C 12/10 - -AUTHORISATION NO: 000000 -APPROVED 08 - -PURCHASE $1.00 -TOTAL AUD $1.00 - -PLEASE RETAIN AS RECORD - OF PURCHASE - -(SUBJECT TO CARDHOLDER'S - ACCEPTANCE) ------------------------- -. -settlement_date=16/07/07 -card_desc=VISA -status=approved -txn_ref=0707161858000000 -refund_mode=0 -transaction_no=000000 -rrn=00015X000000 -response_text=SIGNATURE REQUIRED -pld=0 -total_amount=100 -card_no=4111111111111111 -version=V1.0 -merchant_index=123 -card_expiry=12/10 -training_mode=0 -operator_no=10000 -response_code=08 -card_type=6 -approved=1 -cashout_amount=0 -receipt_array=ARRAY(0x83725cc) -account_type=CREDIT A/C -result=1 + <<~RESPONSE + approved + 00015X000000 + Transaction No: 00000000 + ------------------------ + MERCHANTNAME + LOCATION AU + + MERCH ID 10000000 + TERM ID Y0TR00 + COUNTRY CODE AU + 16/07/07 18:59 + RRN 00015X000000 + VISA + 411111-111 + CREDIT A/C 12/10 + + AUTHORISATION NO: 000000 + APPROVED 08 + + PURCHASE $1.00 + TOTAL AUD $1.00 + + PLEASE RETAIN AS RECORD + OF PURCHASE + + (SUBJECT TO CARDHOLDER'S + ACCEPTANCE) + ------------------------ + . + settlement_date=16/07/07 + card_desc=VISA + status=approved + txn_ref=0707161858000000 + refund_mode=0 + transaction_no=000000 + rrn=00015X000000 + response_text=SIGNATURE REQUIRED + pld=0 + total_amount=100 + card_no=4111111111111111 + version=V1.0 + merchant_index=123 + card_expiry=12/10 + training_mode=0 + operator_no=10000 + response_code=08 + card_type=6 + approved=1 + cashout_amount=0 + receipt_array=ARRAY(0x83725cc) + account_type=CREDIT A/C + result=1 RESPONSE end def successful_credit_response - <<-RESPONSE -approved -00015X000000 -Transaction No: 00000000 ------------------------- -MERCHANTNAME -LOCATION AU - -MERCH ID 10000000 -TERM ID Y0TR00 -COUNTRY CODE AU -16/07/07 19:03 -RRN 00015X000000 -VISA -411111-111 -CREDIT A/C 12/10 - -AUTHORISATION NO: -APPROVED 08 - -** REFUND ** $1.00 -TOTAL AUD $1.00 - -PLEASE RETAIN AS RECORD - OF REFUND - -(SUBJECT TO CARDHOLDER'S - ACCEPTANCE) ------------------------- -. -settlement_date=16/07/07 -card_desc=VISA -status=approved -txn_ref=0707161902000000 -refund_mode=1 -transaction_no=000000 -rrn=00015X000000 -response_text=SIGNATURE REQUIRED -pld=0 -total_amount=100 -card_no=4111111111111111 -version=V1.0 -merchant_index=123 -card_expiry=12/10 -training_mode=0 -operator_no=10000 -response_code=08 -card_type=6 -approved=1 -cashout_amount=0 -receipt_array=ARRAY(0x837241c) -account_type=CREDIT A/C -result=1 + <<~RESPONSE + approved + 00015X000000 + Transaction No: 00000000 + ------------------------ + MERCHANTNAME + LOCATION AU + + MERCH ID 10000000 + TERM ID Y0TR00 + COUNTRY CODE AU + 16/07/07 19:03 + RRN 00015X000000 + VISA + 411111-111 + CREDIT A/C 12/10 + + AUTHORISATION NO: + APPROVED 08 + + ** REFUND ** $1.00 + TOTAL AUD $1.00 + + PLEASE RETAIN AS RECORD + OF REFUND + + (SUBJECT TO CARDHOLDER'S + ACCEPTANCE) + ------------------------ + . + settlement_date=16/07/07 + card_desc=VISA + status=approved + txn_ref=0707161902000000 + refund_mode=1 + transaction_no=000000 + rrn=00015X000000 + response_text=SIGNATURE REQUIRED + pld=0 + total_amount=100 + card_no=4111111111111111 + version=V1.0 + merchant_index=123 + card_expiry=12/10 + training_mode=0 + operator_no=10000 + response_code=08 + card_type=6 + approved=1 + cashout_amount=0 + receipt_array=ARRAY(0x837241c) + account_type=CREDIT A/C + result=1 RESPONSE end def successful_authorization_response - <<-RESPONSE -approved -00015X000000 -Transaction No: 00000000 ------------------------- -MERCHANTNAME -LOCATION AU - -MERCH ID 10000000 -TERM ID Y0TR00 -COUNTRY CODE AU -17/07/07 15:22 -RRN 00015X000000 -VISA -411111-111 -CREDIT A/C 12/10 - -AUTHORISATION NO: 000000 -APPROVED 08 - -PURCHASE $1.00 -TOTAL AUD $1.00 - -PLEASE RETAIN AS RECORD - OF PURCHASE - -(SUBJECT TO CARDHOLDER'S - ACCEPTANCE) ------------------------- -. -settlement_date=17/07/07 -card_desc=VISA -status=approved -txn_ref=0707171521000000 -refund_mode=0 -transaction_no=000000 -rrn=00015X000000 -response_text=SIGNATURE REQUIRED -pld=0 -total_amount=100 -card_no=4111111111111111 -version=V1.0 -merchant_index=123 -card_expiry=12/10 -training_mode=0 -operator_no=10000 -response_code=08 -card_type=6 -approved=1 -cashout_amount=0 -receipt_array=ARRAY(0x836a25c) -account_type=CREDIT A/C -result=1 + <<~RESPONSE + approved + 00015X000000 + Transaction No: 00000000 + ------------------------ + MERCHANTNAME + LOCATION AU + + MERCH ID 10000000 + TERM ID Y0TR00 + COUNTRY CODE AU + 17/07/07 15:22 + RRN 00015X000000 + VISA + 411111-111 + CREDIT A/C 12/10 + + AUTHORISATION NO: 000000 + APPROVED 08 + + PURCHASE $1.00 + TOTAL AUD $1.00 + + PLEASE RETAIN AS RECORD + OF PURCHASE + + (SUBJECT TO CARDHOLDER'S + ACCEPTANCE) + ------------------------ + . + settlement_date=17/07/07 + card_desc=VISA + status=approved + txn_ref=0707171521000000 + refund_mode=0 + transaction_no=000000 + rrn=00015X000000 + response_text=SIGNATURE REQUIRED + pld=0 + total_amount=100 + card_no=4111111111111111 + version=V1.0 + merchant_index=123 + card_expiry=12/10 + training_mode=0 + operator_no=10000 + response_code=08 + card_type=6 + approved=1 + cashout_amount=0 + receipt_array=ARRAY(0x836a25c) + account_type=CREDIT A/C + result=1 RESPONSE end def successful_capture_response - <<-RESPONSE -approved -00015X000000 -Transaction No: 00000000 ------------------------- -MERCHANTNAME -LOCATION AU - -MERCH ID 10000000 -TERM ID Y0TR00 -COUNTRY CODE AU -17/07/07 15:23 -RRN 00015X000000 -VISA -411111-111 -CREDIT A/C 12/10 - -AUTHORISATION NO: 000000 -APPROVED 08 - -PURCHASE $1.00 -TOTAL AUD $1.00 - -PLEASE RETAIN AS RECORD - OF PURCHASE - -(SUBJECT TO CARDHOLDER'S - ACCEPTANCE) ------------------------- -. -settlement_date=17/07/07 -card_desc=VISA -status=approved -txn_ref=0707171522000000 -refund_mode=0 -transaction_no=000000 -rrn=00015X000000 -response_text=SIGNATURE REQUIRED -pld=0 -total_amount=100 -card_no=4111111111111111 -version=V1.0 -merchant_index=123 -card_expiry=12/10 -training_mode=0 -operator_no=10000 -response_code=08 -card_type=6 -approved=1 -cashout_amount=0 -receipt_array=ARRAY(0x8378200) -account_type=CREDIT A/C -result=1 + <<~RESPONSE + approved + 00015X000000 + Transaction No: 00000000 + ------------------------ + MERCHANTNAME + LOCATION AU + + MERCH ID 10000000 + TERM ID Y0TR00 + COUNTRY CODE AU + 17/07/07 15:23 + RRN 00015X000000 + VISA + 411111-111 + CREDIT A/C 12/10 + + AUTHORISATION NO: 000000 + APPROVED 08 + + PURCHASE $1.00 + TOTAL AUD $1.00 + + PLEASE RETAIN AS RECORD + OF PURCHASE + + (SUBJECT TO CARDHOLDER'S + ACCEPTANCE) + ------------------------ + . + settlement_date=17/07/07 + card_desc=VISA + status=approved + txn_ref=0707171522000000 + refund_mode=0 + transaction_no=000000 + rrn=00015X000000 + response_text=SIGNATURE REQUIRED + pld=0 + total_amount=100 + card_no=4111111111111111 + version=V1.0 + merchant_index=123 + card_expiry=12/10 + training_mode=0 + operator_no=10000 + response_code=08 + card_type=6 + approved=1 + cashout_amount=0 + receipt_array=ARRAY(0x8378200) + account_type=CREDIT A/C + result=1 RESPONSE end def purchase_with_invalid_credit_card_response - <<-RESPONSE -declined -00015X000000 -Transaction No: 00000000 ------------------------- -MERCHANTNAME -LOCATION AU - -MERCH ID 10000000 -TERM ID Y0TR40 -COUNTRY CODE AU -16/07/07 19:20 -RRN 00015X000000 -VISA -411111-111 -CREDIT A/C 12/10 - -AUTHORISATION NO: -DECLINED 31 - -PURCHASE $1.00 -TOTAL AUD $1.00 - -(SUBJECT TO CARDHOLDER'S - ACCEPTANCE) ------------------------- -. -settlement_date=16/07/07 -card_desc=VISA -status=declined -txn_ref=0707161919000000 -refund_mode=0 -transaction_no=000000 -rrn=00015X000000 -response_text=INVALID CARD -pld=0 -total_amount=100 -card_no=4111111111111111 -version=V1.0 -merchant_index=123 -card_expiry=12/10 -training_mode=0 -operator_no=10000 -response_code=31 -card_type=6 -approved=0 -cashout_amount=0 -receipt_array=ARRAY(0x83752d0) -account_type=CREDIT A/C -result=0 -RESPONSE + <<~RESPONSE + declined + 00015X000000 + Transaction No: 00000000 + ------------------------ + MERCHANTNAME + LOCATION AU + + MERCH ID 10000000 + TERM ID Y0TR40 + COUNTRY CODE AU + 16/07/07 19:20 + RRN 00015X000000 + VISA + 411111-111 + CREDIT A/C 12/10 + + AUTHORISATION NO: + DECLINED 31 + + PURCHASE $1.00 + TOTAL AUD $1.00 + + (SUBJECT TO CARDHOLDER'S + ACCEPTANCE) + ------------------------ + . + settlement_date=16/07/07 + card_desc=VISA + status=declined + txn_ref=0707161919000000 + refund_mode=0 + transaction_no=000000 + rrn=00015X000000 + response_text=INVALID CARD + pld=0 + total_amount=100 + card_no=4111111111111111 + version=V1.0 + merchant_index=123 + card_expiry=12/10 + training_mode=0 + operator_no=10000 + response_code=31 + card_type=6 + approved=0 + cashout_amount=0 + receipt_array=ARRAY(0x83752d0) + account_type=CREDIT A/C + result=0 + RESPONSE end def purchase_with_expired_credit_card_response - <<-RESPONSE -failed - - -. -response_text=CARD EXPIRED -approved=0 -status=failed -txn_ref=0707161910000000 -version=V1.0 -pld=0 -response_code=Q816 -result=-1 + <<~RESPONSE + failed + + + . + response_text=CARD EXPIRED + approved=0 + status=failed + txn_ref=0707161910000000 + version=V1.0 + pld=0 + response_code=Q816 + result=-1 RESPONSE end def purchase_with_invalid_month_response - <<-RESPONSE -failed -Invalid month + <<~RESPONSE + failed + Invalid month RESPONSE end def bad_login_response - <<-RESPONSE -failed + <<~RESPONSE + failed -. -status=failed -result=-1 + . + status=failed + result=-1 RESPONSE end end diff --git a/test/unit/gateways/netaxept_test.rb b/test/unit/gateways/netaxept_test.rb index 5dddbde3427..92d60776572 100644 --- a/test/unit/gateways/netaxept_test.rb +++ b/test/unit/gateways/netaxept_test.rb @@ -5,15 +5,15 @@ class NetaxeptTest < Test::Unit::TestCase def setup @gateway = NetaxeptGateway.new( - :login => 'login', - :password => 'password' - ) + login: 'login', + password: 'password' + ) @credit_card = credit_card @amount = 100 @options = { - :order_id => '1' + order_id: '1' } end @@ -57,7 +57,7 @@ def test_handles_currency_with_money @gateway.expects(:ssl_get).returns(successful_purchase_response[2]).in_sequence(s) @gateway.expects(:ssl_get).returns(successful_purchase_response[3]).in_sequence(s) - assert_success @gateway.purchase(100, @credit_card, @options.merge(:currency => 'USD')) + assert_success @gateway.purchase(100, @credit_card, @options.merge(currency: 'USD')) end def test_handles_currency_with_option @@ -67,7 +67,7 @@ def test_handles_currency_with_option @gateway.expects(:ssl_get).returns(successful_purchase_response[2]).in_sequence(s) @gateway.expects(:ssl_get).returns(successful_purchase_response[3]).in_sequence(s) - assert_success @gateway.purchase(@amount, @credit_card, @options.merge(:currency => 'USD')) + assert_success @gateway.purchase(@amount, @credit_card, @options.merge(currency: 'USD')) end def test_handles_setup_transaction_error @@ -91,7 +91,7 @@ def test_handles_query_error end def test_url_escape_password - @gateway = NetaxeptGateway.new(:login => 'login', :password => '1a=W+Yr2') + @gateway = NetaxeptGateway.new(login: 'login', password: '1a=W+Yr2') s = sequence('request') @gateway.expects(:ssl_get).with(regexp_matches(/token=1a%3DW%2BYr2/)).returns(successful_purchase_response[0]).in_sequence(s) diff --git a/test/unit/gateways/netbanx_test.rb b/test/unit/gateways/netbanx_test.rb index 56a74c156aa..359521c8c35 100644 --- a/test/unit/gateways/netbanx_test.rb +++ b/test/unit/gateways/netbanx_test.rb @@ -15,7 +15,7 @@ def setup end def test_successful_purchase - @gateway.expects(:ssl_request).returns(successful_purchase_response) + @gateway.expects(:ssl_request).twice.returns(success_verification_response, successful_purchase_response) response = @gateway.purchase(@amount, @credit_card, @options) assert_success response @@ -34,7 +34,7 @@ def test_failed_purchase end def test_successful_authorize - @gateway.expects(:ssl_request).returns(successful_authorize_response) + @gateway.expects(:ssl_request).twice.returns(auth_verification_response, successful_authorize_response) response = @gateway.authorize(@amount, @credit_card, @options) assert_success response @@ -56,7 +56,7 @@ def test_failed_authorize def test_successful_capture @gateway.expects(:ssl_request).returns(successful_capture_response) - response = @gateway.authorize(@amount, '056ff3a9-5274-4452-92ab-0e3b3e591c3b') + response = @gateway.capture(@amount, '056ff3a9-5274-4452-92ab-0e3b3e591c3b') assert_success response assert_equal '11e0906b-6596-4490-b0e3-825f71a82799', response.authorization @@ -75,7 +75,7 @@ def test_failed_capture end def test_successful_refund - @gateway.expects(:ssl_request).returns(successful_capture_response) + @gateway.expects(:ssl_request).twice.returns(success_verification_response, successful_capture_response) response = @gateway.refund(@amount, '056ff3a9-5274-4452-92ab-0e3b3e591c3b') assert_success response @@ -128,8 +128,8 @@ def test_successful_store assert response.test? end - def test_successful_purchase_with_token - @gateway.expects(:ssl_request).returns(successful_purchase_with_token_response) + def test_successful_purchase_token + @gateway.expects(:ssl_request).twice.returns(success_verification_response, purchase_with_token_response) response = @gateway.purchase(@amount, 'CL0RCSnrkREnfwA', @options) assert_success response @@ -140,18 +140,17 @@ def test_successful_purchase_with_token end def test_successful_unstore - @gateway.expects(:ssl_request).twice.returns(successful_unstore_response) + @gateway.expects(:ssl_request).twice.returns(successful_unstore_response) - response = @gateway.unstore('2f840ab3-0e71-4387-bad3-4705e6f4b015|e4a3cd5a-56db-4d9b-97d3-fdd9ab3bd0f4') - assert_success response - assert response.test? + response = @gateway.unstore('2f840ab3-0e71-4387-bad3-4705e6f4b015|e4a3cd5a-56db-4d9b-97d3-fdd9ab3bd0f4') + assert_success response + assert response.test? response = @gateway.unstore('2f840ab3-0e71-4387-bad3-4705e6f4b015') assert_success response assert response.test? end - def test_scrub assert @gateway.supports_scrubbing? assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed @@ -312,7 +311,7 @@ def failed_purchase_response RESPONSE end - def successful_purchase_with_token_response + def purchase_with_token_response <<-RESPONSE { "links": [ @@ -417,6 +416,26 @@ def successful_authorize_response RESPONSE end + def auth_verification_response + <<-RESPONSE + { + "id": "b8c53059-9da3-4054-8caf-3769161a3cdc", + "status": "COMPLETED", + "message": "OK" + } + RESPONSE + end + + def success_verification_response + <<-RESPONSE + { + "id": "11e0906b-6596-4490-b0e3-825f71a82799", + "status": "COMPLETED", + "message": "OK" + } + RESPONSE + end + def failed_authorize_response <<-RESPONSE { diff --git a/test/unit/gateways/netbilling_test.rb b/test/unit/gateways/netbilling_test.rb index 1230046dfe1..5b0b7609013 100644 --- a/test/unit/gateways/netbilling_test.rb +++ b/test/unit/gateways/netbilling_test.rb @@ -4,11 +4,11 @@ class NetbillingTest < Test::Unit::TestCase include CommStub def setup - @gateway = NetbillingGateway.new(:login => 'login') + @gateway = NetbillingGateway.new(login: 'login') @credit_card = credit_card('4242424242424242') @amount = 100 - @options = { :billing_address => address } + @options = { billing_address: address } end def test_successful_request @@ -43,11 +43,11 @@ def test_cvv_result end def test_site_tag_sent_if_provided - @gateway = NetbillingGateway.new(:login => 'login', :site_tag => 'dummy-site-tag') + @gateway = NetbillingGateway.new(login: 'login', site_tag: 'dummy-site-tag') response = stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/site_tag=dummy-site-tag/, data) end.respond_with(successful_purchase_response) @@ -57,7 +57,7 @@ def test_site_tag_sent_if_provided def test_site_tag_not_sent_if_not_provided response = stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_no_match(/site_tag/, data) end.respond_with(successful_purchase_response) @@ -114,6 +114,7 @@ def test_transcript_scrubbing end private + def successful_purchase_response 'avs_code=X&cvv2_code=M&status_code=1&auth_code=999999&trans_id=110270311543&auth_msg=TEST+APPROVED&auth_date=2008-01-25+16:43:54' end diff --git a/test/unit/gateways/netpay_test.rb b/test/unit/gateways/netpay_test.rb index 42a47d8b3da..92cd1ff0452 100644 --- a/test/unit/gateways/netpay_test.rb +++ b/test/unit/gateways/netpay_test.rb @@ -3,10 +3,10 @@ class NetpayTest < Test::Unit::TestCase def setup @gateway = NetpayGateway.new( - :store_id => '12345', - :login => 'login', - :password => 'password' - ) + store_id: '12345', + login: 'login', + password: 'password' + ) @credit_card = credit_card @amount = 1000 @@ -14,7 +14,7 @@ def setup @order_id = 'C3836048-631F-112B-001E-7C08C0406975' @options = { - :description => 'Store Purchase' + description: 'Store Purchase' } end @@ -64,7 +64,7 @@ def test_successful_purchase_with_ip ) ).returns(successful_response) - assert response = @gateway.purchase(@amount, @credit_card, :ip => '127.0.0.1') + assert response = @gateway.purchase(@amount, @credit_card, ip: '127.0.0.1') assert_success response end @@ -76,7 +76,6 @@ def test_unsuccessful_purchase assert response.test? end - def test_successful_authorize @gateway.expects(:ssl_post).with( anything, @@ -151,7 +150,7 @@ def test_supported_countries end def test_supported_cardtypes - assert_equal [:visa, :master, :american_express, :diners_club], NetpayGateway.supported_cardtypes + assert_equal %i[visa master american_express diners_club], NetpayGateway.supported_cardtypes end private diff --git a/test/unit/gateways/network_merchants_test.rb b/test/unit/gateways/network_merchants_test.rb index 57fbb837167..5ea99c7da9e 100644 --- a/test/unit/gateways/network_merchants_test.rb +++ b/test/unit/gateways/network_merchants_test.rb @@ -3,18 +3,18 @@ class NetworkMerchantsTest < Test::Unit::TestCase def setup @gateway = NetworkMerchantsGateway.new( - :login => 'login', - :password => 'password' - ) + login: 'login', + password: 'password' + ) @credit_card = credit_card @check = check @amount = 100 @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase' + order_id: '1', + billing_address: address, + description: 'Store Purchase' } end @@ -60,7 +60,7 @@ def test_successful_check_purchase def test_purchase_and_store @gateway.expects(:ssl_post).returns(successful_purchase_and_store) - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(:store => true)) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(store: true)) assert_success response assert_equal 'SUCCESS', response.message assert_equal '1378262091', response.params['customer_vault_id'] @@ -156,7 +156,7 @@ def test_purchase_on_stored_card end def test_invalid_login - gateway = NetworkMerchantsGateway.new(:login => '', :password => '') + gateway = NetworkMerchantsGateway.new(login: '', password: '') gateway.expects(:ssl_post).returns(failed_login) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response diff --git a/test/unit/gateways/nmi_test.rb b/test/unit/gateways/nmi_test.rb index 8512e3d9ba1..c1d56f3b57a 100644 --- a/test/unit/gateways/nmi_test.rb +++ b/test/unit/gateways/nmi_test.rb @@ -5,17 +5,146 @@ class NmiTest < Test::Unit::TestCase def setup @gateway = NmiGateway.new(fixtures(:nmi)) + @gateway_secure = NmiGateway.new(fixtures(:nmi_secure)) @amount = 100 @credit_card = credit_card @check = check - @options = {} + + @merchant_defined_fields = { merchant_defined_field_8: 'value8' } + + @transaction_options = { + recurring: true, order_id: '#1001', description: 'AM test', currency: 'GBP', dup_seconds: 15, + customer: '123', tax: 5.25, shipping: 10.51, ponumber: 1002 + } + @descriptor_options = { + descriptor: 'test', + descriptor_phone: '123', + descriptor_address: 'address', + descriptor_city: 'city', + descriptor_state: 'state', + descriptor_postal: 'postal', + descriptor_country: 'country', + descriptor_mcc: 'mcc', + descriptor_merchant_id: '120', + descriptor_url: 'url' + } + + @google_pay = network_tokenization_credit_card( + '4111111111111111', + brand: 'visa', + eci: '05', + month: '02', + year: '2035', + source: :google_pay, + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + transaction_id: '13456789' + ) + + @apple_pay = network_tokenization_credit_card( + '4111111111111111', + brand: 'visa', + eci: '05', + month: '02', + year: '2035', + source: :apple_pay, + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + transaction_id: '13456789' + ) + end + + def test_successful_apple_pay_purchase + stub_comms do + @gateway.purchase(@amount, @apple_pay) + end.check_request do |_endpoint, data, _headers| + assert_match(/type=sale/, data) + assert_match(/ccnumber=4111111111111111/, data) + assert_match(/ccexp=#{sprintf("%.2i", @apple_pay.month)}#{@apple_pay.year.to_s[-2..-1]}/, data) + assert_match(/cavv=EHuWW9PiBkWvqE5juRwDzAUFBAk%3D/, data) + assert_match(/eci=05/, data) + assert_match(/decrypted_applepay_data/, data) + assert_nil data['decrypted_googlepay_data'] + end.respond_with(successful_purchase_response) + end + + def test_successful_google_pay_purchase + stub_comms do + @gateway.purchase(@amount, @google_pay) + end.check_request do |_endpoint, data, _headers| + assert_match(/type=sale/, data) + assert_match(/ccnumber=4111111111111111/, data) + assert_match(/ccexp=#{sprintf("%.2i", @google_pay.month)}#{@google_pay.year.to_s[-2..-1]}/, data) + assert_match(/cavv=EHuWW9PiBkWvqE5juRwDzAUFBAk%3D/, data) + assert_match(/eci=05/, data) + assert_match(/decrypted_googlepay_data/, data) + assert_nil data['decrypted_applepay_data'] + end.respond_with(successful_purchase_response) + end + + def test_successful_authorize_and_capture_using_security_key + @credit_card.number = '4111111111111111' + response = stub_comms do + @gateway_secure.authorize(@amount, @credit_card) + end.check_request do |_endpoint, data, _headers| + assert_match(/security_key=#{@gateway_secure.options[:security_key]}/, data) + assert_match(/type=auth/, data) + 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) + end.respond_with(successful_authorization_response) + assert_success response + capture = stub_comms do + @gateway_secure.capture(@amount, response.authorization) + end.check_request do |_endpoint, data, _headers| + assert_match(/security_key=#{@gateway_secure.options[:security_key]}/, data) + assert_match(/type=capture/, data) + assert_match(/amount=1.00/, data) + assert_match(/transactionid=2762787830/, data) + end.respond_with(successful_capture_response) + assert_success capture + end + + def test_failed_authorize_using_security_key + response = stub_comms do + @gateway_secure.authorize(@amount, @credit_card) + end.respond_with(failed_authorization_response) + + assert_failure response + assert_equal 'DECLINE', response.message + assert response.test? + end + + def test_successful_purchase_using_security_key + @credit_card.number = '4111111111111111' + response = stub_comms do + @gateway_secure.purchase(@amount, @credit_card) + end.check_request do |_endpoint, data, _headers| + assert_match(/security_key=#{@gateway_secure.options[:security_key]}/, data) + assert_match(/type=sale/, data) + assert_match(/amount=1.00/, data) + 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) + end.respond_with(successful_purchase_response) + assert_success response + assert response.test? + end + + def test_failed_purchase_using_security_key + response = stub_comms do + @gateway_secure.purchase(@amount, @credit_card) + end.respond_with(failed_purchase_response) + assert_failure response + assert response.test? + assert_equal 'DECLINE', response.message end def test_successful_purchase response = stub_comms do @gateway.purchase(@amount, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/username=#{@gateway.options[:login]}/, data) assert_match(/password=#{@gateway.options[:password]}/, data) assert_match(/type=sale/, data) @@ -33,24 +162,100 @@ def test_successful_purchase end def test_purchase_with_options + options = @transaction_options.merge(@merchant_defined_fields) + response = stub_comms do - @gateway.purchase(@amount, @credit_card, - recurring: true, order_id: '#1001', description: 'AM test', - currency: 'GBP', dup_seconds: 15, customer: '123', - merchant_defined_field_8: 'value8') - end.check_request do |endpoint, data, headers| - assert_match(/billing_method=recurring/, data) - assert_match(/orderid=#{CGI.escape("#1001")}/, data) - assert_match(/orderdescription=AM\+test/, data) - assert_match(/currency=GBP/, data) - assert_match(/dup_seconds=15/, data) - assert_match(/customer_id=123/, data) + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + test_transaction_options(data) + assert_match(/merchant_defined_field_8=value8/, data) end.respond_with(successful_purchase_response) 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: }) + + 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_customer_vault_options + options = { + description: 'Store purchase', + customer_vault: 'add_customer', + customer_vault_id: '12345abcde' + } + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/customer_vault=add_customer/, data) + assert_match(/customer_vault_id=12345abcde/, 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_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) @@ -64,7 +269,7 @@ def test_failed_purchase def test_successful_purchase_with_echeck response = stub_comms do @gateway.purchase(@amount, @check) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/username=#{@gateway.options[:login]}/, data) assert_match(/password=#{@gateway.options[:password]}/, data) assert_match(/type=sale/, data) @@ -95,10 +300,107 @@ def test_failed_purchase_with_echeck assert_equal 'FAILED', response.message end + def test_successful_purchase_with_3ds_verified + version = '2.1.0' + authentication_response_status = 'Y' + cavv = 'jJ81HADVRtXfCBATEp01CJUAAAA' + ds_transaction_id = '97267598-FAE6-48F2-8083-C23433990FBC' + xid = '00000000000000000501' + options_with_3ds = @transaction_options.merge( + three_d_secure: { + version:, + authentication_response_status:, + cavv:, + ds_transaction_id:, + xid: + } + ) + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options_with_3ds) + end.check_request do |_endpoint, data, _headers| + assert_match(/three_ds_version=2.1.0/, data) + assert_match(/cardholder_auth=verified/, data) + assert_match(/cavv=jJ81HADVRtXfCBATEp01CJUAAAA/, data) + assert_match(/directory_server_id=97267598-FAE6-48F2-8083-C23433990FBC/, data) + assert_match(/xid=00000000000000000501/, data) + end.respond_with(successful_3ds_purchase_response) + + assert_success response + assert response.test? + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_3ds_attempted + version = '2.1.0' + authentication_response_status = 'A' + cavv = 'jJ81HADVRtXfCBATEp01CJUAAAA' + ds_transaction_id = '97267598-FAE6-48F2-8083-C23433990FBC' + xid = '00000000000000000501' + options_with_3ds = @transaction_options.merge( + three_d_secure: { + version:, + authentication_response_status:, + cavv:, + ds_transaction_id:, + xid: + } + ) + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options_with_3ds) + end.check_request do |_endpoint, data, _headers| + assert_match(/three_ds_version=2.1.0/, data) + assert_match(/cardholder_auth=attempted/, data) + assert_match(/cavv=jJ81HADVRtXfCBATEp01CJUAAAA/, data) + assert_match(/directory_server_id=97267598-FAE6-48F2-8083-C23433990FBC/, data) + assert_match(/xid=00000000000000000501/, data) + end.respond_with(successful_3ds_purchase_response) + + assert_success response + assert response.test? + assert_equal 'Succeeded', response.message + end + + def test_purchase_with_descriptor_options + options = @transaction_options.merge({ descriptors: @descriptor_options }) + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/descriptor=test/, data) + assert_match(/descriptor_phone=123/, data) + assert_match(/descriptor_address=address/, data) + assert_match(/descriptor_city=city/, data) + assert_match(/descriptor_state=state/, data) + assert_match(/descriptor_postal=postal/, data) + assert_match(/descriptor_country=country/, data) + assert_match(/descriptor_mcc=mcc/, data) + assert_match(/descriptor_merchant_id=120/, data) + assert_match(/descriptor_url=url/, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_authorize_with_options + options = @transaction_options.merge(@merchant_defined_fields) + + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + test_transaction_options(data) + + assert_match(/merchant_defined_field_8=value8/, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + def test_successful_authorize_and_capture response = stub_comms do @gateway.authorize(@amount, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/username=#{@gateway.options[:login]}/, data) assert_match(/password=#{@gateway.options[:password]}/, data) assert_match(/type=auth/, data) @@ -113,7 +415,7 @@ def test_successful_authorize_and_capture capture = stub_comms do @gateway.capture(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/username=#{@gateway.options[:login]}/, data) assert_match(/password=#{@gateway.options[:password]}/, data) assert_match(/type=capture/, data) @@ -152,7 +454,7 @@ def test_successful_void void = stub_comms do @gateway.void(response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/username=#{@gateway.options[:login]}/, data) assert_match(/password=#{@gateway.options[:password]}/, data) assert_match(/type=void/, data) @@ -180,7 +482,7 @@ def test_successful_refund refund = stub_comms do @gateway.refund(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/username=#{@gateway.options[:login]}/, data) assert_match(/password=#{@gateway.options[:password]}/, data) assert_match(/type=refund/, data) @@ -202,7 +504,7 @@ def test_failed_refund def test_successful_credit response = stub_comms do @gateway.credit(@amount, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/username=#{@gateway.options[:login]}/, data) assert_match(/password=#{@gateway.options[:password]}/, data) assert_match(/type=credit/, data) @@ -219,6 +521,16 @@ def test_successful_credit assert response.test? end + def test_credit_with_options + response = stub_comms do + @gateway.credit(@amount, @credit_card, @transaction_options) + end.check_request do |_endpoint, data, _headers| + test_transaction_options(data) + end.respond_with(successful_credit_response) + + assert_success response + end + def test_failed_credit response = stub_comms do @gateway.credit(@amount, @credit_card) @@ -230,20 +542,11 @@ def test_failed_credit end def test_successful_verify - response = stub_comms do - @gateway.verify(@credit_card) - end.check_request do |endpoint, data, headers| - assert_match(/username=#{@gateway.options[:login]}/, data) - assert_match(/password=#{@gateway.options[:password]}/, data) - assert_match(/type=validate/, data) - 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) - end.respond_with(successful_validate_response) + test_verify + end - assert_success response - assert_equal 'Succeeded', response.message + def test_verify_with_options + test_verify(@transaction_options) end def test_failed_verify @@ -258,7 +561,7 @@ def test_failed_verify def test_successful_store response = stub_comms do @gateway.store(@credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/username=#{@gateway.options[:login]}/, data) assert_match(/password=#{@gateway.options[:password]}/, data) assert_match(/customer_vault=add_customer/, data) @@ -272,6 +575,7 @@ def test_successful_store assert response.test? assert_equal 'Succeeded', response.message assert response.params['customer_vault_id'] + assert response.authorization.include?(response.params['customer_vault_id']) end def test_failed_store @@ -287,7 +591,7 @@ def test_failed_store def test_successful_store_with_echeck response = stub_comms do @gateway.store(@check) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/username=#{@gateway.options[:login]}/, data) assert_match(/password=#{@gateway.options[:password]}/, data) assert_match(/customer_vault=add_customer/, data) @@ -303,7 +607,7 @@ def test_successful_store_with_echeck assert_success response assert response.test? assert_equal 'Succeeded', response.message - assert response.params['customer_vault_id'] + assert response.authorization.include?(response.params['customer_vault_id']) end def test_avs_result @@ -329,34 +633,43 @@ def test_transcript_scrubbing def test_includes_cvv_tag stub_comms do @gateway.purchase(@amount, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(%r{cvv}, data) end.respond_with(successful_purchase_response) end + def test_includes_industry_field + @transaction_options[:industry_indicator] = 'ecommerce' + + stub_comms do + @gateway.purchase(@amount, @credit_card, @transaction_options) + end.check_request do |_endpoint, data, _headers| + assert_match(%r{ecommerce}, data) + end.respond_with(successful_purchase_response) + end + def test_blank_cvv_not_sent @credit_card.verification_value = nil stub_comms do @gateway.purchase(@amount, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_no_match(%r{cvv}, data) end.respond_with(successful_purchase_response) @credit_card.verification_value = ' ' stub_comms do @gateway.purchase(@amount, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_no_match(%r{cvv}, data) end.respond_with(successful_purchase_response) 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 - assert_equal [:visa, :master, :american_express, :discover], NmiGateway.supported_cardtypes + assert_equal %i[visa master american_express discover], NmiGateway.supported_cardtypes end def test_duplicate_window_deprecation @@ -365,8 +678,283 @@ def test_duplicate_window_deprecation end end + def test_stored_credential_recurring_cit_initial + options = stored_credential_options(:cardholder, :recurring, :initial) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/initiated_by=customer/, data) + assert_match(/stored_credential_indicator=stored/, data) + assert_match(/billing_method=recurring/, data) + refute_match(/initial_transaction_id/, data) + end.respond_with(successful_authorization_response) + + assert_success response + end + + def test_stored_credential_recurring_cit_used + options = stored_credential_options(:cardholder, :recurring, id: 'abc123') + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/initiated_by=customer/, data) + assert_match(/stored_credential_indicator=used/, data) + assert_match(/billing_method=recurring/, data) + refute_match(/initial_transaction_id/, data) + end.respond_with(successful_authorization_response) + + assert_success response + end + + def test_stored_credential_recurring_mit_initial + options = stored_credential_options(:merchant, :recurring, :initial) + 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=stored/, data) + assert_match(/billing_method=recurring/, data) + refute_match(/initial_transaction_id/, data) + end.respond_with(successful_authorization_response) + + assert_success response + end + + def test_stored_credential_recurring_mit_used + options = stored_credential_options(:merchant, :recurring, id: 'abc123') + 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=abc123/, data) + end.respond_with(successful_authorization_response) + + 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 + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/initiated_by=customer/, data) + assert_match(/stored_credential_indicator=stored/, data) + assert_match(/billing_method=installment/, data) + refute_match(/initial_transaction_id/, data) + end.respond_with(successful_authorization_response) + + assert_success response + end + + def test_stored_credential_installment_cit_used + options = stored_credential_options(:cardholder, :installment, id: 'abc123') + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/initiated_by=customer/, data) + assert_match(/stored_credential_indicator=used/, data) + assert_match(/billing_method=installment/, data) + refute_match(/initial_transaction_id/, data) + end.respond_with(successful_authorization_response) + + assert_success response + end + + def test_stored_credential_installment_mit_initial + options = stored_credential_options(:merchant, :installment, :initial) + 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=stored/, data) + assert_match(/billing_method=installment/, data) + refute_match(/initial_transaction_id/, data) + end.respond_with(successful_authorization_response) + + assert_success response + end + + def test_stored_credential_installment_mit_used + options = stored_credential_options(:merchant, :installment, id: 'abc123') + 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=installment/, data) + assert_match(/initial_transaction_id=abc123/, data) + end.respond_with(successful_authorization_response) + + assert_success response + end + + def test_stored_credential_unscheduled_cit_initial + options = stored_credential_options(:cardholder, :unscheduled, :initial) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/initiated_by=customer/, data) + assert_match(/stored_credential_indicator=stored/, data) + refute_match(/billing_method/, data) + refute_match(/initial_transaction_id/, data) + end.respond_with(successful_authorization_response) + + assert_success response + end + + def test_stored_credential_unscheduled_cit_used + options = stored_credential_options(:cardholder, :unscheduled, id: 'abc123') + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/initiated_by=customer/, data) + assert_match(/stored_credential_indicator=used/, data) + refute_match(/billing_method/, data) + refute_match(/initial_transaction_id/, data) + end.respond_with(successful_authorization_response) + + assert_success response + end + + def test_stored_credential_unscheduled_mit_initial + options = stored_credential_options(:merchant, :unscheduled, :initial) + 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=stored/, data) + refute_match(/billing_method/, data) + refute_match(/initial_transaction_id/, data) + end.respond_with(successful_authorization_response) + + assert_success response + end + + def test_stored_credential_unscheduled_mit_used + options = stored_credential_options(:merchant, :unscheduled, id: 'abc123') + 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) + refute_match(/billing_method/, data) + assert_match(/initial_transaction_id=abc123/, data) + end.respond_with(successful_authorization_response) + + assert_success response + end + + def test_purchase_with_stored_credential + options = stored_credential_options(:merchant, :installment, id: 'abc123') + response = stub_comms do + @gateway.purchase(@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=installment/, data) + assert_match(/initial_transaction_id=abc123/, data) + end.respond_with(successful_authorization_response) + + assert_success response + end + + def test_stored_credential_installment_takes_precedence_over_recurring_option + options = stored_credential_options(:merchant, :installment, id: 'abc123').merge(recurring: true) + 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=installment/, data) + assert_match(/initial_transaction_id=abc123/, data) + end.respond_with(successful_authorization_response) + + assert_success response + end + + def test_stored_credential_unscheduled_takes_precedence_over_recurring_option + options = stored_credential_options(:merchant, :unscheduled, id: 'abc123').merge(recurring: true) + 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) + refute_match(/billing_method/, data) + assert_match(/initial_transaction_id=abc123/, data) + end.respond_with(successful_authorization_response) + + assert_success response + end + private + def test_verify(options = {}) + response = stub_comms do + @gateway.verify(@credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/username=#{@gateway.options[:login]}/, data) + assert_match(/password=#{@gateway.options[:password]}/, data) + assert_match(/type=validate/, data) + 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) + + test_level3_options(data) if options.any? + end.respond_with(successful_validate_response) + + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_level3_options(data) + assert_match(/tax=5.25/, data) + assert_match(/shipping=10.51/, data) + assert_match(/ponumber=1002/, data) + end + + def test_transaction_options(data) + assert_match(/billing_method=recurring/, data) + assert_match(/orderid=#{CGI.escape("#1001")}/, data) + assert_match(/orderdescription=AM\+test/, data) + assert_match(/currency=GBP/, data) + assert_match(/dup_seconds=15/, data) + assert_match(/customer_id=123/, data) + + test_level3_options(data) + end + + def stored_credential_options(*args, id: nil) + { + order_id: '#1001', + description: 'AM test', + currency: 'GBP', + dup_seconds: 15, + customer: '123', + tax: 5.25, + shipping: 10.51, + ponumber: 1002, + stored_credential: stored_credential(*args, id:) + } + end + def successful_purchase_response 'response=1&responsetext=SUCCESS&authcode=123456&transactionid=2762757839&avsresponse=N&cvvresponse=N&orderid=b6c1c57f709cfaa65a5cf5b8532ad181&type=&response_code=100' end @@ -383,6 +971,10 @@ def failed_echeck_purchase_response 'response=2&responsetext=FAILED&authcode=123456&transactionid=2762783009&avsresponse=&cvvresponse=&orderid=8070b75a09d75c3e84e1c17d44bbbf34&type=&response_code=200' end + def successful_3ds_purchase_response + 'response=1&responsetext=SUCCESS&authcode=123456&transactionid=97267598-FAE6-48F2-8083-C23433990FBC&avsresponse=&cvvresponse=&orderid=b6c1c57f709cfaa65a5cf5b8532ad181&type=&response_code=100' + end + def successful_authorization_response 'response=1&responsetext=SUCCESS&authcode=123456&transactionid=2762787830&avsresponse=N&cvvresponse=N&orderid=7655856b032e28d2106d724fc26cd04d&type=&response_code=100' end @@ -444,20 +1036,20 @@ def successful_echeck_store_response end def transcript - %q( - amount=1.00&orderid=c9f2fb356d2a839d315aa6e8d7ed2404&orderdescription=Store+purchase&currency=USD&payment=creditcard&firstname=Longbob&lastname=Longsen&ccnumber=4111111111111111&cvv=917&ccexp=0916&email=&ipaddress=&company=Widgets+Inc&address1=456+My+Street&address2=Apt+1&city=Ottawa&state=ON&country=CA&zip=K1C2N6&phone=%28555%29555-5555&type=sale&username=demo&password=password + ' + amount=1.00&orderid=c9f2fb356d2a839d315aa6e8d7ed2404&orderdescription=Store+purchase&currency=USD&payment=creditcard&firstname=Longbob&lastname=Longsen&ccnumber=4111111111111111&cvv=917&ccexp=0916&email=&ipaddress=&company=Widgets+Inc&address1=456+My+Street&address2=Apt+1&city=Ottawa&state=ON&country=CA&zip=K1C2N6&phone=%28555%29555-5555&type=sale&username=demo&password=passwordw$thsym%ols response=1&responsetext=SUCCESS&authcode=123456&transactionid=2767466670&avsresponse=N&cvvresponse=N&orderid=c9f2fb356d2a839d315aa6e8d7ed2404&type=sale&response_code=100 - amount=1.00&orderid=e88df316d8ba3c8c6b98aa93b78facc0&orderdescription=Store+purchase&currency=USD&payment=check&checkname=Jim+Smith&checkaba=123123123&checkaccount=123123123&account_holder_type=personal&account_type=checking&sec_code=WEB&email=&ipaddress=&company=Widgets+Inc&address1=456+My+Street&address2=Apt+1&city=Ottawa&state=ON&country=CA&zip=K1C2N6&phone=%28555%29555-5555&type=sale&username=demo&password=password + amount=1.00&orderid=e88df316d8ba3c8c6b98aa93b78facc0&orderdescription=Store+purchase&currency=USD&payment=check&checkname=Jim+Smith&checkaba=123123123&checkaccount=123123123&account_holder_type=personal&account_type=checking&sec_code=WEB&email=&ipaddress=&company=Widgets+Inc&address1=456+My+Street&address2=Apt+1&city=Ottawa&state=ON&country=CA&zip=K1C2N6&phone=%28555%29555-5555&type=sale&username=demo&password=passwordw$thsym%ols response=1&responsetext=SUCCESS&authcode=123456&transactionid=2767467157&avsresponse=&cvvresponse=&orderid=e88df316d8ba3c8c6b98aa93b78facc0&type=sale&response_code=100 - ) + ' end def scrubbed_transcript - %q( + ' amount=1.00&orderid=c9f2fb356d2a839d315aa6e8d7ed2404&orderdescription=Store+purchase&currency=USD&payment=creditcard&firstname=Longbob&lastname=Longsen&ccnumber=[FILTERED]&cvv=[FILTERED]&ccexp=0916&email=&ipaddress=&company=Widgets+Inc&address1=456+My+Street&address2=Apt+1&city=Ottawa&state=ON&country=CA&zip=K1C2N6&phone=%28555%29555-5555&type=sale&username=demo&password=[FILTERED] response=1&responsetext=SUCCESS&authcode=123456&transactionid=2767466670&avsresponse=N&cvvresponse=N&orderid=c9f2fb356d2a839d315aa6e8d7ed2404&type=sale&response_code=100 amount=1.00&orderid=e88df316d8ba3c8c6b98aa93b78facc0&orderdescription=Store+purchase&currency=USD&payment=check&checkname=Jim+Smith&checkaba=[FILTERED]&checkaccount=[FILTERED]&account_holder_type=personal&account_type=checking&sec_code=WEB&email=&ipaddress=&company=Widgets+Inc&address1=456+My+Street&address2=Apt+1&city=Ottawa&state=ON&country=CA&zip=K1C2N6&phone=%28555%29555-5555&type=sale&username=demo&password=[FILTERED] response=1&responsetext=SUCCESS&authcode=123456&transactionid=2767467157&avsresponse=&cvvresponse=&orderid=e88df316d8ba3c8c6b98aa93b78facc0&type=sale&response_code=100 - ) + ' end end diff --git a/test/unit/gateways/nuvei_test.rb b/test/unit/gateways/nuvei_test.rb new file mode 100644 index 00000000000..b17229881cd --- /dev/null +++ b/test/unit/gateways/nuvei_test.rb @@ -0,0 +1,713 @@ +require 'test_helper' + +class NuveiTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = NuveiGateway.new( + merchant_id: 'SOMECREDENTIAL', + merchant_site_id: 'SOMECREDENTIAL', + secret_key: 'SOMECREDENTIAL', + session_token: 'fdda0126-674f-4f8c-ad24-31ac846654ab', + token_expires: Time.now.utc.to_i + 900 + ) + @credit_card = credit_card + @amount = 10000 + + @options = { + email: 'test@gmail.com', + billing_address: address.merge(name: 'Cure Tester'), + ip_address: '127.0.0.1', + order_id: '123456' + } + + @three_ds_options = { + 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' + } + } + } + + @three_d_secure_options = @options.merge({ + three_d_secure: { + cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA=', + ds_transaction_id: '97267598-FAE6-48F2-8083-C23433990FBC', + eci: '05' + } + }) + + @post = { + merchantId: 'test_merchant_id', + merchantSiteId: 'test_merchant_site_id', + clientRequestId: 'test_client_request_id', + clientUniqueId: 'test_client_unique_id', + amount: '100', + currency: 'US', + relatedTransactionId: 'test_related_transaction_id', + timeStamp: 'test_time_stamp' + } + + @bank_account = check() + + @apple_pay_card = network_tokenization_credit_card( + '5204 2452 5046 0049', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + month: '12', + year: Time.new.year, + source: :apple_pay, + verification_value: 111, + eci: '5' + ) + + @google_pay_card = network_tokenization_credit_card( + '4761209980011439', + payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', + month: '11', + year: '2022', + source: :google_pay, + verification_value: 111, + eci: '5' + ) + end + + def test_calculate_checksum_authenticate + expected_checksum = Digest::SHA256.hexdigest('test_merchant_idtest_merchant_site_idtest_client_request_idtest_time_stampSOMECREDENTIAL') + assert_equal expected_checksum, @gateway.send(:calculate_checksum, @post, :authenticate) + end + + def test_calculate_checksum_capture + expected_checksum = Digest::SHA256.hexdigest('test_merchant_idtest_merchant_site_idtest_client_request_idtest_client_unique_id100UStest_related_transaction_idtest_time_stampSOMECREDENTIAL') + assert_equal expected_checksum, @gateway.send(:calculate_checksum, @post, :capture) + end + + def test_calculate_checksum_other + expected_checksum = Digest::SHA256.hexdigest('test_merchant_idtest_merchant_site_idtest_client_request_id100UStest_time_stampSOMECREDENTIAL') + assert_equal expected_checksum, @gateway.send(:calculate_checksum, @post, :other) + end + + def supported_card_types + assert_equal %i(visa master american_express discover union_pay), NuveiGateway.supported_cardtypes + end + + def test_supported_countries + assert_equal %w(US CA IN NZ GB AU US), NuveiGateway.supported_countries + end + + def build_request_authenticate_url + action = :authenticate + assert_equal @gateway.send(:url, action), "#{@gateway.test_url}/getSessionToken" + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + def test_client_unique_id_present_without_order_id + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request(skip_response: true) do |_method, endpoint, data, _headers| + if /payment/.match?(endpoint) + json_data = JSON.parse(data) + assert_not_nil(json_data['clientUniqueId']) + end + end + end + + def test_successful_authorize + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_method, endpoint, data, _headers| + json_data = JSON.parse(data) + if /payment/.match?(endpoint) + assert_match(%r(/payment), endpoint) + assert_equal('123456', json_data['clientUniqueId']) + assert_match(/Auth/, json_data['transactionType']) + end + end.respond_with(successful_authorize_response) + end + + def test_valid_money_format + assert_equal :dollars, NuveiGateway.money_format + end + + def test_authorize_sends_decimal_amount + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(10000, @credit_card, @options) + end.check_request(skip_response: true) do |_method, _endpoint, data, _headers| + json_data = JSON.parse(data) + assert_equal '100.00', json_data['amount'] + end + end + + def test_authorize_sends_correct_decimal_amount_with_cents + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(10050, @credit_card, @options) + end.check_request(skip_response: true) do |_method, _endpoint, data, _headers| + json_data = JSON.parse(data) + assert_equal '100.50', json_data['amount'] + end + end + + def test_successful_purchase + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_method, endpoint, data, _headers| + if /payment/.match?(endpoint) + json_data = JSON.parse(data) + assert_equal 'false', json_data['savePM'] + assert_match('100.00', json_data['amount']) + assert_equal('123456', json_data['clientUniqueId']) + assert_match(/#{@credit_card.number}/, json_data['paymentOption']['card']['cardNumber']) + assert_match(/#{@credit_card.verification_value}/, json_data['paymentOption']['card']['CVV']) + assert_match(%r(/payment), endpoint) + end + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_3ds + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge(@three_ds_options)) + end.check_request do |_method, endpoint, data, _headers| + json_data = JSON.parse(data) + payment_option_card = json_data['paymentOption']['card'] + if /(initPayment|payment)/.match?(endpoint) + assert_equal '100.00', json_data['amount'] + assert_equal @credit_card.number, payment_option_card['cardNumber'] + assert_equal @credit_card.verification_value, payment_option_card['CVV'] + end + if /payment/.match?(endpoint) + assert_not_includes payment_option_card['threeD']['v2AdditionalParams'], 'challengePreference' + three_ds_assertions(payment_option_card) + end + end.respond_with(successful_init_payment_response, successful_purchase_response) + end + + def test_successful_purchase_with_null_three_ds_2 + stub_comms(@gateway, :ssl_request) do + options = @options.merge(@three_ds_options) + options[:three_ds_2] = nil + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, endpoint, data, _headers| + json_data = JSON.parse(data) + payment_option_card = json_data['paymentOption']['card'] + if /(initPayment|payment)/.match?(endpoint) + assert_equal '100.00', json_data['amount'] + assert_equal @credit_card.number, payment_option_card['cardNumber'] + assert_equal @credit_card.verification_value, payment_option_card['CVV'] + end + + assert_not_includes payment_option_card, 'threeD' if /payment/.match?(endpoint) + end.respond_with(successful_init_payment_response, successful_purchase_response) + end + + def test_successful_purchase_with_3ds_forced + stub_comms(@gateway, :ssl_request) do + op = @options.dup + op[:force_3d_secure] = true + @gateway.purchase(@amount, @credit_card, op.merge(@three_ds_options)) + end.check_request do |_method, endpoint, data, _headers| + json_data = JSON.parse(data) + payment_option_card = json_data['paymentOption']['card'] + if /payment/.match?(endpoint) + assert_equal '01', payment_option_card['threeD']['v2AdditionalParams']['challengePreference'] + three_ds_assertions(payment_option_card) + end + end.respond_with(successful_init_payment_response, successful_purchase_response) + end + + def test_successful_purchase_with_3ds_exception + stub_comms(@gateway, :ssl_request) do + op = @options.dup + op[:force_3d_secure] = false + @gateway.purchase(@amount, @credit_card, op.merge(@three_ds_options)) + end.check_request do |_method, endpoint, data, _headers| + json_data = JSON.parse(data) + payment_option_card = json_data['paymentOption']['card'] + if /payment/.match?(endpoint) + assert_equal '02', payment_option_card['threeD']['v2AdditionalParams']['challengePreference'] + three_ds_assertions(payment_option_card) + end + end.respond_with(successful_init_payment_response, successful_purchase_response) + end + + def test_not_enrolled_card_purchase_with_3ds + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge(@three_ds_options)) + end.check_request do |_method, endpoint, data, _headers| + json_data = JSON.parse(data) + payment_option_card = json_data['paymentOption']['card'] + if /(initPayment|payment)/.match?(endpoint) + assert_equal '100.00', json_data['amount'] + assert_equal @credit_card.number, payment_option_card['cardNumber'] + assert_equal @credit_card.verification_value, payment_option_card['CVV'] + end + assert_not_includes payment_option_card, 'threeD' if /payment/.match?(endpoint) + end.respond_with(not_enrolled_3ds_init_payment_response, successful_purchase_response) + assert_equal response.message, 'APPROVED' + end + + def test_not_enrolled_card_purchase_with_3ds_and_forced + op = @options.dup + op[:force_3d_secure] = true + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, op.merge(@three_ds_options)) + end.check_request do |_method, endpoint, data, _headers| + json_data = JSON.parse(data) + payment_option_card = json_data['paymentOption']['card'] + if /(initPayment|payment)/.match?(endpoint) + assert_equal '100.00', json_data['amount'] + assert_equal @credit_card.number, payment_option_card['cardNumber'] + assert_equal @credit_card.verification_value, payment_option_card['CVV'] + end + assert_not_includes payment_option_card, 'threeD' if /payment/.match?(endpoint) + end.respond_with(not_enrolled_3ds_init_payment_response, successful_purchase_response) + assert_equal response.message, '3D Secure is required but not supported' + end + + def test_successful_refund + stub_comms(@gateway, :ssl_request) do + @gateway.refund(@amount, '123456', @options) + end.check_request(skip_response: true) do |_method, endpoint, data, _headers| + json_data = JSON.parse(data) + if /refundTransaction/.match?(endpoint) + assert_match(/123456/, json_data['relatedTransactionId']) + assert_match('100.00', json_data['amount']) + end + end + end + + def test_successful_partial_approval + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(55, @credit_card, @options.merge(is_partial_approval: true)) + end.check_request(skip_response: true) do |_method, endpoint, data, _headers| + if /payment/.match?(endpoint) + json_data = JSON.parse(data) + assert_equal 1, json_data['isPartialApproval'] + end + end + end + + def test_successful_unreferenced_refund + stub_comms(@gateway, :ssl_request) do + @gateway.credit(@amount, @credit_card, @options) + end.check_request do |_method, endpoint, data, _headers| + json_data = JSON.parse(data) + assert_match(/refund/, endpoint) + assert_match('100.00', json_data['amount']) + assert_match(/#{@credit_card.number}/, json_data['paymentOption']['card']['cardNumber']) + end.respond_with(successful_purchase_response) + end + + def test_successful_payout + stub_comms(@gateway, :ssl_request) do + @gateway.credit(@amount, @credit_card, @options.merge(user_payment_option_id: '12345678', is_payout: true)) + end.check_request do |_method, endpoint, data, _headers| + json_data = JSON.parse(data) + assert_match(/payout/, endpoint) + assert_match(/#{@credit_card.number}/, json_data['cardData']['cardNumber']) + end.respond_with(successful_purchase_response) + end + + def test_successful_payout_with_google_pay + stub_comms(@gateway, :ssl_request) do + @gateway.credit(@amount, @apple_pay_card, @options.merge(user_payment_option_id: '12345678', is_payout: true)) + end.check_request do |_method, endpoint, data, _headers| + json_data = JSON.parse(data) + assert_match(/payout/, endpoint) + assert_match('12345678', json_data['userPaymentOption']['userPaymentOptionId']) + end.respond_with(successful_purchase_response) + end + + def test_successful_store + stub_comms(@gateway, :ssl_request) do + @gateway.store(@credit_card, @options) + end.check_request do |_method, endpoint, data, _headers| + json_data = JSON.parse(data) + if /payment/.match?(endpoint) + assert_equal 'true', json_data['savePM'] + assert_match(/#{@credit_card.number}/, json_data['paymentOption']['card']['cardNumber']) + assert_equal '0.00', json_data['amount'] + end + end.respond_with(successful_purchase_response) + end + + def test_successful_stored_credentials_cardholder_unscheduled + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential: stored_credential(:cardholder, :unscheduled, :initial))) + end.check_request do |_method, endpoint, data, _headers| + if /payment/.match?(endpoint) + json_data = JSON.parse(data) + assert_equal('0', json_data['isRebilling']) + assert_equal('0', json_data['paymentOption']['card']['storedCredentials']['storedCredentialsMode']) + assert_match(/ADDCARD/, json_data['authenticationOnlyType']) + end + end.respond_with(successful_purchase_response) + end + + def test_successful_stored_credentials_merchant_recurring + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential: stored_credential(:merchant, :recurring, id: 'abc123'))) + end.check_request do |_method, endpoint, data, _headers| + if /payment/.match?(endpoint) + json_data = JSON.parse(data) + assert_equal('1', json_data['isRebilling']) + assert_equal('1', json_data['paymentOption']['card']['storedCredentials']['storedCredentialsMode']) + assert_match(/abc123/, json_data['relatedTransactionId']) + assert_match(/RECURRING/, json_data['authenticationOnlyType']) + end + end.respond_with(successful_purchase_response) + end + + def test_successful_authorize_bank_account + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(1.25, @bank_account, @options) + end.check_request(skip_response: true) do |_method, endpoint, data, _headers| + json_data = JSON.parse(data) + if /payment/.match?(endpoint) + assert_equal('apmgw_ACH', json_data['paymentOption']['alternativePaymentMethod']['paymentMethod']) + assert_match(/#{@bank_account.routing_number}/, json_data['paymentOption']['alternativePaymentMethod']['RoutingNumber']) + assert_match(/#{@bank_account.account_number}/, json_data['paymentOption']['alternativePaymentMethod']['AccountNumber']) + end + end + end + + def test_successful_verify + @options.merge!(authentication_only_type: 'ACCOUNTVERIFICATION') + stub_comms(@gateway, :ssl_request) do + @gateway.verify(@credit_card, @options) + end.check_request(skip_response: true) do |_method, endpoint, data, _headers| + if /payment/.match?(endpoint) + json_data = JSON.parse(data) + assert_match(/Auth/, json_data['transactionType']) + assert_match(/ACCOUNTVERIFICATION/, json_data['authenticationOnlyType']) + assert_equal '0.00', json_data['amount'] + end + end + end + + def test_add_3ds_global_params + stub_comms do + @gateway.authorize(@amount, @credit_card, @three_d_secure_options) + end.check_request do |_method, _endpoint, data, _headers| + assert_equal 'jJ81HADVRtXfCBATEp01CJUAAAA', JSON.parse(data)['threeD']['cavv'] + assert_equal '97267598-FAE6-48F2-8083-C23433990FBC', JSON.parse(data)['threeD']['dsTransactionId'] + assert_equal '05', JSON.parse(data)['threeD']['eci'] + end.respond_with(successful_authorize_response) + end + + def test_add_3ds_global_params_with_challenge_preference + chellange_preference_params = { + challenge_preference: 'ExemptionRequest', + exemption_request_reason: 'AccountVerification' + } + + stub_comms do + @gateway.purchase(@amount, @credit_card, @three_d_secure_options.merge(chellange_preference_params)) + end.check_request(skip_response: true) do |_method, _endpoint, data, _headers| + assert_equal 'ExemptionRequest', JSON.parse(data)['threeD']['externalMpi']['challenge_preference'] + assert_equal 'AccountVerification', JSON.parse(data)['threeD']['externalMpi']['exemptionRequestReason'] + end + end + + def test_successful_purchase_with_apple_pay + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @apple_pay_card, @options) + end.check_request do |_method, endpoint, data, _headers| + if /payment/.match?(endpoint) + json_data = JSON.parse(data) + assert_equal 'ApplePay', json_data['paymentOption']['card']['externalToken']['externalTokenProvider'] + assert_not_nil json_data['paymentOption']['card']['externalToken']['cryptogram'] + end + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_google_pay + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @google_pay_card, @options) + end.check_request do |_method, endpoint, data, _headers| + if /payment/.match?(endpoint) + json_data = JSON.parse(data) + assert_equal 'GooglePay', json_data['paymentOption']['card']['externalToken']['externalTokenProvider'] + assert_not_nil json_data['paymentOption']['card']['externalToken']['cryptogram'] + end + end.respond_with(successful_purchase_response) + end + + def test_successful_account_funding_transactions + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge(is_aft: true, aft_recipient_first_name: 'John', aft_recipient_last_name: 'Doe')) + end.check_request do |_method, endpoint, data, _headers| + if /payment/.match?(endpoint) + json_data = JSON.parse(data) + assert_match('John', json_data['recipientDetails']['firstName']) + assert_match('Doe', json_data['recipientDetails']['lastName']) + assert_match(@credit_card.first_name, json_data['billingAddress']['firstName']) + assert_match(@credit_card.last_name, json_data['billingAddress']['lastName']) + assert_match(@options[:billing_address][:address1], json_data['billingAddress']['address']) + assert_match(@options[:billing_address][:city], json_data['billingAddress']['city']) + assert_match(@options[:billing_address][:state], json_data['billingAddress']['state']) + assert_match(@options[:billing_address][:country], json_data['billingAddress']['country']) + end + end.respond_with(successful_purchase_response) + end + + def test_successful_authorize_cardholder_name_verification + @options.merge!(perform_name_verification: true) + + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_method, endpoint, data, _headers| + json_data = JSON.parse(data) + if /payment/.match?(endpoint) + assert_match(%r(/payment), endpoint) + assert_match(/Auth/, json_data['transactionType']) + assert_equal 'true', json_data['cardHolderNameVerification']['performNameVerification'] + assert_equal 'Longbob', @credit_card.first_name + assert_equal 'Longsen', @credit_card.last_name + end + end.respond_with(successful_authorize_response) + end + + private + + def three_ds_assertions(payment_option_card) + assert_equal @three_ds_options[:three_ds_2][:browser_info][:depth], payment_option_card['threeD']['browserDetails']['colorDepth'] + assert_equal @three_ds_options[:three_ds_2][:browser_info][:height], payment_option_card['threeD']['browserDetails']['screenHeight'] + assert_equal @three_ds_options[:three_ds_2][:browser_info][:width], payment_option_card['threeD']['browserDetails']['screenWidth'] + assert_equal @three_ds_options[:three_ds_2][:browser_info][:timezone], payment_option_card['threeD']['browserDetails']['timeZone'] + assert_equal @three_ds_options[:three_ds_2][:browser_info][:user_agent], payment_option_card['threeD']['browserDetails']['userAgent'] + assert_equal @three_ds_options[:callback_url], payment_option_card['threeD']['notificationURL'] + assert_equal 'U', payment_option_card['threeD']['methodCompletionInd'] + assert_equal '02', payment_option_card['threeD']['platformType'] + end + + def pre_scrubbed + <<-PRE_SCRUBBED + opening connection to ppp-test.nuvei.com:443... + opened + starting SSL for ppp-test.nuvei.com:443... + SSL established, protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384 + I, [2024-07-22T12:21:29.506576 #65153] INFO -- : [ActiveMerchant::Billing::NuveiGateway] connection_ssl_version=TLSv1.3 connection_ssl_cipher=TLS_AES_256_GCM_SHA384 + D, [2024-07-22T12:21:29.506622 #65153] DEBUG -- : {"transactionType":"Auth","merchantId":"3755516963854600967","merchantSiteId":"255388","timeStamp":"20240722172128","clientRequestId":"8fdaf176-67e7-4fee-86f7-efa3bfb2df60","clientUniqueId":"e1c3cb6c583be8f475dff7e25a894f81","amount":"100","currency":"USD","paymentOption":{"card":{"cardNumber":"4761344136141390","cardHolderName":"Cure Tester","expirationMonth":"09","expirationYear":"2025","CVV":"999"}},"billingAddress":{"email":"test@gmail.com","country":"CA","firstName":"Cure","lastName":"Tester","phone":"(555)555-5555"},"deviceDetails":{"ipAddress":"127.0.0.1"},"sessionToken":"fdda0126-674f-4f8c-ad24-31ac846654ab","checksum":"577658357a0b2c33e5f567dc52f40e984e50b6fa0344d55abb7849cca9a79741"} + <- "POST /ppp/api/v1/payment HTTP/1.1\r\nContent-Type: application/json\r\nAuthorization: Bearer fdda0126-674f-4f8c-ad24-31ac846654ab\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: ppp-test.nuvei.com\r\nContent-Length: 702\r\n\r\n" + <- "{\"transactionType\":\"Auth\",\"merchantId\":\"3755516963854600967\",\"merchantSiteId\":\"255388\",\"timeStamp\":\"20240722172128\",\"clientRequestId\":\"8fdaf176-67e7-4fee-86f7-efa3bfb2df60\",\"clientUniqueId\":\"e1c3cb6c583be8f475dff7e25a894f81\",\"amount\":\"100\",\"currency\":\"USD\",\"paymentOption\":{\"card\":{\"cardNumber\":\"4761344136141390\",\"cardHolderName\":\"Cure Tester\",\"expirationMonth\":\"09\",\"expirationYear\":\"2025\",\"CVV\":\"999\"}},\"billingAddress\":{\"email\":\"test@gmail.com\",\"country\":\"CA\",\"firstName\":\"Cure\",\"lastName\":\"Tester\",\"phone\":\"(555)555-5555\"},\"deviceDetails\":{\"ipAddress\":\"127.0.0.1\"},\"sessionToken\":\"fdda0126-674f-4f8c-ad24-31ac846654ab\",\"checksum\":\"577658357a0b2c33e5f567dc52f40e984e50b6fa0344d55abb7849cca9a79741\"}" + -> "HTTP/1.1 200 OK\r\n" + -> "Content-Type: application/json\r\n" + -> "Server: nginx\r\n" + -> "Access-Control-Allow-Headers: content-type, X-PINGOTHER\r\n" + -> "Access-Control-Allow-Methods: GET, POST\r\n" + -> "P3P: CP=\"ALL ADM DEV PSAi COM NAV OUR OTR STP IND DEM\"\r\n" + -> "Content-Length: 1103\r\n" + -> "Date: Mon, 22 Jul 2024 17:21:31 GMT\r\n" + -> "Connection: close\r\n" + -> "Set-Cookie: JSESSIONID=b766cc7f4ed4fe63f992477fbe27; Path=/ppp; Secure; HttpOnly; SameSite=None\r\n" + -> "\r\n" + reading 1103 bytes... + -> "{\"internalRequestId\":1170828168,\"status\":\"SUCCESS\",\"errCode\":0,\"reason\":\"\",\"merchantId\":\"3755516963854600967\",\"merchantSiteId\":\"255388\",\"version\":\"1.0\",\"clientRequestId\":\"8fdaf176-67e7-4fee-86f7-efa3bfb2df60\",\"sessionToken\":\"fdda0126-674f-4f8c-ad24-31ac846654ab\",\"clientUniqueId\":\"e1c3cb6c583be8f475dff7e25a894f81\",\"orderId\":\"471268418\",\"paymentOption\":{\"userPaymentOptionId\":\"\",\"card\":{\"ccCardNumber\":\"4****1390\",\"bin\":\"476134\",\"last4Digits\":\"1390\",\"ccExpMonth\":\"09\",\"ccExpYear\":\"25\",\"acquirerId\":\"19\",\"cvv2Reply\":\"\",\"avsCode\":\"\",\"cardType\":\"Debit\",\"cardBrand\":\"VISA\",\"issuerBankName\":\"INTL HDQTRS-CENTER OWNED\",\"issuerCountry\":\"SG\",\"isPrepaid\":\"false\",\"threeD\":{},\"processedBrand\":\"VISA\"},\"paymentAccountReference\":\"f4iK2pnudYKvTALGdcwEzqj9p4\"},\"transactionStatus\":\"APPROVED\",\"gwErrorCode\":0,\"gwExtendedErrorCode\":0,\"issuerDeclineCode\":\"\",\"issuerDeclineReason\":\"\",\"transactionType\":\"Auth\",\"transactionId\":\"7110000000001884667\",\"externalTransactionId\":\"\",\"authCode\":\"111144\",\"customData\":\"\",\"fraudDetails\":{\"finalDecision\":\"Accept\",\"score\":\"0\"},\"externalSchemeTransactionId\":\"\",\"merchantAdviceCode\":\"\"}" + read 1103 bytes + Conn close + PRE_SCRUBBED + end + + def post_scrubbed + <<-POST_SCRUBBED + opening connection to ppp-test.nuvei.com:443... + opened + starting SSL for ppp-test.nuvei.com:443... + SSL established, protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384 + I, [2024-07-22T12:21:29.506576 #65153] INFO -- : [ActiveMerchant::Billing::NuveiGateway] connection_ssl_version=TLSv1.3 connection_ssl_cipher=TLS_AES_256_GCM_SHA384 + D, [2024-07-22T12:21:29.506622 #65153] DEBUG -- : {"transactionType":"Auth","merchantId":"[FILTERED]","merchantSiteId":"[FILTERED]","timeStamp":"20240722172128","clientRequestId":"8fdaf176-67e7-4fee-86f7-efa3bfb2df60","clientUniqueId":"e1c3cb6c583be8f475dff7e25a894f81","amount":"100","currency":"USD","paymentOption":{"card":{"cardNumber":"[FILTERED]","cardHolderName":"Cure Tester","expirationMonth":"09","expirationYear":"2025","CVV":"999"}},"billingAddress":{"email":"test@gmail.com","country":"CA","firstName":"Cure","lastName":"Tester","phone":"(555)555-5555"},"deviceDetails":{"ipAddress":"127.0.0.1"},"sessionToken":"fdda0126-674f-4f8c-ad24-31ac846654ab","checksum":"577658357a0b2c33e5f567dc52f40e984e50b6fa0344d55abb7849cca9a79741"} + <- "POST /ppp/api/v1/payment HTTP/1.1\r\nContent-Type: application/json\r\nAuthorization: Bearer fdda0126-674f-4f8c-ad24-31ac846654ab\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: ppp-test.nuvei.com\r\nContent-Length: 702\r\n\r\n" + <- "{\"transactionType\":\"Auth\",\"merchantId\":\"[FILTERED]\",\"merchantSiteId\":\"[FILTERED]\",\"timeStamp\":\"20240722172128\",\"clientRequestId\":\"8fdaf176-67e7-4fee-86f7-efa3bfb2df60\",\"clientUniqueId\":\"e1c3cb6c583be8f475dff7e25a894f81\",\"amount\":\"100\",\"currency\":\"USD\",\"paymentOption\":{\"card\":{\"cardNumber\":\"[FILTERED]\",\"cardHolderName\":\"Cure Tester\",\"expirationMonth\":\"09\",\"expirationYear\":\"2025\",\"CVV\":\"999\"}},\"billingAddress\":{\"email\":\"test@gmail.com\",\"country\":\"CA\",\"firstName\":\"Cure\",\"lastName\":\"Tester\",\"phone\":\"(555)555-5555\"},\"deviceDetails\":{\"ipAddress\":\"127.0.0.1\"},\"sessionToken\":\"fdda0126-674f-4f8c-ad24-31ac846654ab\",\"checksum\":\"577658357a0b2c33e5f567dc52f40e984e50b6fa0344d55abb7849cca9a79741\"}" + -> "HTTP/1.1 200 OK\r\n" + -> "Content-Type: application/json\r\n" + -> "Server: nginx\r\n" + -> "Access-Control-Allow-Headers: content-type, X-PINGOTHER\r\n" + -> "Access-Control-Allow-Methods: GET, POST\r\n" + -> "P3P: CP=\"ALL ADM DEV PSAi COM NAV OUR OTR STP IND DEM\"\r\n" + -> "Content-Length: 1103\r\n" + -> "Date: Mon, 22 Jul 2024 17:21:31 GMT\r\n" + -> "Connection: close\r\n" + -> "Set-Cookie: JSESSIONID=b766cc7f4ed4fe63f992477fbe27; Path=/ppp; Secure; HttpOnly; SameSite=None\r\n" + -> "\r\n" + reading 1103 bytes... + -> "{\"internalRequestId\":1170828168,\"status\":\"SUCCESS\",\"errCode\":0,\"reason\":\"\",\"merchantId\":\"[FILTERED]\",\"merchantSiteId\":\"[FILTERED]\",\"version\":\"1.0\",\"clientRequestId\":\"8fdaf176-67e7-4fee-86f7-efa3bfb2df60\",\"sessionToken\":\"fdda0126-674f-4f8c-ad24-31ac846654ab\",\"clientUniqueId\":\"e1c3cb6c583be8f475dff7e25a894f81\",\"orderId\":\"471268418\",\"paymentOption\":{\"userPaymentOptionId\":\"\",\"card\":{\"ccCardNumber\":\"4****1390\",\"bin\":\"476134\",\"last4Digits\":\"1390\",\"ccExpMonth\":\"09\",\"ccExpYear\":\"25\",\"acquirerId\":\"19\",\"cvv2Reply\":\"\",\"avsCode\":\"\",\"cardType\":\"Debit\",\"cardBrand\":\"VISA\",\"issuerBankName\":\"INTL HDQTRS-CENTER OWNED\",\"issuerCountry\":\"SG\",\"isPrepaid\":\"false\",\"threeD\":{},\"processedBrand\":\"VISA\"},\"paymentAccountReference\":\"f4iK2pnudYKvTALGdcwEzqj9p4\"},\"transactionStatus\":\"APPROVED\",\"gwErrorCode\":0,\"gwExtendedErrorCode\":0,\"issuerDeclineCode\":\"\",\"issuerDeclineReason\":\"\",\"transactionType\":\"Auth\",\"transactionId\":\"7110000000001884667\",\"externalTransactionId\":\"\",\"authCode\":\"111144\",\"customData\":\"\",\"fraudDetails\":{\"finalDecision\":\"Accept\",\"score\":\"0\"},\"externalSchemeTransactionId\":\"\",\"merchantAdviceCode\":\"\"}" + read 1103 bytes + Conn close + POST_SCRUBBED + end + + def successful_authorize_response + <<~RESPONSE + {"internalRequestId":1171104468,"status":"SUCCESS","errCode":0,"reason":"","merchantId":"3755516963854600967","merchantSiteId":"255388","version":"1.0","clientRequestId":"02ba666c-e3e5-4ec9-ae30-3f8500b18c96","sessionToken":"29226538-82c7-4a3c-b363-cb6829b8c32a","clientUniqueId":"c00ed73a7d682bf478295d57bdae3028","orderId":"471361708","paymentOption":{"userPaymentOptionId":"","card":{"ccCardNumber":"4****1390","bin":"476134","last4Digits":"1390","ccExpMonth":"09","ccExpYear":"25","acquirerId":"19","cvv2Reply":"","avsCode":"","cardType":"Debit","cardBrand":"VISA","issuerBankName":"INTL HDQTRS-CENTER OWNED","issuerCountry":"SG","isPrepaid":"false","threeD":{},"processedBrand":"VISA"},"paymentAccountReference":"f4iK2pnudYKvTALGdcwEzqj9p4"},"transactionStatus":"APPROVED","gwErrorCode":0,"gwExtendedErrorCode":0,"issuerDeclineCode":"","issuerDeclineReason":"","transactionType":"Auth","transactionId":"7110000000001908486","externalTransactionId":"","authCode":"111397","customData":"","fraudDetails":{"finalDecision":"Accept","score":"0"},"externalSchemeTransactionId":"","merchantAdviceCode":""} + RESPONSE + end + + def successful_purchase_response + <<~RESPONSE + {"internalRequestId":1172848838, "status":"SUCCESS", "errCode":0, "reason":"", "merchantId":"3755516963854600967", "merchantSiteId":"255388", "version":"1.0", "clientRequestId":"a114381a-0f88-46d0-920c-7b5614f29e5b", "sessionToken":"d3424c9c-dd6d-40dc-85da-a2b92107cbe3", "clientUniqueId":"3ba2a81c46d78837ea819d9f3fe644e7", "orderId":"471833818", "paymentOption":{"userPaymentOptionId":"", "card":{"ccCardNumber":"4****1390", "bin":"476134", "last4Digits":"1390", "ccExpMonth":"09", "ccExpYear":"25", "acquirerId":"19", "cvv2Reply":"", "avsCode":"", "cardType":"Debit", "cardBrand":"VISA", "issuerBankName":"INTL HDQTRS-CENTER OWNED", "issuerCountry":"SG", "isPrepaid":"false", "threeD":{}, "processedBrand":"VISA"}, "paymentAccountReference":"f4iK2pnudYKvTALGdcwEzqj9p4"}, "transactionStatus":"APPROVED", "gwErrorCode":0, "gwExtendedErrorCode":0, "issuerDeclineCode":"", "issuerDeclineReason":"", "transactionType":"Sale", "transactionId":"7110000000001990927", "externalTransactionId":"", "authCode":"111711", "customData":"", "fraudDetails":{"finalDecision":"Accept", "score":"0"}, "externalSchemeTransactionId":"", "merchantAdviceCode":""} + RESPONSE + end + + def successful_init_payment_response + <<~RESPONSE + { + "internalRequestId":1281786978, + "status":"SUCCESS", + "errCode":0, + "reason":"", + "merchantId":"SomeMerchantId", + "merchantSiteId":"2XXXXX8", + "version":"1.0", + "clientRequestId":"7XXXXXXXXXXXXXXXXXXXXXXXXf0", + "sessionToken":"6XXXXXXXXXXXXXXXXXXXXX7", + "clientUniqueId":"SOMEe5CLIENTXxXxXId", + "orderId":"489593998", + "transactionId":"7110000000004854308", + "transactionType":"InitAuth3D", + "transactionStatus":"APPROVED", + "gwErrorCode":0, + "gwExtendedErrorCode":0, + "paymentOption": + {"card": + {"ccCardNumber":"2****7736", + "bin":"222100", + "last4Digits":"7736", + "ccExpMonth":"09", + "ccExpYear":"25", + "acquirerId":"19", + "threeD": + {"methodUrl":"https://3dsn.sandbox.safecharge.com/ThreeDSMethod/api/ThreeDSMethod/threeDSMethodURL", + "version":"2.1.0", + "v2supported":"true", + "methodPayload": + "eyJ0PaYloADRTU2VyPaYloADhbnNJRCI6PaYloAD0OPaYloADNWMtPaYloAD4NjPaYloADjM2PaYloAD1ZSIPaYloADVlRPaYloADb2ROPaYloADjYXRpPaYloADi", + "directoryServerId":"A000000004", + "directoryServerPublicKey": + "rsa:rsaRASKsdkanzsclajs,cbaksjcbaksj,cmxazx", + "serverTransId":"21374830-445c-4fdf-8619-d7b36a67cd5e"}, + "processedBrand":"MASTERCARD"}}, + "customData":""} + RESPONSE + end + + def not_enrolled_3ds_init_payment_response + <<~RESPONSE + { + "internalRequestId":1281786978, + "status":"SUCCESS", + "errCode":0, + "reason":"", + "merchantId":"SomeMerchantId", + "merchantSiteId":"2XXXXX8", + "version":"1.0", + "clientRequestId":"7XXXXXXXXXXXXXXXXXXXXXXXXf0", + "sessionToken":"6XXXXXXXXXXXXXXXXXXXXX7", + "clientUniqueId":"SOMEe5CLIENTXxXxXId", + "orderId":"489593998", + "transactionId":"7110000000004854308", + "transactionType":"InitAuth3D", + "transactionStatus":"APPROVED", + "gwErrorCode":0, + "gwExtendedErrorCode":0, + "paymentOption": + {"card": + {"ccCardNumber":"2****7736", + "bin":"222100", + "last4Digits":"7736", + "ccExpMonth":"09", + "ccExpYear":"25", + "acquirerId":"19", + "threeD": + {"methodUrl":"", + "version":"", + "v2supported":"false", + "methodPayload": + "", + "directoryServerId":"", + "directoryServerPublicKey":"", + "serverTransId":""}, + "processedBrand":"MASTERCARD"}}, + "customData":""} + RESPONSE + end + + def successful_3ds_flow_response + <<~RESPONSE + {"internalRequestId":1281822938, + "status":"SUCCESS", + "errCode":0, + "reason":"", + "merchantId":"3755516963854600967", + "merchantSiteId":"255388", + "version":"1.0", + "clientRequestId":"8f8efbef-3346-47f9-9fcb-2fe74de5d13a", + "sessionToken":"96893d76-93af-483d-93b4-7e03b6c5f397", + "clientUniqueId":"660c0fa000b47fd2e9b2071923a97537", + "orderId":"489602168", + "paymentOption": + {"userPaymentOptionId":"", + "card": + {"ccCardNumber":"2****7736", + "bin":"222100", + "last4Digits":"7736", + "ccExpMonth":"09", + "ccExpYear":"25", + "acquirerId":"19", + "cvv2Reply":"", + "avsCode":"", + "cardBrand":"MASTERCARD", + "issuerBankName":"", + "isPrepaid":"false", + "threeD": + {"threeDFlow":"1", + "eci":"7", + "version":"", + "whiteListStatus":"", + "cavv":"", + "acsChallengeMandated":"N", + "cReq":"", + "authenticationType":"", + "cardHolderInfoText":"", + "sdk":{"acsSignedContent":""}, + "xid":"", + "result":"", + "acsTransID":"", + "dsTransID":"", + "threeDReasonId":"", + "isExemptionRequestInAuthentication":"0", + "challengePreferenceReason":"12", + "flow":"none", + "acquirerDecision":"ExemptionRequest", + "decisionReason":"NoPreference"}, + "processedBrand":"MASTERCARD"}}, + "transactionStatus":"ERROR", + "gwErrorCode":-1100, + "gwErrorReason":"sg_transaction must be of type InitAuth3D, Approved and the same merchant", + "gwExtendedErrorCode":1271, + "issuerDeclineCode":"", + "issuerDeclineReason":"", + "transactionType":"Auth3D", + "transactionId":"7110000000004856095", + "externalTransactionId":"", + "authCode":"", + "customData":"", + "externalSchemeTransactionId":"", + "merchantAdviceCode":""} + RESPONSE + end +end diff --git a/test/unit/gateways/ogone_test.rb b/test/unit/gateways/ogone_test.rb index 3118e4b7193..59ba53f59c2 100644 --- a/test/unit/gateways/ogone_test.rb +++ b/test/unit/gateways/ogone_test.rb @@ -1,25 +1,24 @@ require 'test_helper' class OgoneTest < Test::Unit::TestCase - def setup - @credentials = { :login => 'pspid', - :user => 'username', - :password => 'password', - :signature => 'mynicesig', - :signature_encryptor => 'sha512', - :timeout => '30' } + @credentials = { login: 'pspid', + user: 'username', + password: 'password', + signature: 'mynicesig', + signature_encryptor: 'sha512', + timeout: '30' } @gateway = OgoneGateway.new(@credentials) @credit_card = credit_card - @mastercard = credit_card('5399999999999999', :brand => 'mastercard') + @mastercard = credit_card('5399999999999999', brand: 'mastercard') @amount = 100 @identification = '3014726' @billing_id = 'myalias' @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase' + order_id: '1', + billing_address: address, + description: 'Store Purchase' } @parameters = { 'orderID' => '1', @@ -42,6 +41,10 @@ def teardown Base.mode = :test end + def test_should_have_homepage_url + assert_equal 'https://www.ingenico.com/login/ogone/', OgoneGateway.homepage_url + end + def test_successful_purchase @gateway.expects(:add_pair).at_least(1) @gateway.expects(:add_pair).with(anything, 'ECI', '7') @@ -57,7 +60,7 @@ def test_successful_purchase_with_action_param @gateway.expects(:add_pair).at_least(1) @gateway.expects(:add_pair).with(anything, 'ECI', '7') @gateway.expects(:ssl_post).returns(successful_purchase_response) - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(:action => 'SAS')) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(action: 'SAS')) assert_success response assert_equal '3014726;SAS', response.authorization assert response.params['HTML_ANSWER'].nil? @@ -77,7 +80,7 @@ def test_successful_purchase_with_custom_eci @gateway.expects(:add_pair).at_least(1) @gateway.expects(:add_pair).with(anything, 'ECI', '4') @gateway.expects(:ssl_post).returns(successful_purchase_response) - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(:eci => 4)) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(eci: 4)) assert_success response assert_equal '3014726;SAL', response.authorization assert response.test? @@ -85,7 +88,7 @@ def test_successful_purchase_with_custom_eci def test_successful_purchase_with_3dsecure @gateway.expects(:ssl_post).returns(successful_3dsecure_purchase_response) - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(:d3d => true)) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(d3d: true)) assert_success response assert_equal '3014726;SAL', response.authorization assert response.params['HTML_ANSWER'] @@ -127,7 +130,7 @@ def test_successful_authorize_with_custom_eci @gateway.expects(:add_pair).at_least(1) @gateway.expects(:add_pair).with(anything, 'ECI', '4') @gateway.expects(:ssl_post).returns(successful_purchase_response) - assert response = @gateway.authorize(@amount, @credit_card, @options.merge(:eci => 4)) + assert response = @gateway.authorize(@amount, @credit_card, @options.merge(eci: 4)) assert_success response assert_equal '3014726;RES', response.authorization assert response.test? @@ -135,7 +138,7 @@ def test_successful_authorize_with_custom_eci def test_successful_authorize_with_3dsecure @gateway.expects(:ssl_post).returns(successful_3dsecure_purchase_response) - assert response = @gateway.authorize(@amount, @credit_card, @options.merge(:d3d => true)) + assert response = @gateway.authorize(@amount, @credit_card, @options.merge(d3d: true)) assert_success response assert_equal '3014726;RES', response.authorization assert response.params['HTML_ANSWER'] @@ -153,7 +156,7 @@ def test_successful_capture def test_successful_capture_with_action_option @gateway.expects(:ssl_post).returns(successful_capture_response) - assert response = @gateway.capture(@amount, '3048326', :action => 'SAS') + assert response = @gateway.capture(@amount, '3048326', action: 'SAS') assert_success response assert_equal '3048326;SAS', response.authorization assert response.test? @@ -208,19 +211,19 @@ def test_failed_verify end def test_successful_store - @gateway.expects(:authorize).with(1, @credit_card, :billing_id => @billing_id).returns(OgoneResponse.new(true, '', @gateway.send(:parse, successful_purchase_response), :authorization => '3014726;RES')) + @gateway.expects(:authorize).with(1, @credit_card, billing_id: @billing_id).returns(OgoneResponse.new(true, '', @gateway.send(:parse, successful_purchase_response), authorization: '3014726;RES')) @gateway.expects(:void).with('3014726;RES') - assert response = @gateway.store(@credit_card, :billing_id => @billing_id) + assert response = @gateway.store(@credit_card, billing_id: @billing_id) assert_success response assert_equal '3014726;RES', response.authorization assert_equal @billing_id, response.billing_id end def test_store_amount_at_gateway_level - gateway = OgoneGateway.new(@credentials.merge(:store_amount => 100)) - gateway.expects(:authorize).with(100, @credit_card, :billing_id => @billing_id).returns(OgoneResponse.new(true, '', gateway.send(:parse, successful_purchase_response_100), :authorization => '3014726;RES')) + gateway = OgoneGateway.new(@credentials.merge(store_amount: 100)) + gateway.expects(:authorize).with(100, @credit_card, billing_id: @billing_id).returns(OgoneResponse.new(true, '', gateway.send(:parse, successful_purchase_response_100), authorization: '3014726;RES')) gateway.expects(:void).with('3014726;RES') - assert response = gateway.store(@credit_card, :billing_id => @billing_id) + assert response = gateway.store(@credit_card, billing_id: @billing_id) assert_success response assert_equal '3014726;RES', response.authorization assert_equal @billing_id, response.billing_id @@ -231,7 +234,7 @@ def test_deprecated_store_option @gateway.expects(:add_pair).with(anything, 'ECI', '7') @gateway.expects(:ssl_post).times(2).returns(successful_purchase_response) assert_deprecation_warning(OgoneGateway::OGONE_STORE_OPTION_DEPRECATION_MESSAGE) do - assert response = @gateway.store(@credit_card, :store => @billing_id) + assert response = @gateway.store(@credit_card, store: @billing_id) assert_success response assert_equal '3014726;RES', response.authorization assert response.test? @@ -255,11 +258,11 @@ def test_create_readable_error_message_upon_failure end def test_supported_countries - assert_equal ['BE', 'DE', 'FR', 'NL', 'AT', 'CH'], OgoneGateway.supported_countries + assert_equal %w[BE DE FR NL AT CH], OgoneGateway.supported_countries end def test_supported_card_types - assert_equal [:visa, :master, :american_express, :diners_club, :discover, :jcb, :maestro], OgoneGateway.supported_cardtypes + assert_equal %i[visa master american_express diners_club discover jcb maestro], OgoneGateway.supported_cardtypes end def test_default_currency @@ -273,7 +276,7 @@ def test_default_currency end def test_custom_currency_at_gateway_level - gateway = OgoneGateway.new(@credentials.merge(:currency => 'USD')) + gateway = OgoneGateway.new(@credentials.merge(currency: 'USD')) gateway.expects(:add_pair).at_least(1) gateway.expects(:add_pair).with(anything, 'currency', 'USD') gateway.expects(:ssl_post).returns(successful_purchase_response) @@ -281,11 +284,11 @@ def test_custom_currency_at_gateway_level end def test_local_custom_currency_overwrite_gateway_level - gateway = OgoneGateway.new(@credentials.merge(:currency => 'USD')) + gateway = OgoneGateway.new(@credentials.merge(currency: 'USD')) gateway.expects(:add_pair).at_least(1) gateway.expects(:add_pair).with(anything, 'currency', 'EUR') gateway.expects(:ssl_post).returns(successful_purchase_response) - gateway.purchase(@amount, @credit_card, @options.merge(:currency => 'EUR')) + gateway.purchase(@amount, @credit_card, @options.merge(currency: 'EUR')) end def test_avs_result @@ -344,13 +347,13 @@ def test_format_error_message_with_no_separator end def test_without_signature - gateway = OgoneGateway.new(@credentials.merge(:signature => nil, :signature_encryptor => nil)) + gateway = OgoneGateway.new(@credentials.merge(signature: nil, signature_encryptor: nil)) gateway.expects(:ssl_post).returns(successful_purchase_response) assert_deprecation_warning(OgoneGateway::OGONE_NO_SIGNATURE_DEPRECATION_MESSAGE) do gateway.purchase(@amount, @credit_card, @options) end - gateway = OgoneGateway.new(@credentials.merge(:signature => nil, :signature_encryptor => 'none')) + gateway = OgoneGateway.new(@credentials.merge(signature: nil, signature_encryptor: 'none')) gateway.expects(:ssl_post).returns(successful_purchase_response) assert_no_deprecation_warning do gateway.purchase(@amount, @credit_card, @options) @@ -358,27 +361,27 @@ def test_without_signature end def test_signature_for_accounts_created_before_10_may_20101 - gateway = OgoneGateway.new(@credentials.merge(:signature_encryptor => nil)) + gateway = OgoneGateway.new(@credentials.merge(signature_encryptor: nil)) assert signature = gateway.send(:add_signature, @parameters) assert_equal Digest::SHA1.hexdigest('1100EUR4111111111111111MrPSPIDRES2mynicesig').upcase, signature end def test_signature_for_accounts_with_signature_encryptor_to_sha1 - gateway = OgoneGateway.new(@credentials.merge(:signature_encryptor => 'sha1')) + gateway = OgoneGateway.new(@credentials.merge(signature_encryptor: 'sha1')) assert signature = gateway.send(:add_signature, @parameters) assert_equal Digest::SHA1.hexdigest(string_to_digest).upcase, signature end def test_signature_for_accounts_with_signature_encryptor_to_sha256 - gateway = OgoneGateway.new(@credentials.merge(:signature_encryptor => 'sha256')) + gateway = OgoneGateway.new(@credentials.merge(signature_encryptor: 'sha256')) assert signature = gateway.send(:add_signature, @parameters) assert_equal Digest::SHA256.hexdigest(string_to_digest).upcase, signature end def test_signature_for_accounts_with_signature_encryptor_to_sha512 - gateway = OgoneGateway.new(@credentials.merge(:signature_encryptor => 'sha512')) + gateway = OgoneGateway.new(@credentials.merge(signature_encryptor: 'sha512')) assert signature = gateway.send(:add_signature, @parameters) assert_equal Digest::SHA512.hexdigest(string_to_digest).upcase, signature end @@ -393,13 +396,13 @@ def test_3dsecure_win_3ds_option post = {} gateway = OgoneGateway.new(@credentials) - gateway.send(:add_d3d, post, { :win_3ds => :pop_up }) + gateway.send(:add_d3d, post, { win_3ds: :pop_up }) assert 'POPUP', post['WIN3DS'] - gateway.send(:add_d3d, post, { :win_3ds => :pop_ix }) + gateway.send(:add_d3d, post, { win_3ds: :pop_ix }) assert 'POPIX', post['WIN3DS'] - gateway.send(:add_d3d, post, { :win_3ds => :invalid }) + gateway.send(:add_d3d, post, { win_3ds: :invalid }) assert 'MAINW', post['WIN3DS'] end @@ -408,16 +411,16 @@ def test_3dsecure_additional_options gateway = OgoneGateway.new(@credentials) gateway.send(:add_d3d, post, { - :http_accept => 'text/html', - :http_user_agent => 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)', - :accept_url => 'https://accept_url', - :decline_url => 'https://decline_url', - :exception_url => 'https://exception_url', - :cancel_url => 'https://cancel_url', - :paramvar => 'param_var', - :paramplus => 'param_plus', - :complus => 'com_plus', - :language => 'fr_FR' + http_accept: 'text/html', + http_user_agent: 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)', + accept_url: 'https://accept_url', + decline_url: 'https://decline_url', + exception_url: 'https://exception_url', + cancel_url: 'https://cancel_url', + paramvar: 'param_var', + paramplus: 'param_plus', + complus: 'com_plus', + language: 'fr_FR' }) assert_equal post['HTTP_ACCEPT'], 'text/html' assert_equal post['HTTP_USER_AGENT'], 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)' @@ -450,23 +453,44 @@ 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 - 'ALIAS=2mynicesigAMOUNT=100mynicesigCARDNO=4111111111111111mynicesig'+ - 'CN=Client NamemynicesigCURRENCY=EURmynicesigOPERATION=RESmynicesig'+ - 'ORDERID=1mynicesigPSPID=MrPSPIDmynicesig' + 'ALIAS=2mynicesigAMOUNT=100mynicesigCARDNO=4111111111111111mynicesig'\ + 'CN=Client NamemynicesigCURRENCY=EURmynicesigOPERATION=RESmynicesig'\ + 'ORDERID=1mynicesigPSPID=MrPSPIDmynicesig' end def d3d_string_to_digest - 'ALIAS=2mynicesigAMOUNT=100mynicesigCARDNO=4111111111111111mynicesig'+ - 'CN=Client NamemynicesigCURRENCY=EURmynicesigFLAG3D=Ymynicesig'+ - 'HTTP_ACCEPT=*/*mynicesigOPERATION=RESmynicesigORDERID=1mynicesig'+ - 'PSPID=MrPSPIDmynicesigWIN3DS=MAINWmynicesig' + 'ALIAS=2mynicesigAMOUNT=100mynicesigCARDNO=4111111111111111mynicesig'\ + 'CN=Client NamemynicesigCURRENCY=EURmynicesigFLAG3D=Ymynicesig'\ + 'HTTP_ACCEPT=*/*mynicesigOPERATION=RESmynicesigORDERID=1mynicesig'\ + 'PSPID=MrPSPIDmynicesigWIN3DS=MAINWmynicesig' end def successful_authorize_response - <<-END + <<-XML <?xml version="1.0"?><ncresponse orderID="1233680882919266242708828" PAYID="3014726" @@ -487,11 +511,11 @@ def successful_authorize_response BRAND="VISA" ALIAS="2"> </ncresponse> - END + XML end def successful_purchase_response - <<-END + <<-XML <?xml version="1.0"?><ncresponse orderID="1233680882919266242708828" PAYID="3014726" @@ -512,11 +536,11 @@ def successful_purchase_response BRAND="VISA" ALIAS="#{@billing_id}"> </ncresponse> - END + XML end def successful_purchase_response_100 - <<-END + <<-XML <?xml version="1.0"?><ncresponse orderID="1233680882919266242708828" PAYID="3014726" @@ -537,11 +561,11 @@ def successful_purchase_response_100 BRAND="VISA" ALIAS="#{@billing_id}"> </ncresponse> - END + XML end def successful_3dsecure_purchase_response - <<-END + <<-XML <?xml version="1.0"?><ncresponse orderID="1233680882919266242708828" PAYID="3014726" @@ -634,11 +658,11 @@ def successful_3dsecure_purchase_response cmV0dXJuIDE7CiAgfQp9CnNlbGYuZG9jdW1lbnQuZm9ybXMuZG93bmxvYWRm b3JtM0Quc3VibWl0KCk7Ci8vLS0+CjwvU0NSSVBUPgo=\n</HTML_ANSWER> </ncresponse> - END + XML end def failed_purchase_response - <<-END + <<-XML <?xml version="1.0"?> <ncresponse orderID="" @@ -654,11 +678,11 @@ def failed_purchase_response BRAND="" ALIAS="2"> </ncresponse> - END + XML end def successful_capture_response - <<-END + <<-XML <?xml version="1.0"?> <ncresponse orderID="1234956106974734203514539" @@ -673,11 +697,11 @@ def successful_capture_response currency="EUR" ALIAS="2"> </ncresponse> - END + XML end def successful_void_response - <<-END + <<-XML <?xml version="1.0"?> <ncresponse orderID="1234961140253559268757474" @@ -692,11 +716,11 @@ def successful_void_response currency="EUR" ALIAS="2"> </ncresponse> - END + XML end def successful_referenced_credit_response - <<-END + <<-XML <?xml version="1.0"?> <ncresponse orderID="1234976251872867104376350" @@ -711,11 +735,11 @@ def successful_referenced_credit_response currency="EUR" ALIAS="2"> </ncresponse> - END + XML end def successful_unreferenced_credit_response - <<-END + <<-XML <?xml version="1.0"?><ncresponse orderID="1234976330656672481134758" PAYID="3049654" @@ -736,11 +760,11 @@ def successful_unreferenced_credit_response BRAND="VISA" ALIAS="2"> </ncresponse> - END + XML end def failed_authorization_response - <<-END + <<-XML <?xml version="1.0"?> <ncresponse orderID="#1019.22" @@ -756,7 +780,7 @@ def failed_authorization_response BRAND="" ALIAS="2"> </ncresponse> - END + XML end def pre_scrub @@ -806,5 +830,4 @@ def post_scrub Conn close } end - end diff --git a/test/unit/gateways/omise_test.rb b/test/unit/gateways/omise_test.rb index 52eb9cd22e0..de5ca57a74b 100644 --- a/test/unit/gateways/omise_test.rb +++ b/test/unit/gateways/omise_test.rb @@ -4,7 +4,7 @@ class OmiseTest < Test::Unit::TestCase def setup @gateway = OmiseGateway.new( public_key: 'pkey_test_abc', - secret_key: 'skey_test_123', + secret_key: 'skey_test_123' ) @credit_card = credit_card @@ -23,11 +23,11 @@ def setup end def test_supported_countries - assert_equal @gateway.supported_countries, %w( TH JP ) + assert_equal @gateway.supported_countries, %w(TH JP) end def test_supported_cardtypes - assert_equal @gateway.supported_cardtypes, [:visa, :master, :jcb] + assert_equal @gateway.supported_cardtypes, %i[visa master jcb] end def test_supports_scrubbing @@ -39,8 +39,8 @@ def test_scrub end def test_gateway_url - assert_equal 'https://api.omise.co/', OmiseGateway::API_URL - assert_equal 'https://vault.omise.co/', OmiseGateway::VAULT_URL + assert_equal 'https://api.omise.co/', OmiseGateway::API_URL + assert_equal 'https://vault.omise.co/', OmiseGateway::VAULT_URL end def test_request_headers @@ -50,7 +50,7 @@ def test_request_headers end def test_post_data - post_data = @gateway.send(:post_data, { card: {number: '4242424242424242'} }) + post_data = @gateway.send(:post_data, { card: { number: '4242424242424242' } }) assert_equal '{"card":{"number":"4242424242424242"}}', post_data end @@ -73,7 +73,7 @@ def test_error_response def test_error_code_from response = @gateway.send(:parse, invalid_security_code_response) - error_code = @gateway.send(:error_code_from, response) + error_code = @gateway.send(:error_code_from, response) assert_equal 'invalid_security_code', error_code end @@ -90,7 +90,7 @@ def test_invalid_cvc end def test_card_declined - card_declined = @gateway.send(:parse, failed_capture_response) + card_declined = @gateway.send(:parse, failed_capture_response) card_declined_code = @gateway.send(:standard_error_code_mapping, card_declined) assert_equal 'card_declined', card_declined_code end @@ -138,7 +138,7 @@ def test_add_creditcard def test_add_customer_without_card result = {} customer_id = 'cust_test_4zjzcgm8kpdt4xdhdw2' - @gateway.send(:add_customer, result, {customer_id: customer_id}) + @gateway.send(:add_customer, result, { customer_id: }) assert_equal 'cust_test_4zjzcgm8kpdt4xdhdw2', result[:customer] end @@ -146,21 +146,21 @@ def test_add_customer_with_card_id result = {} customer_id = 'cust_test_4zjzcgm8kpdt4xdhdw2' result[:card] = 'card_test_4zguktjcxanu3dw171a' - @gateway.send(:add_customer, result, {customer_id: customer_id}) + @gateway.send(:add_customer, result, { customer_id: }) assert_equal customer_id, result[:customer] end def test_add_amount result = {} desc = 'Charge for order 3947' - @gateway.send(:add_amount, result, @amount, {description: desc}) + @gateway.send(:add_amount, result, @amount, { description: desc }) assert_equal desc, result[:description] end def test_add_amount_with_correct_currency result = {} jpy_currency = 'JPY' - @gateway.send(:add_amount, result, @amount, {currency: jpy_currency}) + @gateway.send(:add_amount, result, @amount, { currency: jpy_currency }) assert_equal jpy_currency, result[:currency] end @@ -812,5 +812,4 @@ def failed_capture_response } RESPONSE end - end diff --git a/test/unit/gateways/openpay_test.rb b/test/unit/gateways/openpay_test.rb index 7355647e382..1cc39b539ec 100644 --- a/test/unit/gateways/openpay_test.rb +++ b/test/unit/gateways/openpay_test.rb @@ -31,6 +31,40 @@ def test_successful_purchase assert response.test? end + def test_successful_purchase_with_mexico_url + gateway = OpenpayGateway.new( + key: 'key', + merchant_id: 'merchant_id', + merchant_country: 'MX' + ) + + gateway.expects(:ssl_request).returns(successful_purchase_response) + assert_equal gateway.gateway_url, OpenpayGateway.mx_test_url + assert response = gateway.purchase(@amount, @credit_card, @options) + assert_instance_of Response, response + assert_success response + + assert_equal 'tay1mauq3re4iuuk8bm4', response.authorization + assert response.test? + end + + def test_default_url_when_merchant_country_is_not_present + gateway = OpenpayGateway.new( + key: 'key', + merchant_id: 'merchant_id' + ) + assert_equal 'https://sandbox-api.openpay.co/v1/', gateway.gateway_url + end + + def test_set_mexico_url_using_merchant_country_flag + gateway = OpenpayGateway.new( + key: 'key', + merchant_id: 'merchant_id', + merchant_country: 'MX' + ) + assert_equal 'https://sandbox-api.openpay.mx/v1/', gateway.gateway_url + end + def test_unsuccessful_request @gateway.expects(:ssl_request).returns(failed_purchase_response) @@ -113,7 +147,7 @@ def test_unsuccessful_verify def test_successful_purchase_with_card_id @gateway.expects(:ssl_request).returns(successful_purchase_response) - assert response = @gateway.purchase(@amount, {credit_card: 'a2b79p8xmzeyvmolqfja'}, @options) + assert response = @gateway.purchase(@amount, { credit_card: 'a2b79p8xmzeyvmolqfja' }, @options) assert_instance_of Response, response assert_success response @@ -171,7 +205,7 @@ def test_successful_unstore def test_passing_device_session_id response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card, device_session_id: 'TheDeviceSessionID') - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(%r{"device_session_id":"TheDeviceSessionID"}, data) end.respond_with(successful_purchase_response) @@ -181,7 +215,7 @@ def test_passing_device_session_id def test_passing_payment_installments response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card, payments: '6') - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(%r{"payments":"6"}, data) assert_match(%r{"payment_plan":}, data) end.respond_with(successful_purchase_response) @@ -208,221 +242,225 @@ def test_whitespace_string_cvv_transcript_scrubbing private def successful_new_card - <<-RESPONSE -{ - "type":"debit", - "brand":"mastercard", - "address":{ - "line1":"Av 5 de Febrero", - "line2":"Roble 207", - "line3":"col carrillo", - "state":"Queretaro", - "city":"Queretaro", - "postal_code":"76900", - "country_code":"MX" - }, - "id":"kgipbqixvjg3gbzowl7l", - "card_number":"1111", - "holder_name":"Juan Perez Ramirez", - "expiration_year":"20", - "expiration_month":"12", - "allows_charges":true, - "allows_payouts":false, - "creation_date":"2013-12-12T17:50:00-06:00", - "bank_name":"DESCONOCIDO", - "bank_code":"000", - "customer_id":"a2b79p8xmzeyvmolqfja" -} + <<~RESPONSE + { + "type":"debit", + "brand":"mastercard", + "address":{ + "line1":"Av 5 de Febrero", + "line2":"Roble 207", + "line3":"col carrillo", + "state":"Queretaro", + "city":"Queretaro", + "postal_code":"76900", + "country_code":"MX" + }, + "id":"kgipbqixvjg3gbzowl7l", + "card_number":"1111", + "holder_name":"Juan Perez Ramirez", + "expiration_year":"20", + "expiration_month":"12", + "allows_charges":true, + "allows_payouts":false, + "creation_date":"2013-12-12T17:50:00-06:00", + "bank_name":"DESCONOCIDO", + "bank_code":"000", + "customer_id":"a2b79p8xmzeyvmolqfja" + } RESPONSE end def successful_new_customer - <<-RESPONSE -{ - "id":"a2b79p8xmzeyvmolqfja", - "name":"Anacleto", - "last_name":"Morones", - "email":"morones.an@elllano.com", - "phone_number":"44209087654", - "status":"active", - "balance":0, - "clabe":"646180109400003235", - "address":{ - "line1":"Camino Real", - "line2":"Col. San Pablo", - "state":"Queretaro", - "city":"Queretaro", - "postal_code":"76000", - "country_code":"MX" - }, - "creation_date":"2013-12-12T16:29:11-06:00" -} + <<~RESPONSE + { + "id":"a2b79p8xmzeyvmolqfja", + "name":"Anacleto", + "last_name":"Morones", + "email":"morones.an@elllano.com", + "phone_number":"44209087654", + "status":"active", + "balance":0, + "clabe":"646180109400003235", + "address":{ + "line1":"Camino Real", + "line2":"Col. San Pablo", + "state":"Queretaro", + "city":"Queretaro", + "postal_code":"76000", + "country_code":"MX" + }, + "creation_date":"2013-12-12T16:29:11-06:00" + } RESPONSE end def successful_refunded_response - <<-RESPONSE -{ - "amount": 1.00, - "authorization": "801585", - "method": "card", - "operation_type": "in", - "transaction_type": "charge", - "card": { - "type": "debit", - "brand": "mastercard", - "address": { - "line1": "1234 My Street", - "line2": "Apt 1", - "line3": null, - "state": "ON", - "city": "Ottawa", - "postal_code": "K1C2N6", - "country_code": "CA" - }, - "card_number": "1111", - "holder_name": "Longbob Longsen", - "expiration_year": "15", - "expiration_month": "09", - "allows_charges": true, - "allows_payouts": false, - "creation_date": "2014-01-20T17:08:43-06:00", - "bank_name": "DESCONOCIDO", - "bank_code": "000", - "customer_id": null - }, - "status": "completed", - "refund": { - "amount": 1.00, - "authorization": "030706", - "method": "card", - "operation_type": "out", - "transaction_type": "refund", - "status": "completed", - "currency": "MXN", - "id": "tspoc4u9msdbnkkhpcmi", - "creation_date": "2014-01-20T17:08:44-06:00", - "description": "Store Purchase", - "error_message": null, - "order_id": null - }, - "currency": "MXN", - "id": "tei4hnvyp4agt5ecnbow", - "creation_date": "2014-01-20T17:08:43-06:00", - "description": "Store Purchase", - "error_message": null, - "order_id": null -} + <<~RESPONSE + { + "amount": 1.00, + "authorization": "801585", + "method": "card", + "operation_type": "in", + "transaction_type": "charge", + "card": { + "type": "debit", + "brand": "mastercard", + "address": { + "line1": "1234 My Street", + "line2": "Apt 1", + "line3": null, + "state": "ON", + "city": "Ottawa", + "postal_code": "K1C2N6", + "country_code": "CA" + }, + "card_number": "1111", + "holder_name": "Longbob Longsen", + "expiration_year": "15", + "expiration_month": "09", + "allows_charges": true, + "allows_payouts": false, + "creation_date": "2014-01-20T17:08:43-06:00", + "bank_name": "DESCONOCIDO", + "bank_code": "000", + "customer_id": null + }, + "status": "completed", + "refund": { + "amount": 1.00, + "authorization": "030706", + "method": "card", + "operation_type": "out", + "transaction_type": "refund", + "status": "completed", + "currency": "MXN", + "id": "tspoc4u9msdbnkkhpcmi", + "creation_date": "2014-01-20T17:08:44-06:00", + "description": "Store Purchase", + "error_message": null, + "order_id": null + }, + "currency": "MXN", + "id": "tei4hnvyp4agt5ecnbow", + "creation_date": "2014-01-20T17:08:43-06:00", + "description": "Store Purchase", + "error_message": null, + "order_id": null, + "error_code": null + } RESPONSE end def successful_capture_response - <<-RESPONSE -{ - "amount": 1.00, - "authorization": "801585", - "method": "card", - "operation_type": "in", - "transaction_type": "charge", - "card": { - "type": "debit", - "brand": "mastercard", - "address": null, - "card_number": "1111", - "holder_name": "Longbob Longsen", - "expiration_year": "15", - "expiration_month": "09", - "allows_charges": true, - "allows_payouts": false, - "creation_date": "2014-01-18T21:01:10-06:00", - "bank_name": "DESCONOCIDO", - "bank_code": "000", - "customer_id": null - }, - "status": "completed", - "currency": "MXN", - "id": "tubpycc6gtsk71fu3tsd", - "creation_date": "2014-01-18T21:01:10-06:00", - "description": "Store Purchase", - "error_message": null, - "order_id": null -} + <<~RESPONSE + { + "amount": 1.00, + "authorization": "801585", + "method": "card", + "operation_type": "in", + "transaction_type": "charge", + "card": { + "type": "debit", + "brand": "mastercard", + "address": null, + "card_number": "1111", + "holder_name": "Longbob Longsen", + "expiration_year": "15", + "expiration_month": "09", + "allows_charges": true, + "allows_payouts": false, + "creation_date": "2014-01-18T21:01:10-06:00", + "bank_name": "DESCONOCIDO", + "bank_code": "000", + "customer_id": null + }, + "status": "completed", + "currency": "MXN", + "id": "tubpycc6gtsk71fu3tsd", + "creation_date": "2014-01-18T21:01:10-06:00", + "description": "Store Purchase", + "error_message": null, + "order_id": null, + "error_code": null + } RESPONSE end def successful_authorization_response - <<-RESPONSE -{ - "amount": 1.00, - "authorization": "801585", - "method": "card", - "operation_type": "in", - "transaction_type": "charge", - "card": { - "type": "debit", - "brand": "mastercard", - "address": null, - "card_number": "1111", - "holder_name": "Longbob Longsen", - "expiration_year": "15", - "expiration_month": "09", - "allows_charges": true, - "allows_payouts": false, - "creation_date": "2014-01-18T21:01:10-06:00", - "bank_name": "DESCONOCIDO", - "bank_code": "000", - "customer_id": null - }, - "status": "in_progress", - "currency": "MXN", - "id": "tubpycc6gtsk71fu3tsd", - "creation_date": "2014-01-18T21:01:10-06:00", - "description": "Store Purchase", - "error_message": null, - "order_id": null -} - RESPONSE + <<~RESPONSE + { + "amount": 1.00, + "authorization": "801585", + "method": "card", + "operation_type": "in", + "transaction_type": "charge", + "card": { + "type": "debit", + "brand": "mastercard", + "address": null, + "card_number": "1111", + "holder_name": "Longbob Longsen", + "expiration_year": "15", + "expiration_month": "09", + "allows_charges": true, + "allows_payouts": false, + "creation_date": "2014-01-18T21:01:10-06:00", + "bank_name": "DESCONOCIDO", + "bank_code": "000", + "customer_id": null + }, + "status": "in_progress", + "currency": "MXN", + "id": "tubpycc6gtsk71fu3tsd", + "creation_date": "2014-01-18T21:01:10-06:00", + "description": "Store Purchase", + "error_message": null, + "order_id": null, + "error_code": null + } + RESPONSE end def successful_purchase_response(status = 'completed') - <<-RESPONSE -{ - "amount": 1.00, - "authorization": "801585", - "method": "card", - "operation_type": "in", - "transaction_type": "charge", - "card": { - "type": "debit", - "brand": "mastercard", - "address": { - "line1": "1234 My Street", - "line2": "Apt 1", - "line3": null, - "state": "ON", - "city": "Ottawa", - "postal_code": "K1C2N6", - "country_code": "CA" - }, - "card_number": "1111", - "holder_name": "Longbob Longsen", - "expiration_year": "15", - "expiration_month": "09", - "allows_charges": true, - "allows_payouts": false, - "creation_date": "2014-01-18T21:49:38-06:00", - "bank_name": "BANCOMER", - "bank_code": "012", - "customer_id": null - }, - "status": "#{status}", - "currency": "MXN", - "id": "tay1mauq3re4iuuk8bm4", - "creation_date": "2014-01-18T21:49:38-06:00", - "description": "Store Purchase", - "error_message": null, - "order_id": null -} + <<~RESPONSE + { + "amount": 1.00, + "authorization": "801585", + "method": "card", + "operation_type": "in", + "transaction_type": "charge", + "card": { + "type": "debit", + "brand": "mastercard", + "address": { + "line1": "1234 My Street", + "line2": "Apt 1", + "line3": null, + "state": "ON", + "city": "Ottawa", + "postal_code": "K1C2N6", + "country_code": "CA" + }, + "card_number": "1111", + "holder_name": "Longbob Longsen", + "expiration_year": "15", + "expiration_month": "09", + "allows_charges": true, + "allows_payouts": false, + "creation_date": "2014-01-18T21:49:38-06:00", + "bank_name": "BANCOMER", + "bank_code": "012", + "customer_id": null + }, + "status": "#{status}", + "currency": "MXN", + "id": "tay1mauq3re4iuuk8bm4", + "creation_date": "2014-01-18T21:49:38-06:00", + "description": "Store Purchase", + "error_message": null, + "order_id": null, + "error_code": null + } RESPONSE end @@ -431,26 +469,26 @@ def successful_void_response end def failed_purchase_response - <<-RESPONSE -{ - "category": "gateway", - "description": "The card was declined", - "http_code": 402, - "error_code": 3001, - "request_id": "337cf033-9cd6-4314-a880-c71700e1625f" -} + <<~RESPONSE + { + "category": "gateway", + "description": "The card was declined", + "http_code": 402, + "error_code": 3001, + "request_id": "337cf033-9cd6-4314-a880-c71700e1625f" + } RESPONSE end def failed_authorize_response - <<-RESPONSE -{ - "category":"gateway", - "description":"The card is not supported on online transactions", - "http_code":412, - "error_code":3008, - "request_id":"a4001ef2-7613-4ec8-a23b-4de45154dbe4" -} + <<~RESPONSE + { + "category":"gateway", + "description":"The card is not supported on online transactions", + "http_code":412, + "error_code":3008, + "request_id":"a4001ef2-7613-4ec8-a23b-4de45154dbe4" + } RESPONSE end diff --git a/test/unit/gateways/opp_test.rb b/test/unit/gateways/opp_test.rb index c955eb75d1e..fd3dd4563fb 100644 --- a/test/unit/gateways/opp_test.rb +++ b/test/unit/gateways/opp_test.rb @@ -7,8 +7,8 @@ def setup @gateway = OppGateway.new(fixtures(:opp)) @amount = 100 - @valid_card = credit_card('4200000000000000', month: 05, year: 2018, verification_value: '123') - @invalid_card = credit_card('4444444444444444', month: 05, year: 2018, verification_value: '123') + @valid_card = credit_card('4200000000000000', month: 05, year: Date.today.year + 2, verification_value: '123') + @invalid_card = credit_card('4444444444444444', month: 05, year: Date.today.year + 2, verification_value: '123') request_type = 'complete' # 'minimal' || 'complete' time = Time.now.to_i @@ -16,10 +16,10 @@ def setup @complete_request_options = { order_id: "Order #{time}", merchant_transaction_id: "active_merchant_test_complete #{time}", - address: address, + address:, description: 'Store Purchase - Books', -# risk_workflow: true, -# test_mode: 'EXTERNAL' # or 'INTERNAL', valid only for test system + # risk_workflow: true, + # test_mode: 'EXTERNAL' # or 'INTERNAL', valid only for test system billing_address: { name: 'Billy Billing', @@ -27,7 +27,7 @@ def setup city: 'Istambul', state: 'IS', zip: 'H12JK2354', - country: 'TR', + country: 'TR' }, shipping_address: { name: '', @@ -35,7 +35,7 @@ def setup city: 'Moskau', state: 'MO', zip: 'MO2342432', - country: 'RU', + country: 'RU' }, customer: { merchant_customer_id: "merchantCustomerId #{ip}", @@ -48,13 +48,13 @@ def setup company_name: 'No such deal Ltd.', identification_doctype: 'PASSPORT', identification_docid: 'FakeID2342431234123', - ip: ip, - }, + ip: + } } @minimal_request_options = { order_id: "Order #{time}", - description: 'Store Purchase - Books', + description: 'Store Purchase - Books' } @complete_request_options['customParameters[SHOPPER_test124TestName009]'] = 'customParameters_test' @@ -67,7 +67,7 @@ def setup @options = @complete_request_options if request_type == 'complete' end -# ****************************************** SUCCESSFUL TESTS ****************************************** + # ****************************************** SUCCESSFUL TESTS ****************************************** def test_successful_purchase @gateway.expects(:raw_ssl_request).returns(successful_response('DB', @test_success_id)) response = @gateway.purchase(@amount, @valid_card, @options) @@ -121,7 +121,15 @@ def test_successful_void assert void.test? end -# ****************************************** FAILURE TESTS ****************************************** + def test_successful_store + @gateway.expects(:raw_ssl_request).returns(successful_store_response(@test_success_id)) + store = @gateway.store(@valid_card) + assert_success store + assert_equal "Request successfully processed in 'Merchant in Integrator Test Mode'", store.message + assert_equal @test_success_id, store.authorization + end + + # ****************************************** FAILURE TESTS ****************************************** def test_failed_purchase @gateway.expects(:raw_ssl_request).returns(failed_response('DB', @test_failure_id)) response = @gateway.purchase(@amount, @invalid_card, @options) @@ -157,12 +165,19 @@ def test_failed_void assert_equal '100.100.101', response.error_code end + def test_failed_store + @gateway.expects(:raw_ssl_request).returns(failed_store_response(@test_failure_id)) + store = @gateway.store(@invalid_card) + assert_failure store + assert_equal '100.100.101', store.error_code + end + def test_passes_3d_secure_fields - options = @complete_request_options.merge({eci: 'eci', cavv: 'cavv', xid: 'xid'}) + options = @complete_request_options.merge({ eci: 'eci', cavv: 'cavv', xid: 'xid' }) response = stub_comms(@gateway, :raw_ssl_request) do @gateway.purchase(@amount, @valid_card, options) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/threeDSecure.eci=eci/, data) assert_match(/threeDSecure.verificationId=cavv/, data) assert_match(/threeDSecure.xid=xid/, data) @@ -179,27 +194,109 @@ def test_scrub private def pre_scrubbed - 'paymentType=DB&amount=1.00&currency=EUR&paymentBrand=VISA&card.holder=Longbob+Longsen&card.number=4200000000000000&card.expiryMonth=05&card.expiryYear=2018& card.cvv=123&billing.street1=456+My+Street&billing.street2=Apt+1&billing.city=Ottawa&billing.state=ON&billing.postcode=K1C2N6&billing.country=CA&authentication.entityId=8a8294174b7ecb28014b9699220015ca&authentication.password=sy6KJsT8&authentication.userId=8a8294174b7ecb28014b9699220015cc' + 'paymentType=DB&amount=1.00&currency=EUR&descriptor=&merchantInvoiceId=&merchantTransactionId=50b5c1763c20c456a6208f7831dd0a04&paymentBrand=VISA&card.holder=Longbob+Longsen&card.number=4200000000000000&card.expiryMonth=05&card.expiryYear=2022&card.cvv=123&customParameters[SHOPPER_pluginId]=activemerchant&authentication.entityId=5c6602174b7ecb28014b96992' end def post_scrubbed - 'paymentType=DB&amount=1.00&currency=EUR&paymentBrand=VISA&card.holder=Longbob+Longsen&card.number=[FILTERED]&card.expiryMonth=05&card.expiryYear=2018& card.cvv=[FILTERED]&billing.street1=456+My+Street&billing.street2=Apt+1&billing.city=Ottawa&billing.state=ON&billing.postcode=K1C2N6&billing.country=CA&authentication.entityId=8a8294174b7ecb28014b9699220015ca&authentication.password=[FILTERED]&authentication.userId=8a8294174b7ecb28014b9699220015cc' + 'paymentType=DB&amount=1.00&currency=EUR&descriptor=&merchantInvoiceId=&merchantTransactionId=50b5c1763c20c456a6208f7831dd0a04&paymentBrand=VISA&card.holder=Longbob+Longsen&card.number=[FILTERED]&card.expiryMonth=05&card.expiryYear=2022&card.cvv=[FILTERED]&customParameters[SHOPPER_pluginId]=activemerchant&authentication.entityId=5c6602174b7ecb28014b96992' end def successful_response(type, id) - OppMockResponse.new(200, - JSON.generate({'id' => id,'paymentType' => type,'paymentBrand' => 'VISA','amount' => '1.00','currency' => 'EUR',"des - criptor" => '5410.9959.0306 OPP_Channel ','result' => {'code' => '000.100.110','description' => "Request successfully processed in 'Merchant in Integrator Test Mode'"},'card' => {"bin - " => '420000','last4Digits' => '0000','holder' => 'Longbob Longsen','expiryMonth' => '05','expiryYear' => '2018'},'buildNumber' => '20150618-111601.r185004.opp-tags-20150618_stage',"time - stamp" => '2015-06-20 19:31:01+0000','ndc' => '8a8294174b7ecb28014b9699220015ca_4453edbc001f405da557c05cb3c3add9'}) + OppMockResponse.new( + 200, + JSON.generate({ + 'id' => id, + 'paymentType' => type, + 'paymentBrand' => 'VISA', + 'amount' => '1.00', + 'currency' => 'EUR', + 'descriptor' => '5410.9959.0306 OPP_Channel', + 'result' => { + 'code' => '000.100.110', + 'description' => "Request successfully processed in 'Merchant in Integrator Test Mode'" + }, + 'card' => { + 'bin' => '420000', + 'last4Digits' => '0000', + 'holder' => 'Longbob Longsen', + 'expiryMonth' => '05', + 'expiryYear' => '2018' + }, + '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, + JSON.generate({ + 'id' => id, + 'result' => { + 'code' => '000.100.110', + 'description' => "Request successfully processed in 'Merchant in Integrator Test Mode'" + }, + 'card' => { + 'bin' => '420000', + 'last4Digits' => '0000', + 'holder' => 'Longbob Longsen', + 'expiryMonth' => '05', + 'expiryYear' => '2018' + }, + '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, - JSON.generate({'id' => id,'paymentType' => type,'paymentBrand' => 'VISA','result' => {'code' => code,"des - cription" => 'invalid creditcard, bank account number or bank name'},'card' => {'bin' => '444444','last4Digits' => '4444','holder' => 'Longbob Longsen','expiryMonth' => '05','expiryYear' => '2018'}, - 'buildNumber' => '20150618-111601.r185004.opp-tags-20150618_stage','timestamp' => '2015-06-20 20:40:26+0000','ndc' => '8a8294174b7ecb28014b9699220015ca_5200332e7d664412a84ed5f4777b3c7d'}) + def failed_response(type, id, code = '100.100.101') + OppMockResponse.new( + 400, + JSON.generate({ + 'id' => id, + 'paymentType' => type, + 'paymentBrand' => 'VISA', + 'result' => { + 'code' => code, + 'description' => 'invalid creditcard, bank account number or bank name' + }, + 'card' => { + 'bin' => '444444', + 'last4Digits' => '4444', + 'holder' => 'Longbob Longsen', + 'expiryMonth' => '05', + 'expiryYear' => '2018' + }, + '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, + JSON.generate({ + 'id' => id, + 'result' => { + 'code' => code, + 'description' => 'invalid creditcard, bank account number or bank name' + }, + 'card' => { + 'bin' => '444444', + 'last4Digits' => '4444', + 'holder' => 'Longbob Longsen', + 'expiryMonth' => '05', + 'expiryYear' => '2018' + }, + 'buildNumber' => '20150618-111601.r185004.opp-tags-20150618_stage', + 'timestamp' => '2015-06-20 20:40:26+0000', + 'ndc' => '8a8294174b7ecb28014b9699220015ca_5200332e7d664412a84ed5f4777b3c7d' + }) ) end @@ -211,6 +308,4 @@ def initialize(code, body) @body = body end end - end - diff --git a/test/unit/gateways/optimal_payment_test.rb b/test/unit/gateways/optimal_payment_test.rb index d2c255dfe61..f5622504397 100644 --- a/test/unit/gateways/optimal_payment_test.rb +++ b/test/unit/gateways/optimal_payment_test.rb @@ -10,19 +10,19 @@ class OptimalPaymentTest < Test::Unit::TestCase def setup @gateway = OptimalPaymentGateway.new( - :account_number => '12345678', - :store_id => 'login', - :password => 'password' + account_number: '12345678', + store_id: 'login', + password: 'password' ) @credit_card = credit_card @amount = 100 @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase', - :email => 'email@example.com' + order_id: '1', + billing_address: address, + description: 'Store Purchase', + email: 'email@example.com' } end @@ -34,26 +34,26 @@ def test_full_request def test_ip_address_is_passed stub_comms do @gateway.purchase(@amount, @credit_card, @options.merge(ip: '1.2.3.4')) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match %r{customerIP%3E1.2.3.4%3C}, data end.respond_with(successful_purchase_response) end def test_minimal_request options = { - :order_id => '1', - :description => 'Store Purchase', - :billing_address => { - :zip => 'K1C2N6', + order_id: '1', + description: 'Store Purchase', + billing_address: { + zip: 'K1C2N6' } } credit_card = CreditCard.new( - :number => '4242424242424242', - :month => 9, - :year => Time.now.year + 1, - :first_name => 'Longbob', - :last_name => 'Longsen', - :brand => 'visa' + number: '4242424242424242', + month: 9, + year: Time.now.year + 1, + first_name: 'Longbob', + last_name: 'Longsen', + brand: 'visa' ) @gateway.instance_variable_set('@credit_card', credit_card) assert_match minimal_request, @gateway.cc_auth_request(@amount, options) @@ -73,7 +73,7 @@ def test_successful_purchase def test_purchase_from_canada_includes_state_field @options[:billing_address][:country] = 'CA' - @gateway.expects(:ssl_post).with do |url, data| + @gateway.expects(:ssl_post).with do |_url, data| data =~ /state/ && data !~ /region/ end.returns(successful_purchase_response) @@ -82,7 +82,7 @@ def test_purchase_from_canada_includes_state_field def test_purchase_from_us_includes_state_field @options[:billing_address][:country] = 'US' - @gateway.expects(:ssl_post).with do |url, data| + @gateway.expects(:ssl_post).with do |_url, data| data =~ /state/ && data !~ /region/ end.returns(successful_purchase_response) @@ -91,7 +91,7 @@ def test_purchase_from_us_includes_state_field def test_purchase_from_any_other_country_includes_region_field @options[:billing_address][:country] = 'GB' - @gateway.expects(:ssl_post).with do |url, data| + @gateway.expects(:ssl_post).with do |_url, data| data =~ /region/ && data !~ /state/ end.returns(successful_purchase_response) @@ -99,9 +99,9 @@ def test_purchase_from_any_other_country_includes_region_field end def test_purchase_with_shipping_address - @options[:shipping_address] = {:country => 'CA'} - @gateway.expects(:ssl_post).with do |url, data| - xml = data.split('&').detect{|string| string =~ /txnRequest=/}.gsub('txnRequest=','') + @options[:shipping_address] = { country: 'CA' } + @gateway.expects(:ssl_post).with do |_url, data| + xml = data.split('&').detect { |string| string =~ /txnRequest=/ }.gsub('txnRequest=', '') doc = Nokogiri::XML.parse(CGI.unescape(xml)) doc.xpath('//xmlns:shippingDetails/xmlns:country').first.text == 'CA' && doc.to_s.include?('<shippingDetails>') end.returns(successful_purchase_response) @@ -111,8 +111,8 @@ def test_purchase_with_shipping_address def test_purchase_without_shipping_address @options[:shipping_address] = nil - @gateway.expects(:ssl_post).with do |url, data| - xml = data.split('&').detect{|string| string =~ /txnRequest=/}.gsub('txnRequest=','') + @gateway.expects(:ssl_post).with do |_url, data| + xml = data.split('&').detect { |string| string =~ /txnRequest=/ }.gsub('txnRequest=', '') doc = Nokogiri::XML.parse(CGI.unescape(xml)) doc.to_s.include?('<shippingDetails>') == false end.returns(successful_purchase_response) @@ -131,23 +131,23 @@ def test_purchase_without_billing_address def test_cvd_fields_pass_correctly stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| - assert_match (/cvdIndicator%3E1%3C\/cvdIndicator%3E%0A%20%20%20%20%3Ccvd%3E123%3C\/cvd/), data + end.check_request do |_endpoint, data, _headers| + assert_match(/cvdIndicator%3E1%3C\/cvdIndicator%3E%0A%20%20%20%20%3Ccvd%3E123%3C\/cvd/, data) end.respond_with(successful_purchase_response) credit_card = CreditCard.new( - :number => '4242424242424242', - :month => 9, - :year => Time.now.year + 1, - :first_name => 'Longbob', - :last_name => 'Longsen', - :brand => 'visa' + number: '4242424242424242', + month: 9, + year: Time.now.year + 1, + first_name: 'Longbob', + last_name: 'Longsen', + brand: 'visa' ) stub_comms do @gateway.purchase(@amount, credit_card, @options) - end.check_request do |endpoint, data, headers| - assert_match (/cvdIndicator%3E0%3C\/cvdIndicator%3E%0A%20%20%3C\/card/), data + end.check_request do |_endpoint, data, _headers| + assert_match(/cvdIndicator%3E0%3C\/cvdIndicator%3E%0A%20%20%3C\/card/, data) end.respond_with(failed_purchase_response) end @@ -172,23 +172,21 @@ def test_unsuccessful_request end def test_in_production_with_test_param_sends_request_to_test_server - begin - ActiveMerchant::Billing::Base.mode = :production - @gateway = OptimalPaymentGateway.new( - :account_number => '12345678', - :store_id => 'login', - :password => 'password', - :test => true - ) - @gateway.expects(:ssl_post).with('https://webservices.test.optimalpayments.com/creditcardWS/CreditCardServlet/v1', anything).returns(successful_purchase_response) - - assert response = @gateway.purchase(@amount, @credit_card, @options) - assert_instance_of Response, response - assert_success response - assert response.test? - ensure - ActiveMerchant::Billing::Base.mode = :test - end + ActiveMerchant::Billing::Base.mode = :production + @gateway = OptimalPaymentGateway.new( + account_number: '12345678', + store_id: 'login', + password: 'password', + test: true + ) + @gateway.expects(:ssl_post).with('https://webservices.test.optimalpayments.com/creditcardWS/CreditCardServlet/v1', anything).returns(successful_purchase_response) + + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_instance_of Response, response + assert_success response + assert response.test? + ensure + ActiveMerchant::Billing::Base.mode = :test end def test_avs_result_in_response @@ -214,20 +212,19 @@ def test_avs_results_not_in_response end def test_deprecated_options - assert_deprecation_warning("The 'account' option is deprecated in favor of 'account_number' and will be removed in a future version.") do @gateway = OptimalPaymentGateway.new( - :account => '12345678', - :store_id => 'login', - :password => 'password' + account: '12345678', + store_id: 'login', + password: 'password' ) end assert_deprecation_warning("The 'login' option is deprecated in favor of 'store_id' and will be removed in a future version.") do @gateway = OptimalPaymentGateway.new( - :account_number => '12345678', - :login => 'login', - :password => 'password' + account_number: '12345678', + login: 'login', + password: 'password' ) end end @@ -240,202 +237,202 @@ def test_scrub private def full_request - str = <<-XML -<ccAuthRequestV1 xmlns> - <merchantAccount> - <accountNum>12345678</accountNum> - <storeID>login</storeID> - <storePwd>password</storePwd> - </merchantAccount> - <merchantRefNum>1</merchantRefNum> - <amount>1.0</amount> - <card> - <cardNum>4242424242424242</cardNum> - <cardExpiry> - <month>9</month> - <year>#{Time.now.year + 1}</year> - </cardExpiry> - <cardType>VI</cardType> - <cvdIndicator>1</cvdIndicator> - <cvd>123</cvd> - </card> - <billingDetails> - <cardPayMethod>WEB</cardPayMethod> - <firstName>Jim</firstName> - <lastName>Smith</lastName> - <street>456 My Street</street> - <street2>Apt 1</street2> - <city>Ottawa</city> - <state>ON</state> - <country>CA</country> - <zip>K1C2N6</zip> - <phone>(555)555-5555</phone> - <email>email@example.com</email> - </billingDetails> -</ccAuthRequestV1> + str = <<~XML + <ccAuthRequestV1 xmlns> + <merchantAccount> + <accountNum>12345678</accountNum> + <storeID>login</storeID> + <storePwd>password</storePwd> + </merchantAccount> + <merchantRefNum>1</merchantRefNum> + <amount>1.0</amount> + <card> + <cardNum>4242424242424242</cardNum> + <cardExpiry> + <month>9</month> + <year>#{Time.now.year + 1}</year> + </cardExpiry> + <cardType>VI</cardType> + <cvdIndicator>1</cvdIndicator> + <cvd>123</cvd> + </card> + <billingDetails> + <cardPayMethod>WEB</cardPayMethod> + <firstName>Jim</firstName> + <lastName>Smith</lastName> + <street>456 My Street</street> + <street2>Apt 1</street2> + <city>Ottawa</city> + <state>ON</state> + <country>CA</country> + <zip>K1C2N6</zip> + <phone>(555)555-5555</phone> + <email>email@example.com</email> + </billingDetails> + </ccAuthRequestV1> XML Regexp.new(Regexp.escape(str).sub('xmlns', '[^>]+').sub('/>', '(/>|></[^>]+>)')) end def minimal_request - str = <<-XML -<ccAuthRequestV1 xmlns> - <merchantAccount> - <accountNum>12345678</accountNum> - <storeID>login</storeID> - <storePwd>password</storePwd> - </merchantAccount> - <merchantRefNum>1</merchantRefNum> - <amount>1.0</amount> - <card> - <cardNum>4242424242424242</cardNum> - <cardExpiry> - <month>9</month> - <year>#{Time.now.year + 1}</year> - </cardExpiry> - <cardType>VI</cardType> - <cvdIndicator>0</cvdIndicator> - </card> - <billingDetails> - <cardPayMethod>WEB</cardPayMethod> - <zip>K1C2N6</zip> - </billingDetails> -</ccAuthRequestV1> + str = <<~XML + <ccAuthRequestV1 xmlns> + <merchantAccount> + <accountNum>12345678</accountNum> + <storeID>login</storeID> + <storePwd>password</storePwd> + </merchantAccount> + <merchantRefNum>1</merchantRefNum> + <amount>1.0</amount> + <card> + <cardNum>4242424242424242</cardNum> + <cardExpiry> + <month>9</month> + <year>#{Time.now.year + 1}</year> + </cardExpiry> + <cardType>VI</cardType> + <cvdIndicator>0</cvdIndicator> + </card> + <billingDetails> + <cardPayMethod>WEB</cardPayMethod> + <zip>K1C2N6</zip> + </billingDetails> + </ccAuthRequestV1> XML Regexp.new(Regexp.escape(str).sub('xmlns', '[^>]+').sub('/>', '(/>|></[^>]+>)')) end # Place raw successful response from gateway here def successful_purchase_response - <<-XML -<ccTxnResponseV1 xmlns="http://www.optimalpayments.com/creditcard/xmlschema/v1"> - <confirmationNumber>126740505</confirmationNumber> - <decision>ACCEPTED</decision> - <code>0</code> - <description>No Error</description> - <authCode>112232</authCode> - <avsResponse>B</avsResponse> - <cvdResponse>M</cvdResponse> - <detail> - <tag>InternalResponseCode</tag> - <value>0</value> - </detail> - <detail> - <tag>SubErrorCode</tag> - <value>0</value> - </detail> - <detail> - <tag>InternalResponseDescription</tag> - <value>no_error</value> - </detail> - <txnTime>2009-01-08T17:00:45.210-05:00</txnTime> - <duplicateFound>false</duplicateFound> -</ccTxnResponseV1> + <<~XML + <ccTxnResponseV1 xmlns="http://www.optimalpayments.com/creditcard/xmlschema/v1"> + <confirmationNumber>126740505</confirmationNumber> + <decision>ACCEPTED</decision> + <code>0</code> + <description>No Error</description> + <authCode>112232</authCode> + <avsResponse>B</avsResponse> + <cvdResponse>M</cvdResponse> + <detail> + <tag>InternalResponseCode</tag> + <value>0</value> + </detail> + <detail> + <tag>SubErrorCode</tag> + <value>0</value> + </detail> + <detail> + <tag>InternalResponseDescription</tag> + <value>no_error</value> + </detail> + <txnTime>2009-01-08T17:00:45.210-05:00</txnTime> + <duplicateFound>false</duplicateFound> + </ccTxnResponseV1> XML end # Place raw successful response from gateway here def successful_purchase_response_without_avs_results - <<-XML -<ccTxnResponseV1 xmlns="http://www.optimalpayments.com/creditcard/xmlschema/v1"> - <confirmationNumber>126740505</confirmationNumber> - <decision>ACCEPTED</decision> - <code>0</code> - <description>No Error</description> - <authCode>112232</authCode> - <detail> - <tag>InternalResponseCode</tag> - <value>0</value> - </detail> - <detail> - <tag>SubErrorCode</tag> - <value>0</value> - </detail> - <detail> - <tag>InternalResponseDescription</tag> - <value>no_error</value> - </detail> - <txnTime>2009-01-08T17:00:45.210-05:00</txnTime> - <duplicateFound>false</duplicateFound> -</ccTxnResponseV1> + <<~XML + <ccTxnResponseV1 xmlns="http://www.optimalpayments.com/creditcard/xmlschema/v1"> + <confirmationNumber>126740505</confirmationNumber> + <decision>ACCEPTED</decision> + <code>0</code> + <description>No Error</description> + <authCode>112232</authCode> + <detail> + <tag>InternalResponseCode</tag> + <value>0</value> + </detail> + <detail> + <tag>SubErrorCode</tag> + <value>0</value> + </detail> + <detail> + <tag>InternalResponseDescription</tag> + <value>no_error</value> + </detail> + <txnTime>2009-01-08T17:00:45.210-05:00</txnTime> + <duplicateFound>false</duplicateFound> + </ccTxnResponseV1> XML end # Place raw failed response from gateway here def failed_purchase_response - <<-XML -<ccTxnResponseV1 xmlns="http://www.optimalpayments.com/creditcard/xmlschema/v1"> - <confirmationNumber>126740506</confirmationNumber> - <decision>DECLINED</decision> - <code>3009</code> - <actionCode>D</actionCode> - <description>Your request has been declined by the issuing bank.</description> - <avsResponse>B</avsResponse> - <cvdResponse>M</cvdResponse> - <detail> - <tag>InternalResponseCode</tag> - <value>160</value> - </detail> - <detail> - <tag>SubErrorCode</tag> - <value>1005</value> - </detail> - <detail> - <tag>InternalResponseDescription</tag> - <value>auth declined</value> - </detail> - <txnTime>2009-01-08T17:00:46.529-05:00</txnTime> - <duplicateFound>false</duplicateFound> -</ccTxnResponseV1> + <<~XML + <ccTxnResponseV1 xmlns="http://www.optimalpayments.com/creditcard/xmlschema/v1"> + <confirmationNumber>126740506</confirmationNumber> + <decision>DECLINED</decision> + <code>3009</code> + <actionCode>D</actionCode> + <description>Your request has been declined by the issuing bank.</description> + <avsResponse>B</avsResponse> + <cvdResponse>M</cvdResponse> + <detail> + <tag>InternalResponseCode</tag> + <value>160</value> + </detail> + <detail> + <tag>SubErrorCode</tag> + <value>1005</value> + </detail> + <detail> + <tag>InternalResponseDescription</tag> + <value>auth declined</value> + </detail> + <txnTime>2009-01-08T17:00:46.529-05:00</txnTime> + <duplicateFound>false</duplicateFound> + </ccTxnResponseV1> XML end def pre_scrubbed - <<-EOS -opening connection to webservices.test.optimalpayments.com:443... -opened -starting SSL for webservices.test.optimalpayments.com:443... -SSL established -<- "POST /creditcardWS/CreditCardServlet/v1 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: webservices.test.optimalpayments.com\r\nContent-Length: 1616\r\n\r\n" -<- "txnMode=ccPurchase&txnRequest=%3CccAuthRequestV1%20xmlns=%22http://www.optimalpayments.com/creditcard/xmlschema/v1%22%20xmlns:xsi=%22http://www.w3.org/2001/XMLSchema-instance%22%20xsi:schemaLocation=%22http://www.optimalpayments.com/creditcard/xmlschema/v1%22%3E%0A%20%20%3CmerchantAccount%3E%0A%20%20%20%20%3CaccountNum%3E1001134550%3C/accountNum%3E%0A%20%20%20%20%3CstoreID%3Etest%3C/storeID%3E%0A%20%20%20%20%3CstorePwd%3Etest%3C/storePwd%3E%0A%20%20%3C/merchantAccount%3E%0A%20%20%3CmerchantRefNum%3E1%3C/merchantRefNum%3E%0A%20%20%3Camount%3E1.0%3C/amount%3E%0A%20%20%3Ccard%3E%0A%20%20%20%20%3CcardNum%3E4387751111011%3C/cardNum%3E%0A%20%20%20%20%3CcardExpiry%3E%0A%20%20%20%20%20%20%3Cmonth%3E9%3C/month%3E%0A%20%20%20%20%20%20%3Cyear%3E2019%3C/year%3E%0A%20%20%20%20%3C/cardExpiry%3E%0A%20%20%20%20%3CcardType%3EVI%3C/cardType%3E%0A%20%20%20%20%3CcvdIndicator%3E1%3C/cvdIndicator%3E%0A%20%20%20%20%3Ccvd%3E123%3C/cvd%3E%0A%20%20%3C/card%3E%0A%20%20%3CbillingDetails%3E%0A%20%20%20%20%3CcardPayMethod%3EWEB%3C/cardPayMethod%3E%0A%20%20%20%20%3CfirstName%3EJim%3C/firstName%3E%0A%20%20%20%20%3ClastName%3ESmith%3C/lastName%3E%0A%20%20%20%20%3Cstreet%3E456%20My%20Street%3C/street%3E%0A%20%20%20%20%3Cstreet2%3EApt%201%3C/street2%3E%0A%20%20%20%20%3Ccity%3EOttawa%3C/city%3E%0A%20%20%20%20%3Cstate%3EON%3C/state%3E%0A%20%20%20%20%3Ccountry%3ECA%3C/country%3E%0A%20%20%20%20%3Czip%3EK1C2N6%3C/zip%3E%0A%20%20%20%20%3Cphone%3E(555)555-5555%3C/phone%3E%0A%20%20%20%20%3Cemail%3Eemail@example.com%3C/email%3E%0A%20%20%3C/billingDetails%3E%0A%20%20%3CcustomerIP%3E1.2.3.4%3C/customerIP%3E%0A%3C/ccAuthRequestV1%3E%0A" --> "HTTP/1.1 200 OK\r\n" --> "Server: WebServer32xS10i3\r\n" --> "Content-Length: 632\r\n" --> "X-ApplicationUid: GUID=610a301289c34e8254330b7edc724f5b\r\n" --> "Content-Type: application/xml\r\n" --> "Date: Mon, 12 Feb 2018 21:57:42 GMT\r\n" --> "Connection: close\r\n" --> "\r\n" -reading 632 bytes... --> "<" --> "?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ccTxnResponseV1 xmlns=\"http://www.optimalpayments.com/creditcard/xmlschema/v1\"><confirmationNumber>498871860</confirmationNumber><decision>ACCEPTED</decision><code>0</code><description>No Error</description><authCode>369231</authCode><avsResponse>X</avsResponse><cvdResponse>M</cvdResponse><detail><tag>InternalResponseCode</tag><value>0</value></detail><detail><tag>SubErrorCode</tag><value>0</value></detail><detail><tag>InternalResponseDescription</tag><value>no_error</value></detail><txnTime>2018-02-12T16:57:42.289-05:00</txnTime><duplicateFound>false</duplicateFound></ccTxnResponseV1>" -read 632 bytes -Conn close - EOS + <<~REQUEST + opening connection to webservices.test.optimalpayments.com:443... + opened + starting SSL for webservices.test.optimalpayments.com:443... + SSL established + <- "POST /creditcardWS/CreditCardServlet/v1 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: webservices.test.optimalpayments.com\r\nContent-Length: 1616\r\n\r\n" + <- "txnMode=ccPurchase&txnRequest=%3CccAuthRequestV1%20xmlns=%22http://www.optimalpayments.com/creditcard/xmlschema/v1%22%20xmlns:xsi=%22http://www.w3.org/2001/XMLSchema-instance%22%20xsi:schemaLocation=%22http://www.optimalpayments.com/creditcard/xmlschema/v1%22%3E%0A%20%20%3CmerchantAccount%3E%0A%20%20%20%20%3CaccountNum%3E1001134550%3C/accountNum%3E%0A%20%20%20%20%3CstoreID%3Etest%3C/storeID%3E%0A%20%20%20%20%3CstorePwd%3Etest%3C/storePwd%3E%0A%20%20%3C/merchantAccount%3E%0A%20%20%3CmerchantRefNum%3E1%3C/merchantRefNum%3E%0A%20%20%3Camount%3E1.0%3C/amount%3E%0A%20%20%3Ccard%3E%0A%20%20%20%20%3CcardNum%3E4387751111011%3C/cardNum%3E%0A%20%20%20%20%3CcardExpiry%3E%0A%20%20%20%20%20%20%3Cmonth%3E9%3C/month%3E%0A%20%20%20%20%20%20%3Cyear%3E2019%3C/year%3E%0A%20%20%20%20%3C/cardExpiry%3E%0A%20%20%20%20%3CcardType%3EVI%3C/cardType%3E%0A%20%20%20%20%3CcvdIndicator%3E1%3C/cvdIndicator%3E%0A%20%20%20%20%3Ccvd%3E123%3C/cvd%3E%0A%20%20%3C/card%3E%0A%20%20%3CbillingDetails%3E%0A%20%20%20%20%3CcardPayMethod%3EWEB%3C/cardPayMethod%3E%0A%20%20%20%20%3CfirstName%3EJim%3C/firstName%3E%0A%20%20%20%20%3ClastName%3ESmith%3C/lastName%3E%0A%20%20%20%20%3Cstreet%3E456%20My%20Street%3C/street%3E%0A%20%20%20%20%3Cstreet2%3EApt%201%3C/street2%3E%0A%20%20%20%20%3Ccity%3EOttawa%3C/city%3E%0A%20%20%20%20%3Cstate%3EON%3C/state%3E%0A%20%20%20%20%3Ccountry%3ECA%3C/country%3E%0A%20%20%20%20%3Czip%3EK1C2N6%3C/zip%3E%0A%20%20%20%20%3Cphone%3E(555)555-5555%3C/phone%3E%0A%20%20%20%20%3Cemail%3Eemail@example.com%3C/email%3E%0A%20%20%3C/billingDetails%3E%0A%20%20%3CcustomerIP%3E1.2.3.4%3C/customerIP%3E%0A%3C/ccAuthRequestV1%3E%0A" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: WebServer32xS10i3\r\n" + -> "Content-Length: 632\r\n" + -> "X-ApplicationUid: GUID=610a301289c34e8254330b7edc724f5b\r\n" + -> "Content-Type: application/xml\r\n" + -> "Date: Mon, 12 Feb 2018 21:57:42 GMT\r\n" + -> "Connection: close\r\n" + -> "\r\n" + reading 632 bytes... + -> "<" + -> "?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ccTxnResponseV1 xmlns=\"http://www.optimalpayments.com/creditcard/xmlschema/v1\"><confirmationNumber>498871860</confirmationNumber><decision>ACCEPTED</decision><code>0</code><description>No Error</description><authCode>369231</authCode><avsResponse>X</avsResponse><cvdResponse>M</cvdResponse><detail><tag>InternalResponseCode</tag><value>0</value></detail><detail><tag>SubErrorCode</tag><value>0</value></detail><detail><tag>InternalResponseDescription</tag><value>no_error</value></detail><txnTime>2018-02-12T16:57:42.289-05:00</txnTime><duplicateFound>false</duplicateFound></ccTxnResponseV1>" + read 632 bytes + Conn close + REQUEST end def post_scrubbed - <<-EOS -opening connection to webservices.test.optimalpayments.com:443... -opened -starting SSL for webservices.test.optimalpayments.com:443... -SSL established -<- "POST /creditcardWS/CreditCardServlet/v1 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: webservices.test.optimalpayments.com\r\nContent-Length: 1616\r\n\r\n" -<- "txnMode=ccPurchase&txnRequest=%3CccAuthRequestV1%20xmlns=%22http://www.optimalpayments.com/creditcard/xmlschema/v1%22%20xmlns:xsi=%22http://www.w3.org/2001/XMLSchema-instance%22%20xsi:schemaLocation=%22http://www.optimalpayments.com/creditcard/xmlschema/v1%22%3E%0A%20%20%3CmerchantAccount%3E%0A%20%20%20%20%3CaccountNum%3E1001134550%3C/accountNum%3E%0A%20%20%20%20%3CstoreID%3Etest%3C/storeID%3E%0A%20%20%20%20%3CstorePwd%3E[FILTERED]%3C/storePwd%3E%0A%20%20%3C/merchantAccount%3E%0A%20%20%3CmerchantRefNum%3E1%3C/merchantRefNum%3E%0A%20%20%3Camount%3E1.0%3C/amount%3E%0A%20%20%3Ccard%3E%0A%20%20%20%20%3CcardNum%3E[FILTERED]%3C/cardNum%3E%0A%20%20%20%20%3CcardExpiry%3E%0A%20%20%20%20%20%20%3Cmonth%3E9%3C/month%3E%0A%20%20%20%20%20%20%3Cyear%3E2019%3C/year%3E%0A%20%20%20%20%3C/cardExpiry%3E%0A%20%20%20%20%3CcardType%3EVI%3C/cardType%3E%0A%20%20%20%20%3CcvdIndicator%3E1%3C/cvdIndicator%3E%0A%20%20%20%20%3Ccvd%3E[FILTERED]%3C/cvd%3E%0A%20%20%3C/card%3E%0A%20%20%3CbillingDetails%3E%0A%20%20%20%20%3CcardPayMethod%3EWEB%3C/cardPayMethod%3E%0A%20%20%20%20%3CfirstName%3EJim%3C/firstName%3E%0A%20%20%20%20%3ClastName%3ESmith%3C/lastName%3E%0A%20%20%20%20%3Cstreet%3E456%20My%20Street%3C/street%3E%0A%20%20%20%20%3Cstreet2%3EApt%201%3C/street2%3E%0A%20%20%20%20%3Ccity%3EOttawa%3C/city%3E%0A%20%20%20%20%3Cstate%3EON%3C/state%3E%0A%20%20%20%20%3Ccountry%3ECA%3C/country%3E%0A%20%20%20%20%3Czip%3EK1C2N6%3C/zip%3E%0A%20%20%20%20%3Cphone%3E(555)555-5555%3C/phone%3E%0A%20%20%20%20%3Cemail%3Eemail@example.com%3C/email%3E%0A%20%20%3C/billingDetails%3E%0A%20%20%3CcustomerIP%3E1.2.3.4%3C/customerIP%3E%0A%3C/ccAuthRequestV1%3E%0A" --> "HTTP/1.1 200 OK\r\n" --> "Server: WebServer32xS10i3\r\n" --> "Content-Length: 632\r\n" --> "X-ApplicationUid: GUID=610a301289c34e8254330b7edc724f5b\r\n" --> "Content-Type: application/xml\r\n" --> "Date: Mon, 12 Feb 2018 21:57:42 GMT\r\n" --> "Connection: close\r\n" --> "\r\n" -reading 632 bytes... --> "<" --> "?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ccTxnResponseV1 xmlns=\"http://www.optimalpayments.com/creditcard/xmlschema/v1\"><confirmationNumber>498871860</confirmationNumber><decision>ACCEPTED</decision><code>0</code><description>No Error</description><authCode>369231</authCode><avsResponse>X</avsResponse><cvdResponse>M</cvdResponse><detail><tag>InternalResponseCode</tag><value>0</value></detail><detail><tag>SubErrorCode</tag><value>0</value></detail><detail><tag>InternalResponseDescription</tag><value>no_error</value></detail><txnTime>2018-02-12T16:57:42.289-05:00</txnTime><duplicateFound>false</duplicateFound></ccTxnResponseV1>" -read 632 bytes -Conn close - EOS + <<~REQUEST + opening connection to webservices.test.optimalpayments.com:443... + opened + starting SSL for webservices.test.optimalpayments.com:443... + SSL established + <- "POST /creditcardWS/CreditCardServlet/v1 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: webservices.test.optimalpayments.com\r\nContent-Length: 1616\r\n\r\n" + <- "txnMode=ccPurchase&txnRequest=%3CccAuthRequestV1%20xmlns=%22http://www.optimalpayments.com/creditcard/xmlschema/v1%22%20xmlns:xsi=%22http://www.w3.org/2001/XMLSchema-instance%22%20xsi:schemaLocation=%22http://www.optimalpayments.com/creditcard/xmlschema/v1%22%3E%0A%20%20%3CmerchantAccount%3E%0A%20%20%20%20%3CaccountNum%3E1001134550%3C/accountNum%3E%0A%20%20%20%20%3CstoreID%3Etest%3C/storeID%3E%0A%20%20%20%20%3CstorePwd%3E[FILTERED]%3C/storePwd%3E%0A%20%20%3C/merchantAccount%3E%0A%20%20%3CmerchantRefNum%3E1%3C/merchantRefNum%3E%0A%20%20%3Camount%3E1.0%3C/amount%3E%0A%20%20%3Ccard%3E%0A%20%20%20%20%3CcardNum%3E[FILTERED]%3C/cardNum%3E%0A%20%20%20%20%3CcardExpiry%3E%0A%20%20%20%20%20%20%3Cmonth%3E9%3C/month%3E%0A%20%20%20%20%20%20%3Cyear%3E2019%3C/year%3E%0A%20%20%20%20%3C/cardExpiry%3E%0A%20%20%20%20%3CcardType%3EVI%3C/cardType%3E%0A%20%20%20%20%3CcvdIndicator%3E1%3C/cvdIndicator%3E%0A%20%20%20%20%3Ccvd%3E[FILTERED]%3C/cvd%3E%0A%20%20%3C/card%3E%0A%20%20%3CbillingDetails%3E%0A%20%20%20%20%3CcardPayMethod%3EWEB%3C/cardPayMethod%3E%0A%20%20%20%20%3CfirstName%3EJim%3C/firstName%3E%0A%20%20%20%20%3ClastName%3ESmith%3C/lastName%3E%0A%20%20%20%20%3Cstreet%3E456%20My%20Street%3C/street%3E%0A%20%20%20%20%3Cstreet2%3EApt%201%3C/street2%3E%0A%20%20%20%20%3Ccity%3EOttawa%3C/city%3E%0A%20%20%20%20%3Cstate%3EON%3C/state%3E%0A%20%20%20%20%3Ccountry%3ECA%3C/country%3E%0A%20%20%20%20%3Czip%3EK1C2N6%3C/zip%3E%0A%20%20%20%20%3Cphone%3E(555)555-5555%3C/phone%3E%0A%20%20%20%20%3Cemail%3Eemail@example.com%3C/email%3E%0A%20%20%3C/billingDetails%3E%0A%20%20%3CcustomerIP%3E1.2.3.4%3C/customerIP%3E%0A%3C/ccAuthRequestV1%3E%0A" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: WebServer32xS10i3\r\n" + -> "Content-Length: 632\r\n" + -> "X-ApplicationUid: GUID=610a301289c34e8254330b7edc724f5b\r\n" + -> "Content-Type: application/xml\r\n" + -> "Date: Mon, 12 Feb 2018 21:57:42 GMT\r\n" + -> "Connection: close\r\n" + -> "\r\n" + reading 632 bytes... + -> "<" + -> "?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ccTxnResponseV1 xmlns=\"http://www.optimalpayments.com/creditcard/xmlschema/v1\"><confirmationNumber>498871860</confirmationNumber><decision>ACCEPTED</decision><code>0</code><description>No Error</description><authCode>369231</authCode><avsResponse>X</avsResponse><cvdResponse>M</cvdResponse><detail><tag>InternalResponseCode</tag><value>0</value></detail><detail><tag>SubErrorCode</tag><value>0</value></detail><detail><tag>InternalResponseDescription</tag><value>no_error</value></detail><txnTime>2018-02-12T16:57:42.289-05:00</txnTime><duplicateFound>false</duplicateFound></ccTxnResponseV1>" + read 632 bytes + Conn close + REQUEST end def pre_scrubbed_double_escaped diff --git a/test/unit/gateways/orbital_avs_result_test.rb b/test/unit/gateways/orbital_avs_result_test.rb index 9596e3c7e4c..32abbbded5f 100644 --- a/test/unit/gateways/orbital_avs_result_test.rb +++ b/test/unit/gateways/orbital_avs_result_test.rb @@ -26,13 +26,13 @@ def test_empty_data end def test_response_with_orbital_avs - response = Response.new(true, 'message', {}, :avs_result => OrbitalGateway::AVSResult.new('A')) + response = Response.new(true, 'message', {}, avs_result: OrbitalGateway::AVSResult.new('A')) assert_equal 'A', response.avs_result['code'] end def test_response_with_orbital_avs_nil - response = Response.new(true, 'message', {}, :avs_result => OrbitalGateway::AVSResult.new(nil)) + response = Response.new(true, 'message', {}, avs_result: OrbitalGateway::AVSResult.new(nil)) assert response.avs_result.has_key?('code') end diff --git a/test/unit/gateways/orbital_test.rb b/test/unit/gateways/orbital_test.rb index aac02cff291..7a3f371a757 100644 --- a/test/unit/gateways/orbital_test.rb +++ b/test/unit/gateways/orbital_test.rb @@ -7,14 +7,18 @@ class OrbitalGatewayTest < Test::Unit::TestCase include CommStub def setup + @schema_version = '9.5' @gateway = ActiveMerchant::Billing::OrbitalGateway.new( - :login => 'login', - :password => 'password', - :merchant_id => 'merchant_id' + login: 'login', + password: 'password', + merchant_id: 'test12' ) @customer_ref_num = 'ABC' + @credit_card = credit_card('4556761029983886') + # Electronic Check object with test credentials of saving account + @echeck = check(account_number: '072403004', account_type: 'savings', routing_number: '072403004') - @level_2 = { + @level2 = { tax_indicator: '1', tax: '10', advice_addendum_1: 'taa1 - test', @@ -28,67 +32,483 @@ def setup city: address[:city], state: address[:state], 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' } - @options = { :order_id => '1'} + @level3 = { + freight_amount: '15', + duty_amount: '10', + dest_country: 'US', + ship_from_zip: '12345', + discount_amount: '20', + vat_tax: '25', + alt_tax: '30', + vat_rate: '7', + alt_ind: 'Y', + invoice_discount_treatment: 1, + tax_treatment: 1, + ship_vat_rate: 10, + unique_vat_invoice_ref: 'ABC123' + } + + @line_items = + [ + { + desc: 'credit card payment', + prod_cd: 'service', + qty: '30', + u_o_m: 'EAC', + tax_amt: '10', + tax_rate: '8.25', + line_tot: '20', + disc: '6', + unit_cost: '5', + gross_net: 'Y', + disc_ind: 'Y' + }, + { + desc: 'credit card payment', + prod_cd: 'service', + qty: '30', + u_o_m: 'EAC', + tax_amt: '10', + tax_rate: '8.25', + line_tot: '20', + disc: '6', + unit_cost: '5', + gross_net: 'Y', + disc_ind: 'Y' + } + ] + + @options = { + order_id: '1', + card_indicators: 'y' + } + + @options_stored_credentials = { + mit_msg_type: 'MRSB', + mit_stored_credential_ind: 'Y', + mit_submitted_transaction_id: '123456abcdef' + } + @normalized_mit_stored_credential = { + stored_credential: { + initial_transaction: false, + initiator: 'merchant', + reason_type: 'unscheduled', + network_transaction_id: 'abcdefg12345678' + } + } + @three_d_secure_options = { + three_d_secure: { + eci: '5', + xid: 'TESTXID', + cavv: 'TESTCAVV', + version: '2.2.0', + ds_transaction_id: '97267598FAE648F28083C23433990FBC' + } + } + + @three_d_secure_options_eci_6 = { + three_d_secure: { + eci: '6', + xid: 'TESTXID', + cavv: 'TESTCAVV', + version: '2.2.0', + ds_transaction_id: '97267598FAE648F28083C23433990FBC' + } + } + + @google_pay_card = network_tokenization_credit_card( + '4777777777777778', + payment_cryptogram: 'BwAQCFVQdwEAABNZI1B3EGLyGC8=', + verification_value: '987', + source: :google_pay, + brand: 'visa', + eci: '5' + ) + end + + def test_supports_network_tokenization + assert_true @gateway.supports_network_tokenization? end def test_successful_purchase @gateway.expects(:ssl_post).returns(successful_purchase_response) - assert response = @gateway.purchase(50, credit_card, :order_id => '1') + 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 + @gateway.expects(:ssl_post).returns(successful_purchase_with_echeck_response) + + assert response = @gateway.purchase(50, @echeck, order_id: '9baedc697f2cf06457de78') + assert_instance_of Response, response + assert_equal 'Approved', response.message + assert_success response + assert_equal '5F8E8BEE7299FD339A38F70CFF6E5D010EF55498;9baedc697f2cf06457de78;EC', response.authorization + end + + def test_successful_purchase_with_commercial_echeck + commercial_echeck = check(account_number: '072403004', account_type: 'checking', account_holder_type: 'business', routing_number: '072403004') + + stub_comms do + @gateway.purchase(50, commercial_echeck, order_id: '9baedc697f2cf06457de78') + end.check_request do |_endpoint, data, _headers| + assert_match %{<BankAccountType>X</BankAccountType>}, data + assert_xml_valid_to_xsd(data) + end.respond_with(successful_purchase_with_echeck_response) + end + + def test_failed_purchase_with_echeck + @gateway.expects(:ssl_post).returns(failed_echeck_for_invalid_routing_response) + + assert response = @gateway.purchase(50, @echeck, order_id: '9baedc697f2cf06457de78') + assert_instance_of Response, response + assert_failure response + assert_equal 'Invalid ECP Account Route: []. The field is missing, invalid, or it has exceeded the max length of: [9].', response.message + assert_equal '888', response.params['proc_status'] + end + + def test_successful_force_capture_with_echeck + @gateway.expects(:ssl_post).returns(successful_force_capture_with_echeck_response) + + assert response = @gateway.purchase(31, @echeck, order_id: '2', force_capture: true) + assert_instance_of Response, response + assert_match 'APPROVAL', response.message + assert_equal 'Approved and Completed', response.params['status_msg'] + assert_equal '5F8ED3D950A43BD63369845D5385B6354C3654B4;2930847bc732eb4e8102cf;EC', response.authorization + end + + def test_successful_force_capture_with_echeck_prenote + @gateway.expects(:ssl_post).returns(successful_force_capture_with_echeck_prenote_response) + + assert response = @gateway.authorize(0, @echeck, order_id: '2', force_capture: true, action_code: 'W9') + assert_instance_of Response, response + assert_match 'APPROVAL', response.message + assert_equal 'Approved and Completed', response.params['status_msg'] + assert_equal '5F8ED3D950A43BD63369845D5385B6354C3654B4;2930847bc732eb4e8102cf;EC', response.authorization + end + + def test_failed_force_capture_with_echeck_prenote + @gateway.expects(:ssl_post).returns(failed_force_capture_with_echeck_prenote_response) + + assert response = @gateway.authorize(0, @echeck, order_id: '2', force_capture: true, action_code: 'W7') + assert_instance_of Response, response + assert_failure response + assert_equal ' EWS: Invalid Action Code [W7], For Transaction Type [A].', response.message + end + + def test_level2_data + stub_comms do + @gateway.purchase(50, credit_card, @options.merge(level_2_data: @level2)) + end.check_request do |_endpoint, data, _headers| + assert_match %{<TaxInd>#{@level2[:tax_indicator].to_i}</TaxInd>}, data + assert_match %{<Tax>#{@level2[:tax].to_i}</Tax>}, data + assert_match %{<AMEXTranAdvAddn1>#{@level2[:advice_addendum_1]}</AMEXTranAdvAddn1>}, data + assert_match %{<AMEXTranAdvAddn2>#{@level2[:advice_addendum_2]}</AMEXTranAdvAddn2>}, data + assert_match %{<AMEXTranAdvAddn3>#{@level2[:advice_addendum_3]}</AMEXTranAdvAddn3>}, data + assert_match %{<AMEXTranAdvAddn4>#{@level2[:advice_addendum_4]}</AMEXTranAdvAddn4>}, data + assert_match %{<PCOrderNum>#{@level2[:purchase_order]}</PCOrderNum>}, data + assert_match %{<PCDestZip>#{@level2[:zip]}</PCDestZip>}, data + assert_match %{<PCDestName>#{@level2[:name]}</PCDestName>}, data + assert_match %{<PCDestAddress1>#{@level2[:address1]}</PCDestAddress1>}, data + assert_match %{<PCDestAddress2>#{@level2[:address2]}</PCDestAddress2>}, data + assert_match %{<PCDestCity>#{@level2[:city]}</PCDestCity>}, data + assert_match %{<PCDestState>#{@level2[:state]}</PCDestState>}, data + assert_match %{<PCardRequestorName>#{@level2[:requestor_name]}</PCardRequestorName>}, data + assert_match %{<PCardTotalTaxAmount>#{@level2[:total_tax_amount]}</PCardTotalTaxAmount>}, data + assert_match %{<PCardNationalTax>#{@level2[:national_tax]}</PCardNationalTax>}, data + assert_match %{<PCardPstTaxRegNumber>#{@level2[:pst_tax_reg_number]}</PCardPstTaxRegNumber>}, data + assert_match %{<PCardCustomerVatRegNumber>#{@level2[:customer_vat_reg_number]}</PCardCustomerVatRegNumber>}, data + assert_match %{<PCardMerchantVatRegNumber>#{@level2[:merchant_vat_reg_number]}</PCardMerchantVatRegNumber>}, data + assert_match %{<PCardCommodityCode>#{@level2[:commodity_code]}</PCardCommodityCode>}, data + assert_match %{<PCardLocalTaxRate>#{@level2[:local_tax_rate]}</PCardLocalTaxRate>}, data + assert_xml_valid_to_xsd(data) + end.respond_with(successful_purchase_response) + end + + def test_level3_data + stub_comms do + @gateway.purchase(50, credit_card, @options.merge(level_3_data: @level3)) + end.check_request do |_endpoint, data, _headers| + assert_match %{<PC3FreightAmt>#{@level3[:freight_amount].to_i}</PC3FreightAmt>}, data + assert_match %{<PC3DutyAmt>#{@level3[:duty_amount].to_i}</PC3DutyAmt>}, data + assert_match %{<PC3DestCountryCd>#{@level3[:dest_country]}</PC3DestCountryCd>}, data + assert_match %{<PC3ShipFromZip>#{@level3[:ship_from_zip].to_i}</PC3ShipFromZip>}, data + assert_match %{<PC3DiscAmt>#{@level3[:discount_amount].to_i}</PC3DiscAmt>}, data + assert_match %{<PC3VATtaxAmt>#{@level3[:vat_tax].to_i}</PC3VATtaxAmt>}, data + assert_match %{<PC3VATtaxRate>#{@level3[:vat_rate].to_i}</PC3VATtaxRate>}, data + assert_match %{<PC3AltTaxAmt>#{@level3[:alt_tax].to_i}</PC3AltTaxAmt>}, data + assert_match %{<PC3AltTaxInd>#{@level3[:alt_ind]}</PC3AltTaxInd>}, data + assert_match %{<PC3InvoiceDiscTreatment>#{@level3[:invoice_discount_treatment]}</PC3InvoiceDiscTreatment>}, data + assert_match %{<PC3TaxTreatment>#{@level3[:tax_treatment]}</PC3TaxTreatment>}, data + assert_match %{<PC3ShipVATRate>#{@level3[:ship_vat_rate]}</PC3ShipVATRate>}, data + assert_match %{<PC3UniqueVATInvoiceRefNum>#{@level3[:unique_vat_invoice_ref]}</PC3UniqueVATInvoiceRefNum>}, data + assert_xml_valid_to_xsd(data) + end.respond_with(successful_purchase_response) + end + + def test_line_items_data + stub_comms do + @gateway.purchase(50, credit_card, @options.merge(line_items: @line_items)) + end.check_request do |_endpoint, data, _headers| + assert_match %{<PC3DtlIndex>1</PC3DtlIndex>}, data + assert_match %{<PC3DtlDesc>#{@line_items[1][:desc]}</PC3DtlDesc>}, data + assert_match %{<PC3DtlProdCd>#{@line_items[1][:prod_cd]}</PC3DtlProdCd>}, data + assert_match %{<PC3DtlQty>#{@line_items[1][:qty].to_i}</PC3DtlQty>}, data + assert_match %{<PC3DtlUOM>#{@line_items[1][:u_o_m]}</PC3DtlUOM>}, data + assert_match %{<PC3DtlTaxAmt>#{@line_items[1][:tax_amt].to_i}</PC3DtlTaxAmt>}, data + assert_match %{<PC3DtlTaxRate>#{@line_items[1][:tax_rate]}</PC3DtlTaxRate>}, data + assert_match %{<PC3Dtllinetot>#{@line_items[1][:line_tot].to_i}</PC3Dtllinetot>}, data + assert_match %{<PC3DtlDisc>#{@line_items[1][:disc].to_i}</PC3DtlDisc>}, data + assert_match %{<PC3DtlUnitCost>#{@line_items[1][:unit_cost].to_i}</PC3DtlUnitCost>}, data + assert_match %{<PC3DtlGrossNet>#{@line_items[1][:gross_net]}</PC3DtlGrossNet>}, data + assert_match %{<PC3DtlDiscInd>#{@line_items[1][:disc_ind]}</PC3DtlDiscInd>}, data + assert_match %{<PC3DtlIndex>2</PC3DtlIndex>}, data + assert_xml_valid_to_xsd(data) + end.respond_with(successful_purchase_response) end - def test_level_2_data + def test_payment_action_ind_field stub_comms do - @gateway.purchase(50, credit_card, @options.merge(level_2_data: @level_2)) - end.check_request do |endpoint, data, headers| - assert_match %{<TaxInd>#{@level_2[:tax_indicator].to_i}</TaxInd>}, data - assert_match %{<Tax>#{@level_2[:tax].to_i}</Tax>}, data - assert_match %{<AMEXTranAdvAddn1>#{@level_2[:advice_addendum_1]}</AMEXTranAdvAddn1>}, data - assert_match %{<AMEXTranAdvAddn2>#{@level_2[:advice_addendum_2]}</AMEXTranAdvAddn2>}, data - assert_match %{<AMEXTranAdvAddn3>#{@level_2[:advice_addendum_3]}</AMEXTranAdvAddn3>}, data - assert_match %{<AMEXTranAdvAddn4>#{@level_2[:advice_addendum_4]}</AMEXTranAdvAddn4>}, data - assert_match %{<PCOrderNum>#{@level_2[:purchase_order]}</PCOrderNum>}, data - assert_match %{<PCDestZip>#{@level_2[:zip]}</PCDestZip>}, data - assert_match %{<PCDestName>#{@level_2[:name]}</PCDestName>}, data - assert_match %{<PCDestAddress1>#{@level_2[:address1]}</PCDestAddress1>}, data - assert_match %{<PCDestAddress2>#{@level_2[:address2]}</PCDestAddress2>}, data - assert_match %{<PCDestCity>#{@level_2[:city]}</PCDestCity>}, data - assert_match %{<PCDestState>#{@level_2[:state]}</PCDestState>}, data + @gateway.purchase(50, credit_card, @options.merge(payment_action_ind: 'P')) + end.check_request do |_endpoint, data, _headers| + assert_match %{<PaymentActionInd>P</PaymentActionInd>}, data + end.respond_with(successful_purchase_response) + end + + def test_purchase_with_secondary_url + stub_comms do + @gateway.purchase(50, credit_card, @options.merge(use_secondary_url: 'true')) + end.check_request do |endpoint, _data, _headers| + assert endpoint.include? 'orbitalvar2' end.respond_with(successful_purchase_response) end def test_network_tokenization_credit_card_data stub_comms do @gateway.purchase(50, network_tokenization_credit_card(nil, eci: '5', transaction_id: 'BwABB4JRdgAAAAAAiFF2AAAAAAA='), @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match %{<AuthenticationECIInd>5</AuthenticationECIInd>}, data assert_match %{<DPANInd>Y</DPANInd>}, data assert_match %{DigitalTokenCryptogram}, data - assert_match %{XID}, 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', + merchant_email: 'email@example' + } + ) + stub_comms do + @gateway.purchase(50, network_tokenization_credit_card(nil, eci: '5', transaction_id: 'BwABB4JRdgAAAAAAiFF2AAAAAAA='), options) + end.check_request do |_endpoint, data, _headers| + assert_xml_valid_to_xsd(data) + end.respond_with(successful_purchase_response) + end + + def test_three_d_secure_data_on_visa_purchase + stub_comms do + @gateway.purchase(50, credit_card, @options.merge(@three_d_secure_options)) + end.check_request do |_endpoint, data, _headers| + assert_match %{<AuthenticationECIInd>5</AuthenticationECIInd>}, data + assert_match %{<CAVV>TESTCAVV</CAVV>}, data + assert_match %{<XID>TESTXID</XID>}, data + assert_xml_valid_to_xsd(data) + end.respond_with(successful_purchase_response) + end + + def test_three_d_secure_data_on_visa_authorization + stub_comms do + @gateway.authorize(50, credit_card, @options.merge(@three_d_secure_options)) + end.check_request do |_endpoint, data, _headers| + assert_match %{<AuthenticationECIInd>5</AuthenticationECIInd>}, data + assert_match %{<CAVV>TESTCAVV</CAVV>}, data + assert_match %{<XID>TESTXID</XID>}, data + assert_xml_valid_to_xsd(data) + end.respond_with(successful_purchase_response) + end + + def test_three_d_secure_data_on_master_purchase + stub_comms do + @gateway.purchase(50, credit_card(nil, brand: 'master'), @options.merge(@three_d_secure_options)) + end.check_request do |_endpoint, data, _headers| + assert_match %{<AuthenticationECIInd>5</AuthenticationECIInd>}, data + assert_match %{<AAV>TESTCAVV</AAV>}, data + assert_match %{<MCProgramProtocol>2</MCProgramProtocol>}, data + assert_match %{<MCDirectoryTransID>97267598FAE648F28083C23433990FBC</MCDirectoryTransID>}, data + assert_not_match %{<UCAFInd>4</UCAFInd>}, data + assert_xml_valid_to_xsd(data) + end.respond_with(successful_purchase_response) + end + + def test_three_d_secure_data_on_master_authorization + stub_comms do + @gateway.authorize(50, credit_card(nil, brand: 'master'), @options.merge(@three_d_secure_options)) + end.check_request do |_endpoint, data, _headers| + assert_match %{<AuthenticationECIInd>5</AuthenticationECIInd>}, data + assert_match %{<AAV>TESTCAVV</AAV>}, data + assert_match %{<MCProgramProtocol>2</MCProgramProtocol>}, data + assert_match %{<MCDirectoryTransID>97267598FAE648F28083C23433990FBC</MCDirectoryTransID>}, data + assert_not_match %{<UCAFInd>4</UCAFInd>}, data + assert_xml_valid_to_xsd(data) + end.respond_with(successful_purchase_response) + end + + def test_three_d_secure_data_on_master_sca_recurring + options_local = { + three_d_secure: { + eci: '7', + xid: 'TESTXID', + cavv: 'AAAEEEDDDSSSAAA2243234', + ds_transaction_id: '97267598FAE648F28083C23433990FBC', + version: '2.2.0' + }, + sca_recurring: 'Y' + } + + stub_comms do + @gateway.authorize(50, credit_card(nil, brand: 'master'), @options.merge(options_local)) + end.check_request do |_endpoint, data, _headers| + assert_match %{<AuthenticationECIInd>7</AuthenticationECIInd>}, data + assert_match %{<AAV>AAAEEEDDDSSSAAA2243234</AAV>}, data + assert_match %{<MCProgramProtocol>2</MCProgramProtocol>}, data + assert_match %{<MCDirectoryTransID>97267598FAE648F28083C23433990FBC</MCDirectoryTransID>}, data + assert_match %{<SCARecurringPayment>Y</SCARecurringPayment>}, data + assert_not_match %{<UCAFInd>4</UCAFInd>}, data + assert_xml_valid_to_xsd(data) + end.respond_with(successful_purchase_response) + end + + def test_three_d_secure_data_on_sca_merchant_initiated_master_card + options_local = { + three_d_secure: { + eci: '7', + xid: 'TESTXID', + cavv: 'AAAEEEDDDSSSAAA2243234', + ds_transaction_id: '97267598FAE648F28083C23433990FBC', + version: '2.2.0' + }, + sca_merchant_initiated: 'Y' + } + + stub_comms do + @gateway.purchase(50, credit_card(nil, brand: 'master'), @options.merge(options_local)) + end.check_request do |_endpoint, data, _headers| + assert_match %{<AuthenticationECIInd>7</AuthenticationECIInd>}, data + assert_match %{<AAV>AAAEEEDDDSSSAAA2243234</AAV>}, data + assert_match %{<MCProgramProtocol>2</MCProgramProtocol>}, data + assert_match %{<MCDirectoryTransID>97267598FAE648F28083C23433990FBC</MCDirectoryTransID>}, data + assert_match %{<SCAMerchantInitiatedTransaction>Y</SCAMerchantInitiatedTransaction>}, data + assert_not_match %{<UCAFInd>4</UCAFInd>}, data + end.respond_with(successful_purchase_response) + end + + def test_three_d_secure_data_on_master_sca_recurring_with_invalid_eci + options_local = { + three_d_secure: { + eci: '5', + xid: 'TESTXID', + cavv: 'AAAEEEDDDSSSAAA2243234', + ds_transaction_id: '97267598FAE648F28083C23433990FBC', + version: '2.2.0' + }, + sca_recurring: 'Y' + } + + stub_comms do + @gateway.authorize(50, credit_card(nil, brand: 'master'), @options.merge(options_local)) + end.check_request do |_endpoint, data, _headers| + assert_match %{<AuthenticationECIInd>5</AuthenticationECIInd>}, data + assert_match %{<AAV>AAAEEEDDDSSSAAA2243234</AAV>}, data + assert_match %{<MCProgramProtocol>2</MCProgramProtocol>}, data + assert_match %{<MCDirectoryTransID>97267598FAE648F28083C23433990FBC</MCDirectoryTransID>}, data + assert_not_match %{<UCAFInd>4</UCAFInd>}, data + end.respond_with(successful_purchase_response) + end + + def test_three_d_secure_data_on_american_express_purchase + stub_comms do + @gateway.purchase(50, credit_card(nil, brand: 'american_express'), @options.merge(@three_d_secure_options)) + end.check_request do |_endpoint, data, _headers| + assert_match %{<AuthenticationECIInd>5</AuthenticationECIInd>}, data + assert_match %{<AEVV>TESTCAVV</AEVV>}, data + assert_match %{<PymtBrandProgramCode>ASK</PymtBrandProgramCode>}, data + end.respond_with(successful_purchase_response) + end + + def test_three_d_secure_data_on_american_express_authorization + stub_comms do + @gateway.authorize(50, credit_card(nil, brand: 'american_express'), @options.merge(@three_d_secure_options)) + end.check_request do |_endpoint, data, _headers| + assert_match %{<AuthenticationECIInd>5</AuthenticationECIInd>}, data + assert_match %{<AEVV>TESTCAVV</AEVV>}, data + assert_match %{<PymtBrandProgramCode>ASK</PymtBrandProgramCode>}, data + end.respond_with(successful_purchase_response) + end + + def test_three_d_secure_data_on_discover_purchase + stub_comms do + @gateway.purchase(50, credit_card(nil, brand: 'discover'), @options.merge(@three_d_secure_options)) + end.check_request do |_endpoint, data, _headers| + assert_match %{<AuthenticationECIInd>5</AuthenticationECIInd>}, data + assert_match %{<DigitalTokenCryptogram>TESTCAVV</DigitalTokenCryptogram>}, data + assert_match %{<PymtBrandProgramCode>DPB</PymtBrandProgramCode>}, data + end.respond_with(successful_purchase_response) + end + + def test_three_d_secure_data_on_discover_authorization + stub_comms do + @gateway.authorize(50, credit_card(nil, brand: 'discover'), @options.merge(@three_d_secure_options)) + end.check_request do |_endpoint, data, _headers| + assert_match %{<AuthenticationECIInd>5</AuthenticationECIInd>}, data + assert_match %{<DigitalTokenCryptogram>TESTCAVV</DigitalTokenCryptogram>}, data + assert_match %{<PymtBrandProgramCode>DPB</PymtBrandProgramCode>}, data + end.respond_with(successful_purchase_response) + end + + def test_supported_inr_currency + stub_comms do + @gateway.purchase(50, credit_card, order_id: '1', currency: 'INR') + end.check_request do |_endpoint, data, _headers| + assert_match %r{<CurrencyCode>356<\/CurrencyCode>}, data end.respond_with(successful_purchase_response) end def test_currency_exponents stub_comms do - @gateway.purchase(50, credit_card, :order_id => '1') - end.check_request do |endpoint, data, headers| + @gateway.purchase(50, credit_card, order_id: '1') + end.check_request do |_endpoint, data, _headers| assert_match %r{<CurrencyExponent>2<\/CurrencyExponent>}, data end.respond_with(successful_purchase_response) stub_comms do - @gateway.purchase(50, credit_card, :order_id => '1', :currency => 'CAD') - end.check_request do |endpoint, data, headers| + @gateway.purchase(50, credit_card, order_id: '1', currency: 'CAD') + end.check_request do |_endpoint, data, _headers| assert_match %r{<CurrencyExponent>2<\/CurrencyExponent>}, data end.respond_with(successful_purchase_response) stub_comms do - @gateway.purchase(50, credit_card, :order_id => '1', :currency => 'JPY') - end.check_request do |endpoint, data, headers| + @gateway.purchase(50, credit_card, order_id: '1', currency: 'JPY') + end.check_request do |_endpoint, data, _headers| assert_match %r{<CurrencyExponent>0<\/CurrencyExponent>}, data end.respond_with(successful_purchase_response) end @@ -96,7 +516,7 @@ def test_currency_exponents def test_unauthenticated_response @gateway.expects(:ssl_post).returns(failed_purchase_response) - assert response = @gateway.purchase(101, credit_card, :order_id => '1') + assert response = @gateway.purchase(101, credit_card, order_id: '1') assert_instance_of Response, response assert_failure response assert_equal 'AUTH DECLINED 12001', response.message @@ -131,14 +551,14 @@ def test_order_id_required def test_order_id_as_number @gateway.expects(:ssl_post).returns(successful_purchase_response) assert_nothing_raised do - @gateway.purchase(101, credit_card, :order_id => 1) + @gateway.purchase(101, credit_card, order_id: 1) end end def test_order_id_format response = stub_comms do - @gateway.purchase(101, credit_card, :order_id => ' #101.23,56 $Hi &thére@Friends') - end.check_request do |endpoint, data, headers| + @gateway.purchase(101, credit_card, order_id: ' #101.23,56 $Hi &thére@Friends') + end.check_request do |_endpoint, data, _headers| assert_match(/<OrderID>101-23,56 \$Hi &amp;thre@Fr<\/OrderID>/, data) end.respond_with(successful_purchase_response) assert_success response @@ -146,9 +566,25 @@ 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') - end.check_request do |endpoint, data, headers| - assert_match(/<OrderID>1001-1<\/OrderID>/, data) + @gateway.capture(101, '4A5398CF9B87744GG84A1D30F2F2321C66249416;1;VI001.1;VI', order_id: '#1001.1') + end.check_request do |_endpoint, data, _headers| + assert_match(/<OrderID>1<\/OrderID>/, data) + end.respond_with(successful_purchase_response) + assert_success response + end + + def test_numeric_merchant_id_for_caputre + gateway = ActiveMerchant::Billing::OrbitalGateway.new( + login: 'login', + password: 'password', + merchant_id: 700000123456 + ) + + response = stub_comms(gateway) do + gateway.capture(101, '4A5398CF9B87744GG84A1D30F2F2321C66249416;1;VI', @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<MerchantID>700000123456<\/MerchantID>/, data) + assert_xml_valid_to_xsd(data) end.respond_with(successful_purchase_response) assert_success response end @@ -160,8 +596,8 @@ def test_expiry_date def test_phone_number response = stub_comms do - @gateway.purchase(50, credit_card, :order_id => 1, :billing_address => address(:phone => '123-456-7890')) - end.check_request do |endpoint, data, headers| + @gateway.purchase(50, credit_card, order_id: 1, billing_address: address(phone: '123-456-7890')) + end.check_request do |_endpoint, data, _headers| assert_match(/1234567890/, data) end.respond_with(successful_purchase_response) assert_success response @@ -171,8 +607,8 @@ def test_truncates_address long_address = '1850 Treebeard Drive in Fangorn Forest by the Fields of Rohan' response = stub_comms do - @gateway.purchase(50, credit_card, :order_id => 1, :billing_address => address(:address1 => long_address)) - end.check_request do |endpoint, data, headers| + @gateway.purchase(50, credit_card, order_id: 1, billing_address: address(address1: long_address)) + end.check_request do |_endpoint, data, _headers| assert_match(/1850 Treebeard Drive/, data) assert_no_match(/Fields of Rohan/, data) end.respond_with(successful_purchase_response) @@ -180,25 +616,37 @@ 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) - end.check_request do |endpoint, data, headers| + @gateway.purchase(50, card, order_id: 1, billing_address: address) + end.check_request do |_endpoint, data, _headers| assert_match(/John Jacob/, data) assert_no_match(/Jones/, data) end.respond_with(successful_purchase_response) assert_success response end + def test_splits_first_middle_name + name_test_check = check(name: 'Jane P Doe', + account_number: '072403004', account_type: 'checking', routing_number: '072403004') + + response = stub_comms do + @gateway.purchase(50, name_test_check, order_id: 1, action_code: 'W3') + end.check_request do |_endpoint, data, _headers| + assert_match(/<EWSFirstName>Jane</, data) + assert_match(/<EWSMiddleName>P</, data) + assert_match(/<EWSLastName>Doe</, data) + end.respond_with(successful_purchase_response) + assert_success response + end + def test_truncates_city long_city = 'Friendly Village of Crooked Creek' response = stub_comms do - @gateway.purchase(50, credit_card, :order_id => 1, :billing_address => address(:city => long_city)) - end.check_request do |endpoint, data, headers| + @gateway.purchase(50, credit_card, order_id: 1, billing_address: address(city: long_city)) + end.check_request do |_endpoint, data, _headers| assert_match(/Friendly Village/, data) assert_no_match(/Creek/, data) end.respond_with(successful_purchase_response) @@ -209,19 +657,19 @@ def test_truncates_phone long_phone = '123456789012345' response = stub_comms do - @gateway.purchase(50, credit_card, :order_id => 1, :billing_address => address(:phone => long_phone)) - end.check_request do |endpoint, data, headers| + @gateway.purchase(50, credit_card, order_id: 1, billing_address: address(phone: long_phone)) + end.check_request do |_endpoint, data, _headers| assert_match(/12345678901234</, data) end.respond_with(successful_purchase_response) assert_success response end def test_truncates_zip - long_zip = '1234567890123' + long_zip = '1234567890123' response = stub_comms do - @gateway.purchase(50, credit_card, :order_id => 1, :billing_address => address(:zip => long_zip)) - end.check_request do |endpoint, data, headers| + @gateway.purchase(50, credit_card, order_id: 1, billing_address: address(zip: long_zip)) + end.check_request do |_endpoint, data, _headers| assert_match(/1234567890</, data) end.respond_with(successful_purchase_response) assert_success response @@ -229,21 +677,21 @@ def test_truncates_zip def test_address_format address_with_invalid_chars = address( - :address1 => '456% M|a^in \\S/treet', - :address2 => '|Apt. ^Num\\ber /One%', - :city => 'R^ise o\\f /th%e P|hoenix', - :state => '%O|H\\I/O', - :dest_address1 => '2/21%B |B^aker\\ St.', - :dest_address2 => 'L%u%xury S|u^i\\t/e', - :dest_city => '/Winn/i%p|e^g\\', - :dest_zip => 'A1A 2B2', - :dest_state => '^MB', + address1: '456% M|a^in \\S/treet', + address2: '|Apt. ^Num\\ber /One%', + city: 'R^ise o\\f /th%e P|hoenix', + state: '%O|H\\I/O', + dest_address1: '2/21%B |B^aker\\ St.', + dest_address2: 'L%u%xury S|u^i\\t/e', + dest_city: '/Winn/i%p|e^g\\', + dest_zip: 'A1A 2B2', + dest_state: '^MB' ) response = stub_comms do - @gateway.purchase(50, credit_card, :order_id => 1, - :billing_address => address_with_invalid_chars) - end.check_request do |endpoint, data, headers| + @gateway.purchase(50, credit_card, order_id: 1, + billing_address: address_with_invalid_chars) + end.check_request do |_endpoint, data, _headers| assert_match(/456 Main Street</, data) assert_match(/Apt. Number One</, data) assert_match(/Rise of the Phoenix</, data) @@ -252,15 +700,15 @@ def test_address_format assert_match(/Luxury Suite</, data) assert_match(/Winnipeg</, data) assert_match(/MB</, data) + assert_xml_valid_to_xsd(data) end.respond_with(successful_purchase_response) assert_success response response = stub_comms do assert_deprecation_warning do - @gateway.add_customer_profile(credit_card, - :billing_address => address_with_invalid_chars) + @gateway.add_customer_profile(credit_card, billing_address: address_with_invalid_chars) end - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/456 Main Street</, data) assert_match(/Apt. Number One</, data) assert_match(/Rise of the Phoenix</, data) @@ -269,28 +717,26 @@ def test_address_format end def test_truncates_by_byte_length - 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') long_address = address( - :address1 => '456 Stréêt Name is Really Long', - :address2 => 'Apårtmeñt 123456789012345678901', - :city => '¡Vancouver-by-the-sea!', - :state => 'ßC', - :zip => 'Postäl Cøde', - :dest_name => 'Pierré von Bürgermeister de Queso', - :dest_address1 => '9876 Stréêt Name is Really Long', - :dest_address2 => 'Apårtmeñt 987654321098765432109', - :dest_city => 'Montréal-of-the-south!', - :dest_state => 'Oñtario', - :dest_zip => 'Postäl Zïps' + address1: '456 Stréêt Name is Really Long', + address2: 'Apårtmeñt 123456789012345678901', + city: '¡Vancouver-by-the-sea!', + state: 'ßC', + zip: 'Postäl Cøde', + dest_name: 'Pierré von Bürgermeister de Queso', + dest_address1: '9876 Stréêt Name is Really Long', + dest_address2: 'Apårtmeñt 987654321098765432109', + dest_city: 'Montréal-of-the-south!', + dest_state: 'Oñtario', + dest_zip: 'Postäl Zïps' ) response = stub_comms do - @gateway.purchase(50, card, :order_id => 1, - :billing_address => long_address) - end.check_request do |endpoint, data, headers| + @gateway.purchase(50, card, order_id: 1, + billing_address: long_address) + end.check_request do |_endpoint, data, _headers| assert_match(/456 Stréêt Name is Really Lo</, data) assert_match(/Apårtmeñt 123456789012345678</, data) assert_match(/¡Vancouver-by-the-s</, data) @@ -307,10 +753,9 @@ def test_truncates_by_byte_length response = stub_comms do assert_deprecation_warning do - @gateway.add_customer_profile(credit_card, - :billing_address => long_address) + @gateway.add_customer_profile(credit_card, billing_address: long_address) end - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/456 Stréêt Name is Really Lo</, data) assert_match(/Apårtmeñt 123456789012345678</, data) assert_match(/¡Vancouver-by-the-s</, data) @@ -324,34 +769,35 @@ def test_nil_address_values_should_not_throw_exceptions @gateway.expects(:ssl_post).returns(successful_purchase_response) address_options = { - :address1 => nil, - :address2 => nil, - :city => nil, - :state => nil, - :zip => nil, - :email => nil, - :phone => nil, - :fax => nil + address1: nil, + address2: nil, + city: nil, + state: nil, + zip: nil, + email: nil, + phone: nil, + fax: nil } - response = @gateway.purchase(50, credit_card, :order_id => 1, :billing_address => address(address_options)) + response = @gateway.purchase(50, credit_card, order_id: 1, billing_address: address(address_options)) assert_success response end def test_dest_address billing_address = address( - :dest_zip => '90001', - :dest_address1 => '456 Main St.', - :dest_city => 'Somewhere', - :dest_state => 'CA', - :dest_name => 'Joan Smith', - :dest_phone => '(123) 456-7890', - :dest_country => 'US') - - response = stub_comms do - @gateway.purchase(50, credit_card, :order_id => 1, - :billing_address => billing_address) - end.check_request do |endpoint, data, headers| + dest_zip: '90001', + dest_address1: '456 Main St.', + dest_city: 'Somewhere', + dest_state: 'CA', + dest_name: 'Joan Smith', + dest_phone: '(123) 456-7890', + dest_country: 'US' + ) + + response = stub_comms do + @gateway.purchase(50, credit_card, order_id: 1, + billing_address:) + end.check_request do |_endpoint, data, _headers| assert_match(/<AVSDestzip>90001/, data) assert_match(/<AVSDestaddress1>456 Main St./, data) assert_match(/<AVSDestaddress2/, data) @@ -360,31 +806,382 @@ def test_dest_address assert_match(/<AVSDestname>Joan Smith/, data) assert_match(/<AVSDestphoneNum>1234567890/, data) assert_match(/<AVSDestcountryCode>US/, data) + assert_xml_valid_to_xsd(data) + end.respond_with(successful_purchase_response) + assert_success response + + # non-AVS country + response = stub_comms do + @gateway.purchase(50, credit_card, order_id: 1, + billing_address: billing_address.merge(dest_country: 'BR')) + end.check_request do |_endpoint, data, _headers| + assert_match(/<AVSDestcountryCode></, data) + end.respond_with(successful_purchase_response) + assert_success response + end + + def test_name_sends_for_credit_card_with_address + address = address( + dest_zip: '90001', + dest_address1: '456 Main St.', + dest_city: 'Somewhere', + dest_state: 'CA', + dest_name: 'Joan Smith', + dest_phone: '(123) 456-7890', + dest_country: 'US' + ) + + 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:) + end.check_request do |_endpoint, data, _headers| + assert_match(/John Jacob/, data) + assert_no_match(/Jones/, data) + end.respond_with(successful_purchase_response) + assert_success response + end + + def test_name_sends_for_echeck_with_address + name_test_check = check(name: 'John Jacob Jingleheimer Smith-Jones', + account_number: '072403004', account_type: 'checking', routing_number: '072403004') + + response = stub_comms do + @gateway.purchase(50, name_test_check, order_id: 1) + end.check_request do |_endpoint, data, _headers| + assert_match(/John Jacob/, data) + assert_no_match(/Jones/, data) + end.respond_with(successful_purchase_response) + assert_success response + end + + def test_name_sends_for_echeck_with_no_address + name_test_check = check(name: 'John Jacob Jingleheimer Smith-Jones', + account_number: '072403004', account_type: 'checking', routing_number: '072403004') + + response = stub_comms do + @gateway.purchase(50, name_test_check, order_id: 1, address: nil, billing_address: nil) + end.check_request do |_endpoint, data, _headers| + assert_match(/John Jacob/, data) + assert_no_match(/Jones/, data) + end.respond_with(successful_purchase_response) + assert_success response + 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') + + response = stub_comms do + @gateway.purchase(50, card, order_id: 1, address: nil, billing_address: nil) + end.check_request do |_endpoint, data, _headers| + assert_no_match(/John Jacob/, data) + end.respond_with(successful_purchase_response) + assert_success response + end + + def test_avs_name_falls_back_to_billing_address + billing_address = address( + zip: '90001', + address1: '456 Main St.', + city: 'Somewhere', + state: 'CA', + name: 'Joan Smith', + phone: '(123) 456-7890', + country: 'US' + ) + + card = credit_card('4242424242424242', first_name: nil, last_name: '') + + response = stub_comms do + @gateway.purchase(50, card, order_id: 1, billing_address:) + end.check_request do |_endpoint, data, _headers| + assert_match(/Joan Smith/, data) + end.respond_with(successful_purchase_response) + assert_success response + end + + def test_completely_blank_name + billing_address = address( + zip: '90001', + address1: '456 Main St.', + city: 'Somewhere', + state: 'CA', + name: nil, + phone: '(123) 456-7890', + country: 'US' + ) + + card = credit_card('4242424242424242', first_name: nil, last_name: nil) + + response = stub_comms do + @gateway.purchase(50, card, order_id: 1, billing_address:) + end.check_request do |_endpoint, data, _headers| + assert_match(/\<AVSname\/>\n/, data) + end.respond_with(successful_purchase_response) + assert_success response + end + + def test_successful_purchase_with_negative_stored_credentials_indicator + stub_comms do + @gateway.purchase(50, credit_card, @options.merge(mit_stored_credential_ind: 'N')) + end.check_request do |_endpoint, data, _headers| + assert_no_match(/<MITMsgType>/, data) + assert_no_match(/<MITStoredCredentialInd>/, data) + assert_xml_valid_to_xsd(data) + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_stored_credentials + stub_comms do + @gateway.purchase(50, credit_card, @options.merge(@options_stored_credentials)) + end.check_request do |_endpoint, data, _headers| + assert_match %{<MITMsgType>#{@options_stored_credentials[:mit_msg_type]}</MITMsgType>}, data + assert_match %{<MITStoredCredentialInd>#{@options_stored_credentials[:mit_stored_credential_ind]}</MITStoredCredentialInd>}, data + assert_match %{<MITSubmittedTransactionID>#{@options_stored_credentials[:mit_submitted_transaction_id]}</MITSubmittedTransactionID>}, data + assert_xml_valid_to_xsd(data) + end.respond_with(successful_purchase_response) + end + + def test_dpanind_for_rc_and_ec_transactions + stub_comms do + @gateway.purchase(50, @google_pay_card, @options.merge(industry_type: 'RC')) + end.check_request do |_endpoint, data, _headers| + assert_false data.include?('DPANInd') + end.respond_with(successful_purchase_response) + + stub_comms do + @gateway.purchase(50, @google_pay_card, @options.merge(industry_type: 'EC')) + end.check_request do |_endpoint, data, _headers| + assert data.include?('DPANInd') + end.respond_with(successful_purchase_response) + end + + def test_stored_credential_recurring_cit_initial + options = stored_credential_options(:cardholder, :recurring, :initial) + response = stub_comms do + @gateway.purchase(50, credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<MITMsgType>CSTO</, data) + assert_match(/<MITStoredCredentialInd>Y</, data) + assert_xml_valid_to_xsd(data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_stored_credential_recurring_cit_used + credit_card.verification_value = nil + options = stored_credential_options(:cardholder, :recurring, id: 'abc123') + response = stub_comms do + @gateway.purchase(50, credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<MITMsgType>CREC</, data) + assert_match(/<MITStoredCredentialInd>Y</, data) + assert_xml_valid_to_xsd(data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_stored_credential_recurring_mit_initial + options = stored_credential_options(:merchant, :recurring, :initial) + response = stub_comms do + @gateway.purchase(50, credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<MITMsgType>CSTO</, data) + assert_match(/<MITStoredCredentialInd>Y</, data) + assert_xml_valid_to_xsd(data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_stored_credential_recurring_mit_used + credit_card.verification_value = nil + options = stored_credential_options(:merchant, :recurring, id: 'abc123') + response = stub_comms do + @gateway.purchase(50, credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<MITMsgType>MREC</, data) + assert_match(/<MITStoredCredentialInd>Y</, data) + assert_match(/<MITSubmittedTransactionID>abc123</, data) + assert_xml_valid_to_xsd(data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_stored_credential_unscheduled_cit_initial + options = stored_credential_options(:cardholder, :unscheduled, :initial) + response = stub_comms do + @gateway.purchase(50, credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<MITMsgType>CSTO</, data) + assert_match(/<MITStoredCredentialInd>Y</, data) + assert_xml_valid_to_xsd(data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_stored_credential_unscheduled_cit_used + credit_card.verification_value = nil + options = stored_credential_options(:cardholder, :unscheduled, id: 'abc123') + response = stub_comms do + @gateway.purchase(50, credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<MITMsgType>CUSE</, data) + assert_match(/<MITStoredCredentialInd>Y</, data) + assert_xml_valid_to_xsd(data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_stored_credential_unscheduled_mit_initial + options = stored_credential_options(:merchant, :unscheduled, :initial) + response = stub_comms do + @gateway.purchase(50, credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<MITMsgType>CSTO</, data) + assert_match(/<MITStoredCredentialInd>Y</, data) + assert_xml_valid_to_xsd(data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_stored_credential_unscheduled_mit_used + credit_card.verification_value = nil + options = stored_credential_options(:merchant, :unscheduled, id: 'abc123') + response = stub_comms do + @gateway.purchase(50, credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<MITMsgType>MUSE</, data) + assert_match(/<MITStoredCredentialInd>Y</, data) + assert_match(/<MITSubmittedTransactionID>abc123</, data) + assert_xml_valid_to_xsd(data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_stored_credential_installment_cit_initial + options = stored_credential_options(:cardholder, :installment, :initial) + response = stub_comms do + @gateway.purchase(50, credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<MITMsgType>CSTO</, data) + assert_match(/<MITStoredCredentialInd>Y</, data) + assert_xml_valid_to_xsd(data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_stored_credential_installment_cit_used + credit_card.verification_value = nil + options = stored_credential_options(:cardholder, :installment, id: 'abc123') + response = stub_comms do + @gateway.purchase(50, credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<MITMsgType>CINS</, data) + assert_match(/<MITStoredCredentialInd>Y</, data) + assert_xml_valid_to_xsd(data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_stored_credential_installment_mit_initial + options = stored_credential_options(:merchant, :installment, :initial) + response = stub_comms do + @gateway.purchase(50, credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<MITMsgType>CSTO</, data) + assert_match(/<MITStoredCredentialInd>Y</, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_stored_credential_installment_mit_used + credit_card.verification_value = nil + options = stored_credential_options(:merchant, :installment, id: 'abc123') + response = stub_comms do + @gateway.purchase(50, credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<MITMsgType>MINS</, data) + assert_match(/<MITStoredCredentialInd>Y</, data) + assert_match(/<MITSubmittedTransactionID>abc123</, data) end.respond_with(successful_purchase_response) + assert_success response + end + + def test_successful_store_request + stub_comms do + @gateway.store(credit_card, @options) + end.check_request(skip_response: true) do |_endpoint, data, _headers| + assert_match %{<TokenTxnType>GT</TokenTxnType>}, 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 %{<CardBrand>VI</CardBrand>}, data + assert_match %{<AccountNum>2521002395820006</AccountNum>}, data + end + end - # non-AVS country - response = stub_comms do - @gateway.purchase(50, credit_card, :order_id => 1, - :billing_address => billing_address.merge(:dest_country => 'BR')) - end.check_request do |endpoint, data, headers| - assert_match(/<AVSDestcountryCode></, data) + 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)) + end.check_request do |_endpoint, data, _headers| + assert_match %{<MITMsgType>MRSB</MITMsgType>}, data + assert_match %{<MITStoredCredentialInd>Y</MITStoredCredentialInd>}, data + assert_match %{<MITSubmittedTransactionID>123456abcdef</MITSubmittedTransactionID>}, data + assert_xml_valid_to_xsd(data) end.respond_with(successful_purchase_response) - assert_success response end def test_default_managed_billing response = stub_comms do assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do assert_deprecation_warning do - @gateway.add_customer_profile(credit_card, :managed_billing => {:start_date => '10-10-2014' }) + @gateway.add_customer_profile(credit_card, managed_billing: { start_date: '10-10-2014' }) end end - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/<MBType>R/, data) assert_match(/<MBOrderIdGenerationMethod>IO/, data) assert_match(/<MBRecurringStartDate>10102014/, data) assert_match(/<MBRecurringNoEndDateFlag>N/, data) + assert_xml_valid_to_xsd(data) end.respond_with(successful_profile_response) assert_success response end @@ -393,13 +1190,18 @@ 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, :managed_billing => {:start_date => '10-10-2014', - :end_date => '10-10-2015', - :max_dollar_value => 1500, - :max_transactions => 12}) + @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| + end.check_request do |_endpoint, data, _headers| assert_match(/<MBType>R/, data) assert_match(/<MBOrderIdGenerationMethod>IO/, data) assert_match(/<MBRecurringStartDate>10102014/, data) @@ -412,8 +1214,8 @@ def test_managed_billing def test_dont_send_customer_data_by_default response = stub_comms do - @gateway.purchase(50, credit_card, :order_id => 1) - end.check_request do |endpoint, data, headers| + @gateway.purchase(50, credit_card, order_id: 1) + end.check_request do |_endpoint, data, _headers| assert_no_match(/<CustomerRefNum>K1C2N6/, data) assert_no_match(/<CustomerProfileFromOrderInd>456 My Street/, data) assert_no_match(/<CustomerProfileOrderOverrideInd>Apt 1/, data) @@ -424,8 +1226,8 @@ def test_dont_send_customer_data_by_default def test_send_customer_data_when_customer_profiles_is_enabled @gateway.options[:customer_profiles] = true response = stub_comms do - @gateway.purchase(50, credit_card, :order_id => 1) - end.check_request do |endpoint, data, headers| + @gateway.purchase(50, credit_card, order_id: 1) + end.check_request do |_endpoint, data, _headers| assert_match(/<CustomerProfileFromOrderInd>A/, data) assert_match(/<CustomerProfileOrderOverrideInd>NO/, data) end.respond_with(successful_purchase_response) @@ -435,8 +1237,8 @@ def test_send_customer_data_when_customer_profiles_is_enabled def test_send_customer_data_when_customer_ref_is_provided @gateway.options[:customer_profiles] = true response = stub_comms do - @gateway.purchase(50, credit_card, :order_id => 1, :customer_ref_num => @customer_ref_num) - end.check_request do |endpoint, data, headers| + @gateway.purchase(50, credit_card, order_id: 1, customer_ref_num: @customer_ref_num) + end.check_request do |_endpoint, data, _headers| assert_match(/<CustomerRefNum>ABC/, data) assert_match(/<CustomerProfileFromOrderInd>S/, data) assert_match(/<CustomerProfileOrderOverrideInd>NO/, data) @@ -444,11 +1246,29 @@ def test_send_customer_data_when_customer_ref_is_provided assert_success response end + def test_send_card_indicators_when_provided_purchase + response = stub_comms do + @gateway.purchase(50, credit_card, order_id: 1, card_indicators: @options[:card_indicators]) + end.check_request do |_endpoint, data, _headers| + assert_match(/<CardIndicators>y/, data) + end.respond_with(successful_purchase_response) + assert_success response + end + + def test_send_card_indicators_when_provided_authorize + response = stub_comms do + @gateway.authorize(50, credit_card, order_id: 1, card_indicators: @options[:card_indicators]) + end.check_request do |_endpoint, data, _headers| + assert_match(/<CardIndicators>y/, data) + end.respond_with(successful_purchase_response) + assert_success response + end + def test_dont_send_customer_profile_from_order_ind_for_profile_purchase @gateway.options[:customer_profiles] = true response = stub_comms do - @gateway.purchase(50, nil, :order_id => 1, :customer_ref_num => @customer_ref_num) - end.check_request do |endpoint, data, headers| + @gateway.purchase(50, nil, order_id: 1, customer_ref_num: @customer_ref_num) + end.check_request do |_endpoint, data, _headers| assert_no_match(/<CustomerProfileFromOrderInd>/, data) end.respond_with(successful_purchase_response) assert_success response @@ -457,8 +1277,8 @@ def test_dont_send_customer_profile_from_order_ind_for_profile_purchase def test_dont_send_customer_profile_from_order_ind_for_profile_authorize @gateway.options[:customer_profiles] = true response = stub_comms do - @gateway.authorize(50, nil, :order_id => 1, :customer_ref_num => @customer_ref_num) - end.check_request do |endpoint, data, headers| + @gateway.authorize(50, nil, order_id: 1, customer_ref_num: @customer_ref_num) + end.check_request do |_endpoint, data, _headers| assert_no_match(/<CustomerProfileFromOrderInd>/, data) end.respond_with(successful_purchase_response) assert_success response @@ -467,8 +1287,8 @@ def test_dont_send_customer_profile_from_order_ind_for_profile_authorize def test_currency_code_and_exponent_are_set_for_profile_purchase @gateway.options[:customer_profiles] = true response = stub_comms do - @gateway.purchase(50, nil, :order_id => 1, :customer_ref_num => @customer_ref_num) - end.check_request do |endpoint, data, headers| + @gateway.purchase(50, nil, order_id: 1, customer_ref_num: @customer_ref_num) + end.check_request do |_endpoint, data, _headers| assert_match(/<CustomerRefNum>ABC/, data) assert_match(/<CurrencyCode>124/, data) assert_match(/<CurrencyExponent>2/, data) @@ -479,8 +1299,8 @@ def test_currency_code_and_exponent_are_set_for_profile_purchase def test_currency_code_and_exponent_are_set_for_profile_authorizations @gateway.options[:customer_profiles] = true response = stub_comms do - @gateway.authorize(50, nil, :order_id => 1, :customer_ref_num => @customer_ref_num) - end.check_request do |endpoint, data, headers| + @gateway.authorize(50, nil, order_id: 1, customer_ref_num: @customer_ref_num) + end.check_request do |_endpoint, data, _headers| assert_match(/<CustomerRefNum>ABC/, data) assert_match(/<CurrencyCode>124/, data) assert_match(/<CurrencyExponent>2/, data) @@ -488,18 +1308,138 @@ def test_currency_code_and_exponent_are_set_for_profile_authorizations assert_success response end - # <AVSzip>K1C2N6</AVSzip> - # <AVSaddress1>456 My Street</AVSaddress1> - # <AVSaddress2>Apt 1</AVSaddress2> - # <AVScity>Ottawa</AVScity> - # <AVSstate>ON</AVSstate> - # <AVSphoneNum>5555555555</AVSphoneNum> - # <AVSname>Longbob Longsen</AVSname> - # <AVScountryCode>CA</AVScountryCode> + def test_successful_authorize_with_echeck + @gateway.expects(:ssl_post).returns(successful_authorize_with_echeck_response) + + assert response = @gateway.authorize(50, @echeck, order_id: '2') + assert_instance_of Response, response + assert_equal 'Approved', response.message + assert_success response + assert_equal '5F8E8D2B077217F3EF1ACD3B61610E4CD12954A3;2;EC', response.authorization + end + + def test_failed_authorize_with_echeck + @gateway.expects(:ssl_post).returns(failed_echeck_for_invalid_amount_response) + + assert response = @gateway.authorize(-1, @echeck, order_id: '9baedc697f2cf06457de78') + assert_instance_of Response, response + assert_failure response + assert_equal 'Error validating amount. Must be numeric, equal to zero or greater [-1]', response.message + assert_equal '885', response.params['proc_status'] + end + + def test_successful_refund_with_echeck + @gateway.expects(:ssl_post).returns(successful_refund_with_echeck_response) + + assert response = @gateway.refund(50, '1;2', @options) + assert_instance_of Response, response + assert_success response + assert_equal '1', response.params['approval_status'] + end + + def test_three_d_secure_data_on_master_purchase_eci_5 + options = @options.merge(@three_d_secure_options_eci_6) + options.merge!({ ucaf_collection_indicator: '5' }) + assert_equal '6', @three_d_secure_options_eci_6.dig(:three_d_secure, :eci) + + stub_comms do + @gateway.purchase(50, credit_card(nil, brand: 'master'), options) + end.check_request do |_endpoint, data, _headers| + assert_match %{<UCAFInd>5</UCAFInd>}, data + end.respond_with(successful_purchase_response) + end + + def test_three_d_secure_data_on_master_with_invalid_ucaf + stub_comms do + @gateway.purchase(50, credit_card(nil, brand: 'master'), @options.merge(@three_d_secure_options)) + end.check_request do |_endpoint, data, _headers| + assert_not_match %{<UCAFInd>4</UCAFInd>}, data + end.respond_with(successful_purchase_response) + end + + def test_three_d_secure_data_on_master_purchase_with_flag_off_and_custom_ucaf + options = @options.merge(@three_d_secure_options) + options.merge!(ucaf_collection_indicator: '5') + stub_comms do + @gateway.purchase(50, credit_card(nil, brand: 'master'), options) + end.check_request do |_endpoint, data, _headers| + assert_not_match %{<UCAFInd>5</UCAFInd>}, data + end.respond_with(successful_purchase_response) + end + + def test_failed_refund_with_echeck + @gateway.expects(:ssl_post).returns(failed_refund_with_echeck_response) + + assert response = @gateway.refund(50, '1;2', @options) + assert_instance_of Response, response + assert_failure response + assert_equal 'Refund Transactions By TxRefNum Are Only Valid When The Original Transaction Was An AUTH Or AUTH CAPTURE.', response.message + assert_equal '9806', response.params['proc_status'] + end + + def test_successful_refund + authorization = '123456789;abc987654321' + @gateway.expects(:ssl_post).returns(successful_refund_response) + + assert response = @gateway.refund(100, authorization, @options) + assert_instance_of Response, response + assert_success response + end + + def test_failed_refund + @gateway.expects(:ssl_post).returns(failed_refund_response) + + assert response = @gateway.refund(100, '', @options) + assert_instance_of Response, response + assert_failure response + end + + def test_not_approved_refund + @gateway.expects(:ssl_post).returns(not_approved_refund_response) + + assert response = @gateway.refund(100, '123;123', @options) + assert_instance_of Response, response + assert_failure response + end + + def test_successful_credit + @gateway.expects(:ssl_post).returns(successful_credit_response) + + assert response = @gateway.credit(100, @credit_card, @options) + assert_instance_of Response, response + assert_success response + assert_equal '1', response.params['approval_status'] + end + + def test_failed_credit + @gateway.expects(:ssl_post).returns(failed_credit_response) + + assert response = @gateway.credit(100, @credit_card, @options) + assert_instance_of Response, response + assert_failure response + end + + def test_not_approved_credit + @gateway.expects(:ssl_post).returns(not_approved_credit_response) + + assert response = @gateway.credit(100, @credit_card, @options) + assert_instance_of Response, response + assert_failure response + end + + def test_always_send_avs_for_echeck + response = stub_comms do + @gateway.purchase(50, @echeck, order_id: 1, address: nil, billing_address: address(country: nil)) + end.check_request do |_endpoint, data, _headers| + assert_match(/<AVSname>Jim Smith</, data) + end.respond_with(successful_purchase_response) + assert_success response + end + def test_send_address_details_for_united_states response = stub_comms do - @gateway.purchase(50, credit_card, :order_id => 1, :billing_address => address) - end.check_request do |endpoint, data, headers| + @gateway.purchase(50, credit_card, order_id: 1, billing_address: address) + end.check_request do |_endpoint, data, _headers| assert_match(/<AVSzip>K1C2N6/, data) assert_match(/<AVSaddress1>456 My Street/, data) assert_match(/<AVSaddress2>Apt 1/, data) @@ -516,8 +1456,8 @@ def test_send_address_details_for_united_states def test_dont_send_address_details_for_germany response = stub_comms do - @gateway.purchase(50, credit_card, :order_id => 1, :billing_address => address(:country => 'DE')) - end.check_request do |endpoint, data, headers| + @gateway.purchase(50, credit_card, order_id: 1, billing_address: address(country: 'DE')) + end.check_request do |_endpoint, data, _headers| assert_no_match(/<AVSzip>K1C2N6/, data) assert_no_match(/<AVSaddress1>456 My Street/, data) assert_no_match(/<AVSaddress2>Apt 1/, data) @@ -532,8 +1472,8 @@ def test_dont_send_address_details_for_germany def test_allow_sending_avs_parts_when_no_country_specified response = stub_comms do - @gateway.purchase(50, credit_card, :order_id => 1, :billing_address => address(:country => nil)) - end.check_request do |endpoint, data, headers| + @gateway.purchase(50, credit_card, order_id: 1, billing_address: address(country: nil)) + end.check_request do |_endpoint, data, _headers| assert_match(/<AVSzip>K1C2N6/, data) assert_match(/<AVSaddress1>456 My Street/, data) assert_match(/<AVSaddress2>Apt 1/, data) @@ -548,24 +1488,18 @@ def test_allow_sending_avs_parts_when_no_country_specified 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_PTI54.xsd") - doc = Nokogiri::XML(data) - xsd = Nokogiri::XML::Schema(schema_file) - assert xsd.valid?(doc), 'Request does not adhere to DTD' + @gateway.purchase(50, credit_card, order_id: 1, billing_address: address) + end.check_request do |_endpoint, data, _headers| + assert_xml_valid_to_xsd(data) end.respond_with(successful_purchase_response) assert_success response end 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_PTI54.xsd") - doc = Nokogiri::XML(data) - xsd = Nokogiri::XML::Schema(schema_file) - assert xsd.valid?(doc), 'Request does not adhere to DTD' + @gateway.purchase(50, credit_card, order_id: 1, billing_address: address(country: 'DE')) + end.check_request do |_endpoint, data, _headers| + assert_xml_valid_to_xsd(data) end.respond_with(successful_purchase_response) assert_success response end @@ -575,7 +1509,7 @@ def test_add_customer_profile assert_deprecation_warning do @gateway.add_customer_profile(credit_card) end - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/<CustomerProfileAction>C/, data) assert_match(/<CustomerName>Longbob Longsen/, data) end.respond_with(successful_profile_response) @@ -585,9 +1519,9 @@ def test_add_customer_profile def test_add_customer_profile_with_email response = stub_comms do assert_deprecation_warning do - @gateway.add_customer_profile(credit_card, { :billing_address => { :email => 'xiaobozzz@example.com' } }) + @gateway.add_customer_profile(credit_card, { billing_address: { email: 'xiaobozzz@example.com' } }) end - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/<CustomerProfileAction>C/, data) assert_match(/<CustomerEmail>xiaobozzz@example.com/, data) end.respond_with(successful_profile_response) @@ -599,7 +1533,7 @@ def test_update_customer_profile assert_deprecation_warning do @gateway.update_customer_profile(credit_card) end - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/<CustomerProfileAction>U/, data) assert_match(/<CustomerName>Longbob Longsen/, data) end.respond_with(successful_profile_response) @@ -611,7 +1545,7 @@ def test_retrieve_customer_profile assert_deprecation_warning do @gateway.retrieve_customer_profile(@customer_ref_num) end - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_no_match(/<CustomerName>Longbob Longsen/, data) assert_match(/<CustomerProfileAction>R/, data) assert_match(/<CustomerRefNum>ABC/, data) @@ -624,7 +1558,7 @@ def test_delete_customer_profile assert_deprecation_warning do @gateway.delete_customer_profile(@customer_ref_num) end - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_no_match(/<CustomerName>Longbob Longsen/, data) assert_match(/<CustomerProfileAction>D/, data) assert_match(/<CustomerRefNum>ABC/, data) @@ -636,7 +1570,7 @@ def test_attempts_seconday_url @gateway.expects(:ssl_post).with(OrbitalGateway.test_url, anything, anything).raises(ActiveMerchant::ConnectionError.new('message', nil)) @gateway.expects(:ssl_post).with(OrbitalGateway.secondary_test_url, anything, anything).returns(successful_purchase_response) - response = @gateway.purchase(50, credit_card, :order_id => '1') + response = @gateway.purchase(50, credit_card, order_id: '1') assert_success response end @@ -644,10 +1578,10 @@ def test_attempts_seconday_url def test_headers_when_retry_logic_is_enabled @gateway.options[:retry_logic] = true response = stub_comms do - @gateway.purchase(50, credit_card, :order_id => 1, :trace_number => 1) - end.check_request do |endpoint, data, headers| + @gateway.purchase(50, credit_card, order_id: 1, trace_number: 1) + end.check_request do |_endpoint, _data, headers| assert_equal('1', headers['Trace-number']) - assert_equal('merchant_id', headers['Merchant-Id']) + assert_equal('test12', headers['Merchant-Id']) end.respond_with(successful_purchase_response) assert_success response end @@ -655,19 +1589,67 @@ def test_headers_when_retry_logic_is_enabled def test_retry_logic_not_enabled @gateway.options[:retry_logic] = false response = stub_comms do - @gateway.purchase(50, credit_card, :order_id => 1, :trace_number => 1) - end.check_request do |endpoint, data, headers| + @gateway.purchase(50, credit_card, order_id: 1, trace_number: 1) + end.check_request do |_endpoint, _data, headers| + assert_equal(false, headers.has_key?('Trace-number')) + assert_equal(false, headers.has_key?('Merchant-Id')) + end.respond_with(successful_purchase_response) + assert_success response + end + + def test_headers_when_retry_logic_param_exists + response = stub_comms do + @gateway.purchase(50, credit_card, order_id: 1, retry_logic: 'true', trace_number: 1) + end.check_request do |_endpoint, _data, headers| + assert_equal('1', headers['Trace-number']) + assert_equal('test12', headers['Merchant-Id']) + end.respond_with(successful_purchase_response) + assert_success response + end + + def test_retry_logic_when_param_nonexistant + response = stub_comms do + @gateway.purchase(50, credit_card, order_id: 1, trace_number: 1) + end.check_request do |_endpoint, _data, headers| assert_equal(false, headers.has_key?('Trace-number')) assert_equal(false, headers.has_key?('Merchant-Id')) end.respond_with(successful_purchase_response) assert_success response end + def test_headers_when_trace_number_nonexistant + response = stub_comms do + @gateway.purchase(50, credit_card, order_id: 1, retry_logic: 'true') + end.check_request do |_endpoint, _data, headers| + assert_equal(nil, headers['Trace-number']) + assert_equal(nil, headers['Merchant-Id']) + end.respond_with(successful_purchase_response) + assert_success response + end + + def test_payment_delivery_when_param_correct + response = stub_comms do + @gateway.purchase(50, @echeck, order_id: 1, payment_delivery: 'A') + end.check_request do |_endpoint, data, _headers| + assert_match(/<BankPmtDelv>A/, data) + end.respond_with(successful_purchase_response) + assert_success response + end + + def test_payment_delivery_when_no_payment_delivery_param + response = stub_comms do + @gateway.purchase(50, @echeck, order_id: 1) + end.check_request do |_endpoint, data, _headers| + assert_match(/<BankPmtDelv>B/, data) + end.respond_with(successful_purchase_response) + assert_success response + end + ActiveMerchant::Billing::OrbitalGateway::APPROVED.each do |resp_code| define_method "test_approval_response_code_#{resp_code}" do @gateway.expects(:ssl_post).returns(successful_purchase_response(resp_code)) - assert response = @gateway.purchase(50, credit_card, :order_id => '1') + assert response = @gateway.purchase(50, credit_card, order_id: '1') assert_instance_of Response, response assert_success response end @@ -676,7 +1658,7 @@ def test_retry_logic_not_enabled def test_account_num_is_removed_from_response @gateway.expects(:ssl_post).returns(successful_purchase_response) - response = @gateway.purchase(50, credit_card, :order_id => '1') + response = @gateway.purchase(50, credit_card, order_id: '1') assert_instance_of Response, response assert_success response assert_nil response.params['account_num'] @@ -688,8 +1670,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 @@ -702,7 +1683,64 @@ 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 + + def test_custom_amount_on_verify + response = stub_comms do + @gateway.verify(credit_card, @options.merge({ verify_amount: '101' })) + end.check_request do |_endpoint, data, _headers| + assert_match %r{<Amount>101<\/Amount>}, data if data.include?('MessageType') + end.respond_with(successful_purchase_response) + assert_success response + end + + def test_valid_amount_with_jcb_card + @credit_card.brand = 'jcb' + stub_comms do + @gateway.verify(@credit_card, @options) + end.check_request(skip_response: true) do |_endpoint, data, _headers| + assert_match %r{<Amount>0<\/Amount>}, data + end + end + + def test_successful_verify_zero_auth_different_cards + @credit_card.brand = 'master' + response = stub_comms do + @gateway.verify(@credit_card, @options) + end.respond_with(successful_purchase_response) + assert_success response + assert_equal '4A5398CF9B87744GG84A1D30F2F2321C66249416;1;VI', response.authorization + assert_equal 'Approved', response.message + end + + def test_valid_amount_with_discover_brand + @credit_card.brand = 'discover' + stub_comms do + @gateway.verify(@credit_card, @options) + end.check_request(skip_response: true) do |_endpoint, data, _headers| + assert_match %r{<Amount>100<\/Amount>}, data + end + end + + def test_successful_verify_with_discover_brand + @credit_card.brand = 'discover' + response = stub_comms do + @gateway.verify(@credit_card, @options) + end.respond_with(successful_purchase_response, successful_void_response) + assert_success response + assert_equal '4A5398CF9B87744GG84A1D30F2F2321C66249416;1;VI', response.authorization + assert_equal 'Approved', response.message + end + + def test_successful_verify_and_failed_void_discover_brand + @credit_card.brand = 'discover' + response = stub_comms do + @gateway.verify(credit_card, @options) + end.respond_with(successful_purchase_response, failed_purchase_response) + assert_success response + assert_equal '4A5398CF9B87744GG84A1D30F2F2321C66249416;1;VI', response.authorization assert_equal 'Approved', response.message end @@ -711,7 +1749,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 @@ -723,104 +1761,305 @@ def test_failed_verify assert_equal 'AUTH DECLINED 12001', response.message end - def test_cvv_indicator_present_for_visas_with_cvvs + def test_cvv_indicator_present_for_visa_and_discovers_with_cvvs + discover = credit_card('4556761029983886', brand: 'discover') + diners_club = credit_card('4556761029983886', brand: 'diners_club') + stub_comms do @gateway.purchase(50, credit_card, @options) end.check_request do |_endpoint, data, _headers| assert_match %r{<CardSecValInd>1<\/CardSecValInd>}, data assert_match %r{<CardSecVal>123<\/CardSecVal>}, data end.respond_with(successful_purchase_response) + + stub_comms do + @gateway.purchase(50, discover, @options) + end.check_request do |_endpoint, data, _headers| + assert_match %r{<CardSecValInd>1<\/CardSecValInd>}, data + assert_match %r{<CardSecVal>123<\/CardSecVal>}, data + end.respond_with(successful_purchase_response) + + stub_comms do + @gateway.purchase(50, diners_club, @options) + end.check_request do |_endpoint, data, _headers| + assert_match %r{<CardSecValInd>1<\/CardSecValInd>}, data + assert_match %r{<CardSecVal>123<\/CardSecVal>}, data + end.respond_with(successful_purchase_response) + end + + def test_cvv_indicator_absent_for_mastercard + mastercard = credit_card('4556761029983886', brand: 'master') + + stub_comms do + @gateway.purchase(50, mastercard, @options) + end.check_request do |_endpoint, data, _headers| + assert_no_match %r{<CardSecValInd>}, data + assert_match %r{<CardSecVal>123<\/CardSecVal>}, data + end.respond_with(successful_purchase_response) end def test_cvv_indicator_absent_for_recurring stub_comms do - @gateway.purchase(50, credit_card(nil, {verification_value: nil}), @options) + @gateway.purchase(50, credit_card(nil, { verification_value: nil }), @options) end.check_request do |_endpoint, data, _headers| assert_no_match %r{<CardSecValInd>}, data assert_no_match %r{<CardSecVal>}, data end.respond_with(successful_purchase_response) end + def test_invalid_characters_in_response + @gateway.expects(:ssl_post).returns(invalid_characters_response) + + assert response = @gateway.purchase(50, credit_card, order_id: '1') + assert_instance_of Response, response + assert_failure response + assert_equal response.message, 'INV FIELD IN MSG AAAAAAAAA1[null][null][null][null][null]' + end + def test_scrub assert @gateway.supports_scrubbing? assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed end + def test_scrub_echeck + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed_echeck), post_scrubbed_echeck + end + private + def stored_credential_options(*args, id: nil) + { + order_id: '#1001', + description: 'AM test', + currency: 'GBP', + customer: '123', + stored_credential: stored_credential(*args, id:) + } + end + def successful_purchase_response(resp_code = '00') %Q{<?xml version="1.0" encoding="UTF-8"?><Response><NewOrderResp><IndustryType></IndustryType><MessageType>AC</MessageType><MerchantID>700000000000</MerchantID><TerminalID>001</TerminalID><CardBrand>VI</CardBrand><AccountNum>4111111111111111</AccountNum><OrderID>1</OrderID><TxRefNum>4A5398CF9B87744GG84A1D30F2F2321C66249416</TxRefNum><TxRefIdx>1</TxRefIdx><ProcStatus>0</ProcStatus><ApprovalStatus>1</ApprovalStatus><RespCode>#{resp_code}</RespCode><AVSRespCode>H </AVSRespCode><CVV2RespCode>N</CVV2RespCode><AuthCode>091922</AuthCode><RecurringAdviceCd></RecurringAdviceCd><CAVVRespCode></CAVVRespCode><StatusMsg>Approved</StatusMsg><RespMsg></RespMsg><HostRespCode>00</HostRespCode><HostAVSRespCode>Y</HostAVSRespCode><HostCVV2RespCode>N</HostCVV2RespCode><CustomerRefNum></CustomerRefNum><CustomerName></CustomerName><ProfileProcStatus></ProfileProcStatus><CustomerProfileMessage></CustomerProfileMessage><RespTime>144951</RespTime></NewOrderResp></Response>} end def failed_purchase_response - %q{<?xml version="1.0" encoding="UTF-8"?><Response><NewOrderResp><IndustryType></IndustryType><MessageType>AC</MessageType><MerchantID>700000000000</MerchantID><TerminalID>001</TerminalID><CardBrand>VI</CardBrand><AccountNum>4000300011112220</AccountNum><OrderID>1</OrderID><TxRefNum>4A5398CF9B87744GG84A1D30F2F2321C66249416</TxRefNum><TxRefIdx>0</TxRefIdx><ProcStatus>0</ProcStatus><ApprovalStatus>0</ApprovalStatus><RespCode>05</RespCode><AVSRespCode>G </AVSRespCode><CVV2RespCode>N</CVV2RespCode><AuthCode></AuthCode><RecurringAdviceCd></RecurringAdviceCd><CAVVRespCode></CAVVRespCode><StatusMsg>Do Not Honor</StatusMsg><RespMsg>AUTH DECLINED 12001</RespMsg><HostRespCode>05</HostRespCode><HostAVSRespCode>N</HostAVSRespCode><HostCVV2RespCode>N</HostCVV2RespCode><CustomerRefNum></CustomerRefNum><CustomerName></CustomerName><ProfileProcStatus></ProfileProcStatus><CustomerProfileMessage></CustomerProfileMessage><RespTime>150214</RespTime></NewOrderResp></Response>} + '<?xml version="1.0" encoding="UTF-8"?><Response><NewOrderResp><IndustryType></IndustryType><MessageType>AC</MessageType><MerchantID>700000000000</MerchantID><TerminalID>001</TerminalID><CardBrand>VI</CardBrand><AccountNum>4000300011112220</AccountNum><OrderID>1</OrderID><TxRefNum>4A5398CF9B87744GG84A1D30F2F2321C66249416</TxRefNum><TxRefIdx>0</TxRefIdx><ProcStatus>0</ProcStatus><ApprovalStatus>0</ApprovalStatus><RespCode>05</RespCode><AVSRespCode>G </AVSRespCode><CVV2RespCode>N</CVV2RespCode><AuthCode></AuthCode><RecurringAdviceCd></RecurringAdviceCd><CAVVRespCode></CAVVRespCode><StatusMsg>Do Not Honor</StatusMsg><RespMsg>AUTH DECLINED 12001</RespMsg><HostRespCode>05</HostRespCode><HostAVSRespCode>N</HostAVSRespCode><HostCVV2RespCode>N</HostCVV2RespCode><CustomerRefNum></CustomerRefNum><CustomerName></CustomerName><ProfileProcStatus></ProfileProcStatus><CustomerProfileMessage></CustomerProfileMessage><RespTime>150214</RespTime></NewOrderResp></Response>' end def successful_profile_response - %q{<?xml version="1.0" encoding="UTF-8"?><Response><ProfileResp><CustomerBin>000001</CustomerBin><CustomerMerchantID>700000000000</CustomerMerchantID><CustomerName>Longbob Longsen</CustomerName><CustomerRefNum>ABC</CustomerRefNum><CustomerProfileAction>CREATE</CustomerProfileAction><ProfileProcStatus>0</ProfileProcStatus><CustomerProfileMessage>Profile Request Processed</CustomerProfileMessage><CustomerAccountType>CC</CustomerAccountType><Status>A</Status><CCAccountNum>4111111111111111</CCAccountNum><RespTime/></ProfileResp></Response>} + '<?xml version="1.0" encoding="UTF-8"?><Response><ProfileResp><CustomerBin>000001</CustomerBin><CustomerMerchantID>700000000000</CustomerMerchantID><CustomerName>Longbob Longsen</CustomerName><CustomerRefNum>ABC</CustomerRefNum><CustomerProfileAction>CREATE</CustomerProfileAction><ProfileProcStatus>0</ProfileProcStatus><CustomerProfileMessage>Profile Request Processed</CustomerProfileMessage><CustomerAccountType>CC</CustomerAccountType><Status>A</Status><CCAccountNum>4111111111111111</CCAccountNum><RespTime/></ProfileResp></Response>' end def successful_void_response - %q{<?xml version="1.0" encoding="UTF-8"?><Response><ReversalResp><MerchantID>700000208761</MerchantID><TerminalID>001</TerminalID><OrderID>2</OrderID><TxRefNum>50FB1C41FEC9D016FF0BEBAD0884B174AD0853B0</TxRefNum><TxRefIdx>1</TxRefIdx><OutstandingAmt>0</OutstandingAmt><ProcStatus>0</ProcStatus><StatusMsg></StatusMsg><RespTime>01192013172049</RespTime></ReversalResp></Response>} + '<?xml version="1.0" encoding="UTF-8"?><Response><ReversalResp><MerchantID>700000208761</MerchantID><TerminalID>001</TerminalID><OrderID>2</OrderID><TxRefNum>50FB1C41FEC9D016FF0BEBAD0884B174AD0853B0</TxRefNum><TxRefIdx>1</TxRefIdx><OutstandingAmt>0</OutstandingAmt><ProcStatus>0</ProcStatus><StatusMsg></StatusMsg><RespTime>01192013172049</RespTime></ReversalResp></Response>' + end + + def successful_purchase_with_echeck_response + '<?xml version="1.0" encoding="UTF-8"?><Response><NewOrderResp><IndustryType></IndustryType><MessageType>AC</MessageType><MerchantID>041756</MerchantID><TerminalID>001</TerminalID><CardBrand>EC</CardBrand><AccountNum></AccountNum><OrderID>9baedc697f2cf06457de78</OrderID><TxRefNum>5F8E8BEE7299FD339A38F70CFF6E5D010EF55498</TxRefNum><TxRefIdx>2</TxRefIdx><ProcStatus>0</ProcStatus><ApprovalStatus>1</ApprovalStatus><RespCode>00</RespCode><AVSRespCode>3 </AVSRespCode><CVV2RespCode> </CVV2RespCode><AuthCode>123456</AuthCode><RecurringAdviceCd></RecurringAdviceCd><CAVVRespCode></CAVVRespCode><StatusMsg>Approved</StatusMsg><RespMsg></RespMsg><HostRespCode>102</HostRespCode><HostAVSRespCode> </HostAVSRespCode><HostCVV2RespCode> </HostCVV2RespCode><CustomerRefNum></CustomerRefNum><CustomerName></CustomerName><ProfileProcStatus></ProfileProcStatus><CustomerProfileMessage></CustomerProfileMessage><RespTime>030414</RespTime><PartialAuthOccurred></PartialAuthOccurred><RequestedAmount></RequestedAmount><RedeemedAmount></RedeemedAmount><RemainingBalance></RemainingBalance><CountryFraudFilterStatus></CountryFraudFilterStatus><IsoCountryCode></IsoCountryCode></NewOrderResp></Response>' + end + + def successful_force_capture_with_echeck_response + '<?xml version="1.0" encoding="UTF-8"?><Response><NewOrderResp><IndustryType></IndustryType><MessageType>FC</MessageType><MerchantID>041756</MerchantID><TerminalID>001</TerminalID><CardBrand>EC</CardBrand><AccountNum></AccountNum><OrderID>2930847bc732eb4e8102cf</OrderID><TxRefNum>5F8ED3D950A43BD63369845D5385B6354C3654B4</TxRefNum><TxRefIdx>2</TxRefIdx><ProcStatus>0</ProcStatus><ApprovalStatus>1</ApprovalStatus><RespCode>00</RespCode><AVSRespCode></AVSRespCode><CVV2RespCode></CVV2RespCode><AuthCode></AuthCode><RecurringAdviceCd></RecurringAdviceCd><CAVVRespCode></CAVVRespCode><StatusMsg>Approved and Completed</StatusMsg><RespMsg>APPROVAL </RespMsg><HostRespCode></HostRespCode><HostAVSRespCode></HostAVSRespCode><HostCVV2RespCode></HostCVV2RespCode><CustomerRefNum></CustomerRefNum><CustomerName></CustomerName><ProfileProcStatus></ProfileProcStatus><CustomerProfileMessage></CustomerProfileMessage><RespTime>081105</RespTime><PartialAuthOccurred></PartialAuthOccurred><RequestedAmount></RequestedAmount><RedeemedAmount></RedeemedAmount><RemainingBalance></RemainingBalance><CountryFraudFilterStatus></CountryFraudFilterStatus><IsoCountryCode></IsoCountryCode></NewOrderResp></Response>' + end + + def successful_force_capture_with_echeck_prenote_response + '<?xml version="1.0" encoding="UTF-8"?><Response><NewOrderResp><IndustryType></IndustryType><MessageType>FC</MessageType><MerchantID>041756</MerchantID><TerminalID>001</TerminalID><CardBrand>EC</CardBrand><AccountNum></AccountNum><OrderID>2930847bc732eb4e8102cf</OrderID><TxRefNum>5F8ED3D950A43BD63369845D5385B6354C3654B4</TxRefNum><TxRefIdx>2</TxRefIdx><ProcStatus>0</ProcStatus><ApprovalStatus>1</ApprovalStatus><RespCode>00</RespCode><AVSRespCode></AVSRespCode><CVV2RespCode></CVV2RespCode><AuthCode></AuthCode><RecurringAdviceCd></RecurringAdviceCd><CAVVRespCode></CAVVRespCode><StatusMsg>Approved and Completed</StatusMsg><RespMsg>APPROVAL </RespMsg><HostRespCode></HostRespCode><HostAVSRespCode></HostAVSRespCode><HostCVV2RespCode></HostCVV2RespCode><CustomerRefNum></CustomerRefNum><CustomerName></CustomerName><ProfileProcStatus></ProfileProcStatus><CustomerProfileMessage></CustomerProfileMessage><RespTime>081105</RespTime><PartialAuthOccurred></PartialAuthOccurred><RequestedAmount></RequestedAmount><RedeemedAmount></RedeemedAmount><RemainingBalance></RemainingBalance><CountryFraudFilterStatus></CountryFraudFilterStatus><IsoCountryCode></IsoCountryCode></NewOrderResp></Response>' + end + + def failed_force_capture_with_echeck_prenote_response + '<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response><QuickResp><ProcStatus>19784</ProcStatus><StatusMsg> EWS: Invalid Action Code [W7], For Transaction Type [A].</StatusMsg></QuickResp></Response>' + end + + def failed_echeck_for_invalid_routing_response + '<?xml version="1.0" encoding="UTF-8"?><Response><QuickResp><ProcStatus>888</ProcStatus><StatusMsg>Invalid ECP Account Route: []. The field is missing, invalid, or it has exceeded the max length of: [9].</StatusMsg></QuickResp></Response>' + end + + def failed_echeck_for_invalid_amount_response + '<?xml version="1.0" encoding="UTF-8"?><Response><QuickResp><ProcStatus>885</ProcStatus><StatusMsg>Error validating amount. Must be numeric, equal to zero or greater [-1]</StatusMsg></QuickResp></Response>' + end + + def successful_authorize_with_echeck_response + '<?xml version="1.0" encoding="UTF-8"?><Response><NewOrderResp><IndustryType></IndustryType><MessageType>A</MessageType><MerchantID>041756</MerchantID><TerminalID>001</TerminalID><CardBrand>EC</CardBrand><AccountNum></AccountNum><OrderID>2</OrderID><TxRefNum>5F8E8D2B077217F3EF1ACD3B61610E4CD12954A3</TxRefNum><TxRefIdx>0</TxRefIdx><ProcStatus>0</ProcStatus><ApprovalStatus>1</ApprovalStatus><RespCode>00</RespCode><AVSRespCode>3 </AVSRespCode><CVV2RespCode> </CVV2RespCode><AuthCode>123456</AuthCode><RecurringAdviceCd></RecurringAdviceCd><CAVVRespCode></CAVVRespCode><StatusMsg>Approved</StatusMsg><RespMsg></RespMsg><HostRespCode>102</HostRespCode><HostAVSRespCode> </HostAVSRespCode><HostCVV2RespCode> </HostCVV2RespCode><CustomerRefNum></CustomerRefNum><CustomerName></CustomerName><ProfileProcStatus></ProfileProcStatus><CustomerProfileMessage></CustomerProfileMessage><RespTime>030931</RespTime><PartialAuthOccurred></PartialAuthOccurred><RequestedAmount></RequestedAmount><RedeemedAmount></RedeemedAmount><RemainingBalance></RemainingBalance><CountryFraudFilterStatus></CountryFraudFilterStatus><IsoCountryCode></IsoCountryCode></NewOrderResp></Response>' + end + + def successful_refund_with_echeck_response + '<?xml version="1.0" encoding="UTF-8"?><Response><NewOrderResp><IndustryType></IndustryType><MessageType>R</MessageType><MerchantID>041756</MerchantID><TerminalID>001</TerminalID><CardBrand>EC</CardBrand><AccountNum>XXXXX3004</AccountNum><OrderID>b67774a1bbfe1387f5e185</OrderID><TxRefNum>5F8E8D8A542ED5CC24449BC4CECD337BE05754C2</TxRefNum><TxRefIdx>2</TxRefIdx><ProcStatus>0</ProcStatus><ApprovalStatus>1</ApprovalStatus><RespCode></RespCode><AVSRespCode></AVSRespCode><CVV2RespCode></CVV2RespCode><AuthCode></AuthCode><RecurringAdviceCd></RecurringAdviceCd><CAVVRespCode></CAVVRespCode><StatusMsg></StatusMsg><RespMsg></RespMsg><HostRespCode></HostRespCode><HostAVSRespCode></HostAVSRespCode><HostCVV2RespCode></HostCVV2RespCode><CustomerRefNum></CustomerRefNum><CustomerName></CustomerName><ProfileProcStatus></ProfileProcStatus><CustomerProfileMessage></CustomerProfileMessage><RespTime>031106</RespTime><PartialAuthOccurred></PartialAuthOccurred><RequestedAmount></RequestedAmount><RedeemedAmount></RedeemedAmount><RemainingBalance></RemainingBalance><CountryFraudFilterStatus></CountryFraudFilterStatus><IsoCountryCode></IsoCountryCode></NewOrderResp></Response>' + end + + def failed_refund_with_echeck_response + '<?xml version="1.0" encoding="UTF-8"?><Response><QuickResp><ProcStatus>9806</ProcStatus><StatusMsg>Refund Transactions By TxRefNum Are Only Valid When The Original Transaction Was An AUTH Or AUTH CAPTURE.</StatusMsg></QuickResp></Response>' + end + + def successful_refund_response + '<?xml version="1.0" encoding="UTF-8"?><Response><NewOrderResp><IndustryType></IndustryType><MessageType>R</MessageType><MerchantID>253997</MerchantID><TerminalID>001</TerminalID><CardBrand>VI</CardBrand><AccountNum>4556761029983886</AccountNum><OrderID>0c1792db5d167e0b96dd9c</OrderID><TxRefNum>60D1E12322FD50E1517A2598593A48EEA9965469</TxRefNum><TxRefIdx>2</TxRefIdx><ProcStatus>0</ProcStatus><ApprovalStatus>1</ApprovalStatus><RespCode>00</RespCode><AVSRespCode>3 </AVSRespCode><CVV2RespCode> </CVV2RespCode><AuthCode>tst743</AuthCode><RecurringAdviceCd></RecurringAdviceCd><CAVVRespCode></CAVVRespCode><StatusMsg>Approved</StatusMsg><RespMsg></RespMsg><HostRespCode>100</HostRespCode><HostAVSRespCode> </HostAVSRespCode><HostCVV2RespCode> </HostCVV2RespCode><CustomerRefNum></CustomerRefNum><CustomerName></CustomerName><ProfileProcStatus></ProfileProcStatus><CustomerProfileMessage></CustomerProfileMessage><RespTime>090955</RespTime><PartialAuthOccurred></PartialAuthOccurred><RequestedAmount></RequestedAmount><RedeemedAmount></RedeemedAmount><RemainingBalance></RemainingBalance><CountryFraudFilterStatus></CountryFraudFilterStatus><IsoCountryCode></IsoCountryCode></NewOrderResp></Response>' + end + + def successful_store_response + '<?xml version="1.0" encoding="UTF-8"?><Response><NewOrderResp><IndustryType></IndustryType><MessageType>AC</MessageType><MerchantID>492310</MerchantID><TerminalID>001</TerminalID><CardBrand>VI</CardBrand><AccountNum>4556761029983886</AccountNum><OrderID>f9269cbc7eb453d75adb1d</OrderID><TxRefNum>6536A0990C37C45D0000082B0001A64E4156534A</TxRefNum><TxRefIdx>1</TxRefIdx><ProcStatus>0</ProcStatus><ApprovalStatus>1</ApprovalStatus><RespCode>00</RespCode><AVSRespCode>7 </AVSRespCode><CVV2RespCode>M</CVV2RespCode><AuthCode>tst443</AuthCode><RecurringAdviceCd></RecurringAdviceCd><CAVVRespCode></CAVVRespCode><StatusMsg>Approved</StatusMsg><RespMsg></RespMsg><HostRespCode>100</HostRespCode><HostAVSRespCode>IU</HostAVSRespCode><HostCVV2RespCode>M</HostCVV2RespCode><CustomerRefNum></CustomerRefNum><CustomerName></CustomerName><ProfileProcStatus></ProfileProcStatus><CustomerProfileMessage></CustomerProfileMessage><RespTime>123433</RespTime><PartialAuthOccurred></PartialAuthOccurred><RequestedAmount></RequestedAmount><RedeemedAmount></RedeemedAmount><RemainingBalance></RemainingBalance><CountryFraudFilterStatus></CountryFraudFilterStatus><IsoCountryCode></IsoCountryCode><CTIPrepaidReloadableCard></CTIPrepaidReloadableCard><SafetechToken>4556761607723886</SafetechToken><PymtBrandAuthResponseCode>00</PymtBrandAuthResponseCode><PymtBrandResponseCodeCategory>A</PymtBrandResponseCodeCategory></NewOrderResp></Response>' + end + + def failed_refund_response + '<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response><QuickResp><ProcStatus>881</ProcStatus><StatusMsg>The LIDM you supplied (3F3F3F) does not match with any existing transaction</StatusMsg></QuickResp></Response>' + end + + def not_approved_refund_response + '<?xml version="1.0" encoding="UTF-8"?><Response><NewOrderResp><IndustryType></IndustryType><MessageType>R</MessageType><MerchantID>253997</MerchantID><TerminalID>001</TerminalID><CardBrand>VI</CardBrand><AccountNum>4556761029983886</AccountNum><OrderID>0c1792db5d167e0b96dd9f</OrderID><TxRefNum>60D1E12322FD50E1517A2598593A48EEA9965445</TxRefNum><TxRefIdx>2</TxRefIdx><ProcStatus>0</ProcStatus><ApprovalStatus>0</ApprovalStatus><RespCode>12</RespCode><AVSRespCode>3 </AVSRespCode><CVV2RespCode> </CVV2RespCode><AuthCode></AuthCode><RecurringAdviceCd></RecurringAdviceCd><CAVVRespCode></CAVVRespCode><StatusMsg>Invalid Transaction Type</StatusMsg><RespMsg></RespMsg><HostRespCode>606</HostRespCode><HostAVSRespCode> </HostAVSRespCode><HostCVV2RespCode> </HostCVV2RespCode><CustomerRefNum></CustomerRefNum><CustomerName></CustomerName><ProfileProcStatus></ProfileProcStatus><CustomerProfileMessage></CustomerProfileMessage><RespTime>110503</RespTime><PartialAuthOccurred></PartialAuthOccurred><RequestedAmount></RequestedAmount><RedeemedAmount></RedeemedAmount><RemainingBalance></RemainingBalance><CountryFraudFilterStatus></CountryFraudFilterStatus><IsoCountryCode></IsoCountryCode></NewOrderResp></Response>' + end + + def successful_credit_response + '<?xml version="1.0" encoding="UTF-8"?><Response><NewOrderResp><IndustryType></IndustryType><MessageType>R</MessageType><MerchantID>253997</MerchantID><TerminalID>001</TerminalID><CardBrand>MC</CardBrand><AccountNum>XXXXX5454</AccountNum><OrderID>6102f8d4ca9d5c08d6ea02</OrderID><TxRefNum>605266890AF5BA833E6190D89256B892981C531D</TxRefNum><TxRefIdx>1</TxRefIdx><ProcStatus>0</ProcStatus><ApprovalStatus>1</ApprovalStatus><RespCode>00</RespCode><AVSRespCode>3</AVSRespCode><CVV2RespCode>M</CVV2RespCode><AuthCode>tst627</AuthCode><RecurringAdviceCd></RecurringAdviceCd><CAVVRespCode></CAVVRespCode><StatusMsg>Approved</StatusMsg><RespMsg></RespMsg><HostRespCode>100</HostRespCode><HostAVSRespCode></HostAVSRespCode><HostCVV2RespCode></HostCVV2RespCode><CustomerRefNum></CustomerRefNum><CustomerName></CustomerName><ProfileProcStatus></ProfileProcStatus><CustomerProfileMessage></CustomerProfileMessage><RespTime>162857</RespTime><PartialAuthOccurred></PartialAuthOccurred><RequestedAmount></RequestedAmount><RedeemedAmount></RedeemedAmount><RemainingBalance></RemainingBalance><CountryFraudFilterStatus></CountryFraudFilterStatus><IsoCountryCode></IsoCountryCode></NewOrderResp></Response>' + end + + def failed_credit_response + '<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response><QuickResp><ProcStatus>881</ProcStatus><StatusMsg>The LIDM you supplied (3F3F3F) does not match with any existing transaction</StatusMsg></QuickResp></Response>' + end + + def not_approved_credit_response + '<?xml version="1.0" encoding="UTF-8"?><Response><NewOrderResp><IndustryType></IndustryType><MessageType>R</MessageType><MerchantID>253997</MerchantID><TerminalID>001</TerminalID><CardBrand>VI</CardBrand><AccountNum>4556761029983886</AccountNum><OrderID>0c1792db5d167e0b96dd9f</OrderID><TxRefNum>60D1E12322FD50E1517A2598593A48EEA9965445</TxRefNum><TxRefIdx>2</TxRefIdx><ProcStatus>0</ProcStatus><ApprovalStatus>0</ApprovalStatus><RespCode>12</RespCode><AVSRespCode>3 </AVSRespCode><CVV2RespCode> </CVV2RespCode><AuthCode></AuthCode><RecurringAdviceCd></RecurringAdviceCd><CAVVRespCode></CAVVRespCode><StatusMsg>Invalid Transaction Type</StatusMsg><RespMsg></RespMsg><HostRespCode>606</HostRespCode><HostAVSRespCode> </HostAVSRespCode><HostCVV2RespCode> </HostCVV2RespCode><CustomerRefNum></CustomerRefNum><CustomerName></CustomerName><ProfileProcStatus></ProfileProcStatus><CustomerProfileMessage></CustomerProfileMessage><RespTime>110503</RespTime><PartialAuthOccurred></PartialAuthOccurred><RequestedAmount></RequestedAmount><RedeemedAmount></RedeemedAmount><RemainingBalance></RemainingBalance><CountryFraudFilterStatus></CountryFraudFilterStatus><IsoCountryCode></IsoCountryCode></NewOrderResp></Response>' + end + + def invalid_characters_response + "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response><NewOrderResp><IndustryType></IndustryType><MessageType>A</MessageType><MerchantID>[FILTERED]</MerchantID><TerminalID>001</TerminalID><CardBrand>VI</CardBrand><AccountNum>[FILTERED]</AccountNum><OrderID>0c1792db5d167e0b96dd9f</OrderID><TxRefNum>60D1E12322FD50E1517A2598593A48EEA9965445</TxRefNum><TxRefIdx>2</TxRefIdx><ProcStatus>0</ProcStatus><ApprovalStatus>2</ApprovalStatus><RespCode>30</RespCode><AVSRespCode> </AVSRespCode><CVV2RespCode> </CVV2RespCode><AuthCode></AuthCode><RecurringAdviceCd></RecurringAdviceCd><CAVVRespCode></CAVVRespCode><StatusMsg>Invalid value in message</StatusMsg><RespMsg>INV FIELD IN MSG AAAAAAAAA1\x00\x00\x00\x00\x00</RespMsg><HostRespCode>30</HostRespCode><HostAVSRespCode></HostAVSRespCode><HostCVV2RespCode></HostCVV2RespCode><CustomerRefNum></CustomerRefNum><CustomerName></CustomerName><ProfileProcStatus></ProfileProcStatus><CustomerProfileMessage></CustomerProfileMessage><RespTime>105700</RespTime><PartialAuthOccurred></PartialAuthOccurred><RequestedAmount></RequestedAmount><RedeemedAmount></RedeemedAmount><RemainingBalance></RemainingBalance><CountryFraudFilterStatus></CountryFraudFilterStatus><IsoCountryCode></IsoCountryCode></NewOrderResp></Response>" end def pre_scrubbed - <<-EOS -opening connection to orbitalvar1.paymentech.net:443... -opened -starting SSL for orbitalvar1.paymentech.net:443... -SSL established -<- "POST /authorize HTTP/1.1\r\nContent-Type: application/PTI71\r\nMime-Version: 1.1\r\nContent-Transfer-Encoding: text\r\nRequest-Number: 1\r\nDocument-Type: Request\r\nInterface-Version: Ruby|ActiveMerchant|Proprietary Gateway\r\nContent-Length: 964\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: orbitalvar1.paymentech.net\r\n\r\n" -<- "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Request>\n <NewOrder>\n <OrbitalConnectionUsername>T16WAYSACT</OrbitalConnectionUsername>\n <OrbitalConnectionPassword>zbp8X1ykGZ</OrbitalConnectionPassword>\n <IndustryType>EC</IndustryType>\n <MessageType>AC</MessageType>\n <BIN>000001</BIN>\n <MerchantID>041756</MerchantID>\n <TerminalID>001</TerminalID>\n <AccountNum>4112344112344113</AccountNum>\n <Exp>0917</Exp>\n <CurrencyCode>840</CurrencyCode>\n <CurrencyExponent>2</CurrencyExponent>\n <CardSecValInd>1</CardSecValInd>\n <CardSecVal>123</CardSecVal>\n <AVSzip>K1C2N6</AVSzip>\n <AVSaddress1>456 My Street</AVSaddress1>\n <AVSaddress2>Apt 1</AVSaddress2>\n <AVScity>Ottawa</AVScity>\n <AVSstate>ON</AVSstate>\n <AVSphoneNum>5555555555</AVSphoneNum>\n <AVSname>Longbob Longsen</AVSname>\n <AVScountryCode>CA</AVScountryCode>\n <OrderID>b141cf3ce2a442732e1906</OrderID>\n <Amount>100</Amount>\n </NewOrder>\n</Request>\n" --> "HTTP/1.1 200 OK\r\n" --> "Date: Thu, 02 Jun 2016 07:04:44 GMT\r\n" --> "content-type: text/plain; charset=ISO-8859-1\r\n" --> "content-length: 1200\r\n" --> "content-transfer-encoding: text/xml\r\n" --> "document-type: Response\r\n" --> "mime-version: 1.0\r\n" --> "Connection: close\r\n" --> "\r\n" -reading 1200 bytes... --> "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response><NewOrderResp><IndustryType></IndustryType><MessageType>AC</MessageType><MerchantID>041756</MerchantID><TerminalID>001</TerminalID><CardBrand>VI</CardBrand><AccountNum>4112344112344113</AccountNum><OrderID>b141cf3ce2a442732e1906</OrderID><TxRefNum>574FDA8CECFBC3DA073FF74A7E6DE4E0BA87545B</TxRefNum><TxRefIdx>2</TxRefIdx><ProcStatus>0</ProcStatus><ApprovalStatus>1</ApprovalStatus><RespCode>00</RespCode><AVSRespCode>7 </AVSRespCode><CVV2RespCode>M</CVV2RespCode><AuthCode>tst595</AuthCode><RecurringAdviceCd></RecurringAdviceCd><CAVVRespCode></CAVVRespCode><StatusMsg>Approved</StatusMsg><RespMsg></RespMsg><HostRespCode>100</HostRespCode><HostAVSRespCode>IU</HostAVSRespCode><HostCVV2RespCode>M</HostCVV2RespCode><CustomerRefNum></CustomerRefNum><CustomerName></CustomerName><ProfileProcStatus></ProfileProcStatus><CustomerProfileMessage></CustomerProfileMessage><RespTime>030444</RespTime><PartialAuthOccurred></PartialAuthOccurred><RequestedAmount></RequestedAmount><RedeemedAmount></RedeemedAmount><RemainingBalance></RemainingBalance><CountryFraudFilterStatus></CountryFraudFilterStatus><IsoCountryCode></IsoCountryCode></NewOrderResp></Response>" -read 1200 bytes -Conn close - EOS + <<~REQUEST + opening connection to orbitalvar1.paymentech.net:443... + opened + starting SSL for orbitalvar1.paymentech.net:443... + SSL established + <- "POST /authorize HTTP/1.1\r\nContent-Type: application/PTI71\r\nMime-Version: 1.1\r\nContent-Transfer-Encoding: text\r\nRequest-Number: 1\r\nDocument-Type: Request\r\nInterface-Version: Ruby|ActiveMerchant|Proprietary Gateway\r\nContent-Length: 964\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: orbitalvar1.paymentech.net\r\n\r\n" + <- "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Request>\n <NewOrder>\n <OrbitalConnectionUsername>T16WAYSACT</OrbitalConnectionUsername>\n <OrbitalConnectionPassword>zbp8X1ykGZ</OrbitalConnectionPassword>\n <IndustryType>EC</IndustryType>\n <MessageType>AC</MessageType>\n <BIN>000001</BIN>\n <MerchantID>041756</MerchantID>\n <TerminalID>001</TerminalID>\n <AccountNum>4112344112344113</AccountNum>\n <Exp>0917</Exp>\n <CurrencyCode>840</CurrencyCode>\n <CurrencyExponent>2</CurrencyExponent>\n <CardSecValInd>1</CardSecValInd>\n <CardSecVal>123</CardSecVal>\n <AVSzip>K1C2N6</AVSzip>\n <AVSaddress1>456 My Street</AVSaddress1>\n <AVSaddress2>Apt 1</AVSaddress2>\n <AVScity>Ottawa</AVScity>\n <AVSstate>ON</AVSstate>\n <AVSphoneNum>5555555555</AVSphoneNum>\n <AVSname>Longbob Longsen</AVSname>\n <AVScountryCode>CA</AVScountryCode>\n <OrderID>b141cf3ce2a442732e1906</OrderID>\n <Amount>100</Amount>\n </NewOrder>\n</Request>\n" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Thu, 02 Jun 2016 07:04:44 GMT\r\n" + -> "content-type: text/plain; charset=ISO-8859-1\r\n" + -> "content-length: 1200\r\n" + -> "content-transfer-encoding: text/xml\r\n" + -> "document-type: Response\r\n" + -> "mime-version: 1.0\r\n" + -> "Connection: close\r\n" + -> "\r\n" + reading 1200 bytes... + -> "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response><NewOrderResp><IndustryType></IndustryType><MessageType>AC</MessageType><MerchantID>041756</MerchantID><TerminalID>001</TerminalID><CardBrand>VI</CardBrand><AccountNum>4112344112344113</AccountNum><OrderID>b141cf3ce2a442732e1906</OrderID><TxRefNum>574FDA8CECFBC3DA073FF74A7E6DE4E0BA87545B</TxRefNum><TxRefIdx>2</TxRefIdx><ProcStatus>0</ProcStatus><ApprovalStatus>1</ApprovalStatus><RespCode>00</RespCode><AVSRespCode>7 </AVSRespCode><CVV2RespCode>M</CVV2RespCode><AuthCode>tst595</AuthCode><RecurringAdviceCd></RecurringAdviceCd><CAVVRespCode></CAVVRespCode><StatusMsg>Approved</StatusMsg><RespMsg></RespMsg><HostRespCode>100</HostRespCode><HostAVSRespCode>IU</HostAVSRespCode><HostCVV2RespCode>M</HostCVV2RespCode><CustomerRefNum></CustomerRefNum><CustomerName></CustomerName><ProfileProcStatus></ProfileProcStatus><CustomerProfileMessage></CustomerProfileMessage><RespTime>030444</RespTime><PartialAuthOccurred></PartialAuthOccurred><RequestedAmount></RequestedAmount><RedeemedAmount></RedeemedAmount><RemainingBalance></RemainingBalance><CountryFraudFilterStatus></CountryFraudFilterStatus><IsoCountryCode></IsoCountryCode></NewOrderResp></Response>" + read 1200 bytes + Conn close + REQUEST end def post_scrubbed - <<-EOS -opening connection to orbitalvar1.paymentech.net:443... -opened -starting SSL for orbitalvar1.paymentech.net:443... -SSL established -<- "POST /authorize HTTP/1.1\r\nContent-Type: application/PTI71\r\nMime-Version: 1.1\r\nContent-Transfer-Encoding: text\r\nRequest-Number: 1\r\nDocument-Type: Request\r\nInterface-Version: Ruby|ActiveMerchant|Proprietary Gateway\r\nContent-Length: 964\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: orbitalvar1.paymentech.net\r\n\r\n" -<- "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Request>\n <NewOrder>\n <OrbitalConnectionUsername>[FILTERED]</OrbitalConnectionUsername>\n <OrbitalConnectionPassword>[FILTERED]</OrbitalConnectionPassword>\n <IndustryType>EC</IndustryType>\n <MessageType>AC</MessageType>\n <BIN>000001</BIN>\n <MerchantID>[FILTERED]</MerchantID>\n <TerminalID>001</TerminalID>\n <AccountNum>[FILTERED]</AccountNum>\n <Exp>0917</Exp>\n <CurrencyCode>840</CurrencyCode>\n <CurrencyExponent>2</CurrencyExponent>\n <CardSecValInd>1</CardSecValInd>\n <CardSecVal>[FILTERED]</CardSecVal>\n <AVSzip>K1C2N6</AVSzip>\n <AVSaddress1>456 My Street</AVSaddress1>\n <AVSaddress2>Apt 1</AVSaddress2>\n <AVScity>Ottawa</AVScity>\n <AVSstate>ON</AVSstate>\n <AVSphoneNum>5555555555</AVSphoneNum>\n <AVSname>Longbob Longsen</AVSname>\n <AVScountryCode>CA</AVScountryCode>\n <OrderID>b141cf3ce2a442732e1906</OrderID>\n <Amount>100</Amount>\n </NewOrder>\n</Request>\n" --> "HTTP/1.1 200 OK\r\n" --> "Date: Thu, 02 Jun 2016 07:04:44 GMT\r\n" --> "content-type: text/plain; charset=ISO-8859-1\r\n" --> "content-length: 1200\r\n" --> "content-transfer-encoding: text/xml\r\n" --> "document-type: Response\r\n" --> "mime-version: 1.0\r\n" --> "Connection: close\r\n" --> "\r\n" -reading 1200 bytes... --> "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response><NewOrderResp><IndustryType></IndustryType><MessageType>AC</MessageType><MerchantID>[FILTERED]</MerchantID><TerminalID>001</TerminalID><CardBrand>VI</CardBrand><AccountNum>[FILTERED]</AccountNum><OrderID>b141cf3ce2a442732e1906</OrderID><TxRefNum>574FDA8CECFBC3DA073FF74A7E6DE4E0BA87545B</TxRefNum><TxRefIdx>2</TxRefIdx><ProcStatus>0</ProcStatus><ApprovalStatus>1</ApprovalStatus><RespCode>00</RespCode><AVSRespCode>7 </AVSRespCode><CVV2RespCode>M</CVV2RespCode><AuthCode>tst595</AuthCode><RecurringAdviceCd></RecurringAdviceCd><CAVVRespCode></CAVVRespCode><StatusMsg>Approved</StatusMsg><RespMsg></RespMsg><HostRespCode>100</HostRespCode><HostAVSRespCode>IU</HostAVSRespCode><HostCVV2RespCode>M</HostCVV2RespCode><CustomerRefNum></CustomerRefNum><CustomerName></CustomerName><ProfileProcStatus></ProfileProcStatus><CustomerProfileMessage></CustomerProfileMessage><RespTime>030444</RespTime><PartialAuthOccurred></PartialAuthOccurred><RequestedAmount></RequestedAmount><RedeemedAmount></RedeemedAmount><RemainingBalance></RemainingBalance><CountryFraudFilterStatus></CountryFraudFilterStatus><IsoCountryCode></IsoCountryCode></NewOrderResp></Response>" -read 1200 bytes -Conn close - EOS + <<~REQUEST + opening connection to orbitalvar1.paymentech.net:443... + opened + starting SSL for orbitalvar1.paymentech.net:443... + SSL established + <- "POST /authorize HTTP/1.1\r\nContent-Type: application/PTI71\r\nMime-Version: 1.1\r\nContent-Transfer-Encoding: text\r\nRequest-Number: 1\r\nDocument-Type: Request\r\nInterface-Version: Ruby|ActiveMerchant|Proprietary Gateway\r\nContent-Length: 964\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: orbitalvar1.paymentech.net\r\n\r\n" + <- "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Request>\n <NewOrder>\n <OrbitalConnectionUsername>[FILTERED]</OrbitalConnectionUsername>\n <OrbitalConnectionPassword>[FILTERED]</OrbitalConnectionPassword>\n <IndustryType>EC</IndustryType>\n <MessageType>AC</MessageType>\n <BIN>000001</BIN>\n <MerchantID>[FILTERED]</MerchantID>\n <TerminalID>001</TerminalID>\n <AccountNum>[FILTERED]</AccountNum>\n <Exp>0917</Exp>\n <CurrencyCode>840</CurrencyCode>\n <CurrencyExponent>2</CurrencyExponent>\n <CardSecValInd>1</CardSecValInd>\n <CardSecVal>[FILTERED]</CardSecVal>\n <AVSzip>K1C2N6</AVSzip>\n <AVSaddress1>456 My Street</AVSaddress1>\n <AVSaddress2>Apt 1</AVSaddress2>\n <AVScity>Ottawa</AVScity>\n <AVSstate>ON</AVSstate>\n <AVSphoneNum>5555555555</AVSphoneNum>\n <AVSname>Longbob Longsen</AVSname>\n <AVScountryCode>CA</AVScountryCode>\n <OrderID>b141cf3ce2a442732e1906</OrderID>\n <Amount>100</Amount>\n </NewOrder>\n</Request>\n" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Thu, 02 Jun 2016 07:04:44 GMT\r\n" + -> "content-type: text/plain; charset=ISO-8859-1\r\n" + -> "content-length: 1200\r\n" + -> "content-transfer-encoding: text/xml\r\n" + -> "document-type: Response\r\n" + -> "mime-version: 1.0\r\n" + -> "Connection: close\r\n" + -> "\r\n" + reading 1200 bytes... + -> "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response><NewOrderResp><IndustryType></IndustryType><MessageType>AC</MessageType><MerchantID>[FILTERED]</MerchantID><TerminalID>001</TerminalID><CardBrand>VI</CardBrand><AccountNum>[FILTERED]</AccountNum><OrderID>b141cf3ce2a442732e1906</OrderID><TxRefNum>574FDA8CECFBC3DA073FF74A7E6DE4E0BA87545B</TxRefNum><TxRefIdx>2</TxRefIdx><ProcStatus>0</ProcStatus><ApprovalStatus>1</ApprovalStatus><RespCode>00</RespCode><AVSRespCode>7 </AVSRespCode><CVV2RespCode>M</CVV2RespCode><AuthCode>tst595</AuthCode><RecurringAdviceCd></RecurringAdviceCd><CAVVRespCode></CAVVRespCode><StatusMsg>Approved</StatusMsg><RespMsg></RespMsg><HostRespCode>100</HostRespCode><HostAVSRespCode>IU</HostAVSRespCode><HostCVV2RespCode>M</HostCVV2RespCode><CustomerRefNum></CustomerRefNum><CustomerName></CustomerName><ProfileProcStatus></ProfileProcStatus><CustomerProfileMessage></CustomerProfileMessage><RespTime>030444</RespTime><PartialAuthOccurred></PartialAuthOccurred><RequestedAmount></RequestedAmount><RedeemedAmount></RedeemedAmount><RemainingBalance></RemainingBalance><CountryFraudFilterStatus></CountryFraudFilterStatus><IsoCountryCode></IsoCountryCode></NewOrderResp></Response>" + read 1200 bytes + Conn close + REQUEST end def pre_scrubbed_profile - <<-EOS -<?xml version="1.0" encoding="UTF-8"?><Response><ProfileResp><CustomerBin>000001</CustomerBin><CustomerMerchantID>253997</CustomerMerchantID><CustomerName>LONGBOB LONGSEN</CustomerName><CustomerRefNum>109273631</CustomerRefNum><CustomerProfileAction>CREATE</CustomerProfileAction><ProfileProcStatus>0</ProfileProcStatus><CustomerProfileMessage>Profile Request Processed</CustomerProfileMessage><CustomerAddress1>456 MY STREET</CustomerAddress1><CustomerAddress2>APT 1</CustomerAddress2><CustomerCity>OTTAWA</CustomerCity><CustomerState>ON</CustomerState><CustomerZIP>K1C2N6</CustomerZIP><CustomerEmail></CustomerEmail><CustomerPhone>5555555555</CustomerPhone><CustomerCountryCode>CA</CustomerCountryCode><CustomerProfileOrderOverrideInd>NO</CustomerProfileOrderOverrideInd><OrderDefaultDescription></OrderDefaultDescription><OrderDefaultAmount></OrderDefaultAmount><CustomerAccountType>CC</CustomerAccountType><Status>A</Status><CCAccountNum>4112344112344113</CCAccountNum><CCExpireDate>0919</CCExpireDate><ECPAccountDDA></ECPAccountDDA><ECPAccountType></ECPAccountType><ECPAccountRT></ECPAccountRT><ECPBankPmtDlv></ECPBankPmtDlv><SwitchSoloStartDate></SwitchSoloStartDate><SwitchSoloIssueNum></SwitchSoloIssueNum><RespTime></RespTime></ProfileResp></Response> - EOS + <<~REQUEST + <?xml version="1.0" encoding="UTF-8"?><Response><ProfileResp><CustomerBin>000001</CustomerBin><CustomerMerchantID>253997</CustomerMerchantID><CustomerName>LONGBOB LONGSEN</CustomerName><CustomerRefNum>109273631</CustomerRefNum><CustomerProfileAction>CREATE</CustomerProfileAction><ProfileProcStatus>0</ProfileProcStatus><CustomerProfileMessage>Profile Request Processed</CustomerProfileMessage><CustomerAddress1>456 MY STREET</CustomerAddress1><CustomerAddress2>APT 1</CustomerAddress2><CustomerCity>OTTAWA</CustomerCity><CustomerState>ON</CustomerState><CustomerZIP>K1C2N6</CustomerZIP><CustomerEmail></CustomerEmail><CustomerPhone>5555555555</CustomerPhone><CustomerCountryCode>CA</CustomerCountryCode><CustomerProfileOrderOverrideInd>NO</CustomerProfileOrderOverrideInd><OrderDefaultDescription></OrderDefaultDescription><OrderDefaultAmount></OrderDefaultAmount><CustomerAccountType>CC</CustomerAccountType><Status>A</Status><CCAccountNum>4112344112344113</CCAccountNum><CCExpireDate>0919</CCExpireDate><ECPAccountDDA></ECPAccountDDA><ECPAccountType></ECPAccountType><ECPAccountRT></ECPAccountRT><ECPBankPmtDlv></ECPBankPmtDlv><SwitchSoloStartDate></SwitchSoloStartDate><SwitchSoloIssueNum></SwitchSoloIssueNum><RespTime></RespTime></ProfileResp></Response> + REQUEST end def post_scrubbed_profile - <<-EOS -<?xml version="1.0" encoding="UTF-8"?><Response><ProfileResp><CustomerBin>000001</CustomerBin><CustomerMerchantID>[FILTERED]</CustomerMerchantID><CustomerName>LONGBOB LONGSEN</CustomerName><CustomerRefNum>109273631</CustomerRefNum><CustomerProfileAction>CREATE</CustomerProfileAction><ProfileProcStatus>0</ProfileProcStatus><CustomerProfileMessage>Profile Request Processed</CustomerProfileMessage><CustomerAddress1>456 MY STREET</CustomerAddress1><CustomerAddress2>APT 1</CustomerAddress2><CustomerCity>OTTAWA</CustomerCity><CustomerState>ON</CustomerState><CustomerZIP>K1C2N6</CustomerZIP><CustomerEmail></CustomerEmail><CustomerPhone>5555555555</CustomerPhone><CustomerCountryCode>CA</CustomerCountryCode><CustomerProfileOrderOverrideInd>NO</CustomerProfileOrderOverrideInd><OrderDefaultDescription></OrderDefaultDescription><OrderDefaultAmount></OrderDefaultAmount><CustomerAccountType>CC</CustomerAccountType><Status>A</Status><CCAccountNum>[FILTERED]</CCAccountNum><CCExpireDate>0919</CCExpireDate><ECPAccountDDA></ECPAccountDDA><ECPAccountType></ECPAccountType><ECPAccountRT></ECPAccountRT><ECPBankPmtDlv></ECPBankPmtDlv><SwitchSoloStartDate></SwitchSoloStartDate><SwitchSoloIssueNum></SwitchSoloIssueNum><RespTime></RespTime></ProfileResp></Response> - EOS + <<~REQUEST + <?xml version="1.0" encoding="UTF-8"?><Response><ProfileResp><CustomerBin>000001</CustomerBin><CustomerMerchantID>[FILTERED]</CustomerMerchantID><CustomerName>LONGBOB LONGSEN</CustomerName><CustomerRefNum>109273631</CustomerRefNum><CustomerProfileAction>CREATE</CustomerProfileAction><ProfileProcStatus>0</ProfileProcStatus><CustomerProfileMessage>Profile Request Processed</CustomerProfileMessage><CustomerAddress1>456 MY STREET</CustomerAddress1><CustomerAddress2>APT 1</CustomerAddress2><CustomerCity>OTTAWA</CustomerCity><CustomerState>ON</CustomerState><CustomerZIP>K1C2N6</CustomerZIP><CustomerEmail></CustomerEmail><CustomerPhone>5555555555</CustomerPhone><CustomerCountryCode>CA</CustomerCountryCode><CustomerProfileOrderOverrideInd>NO</CustomerProfileOrderOverrideInd><OrderDefaultDescription></OrderDefaultDescription><OrderDefaultAmount></OrderDefaultAmount><CustomerAccountType>CC</CustomerAccountType><Status>A</Status><CCAccountNum>[FILTERED]</CCAccountNum><CCExpireDate>0919</CCExpireDate><ECPAccountDDA></ECPAccountDDA><ECPAccountType></ECPAccountType><ECPAccountRT></ECPAccountRT><ECPBankPmtDlv></ECPBankPmtDlv><SwitchSoloStartDate></SwitchSoloStartDate><SwitchSoloIssueNum></SwitchSoloIssueNum><RespTime></RespTime></ProfileResp></Response> + REQUEST + end + + def pre_scrubbed_echeck + <<~REQUEST + opening connection to orbitalvar1.chasepaymentech.com:443... + opened + starting SSL for orbitalvar1.chasepaymentech.com:443... + SSL established, protocol: TLSv1.2, cipher: AES128-GCM-SHA256 + <- "POST /authorize HTTP/1.1\r\nContent-Type: application/PTI81\r\nMime-Version: 1.1\r\nContent-Transfer-Encoding: text\r\nRequest-Number: 1\r\nDocument-Type: Request\r\nInterface-Version: Ruby|ActiveMerchant|Proprietary Gateway\r\nContent-Length: 999\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: orbitalvar1.chasepaymentech.com\r\n\r\n" + <- "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Request>\n <NewOrder>\n <OrbitalConnectionUsername>SPREEDLYTEST1</OrbitalConnectionUsername>\n <OrbitalConnectionPassword>2NnPnYZylV8ft</OrbitalConnectionPassword>\n <IndustryType>EC</IndustryType>\n <MessageType>AC</MessageType>\n <BIN>000001</BIN>\n <MerchantID>408449</MerchantID>\n <TerminalID>001</TerminalID>\n <CardBrand>EC</CardBrand>\n <CurrencyCode>124</CurrencyCode>\n <CurrencyExponent>2</CurrencyExponent>\n <BCRtNum>072403004</BCRtNum>\n <CheckDDA>072403004</CheckDDA>\n <BankAccountType>S</BankAccountType>\n <BankPmtDelv>B</BankPmtDelv>\n <AVSzip>K1C2N6</AVSzip>\n <AVSaddress1>456 My Street</AVSaddress1>\n <AVSaddress2>Apt 1</AVSaddress2>\n <AVScity>Ottawa</AVScity>\n <AVSstate>ON</AVSstate>\n <AVSphoneNum>5555555555</AVSphoneNum>\n <AVSname>Jim Smith</AVSname>\n <AVScountryCode>CA</AVScountryCode>\n <OrderID>5fe1bb6dcd2cd401f2a277</OrderID>\n <Amount>20</Amount>\n </NewOrder>\n</Request>\n" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Mon, 09 Aug 2021 21:00:41 GMT\r\n" + -> "Strict-Transport-Security: max-age=63072000; includeSubdomains; preload\r\n" + -> "X-Frame-Options: SAMEORIGIN\r\n" + -> "Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE, PUT\r\n" + -> "Access-Control-Max-Age: 1000\r\n" + -> "Access-Control-Allow-Headers: x-requested-with, Content-Type, origin, authorization, accept, client-security-token, MerchantID, OrbitalConnectionUsername, OrbitalConnectionPassword\r\n" + -> "Access-Control-Allow-credentials: true\r\n" + -> "content-type: text/plain; charset=ISO-8859-1\r\n" + -> "content-length: 1185\r\n" + -> "content-transfer-encoding: text/xml\r\n" + -> "document-type: Response\r\n" + -> "mime-version: 1.0\r\n" + -> "X-Frame-Options: SAMEORIGIN\r\n" + -> "Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\r\n" + -> "X-XSS-Protection: 1; mode=block\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "Connection: close\r\n" + -> "\r\n" + reading 1185 bytes... + -> "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response><NewOrderResp><IndustryType></IndustryType><MessageType>AC</MessageType><MerchantID>408449</MerchantID><TerminalID>001</TerminalID><CardBrand>EC</CardBrand><AccountNum></AccountNum><OrderID>5fe1bb6dcd2cd401f2a277</OrderID><TxRefNum>611197795A217041FDC714407285C2FC74F9533B</TxRefNum><TxRefIdx>1</TxRefIdx><ProcStatus>0</ProcStatus><ApprovalStatus>1</ApprovalStatus><RespCode>00</RespCode><AVSRespCode>3 </AVSRespCode><CVV2RespCode> </CVV2RespCode><AuthCode>123456</AuthCode><RecurringAdviceCd></RecurringAdviceCd><CAVVRespC" + -> "ode></CAVVRespCode><StatusMsg>Approved</StatusMsg><RespMsg></RespMsg><HostRespCode>102</HostRespCode><HostAVSRespCode> </HostAVSRespCode><HostCVV2RespCode> </HostCVV2RespCode><CustomerRefNum></CustomerRefNum><CustomerName></CustomerName><ProfileProcStatus></ProfileProcStatus><CustomerProfileMessage></CustomerProfileMessage><RespTime>170041</RespTime><PartialAuthOccurred></PartialAuthOccurred><RequestedAmount></RequestedAmount><RedeemedAmount></RedeemedAmount><RemainingBalance></RemainingBalance><CountryFraudFilterStatus></CountryFraudFilterStatus><IsoCountryCode></IsoCountryCode></NewOrderResp></Response>" + read 1185 bytes + Conn close + REQUEST + end + + def post_scrubbed_echeck + <<~REQUEST + opening connection to orbitalvar1.chasepaymentech.com:443... + opened + starting SSL for orbitalvar1.chasepaymentech.com:443... + SSL established, protocol: TLSv1.2, cipher: AES128-GCM-SHA256 + <- "POST /authorize HTTP/1.1\r\nContent-Type: application/PTI81\r\nMime-Version: 1.1\r\nContent-Transfer-Encoding: text\r\nRequest-Number: 1\r\nDocument-Type: Request\r\nInterface-Version: Ruby|ActiveMerchant|Proprietary Gateway\r\nContent-Length: 999\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: orbitalvar1.chasepaymentech.com\r\n\r\n" + <- "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Request>\n <NewOrder>\n <OrbitalConnectionUsername>[FILTERED]</OrbitalConnectionUsername>\n <OrbitalConnectionPassword>[FILTERED]</OrbitalConnectionPassword>\n <IndustryType>EC</IndustryType>\n <MessageType>AC</MessageType>\n <BIN>000001</BIN>\n <MerchantID>[FILTERED]</MerchantID>\n <TerminalID>001</TerminalID>\n <CardBrand>EC</CardBrand>\n <CurrencyCode>124</CurrencyCode>\n <CurrencyExponent>2</CurrencyExponent>\n <BCRtNum>[FILTERED]</BCRtNum>\n <CheckDDA>[FILTERED]</CheckDDA>\n <BankAccountType>S</BankAccountType>\n <BankPmtDelv>B</BankPmtDelv>\n <AVSzip>K1C2N6</AVSzip>\n <AVSaddress1>456 My Street</AVSaddress1>\n <AVSaddress2>Apt 1</AVSaddress2>\n <AVScity>Ottawa</AVScity>\n <AVSstate>ON</AVSstate>\n <AVSphoneNum>5555555555</AVSphoneNum>\n <AVSname>Jim Smith</AVSname>\n <AVScountryCode>CA</AVScountryCode>\n <OrderID>5fe1bb6dcd2cd401f2a277</OrderID>\n <Amount>20</Amount>\n </NewOrder>\n</Request>\n" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Mon, 09 Aug 2021 21:00:41 GMT\r\n" + -> "Strict-Transport-Security: max-age=63072000; includeSubdomains; preload\r\n" + -> "X-Frame-Options: SAMEORIGIN\r\n" + -> "Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE, PUT\r\n" + -> "Access-Control-Max-Age: 1000\r\n" + -> "Access-Control-Allow-Headers: x-requested-with, Content-Type, origin, authorization, accept, client-security-token, MerchantID, OrbitalConnectionUsername, OrbitalConnectionPassword\r\n" + -> "Access-Control-Allow-credentials: true\r\n" + -> "content-type: text/plain; charset=ISO-8859-1\r\n" + -> "content-length: 1185\r\n" + -> "content-transfer-encoding: text/xml\r\n" + -> "document-type: Response\r\n" + -> "mime-version: 1.0\r\n" + -> "X-Frame-Options: SAMEORIGIN\r\n" + -> "Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\r\n" + -> "X-XSS-Protection: 1; mode=block\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "Connection: close\r\n" + -> "\r\n" + reading 1185 bytes... + -> "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response><NewOrderResp><IndustryType></IndustryType><MessageType>AC</MessageType><MerchantID>[FILTERED]</MerchantID><TerminalID>001</TerminalID><CardBrand>EC</CardBrand><AccountNum></AccountNum><OrderID>5fe1bb6dcd2cd401f2a277</OrderID><TxRefNum>611197795A217041FDC714407285C2FC74F9533B</TxRefNum><TxRefIdx>1</TxRefIdx><ProcStatus>0</ProcStatus><ApprovalStatus>1</ApprovalStatus><RespCode>00</RespCode><AVSRespCode>3 </AVSRespCode><CVV2RespCode> </CVV2RespCode><AuthCode>123456</AuthCode><RecurringAdviceCd></RecurringAdviceCd><CAVVRespC" + -> "ode></CAVVRespCode><StatusMsg>Approved</StatusMsg><RespMsg></RespMsg><HostRespCode>102</HostRespCode><HostAVSRespCode> </HostAVSRespCode><HostCVV2RespCode> </HostCVV2RespCode><CustomerRefNum></CustomerRefNum><CustomerName></CustomerName><ProfileProcStatus></ProfileProcStatus><CustomerProfileMessage></CustomerProfileMessage><RespTime>170041</RespTime><PartialAuthOccurred></PartialAuthOccurred><RequestedAmount></RequestedAmount><RedeemedAmount></RedeemedAmount><RemainingBalance></RemainingBalance><CountryFraudFilterStatus></CountryFraudFilterStatus><IsoCountryCode></IsoCountryCode></NewOrderResp></Response>" + read 1185 bytes + 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 + assert_equal @schema_version, @gateway.class::API_VERSION + @schema_file ||= File.read("#{File.dirname(__FILE__)}/../../schema/orbital/Request_PTI#{@schema_version.delete('.')}.xsd") end end diff --git a/test/unit/gateways/pac_net_raven_test.rb b/test/unit/gateways/pac_net_raven_test.rb index 29d99cb9e84..d206b211448 100644 --- a/test/unit/gateways/pac_net_raven_test.rb +++ b/test/unit/gateways/pac_net_raven_test.rb @@ -157,30 +157,30 @@ def test_failed_void end def test_argument_error_prn - exception = assert_raises(ArgumentError){ - PacNetRavenGateway.new(:user => 'user', :secret => 'secret') + exception = assert_raises(ArgumentError) { + PacNetRavenGateway.new(user: 'user', secret: 'secret') } assert_equal 'Missing required parameter: prn', exception.message end def test_argument_error_user - exception = assert_raises(ArgumentError){ - PacNetRavenGateway.new(:secret => 'secret', :prn => 123456) + exception = assert_raises(ArgumentError) { + PacNetRavenGateway.new(secret: 'secret', prn: 123456) } assert_equal 'Missing required parameter: user', exception.message end def test_argument_error_secret - exception = assert_raises(ArgumentError){ - PacNetRavenGateway.new(:user => 'user', :prn => 123456) + exception = assert_raises(ArgumentError) { + PacNetRavenGateway.new(user: 'user', prn: 123456) } assert_equal 'Missing required parameter: secret', exception.message end def test_add_address result = {} - @gateway.send(:add_address, result, :billing_address => {:address1 => 'Address 1', :address2 => 'Address 2', :zip => 'ZIP'} ) - assert_equal ['BillingPostalCode', 'BillingStreetAddressLineFour', 'BillingStreetAddressLineOne'], result.stringify_keys.keys.sort + @gateway.send(:add_address, result, billing_address: { address1: 'Address 1', address2: 'Address 2', zip: 'ZIP' }) + assert_equal %w[BillingPostalCode BillingStreetAddressLineFour BillingStreetAddressLineOne], result.stringify_keys.keys.sort assert_equal 'ZIP', result['BillingPostalCode'] assert_equal 'Address 2', result['BillingStreetAddressLineFour'] assert_equal 'Address 1', result['BillingStreetAddressLineOne'] @@ -189,7 +189,7 @@ def test_add_address def test_add_creditcard result = {} @gateway.send(:add_creditcard, result, @credit_card) - assert_equal ['CVV2', 'CardNumber', 'Expiry'], result.stringify_keys.keys.sort + assert_equal %w[CVV2 CardNumber Expiry], result.stringify_keys.keys.sort assert_equal @credit_card.number, result['CardNumber'] assert_equal @gateway.send(:expdate, @credit_card), result['Expiry'] assert_equal @credit_card.verification_value, result['CVV2'] @@ -203,13 +203,13 @@ def test_add_currency_code_default def test_add_currency_code_from_options result = {} - @gateway.send(:add_currency_code, result, 100, {currency: 'CAN'}) + @gateway.send(:add_currency_code, result, 100, { currency: 'CAN' }) assert_equal 'CAN', result['Currency'] end def test_parse result = @gateway.send(:parse, 'key1=value1&key2=value2') - h = {'key1' => 'value1', 'key2' => 'value2'} + h = { 'key1' => 'value1', 'key2' => 'value2' } assert_equal h, result end @@ -308,45 +308,47 @@ def test_success def test_message_from_approved assert_equal 'This transaction has been approved', @gateway.send(:message_from, { 'Status' => 'Approved', - 'Message'=> nil + 'Message' => nil }) end def test_message_from_declined assert_equal 'This transaction has been declined', @gateway.send(:message_from, { 'Status' => 'Declined', - 'Message'=> nil + 'Message' => nil }) end def test_message_from_voided assert_equal 'This transaction has been voided', @gateway.send(:message_from, { 'Status' => 'Voided', - 'Message'=> nil + 'Message' => nil }) end def test_message_from_status assert_equal 'This is the message', @gateway.send(:message_from, { 'Status' => 'SomeStatus', - 'Message'=> 'This is the message' + 'Message' => 'This is the message' }) end def test_post_data - @gateway.stubs(:request_id => 'wouykiikdvqbwwxueppby') - @gateway.stubs(:timestamp => '2013-10-08T14:31:54.Z') + @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), - 'CVV2' => @credit_card.verification_value, - 'Currency' => 'USD', - 'BillingStreetAddressLineOne' => 'Address 1', - 'BillingStreetAddressLineFour' => 'Address 2', - 'BillingPostalCode' => 'ZIP123' - }) + 'CardNumber' => @credit_card.number, + 'Expiry' => @gateway.send(:expdate, @credit_card), + 'CVV2' => @credit_card.verification_value, + 'Currency' => 'USD', + 'BillingStreetAddressLineOne' => 'Address 1', + 'BillingStreetAddressLineFour' => 'Address 2', + 'BillingPostalCode' => 'ZIP123' + }) + ) end def test_signature_for_cc_preauth_action @@ -356,9 +358,9 @@ def test_signature_for_cc_preauth_action 'RequestID' => 'wouykiikdvqbwwxueppby', 'PymtType' => 'cc_preauth' }, { - 'Amount' => 100, - 'Currency' => 'USD', - 'TrackingNumber' => '123456789' + 'Amount' => 100, + 'Currency' => 'USD', + 'TrackingNumber' => '123456789' }) end @@ -369,9 +371,9 @@ def test_signature_for_cc_settle_action 'RequestID' => 'wouykiikdvqbwwxueppby', 'PymtType' => 'cc_settle' }, { - 'Amount' => 100, - 'Currency' => 'USD', - 'TrackingNumber' => '123456789' + 'Amount' => 100, + 'Currency' => 'USD', + 'TrackingNumber' => '123456789' }) end @@ -382,9 +384,9 @@ def test_signature_for_cc_debit_action 'RequestID' => 'wouykiikdvqbwwxueppby', 'PymtType' => 'cc_debit' }, { - 'Amount' => 100, - 'Currency' => 'USD', - 'TrackingNumber' => '123456789' + 'Amount' => 100, + 'Currency' => 'USD', + 'TrackingNumber' => '123456789' }) end @@ -395,9 +397,9 @@ def test_signature_for_cc_refund_action 'RequestID' => 'wouykiikdvqbwwxueppby', 'PymtType' => 'cc_refund' }, { - 'Amount' => 100, - 'Currency' => 'USD', - 'TrackingNumber' => '123456789' + 'Amount' => 100, + 'Currency' => 'USD', + 'TrackingNumber' => '123456789' }) end @@ -407,9 +409,9 @@ def test_signature_for_void_action 'Timestamp' => '2013-10-08T14:31:54.Z', 'RequestID' => 'wouykiikdvqbwwxueppby' }, { - 'Amount' => 100, - 'Currency' => 'USD', - 'TrackingNumber' => '123456789' + 'Amount' => 100, + 'Currency' => 'USD', + 'TrackingNumber' => '123456789' }) end diff --git a/test/unit/gateways/pagarme_test.rb b/test/unit/gateways/pagarme_test.rb index 4b12bb2739d..b8a0a644db0 100644 --- a/test/unit/gateways/pagarme_test.rb +++ b/test/unit/gateways/pagarme_test.rb @@ -614,7 +614,7 @@ def successful_capture_response end def failed_capture_response - <<-FAILED_RESPONSE + <<-FAILED_RESPONSE { "errors": [ { @@ -626,11 +626,11 @@ def failed_capture_response "method": "post", "url": "/transactions/429356/capture" } - FAILED_RESPONSE + FAILED_RESPONSE end def successful_refund_response - <<-SUCCESS_RESPONSE + <<-SUCCESS_RESPONSE { "acquirer_name": "development", "acquirer_response_code": "00", @@ -684,11 +684,11 @@ def successful_refund_response "subscription_id": null, "tid": "1458844196661" } - SUCCESS_RESPONSE + SUCCESS_RESPONSE end def failed_refund_response - <<-FAILED_RESPONSE + <<-FAILED_RESPONSE { "errors": [ { @@ -700,11 +700,11 @@ def failed_refund_response "method": "post", "url": "/transactions/429356/refund" } - FAILED_RESPONSE + FAILED_RESPONSE end def successful_void_response - <<-SUCCESS_RESPONSE + <<-SUCCESS_RESPONSE { "acquirer_name": "pagarme", "acquirer_response_code": "00", @@ -758,11 +758,11 @@ def successful_void_response "subscription_id": null, "tid": 472218 } - SUCCESS_RESPONSE + SUCCESS_RESPONSE end def failed_void_response - <<-FAILED_RESPONSE + <<-FAILED_RESPONSE { "errors": [ { @@ -774,11 +774,11 @@ def failed_void_response "method": "post", "url": "/transactions/472218/refund" } - FAILED_RESPONSE + FAILED_RESPONSE end def successful_verify_response - <<-SUCCESS_RESPONSE + <<-SUCCESS_RESPONSE { "acquirer_name": "pagarme", "acquirer_response_code": "00", @@ -832,11 +832,11 @@ def successful_verify_response "subscription_id": null, "tid": 476135 } - SUCCESS_RESPONSE + SUCCESS_RESPONSE end def successful_verify_void_response - <<-SUCCESS_RESPONSE + <<-SUCCESS_RESPONSE { "acquirer_name": "pagarme", "acquirer_response_code": "00", @@ -890,11 +890,11 @@ def successful_verify_void_response "subscription_id": null, "tid": 476135 } - SUCCESS_RESPONSE + SUCCESS_RESPONSE end def failed_verify_response - <<-FAILED_RESPONSE + <<-FAILED_RESPONSE { "acquirer_name": "pagarme", "acquirer_response_code": "88", @@ -948,7 +948,7 @@ def failed_verify_response "subscription_id": null, "tid": 476143 } - FAILED_RESPONSE + FAILED_RESPONSE end def failed_error_response @@ -964,15 +964,14 @@ def failed_error_response "method": "post", "url": "/transactions" } - FAILED_RESPONSE + FAILED_RESPONSE end def failed_json_response - <<-SUCCESS_RESPONSE + <<-SUCCESS_RESPONSE { foo: bar } - SUCCESS_RESPONSE + SUCCESS_RESPONSE end - end diff --git a/test/unit/gateways/pago_facil_test.rb b/test/unit/gateways/pago_facil_test.rb index cb75a847402..6d6e119997e 100644 --- a/test/unit/gateways/pago_facil_test.rb +++ b/test/unit/gateways/pago_facil_test.rb @@ -70,159 +70,153 @@ def test_invalid_json private def successful_purchase_response - {'WebServices_Transacciones'=> - {'transaccion'=> - {'autorizado'=>'1', - 'autorizacion'=>'305638', - 'transaccion'=>'S-PFE12S12I12568', - 'texto'=>'Transaction has been successful!-Approved', - 'mode'=>'R', - 'empresa'=>'Usuario Invitado', - 'TransIni'=>'15:33:18 pm 25/02/2014', - 'TransFin'=>'15:33:27 pm 25/02/2014', - 'param1'=>'', - 'param2'=>'', - 'param3'=>'', - 'param4'=>'', - 'param5'=>'', - 'TipoTC'=>'Visa', - 'data'=> - {'anyoExpiracion'=>'(2) **', - 'apellidos'=>'Reyes Garza', - 'calleyNumero'=>'Anatole France 311', - 'celular'=>'5550123456', - 'colonia'=>'Polanco', - 'cp'=>'11560', - 'cvt'=>'(3) ***', - 'email'=>'comprador@correo.com', - 'estado'=>'Distrito Federal', - 'idPedido'=>'1', - 'idServicio'=>'3', - 'idSucursal'=>'60f961360ca187d533d5adba7d969d6334771370', - 'idUsuario'=>'62ad6f592ecf2faa87ef2437ed85a4d175e73c58', - 'mesExpiracion'=>'(2) **', - 'monto'=>'1.00', - 'municipio'=>'Miguel Hidalgo', - 'nombre'=>'Juan', - 'numeroTarjeta'=>'(16) **** **** ****1111', - 'pais'=>'Mexico', - 'telefono'=>'5550220910', - 'transFechaHora'=>'1393363998', - 'bin'=>'(6) ***1'}, - 'dataVal'=> - {'idSucursal'=>'12', - 'cp'=>'11560', - 'nombre'=>'Juan', - 'apellidos'=>'Reyes Garza', - 'numeroTarjeta'=>'(16) **** **** ****1111', - 'cvt'=>'(3) ***', - 'monto'=>'1.00', - 'mesExpiracion'=>'(2) **', - 'anyoExpiracion'=>'(2) **', - 'idUsuario'=>'14', - 'source'=>'1', - 'idServicio'=>'3', - 'recurrente'=>'0', - 'plan'=>'NOR', - 'diferenciado'=>'00', - 'mensualidades'=>'00', - 'ip'=>'187.162.238.170', - 'httpUserAgent'=>'Ruby', - 'idPedido'=>'1', - 'tipoTarjeta'=>'Visa', - 'hashKeyCC'=>'e5be0afe08f125ec4f6f1251141c60df88d65eae', - 'idEmpresa'=>'12', - 'nombre_comercial'=>'Usuario Invitado', - 'transFechaHora'=>'1393363998', - 'noProcess'=>'', - 'noMail'=>'', - 'notaMail'=>'', - 'settingsTransaction'=> - {'noMontoMes'=>'0.00', - 'noTransaccionesDia'=>'0', - 'minTransaccionTc'=>'5', - 'tiempoDevolucion'=>'30', - 'sendPdfTransCliente'=>'1', - 'noMontoDia'=>'0.00', - 'noTransaccionesMes'=>'0'}, - 'email'=>'comprador@correo.com', - 'telefono'=>'5550220910', - 'celular'=>'5550123456', - 'calleyNumero'=>'Anatole France 311', - 'colonia'=>'Polanco', - 'municipio'=>'Miguel Hidalgo', - 'estado'=>'Distrito Federal', - 'pais'=>'Mexico', - 'idCaja'=>'', - 'paisDetectedIP'=>'MX', - 'qa'=>'1', - 'https'=>'on'}, - 'status'=>'success' - } - } - }.to_json + { 'WebServices_Transacciones' => + { 'transaccion' => + { 'autorizado' => '1', + 'autorizacion' => '305638', + 'transaccion' => 'S-PFE12S12I12568', + 'texto' => 'Transaction has been successful!-Approved', + 'mode' => 'R', + 'empresa' => 'Usuario Invitado', + 'TransIni' => '15:33:18 pm 25/02/2014', + 'TransFin' => '15:33:27 pm 25/02/2014', + 'param1' => '', + 'param2' => '', + 'param3' => '', + 'param4' => '', + 'param5' => '', + 'TipoTC' => 'Visa', + 'data' => + { 'anyoExpiracion' => '(2) **', + 'apellidos' => 'Reyes Garza', + 'calleyNumero' => 'Anatole France 311', + 'celular' => '5550123456', + 'colonia' => 'Polanco', + 'cp' => '11560', + 'cvt' => '(3) ***', + 'email' => 'comprador@correo.com', + 'estado' => 'Distrito Federal', + 'idPedido' => '1', + 'idServicio' => '3', + 'idSucursal' => '60f961360ca187d533d5adba7d969d6334771370', + 'idUsuario' => '62ad6f592ecf2faa87ef2437ed85a4d175e73c58', + 'mesExpiracion' => '(2) **', + 'monto' => '1.00', + 'municipio' => 'Miguel Hidalgo', + 'nombre' => 'Juan', + 'numeroTarjeta' => '(16) **** **** ****1111', + 'pais' => 'Mexico', + 'telefono' => '5550220910', + 'transFechaHora' => '1393363998', + 'bin' => '(6) ***1' }, + 'dataVal' => + { 'idSucursal' => '12', + 'cp' => '11560', + 'nombre' => 'Juan', + 'apellidos' => 'Reyes Garza', + 'numeroTarjeta' => '(16) **** **** ****1111', + 'cvt' => '(3) ***', + 'monto' => '1.00', + 'mesExpiracion' => '(2) **', + 'anyoExpiracion' => '(2) **', + 'idUsuario' => '14', + 'source' => '1', + 'idServicio' => '3', + 'recurrente' => '0', + 'plan' => 'NOR', + 'diferenciado' => '00', + 'mensualidades' => '00', + 'ip' => '187.162.238.170', + 'httpUserAgent' => 'Ruby', + 'idPedido' => '1', + 'tipoTarjeta' => 'Visa', + 'hashKeyCC' => 'e5be0afe08f125ec4f6f1251141c60df88d65eae', + 'idEmpresa' => '12', + 'nombre_comercial' => 'Usuario Invitado', + 'transFechaHora' => '1393363998', + 'noProcess' => '', + 'noMail' => '', + 'notaMail' => '', + 'settingsTransaction' => + { 'noMontoMes' => '0.00', + 'noTransaccionesDia' => '0', + 'minTransaccionTc' => '5', + 'tiempoDevolucion' => '30', + 'sendPdfTransCliente' => '1', + 'noMontoDia' => '0.00', + 'noTransaccionesMes' => '0' }, + 'email' => 'comprador@correo.com', + 'telefono' => '5550220910', + 'celular' => '5550123456', + 'calleyNumero' => 'Anatole France 311', + 'colonia' => 'Polanco', + 'municipio' => 'Miguel Hidalgo', + 'estado' => 'Distrito Federal', + 'pais' => 'Mexico', + 'idCaja' => '', + 'paisDetectedIP' => 'MX', + 'qa' => '1', + 'https' => 'on' }, + 'status' => 'success' } } }.to_json end def failed_purchase_response - {'WebServices_Transacciones'=> - {'transaccion'=> - {'autorizado'=>'0', - 'transaccion'=>'n/a', - 'autorizacion'=>'n/a', - 'texto'=>'Errores en los datos de entrada Validaciones', - 'error'=> - {'numeroTarjeta'=>"'1111111111111111' no es de una institucion permitida"}, - 'empresa'=>'Sin determinar', - 'TransIni'=>'16:10:20 pm 25/02/2014', - 'TransFin'=>'16:10:20 pm 25/02/2014', - 'param1'=>'', - 'param2'=>'', - 'param3'=>'', - 'param4'=>'', - 'param5'=>'', - 'TipoTC'=>'', - 'data'=> - {'anyoExpiracion'=>'(2) **', - 'apellidos'=>'Reyes Garza', - 'calleyNumero'=>'Anatole France 311', - 'celular'=>'5550123456', - 'colonia'=>'Polanco', - 'cp'=>'11560', - 'cvt'=>'(3) ***', - 'email'=>'comprador@correo.com', - 'estado'=>'Distrito Federal', - 'idPedido'=>'1', - 'idServicio'=>'3', - 'idSucursal'=>'60f961360ca187d533d5adba7d969d6334771370', - 'idUsuario'=>'62ad6f592ecf2faa87ef2437ed85a4d175e73c58', - 'mesExpiracion'=>'(2) **', - 'monto'=>'1.00', - 'municipio'=>'Miguel Hidalgo', - 'nombre'=>'Juan', - 'numeroTarjeta'=>'(16) **** **** ****1111', - 'pais'=>'Mexico', - 'telefono'=>'5550220910', - 'transFechaHora'=>'1393366220', - 'bin'=>'(6) ***1'}, - 'dataVal'=> - {'email'=>'comprador@correo.com', - 'telefono'=>'5550220910', - 'celular'=>'5550123456', - 'calleyNumero'=>'Anatole France 311', - 'colonia'=>'Polanco', - 'municipio'=>'Miguel Hidalgo', - 'estado'=>'Distrito Federal', - 'pais'=>'Mexico', - 'idCaja'=>'', - 'numeroTarjeta'=>'', - 'cvt'=>'', - 'anyoExpiracion'=>'', - 'mesExpiracion'=>'', - 'https'=>'on'}, - 'status'=>'success' - } - } - }.to_json + { 'WebServices_Transacciones' => + { 'transaccion' => + { 'autorizado' => '0', + 'transaccion' => 'n/a', + 'autorizacion' => 'n/a', + 'texto' => 'Errores en los datos de entrada Validaciones', + 'error' => + { 'numeroTarjeta' => "'1111111111111111' no es de una institucion permitida" }, + 'empresa' => 'Sin determinar', + 'TransIni' => '16:10:20 pm 25/02/2014', + 'TransFin' => '16:10:20 pm 25/02/2014', + 'param1' => '', + 'param2' => '', + 'param3' => '', + 'param4' => '', + 'param5' => '', + 'TipoTC' => '', + 'data' => + { 'anyoExpiracion' => '(2) **', + 'apellidos' => 'Reyes Garza', + 'calleyNumero' => 'Anatole France 311', + 'celular' => '5550123456', + 'colonia' => 'Polanco', + 'cp' => '11560', + 'cvt' => '(3) ***', + 'email' => 'comprador@correo.com', + 'estado' => 'Distrito Federal', + 'idPedido' => '1', + 'idServicio' => '3', + 'idSucursal' => '60f961360ca187d533d5adba7d969d6334771370', + 'idUsuario' => '62ad6f592ecf2faa87ef2437ed85a4d175e73c58', + 'mesExpiracion' => '(2) **', + 'monto' => '1.00', + 'municipio' => 'Miguel Hidalgo', + 'nombre' => 'Juan', + 'numeroTarjeta' => '(16) **** **** ****1111', + 'pais' => 'Mexico', + 'telefono' => '5550220910', + 'transFechaHora' => '1393366220', + 'bin' => '(6) ***1' }, + 'dataVal' => + { 'email' => 'comprador@correo.com', + 'telefono' => '5550220910', + 'celular' => '5550123456', + 'calleyNumero' => 'Anatole France 311', + 'colonia' => 'Polanco', + 'municipio' => 'Miguel Hidalgo', + 'estado' => 'Distrito Federal', + 'pais' => 'Mexico', + 'idCaja' => '', + 'numeroTarjeta' => '', + 'cvt' => '', + 'anyoExpiracion' => '', + 'mesExpiracion' => '', + 'https' => 'on' }, + 'status' => 'success' } } }.to_json end def invalid_json_response diff --git a/test/unit/gateways/pay_arc_test.rb b/test/unit/gateways/pay_arc_test.rb new file mode 100644 index 00000000000..4e883160672 --- /dev/null +++ b/test/unit/gateways/pay_arc_test.rb @@ -0,0 +1,792 @@ +require 'test_helper' + +class PayArcTest < Test::Unit::TestCase + def setup + @gateway = PayArcGateway.new(fixtures(:pay_arc)) + credit_card_options = { + month: '12', + year: '2022', + first_name: 'Rex Joseph', + last_name: '', + verification_value: '999' + } + @credit_card = credit_card('4111111111111111', credit_card_options) + @invalid_credit_card = credit_card('3111111111111111', credit_card_options) + @invalid_cvv_card = credit_card('3111111111111111', credit_card_options.update(verification_value: '123')) + @amount = 100 + + @options = { + billing_address: address, + description: 'Store Purchase', + card_source: 'INTERNET', + address_line1: '920 Sunnyslope Ave', + address_line2: 'Bronx', + city: 'New York', + state: 'New York', + zip: '10469', + country: 'USA' + } + end + + def test_successful_purchase + @gateway.expects(:ssl_post).times(2).returns( + successful_token_response + ).then.returns( + successful_charge_response + ) + response = @gateway.purchase(1022, @credit_card, @options) + assert_success response + + assert_block do + PayArcGateway::SUCCESS_STATUS.include? response.message + end + assert response.test? + end + + # Failed due to already used / invalid token + def test_failure_purchase + @gateway.expects(:ssl_post).times(2).returns( + successful_token_response + ).then.returns( + failed_charge_response + ) + response = @gateway.purchase(1022, @credit_card, @options) + assert_failure response + assert_equal 'error', response.params['status'] + end + + # Failed due to invalid credit card + def test_failed_token + @gateway.expects(:ssl_post).returns(failed_token_response) + response = @gateway.token(@invalid_credit_card, @options) + assert_failure response + end + + # Failed due to invalid cvv + def test_invalid_cvv + @gateway.expects(:ssl_post).returns(failed_token_response) + response = @gateway.token(@invalid_cvv_card, @options) + assert_failure response + end + + def test_successful_verify + @gateway.expects(:ssl_post).returns(successful_token_response) + response = @gateway.verify(@invalid_cvv_card, @options) + assert_success response + end + + def test_failed_verify + @gateway.expects(:ssl_post).returns(failed_token_response) + response = @gateway.verify(@invalid_credit_card, @options) + assert_failure response + end + + def test_successful_void + @gateway.expects(:ssl_post).returns(successful_void_response) + response = @gateway.void('FHBDKH123DFKG', @options) + assert_success response + assert_block do + PayArcGateway::SUCCESS_STATUS.include? response.message + end + end + + def test_failed_void + @gateway.expects(:ssl_post).returns(failed_void_response) + response = @gateway.void('12345', @options) + assert_failure response + end + + def test_successful_authorize + @gateway.expects(:ssl_post).times(2).returns( + successful_token_response + ).then.returns( + successful_authorize_response + ) + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + assert_equal 'authorized', response.message + end + + def test_failed_authorize + @gateway.expects(:ssl_post).times(2).returns( + successful_token_response + ).then.returns( + failed_authorize_response + ) + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert_equal 'error', response.params['status'] + end + + def test_successful_refund + @gateway.expects(:ssl_post).returns(successful_refund_response) + response = @gateway.refund(@amount, 'WSHDHEHKDH') + assert_success response + assert_block do + PayArcGateway::SUCCESS_STATUS.include? response.message + end + assert_equal 'refunded', response.message + end + + def test_successful_partial_refund + @gateway.expects(:ssl_post).returns(successful_partial_refund_response) + response = @gateway.refund(@amount - 1, 'WSHDHEHKDH') + assert_success response + assert_block do + PayArcGateway::SUCCESS_STATUS.include? response.message + end + assert_equal 'partial_refund', response.message + end + + def test_failed_refund + @gateway.expects(:ssl_post).returns(failed_refund_response) + response = @gateway.refund(@amount, 'WSHDHEHKDH') + assert_failure response + assert_equal 'error', response.params['status'] + end + + def test_scrub + assert @gateway.supports_scrubbing? + transcript = @gateway.scrub(pre_scrubbed) + assert_scrubbed('quaslad-test.123.token-for-scrub', transcript) + assert_scrubbed(@credit_card.number, transcript) + assert_scrubbed(@credit_card.verification_value, transcript) + assert_equal transcript, post_scrubbed + end + + private + + def pre_scrubbed + %{ + <- "POST /v1/tokens HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAuthorization: Bearer token-fortesting\nAccept: application/json\r\nUser-Agent: PayArc ActiveMerchantBindings/1.119.0\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nHost: testapi.payarc.net\r\nContent-Length: 253\r\n\r\n" + <- "card_source=INTERNET&amount=100&currency=usd&statement_description=&card_number=23445123456&exp_month=12&exp_year=2022&cvv=983&address_line1=920+Sunnyslope+Ave&address_line2=Bronx&city=New+York&state=New+York&zip=10469&country=USA&card_holder_name=" + + -> "{\"data\":{\"object\":\"Token\",\"id\":\"0q8lLw88mlqEwYNE\",\"used\":false,\"ip\":null,\"tokenization_method\":null,\"created_at\":1620645488,\"updated_at\":1620645488,\"card\":{\"data\":{\"object\":\"Card\",\"id\":\"PMyLv0m5v151095m\",\"address1\":\"920 Sunnyslope Ave\",\"address2\":\"Bronx\",\"card_source\":\"INTERNET\",\"card_holder_name\":\"\",\"is_default\":0,\"exp_month\":\"12\",\"exp_year\":\"2022\",\"is_verified\":0,\"fingerprint\":\"1Lv0NL11yvy5yL05\",\"city\":\"New York\",\"state\":\"New York\",\"zip\":\"10469\",\"brand\":\"V\",\"last4digit\":\"1111\",\"first6digi" + -> "t\":411111,\"country\":\"USA\",\"avs_status\":null,\"cvc_status\":null,\"address_check_passed\":0,\"zip_check_passed\":0,\"customer_id\":null,\"created_at\":1620645488,\"updated_at\":1620645488}}},\"meta\":{\"include\":[],\"custom\":[]}}" + } + end + + def post_scrubbed + %{ + <- "POST /v1/tokens HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAuthorization: Bearer [FILTERED]Accept: application/json\r\nUser-Agent: PayArc ActiveMerchantBindings/1.119.0\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nHost: testapi.payarc.net\r\nContent-Length: 253\r\n\r\n" + <- "card_source=INTERNET&amount=100&currency=usd&statement_description=&card_number=[FILTERED]&exp_month=12&exp_year=2022&cvv=[BLANK]&address_line1=920+Sunnyslope+Ave&address_line2=Bronx&city=New+York&state=New+York&zip=10469&country=USA&card_holder_name=" + + -> "{\"data\":{\"object\":\"Token\",\"id\":\"0q8lLw88mlqEwYNE\",\"used\":false,\"ip\":null,\"tokenization_method\":null,\"created_at\":1620645488,\"updated_at\":1620645488,\"card\":{\"data\":{\"object\":\"Card\",\"id\":\"PMyLv0m5v151095m\",\"address1\":\"920 Sunnyslope Ave\",\"address2\":\"Bronx\",\"card_source\":\"INTERNET\",\"card_holder_name\":\"\",\"is_default\":0,\"exp_month\":\"12\",\"exp_year\":\"2022\",\"is_verified\":0,\"fingerprint\":\"1Lv0NL11yvy5yL05\",\"city\":\"New York\",\"state\":\"New York\",\"zip\":\"10469\",\"brand\":\"V\",\"last4digit\":\"1111\",\"first6digi" + -> "t\":411111,\"country\":\"USA\",\"avs_status\":null,\"cvc_status\":null,\"address_check_passed\":0,\"zip_check_passed\":0,\"customer_id\":null,\"created_at\":1620645488,\"updated_at\":1620645488}}},\"meta\":{\"include\":[],\"custom\":[]}}" + } + end + + def successful_purchase_response + %( + { + "data": { + "object": "Charge", + "id": "LDoBnOnRnRWLOyWX", + "amount": 1010, + "amount_approved": 0, + "amount_refunded": 0, + "amount_captured": 1010, + "amount_voided": 0, + "application_fee_amount": 0, + "tip_amount": 0, + "payarc_fees": 0, + "type": "Sale", + "net_amount": 0, + "captured": 1, + "is_refunded": 0, + "status": "Bad Request - Try Again", + "auth_code": null, + "failure_code": "E0911", + "failure_message": "SystemError", + "charge_description": null, + "statement_description": "Bubbles Shop", + "invoice": null, + "under_review": 0, + "created_at": 1622000885, + "updated_at": 1622000896, + "email": null, + "phone_number": null, + "card_level": "LEVEL2", + "sales_tax": 10, + "purchase_order": "ABCD", + "supplier_reference_number": null, + "customer_ref_id": null, + "ship_to_zip": null, + "amex_descriptor": null, + "customer_vat_number": null, + "summary_commodity_code": null, + "shipping_charges": null, + "duty_charges": null, + "ship_from_zip": null, + "destination_country_code": null, + "vat_invoice": null, + "order_date": null, + "tax_category": null, + "tax_type": null, + "tax_rate": null, + "tax_amount": null, + "created_by": "bubbles@eyepaste.com", + "terminal_register": null, + "tip_amount_refunded": null, + "sales_tax_refunded": null, + "shipping_charges_refunded": null, + "duty_charges_refunded": null, + "pax_reference_number": null, + "refund_reason": null, + "refund_description": null, + "surcharge": 0, + "toll_amount": null, + "refund": { + "data": [] + }, + "card": { + "data": { + "object": "Card", + "id": "15y2901NPMP90MLv", + "address1": "920 Sunnyslope Ave", + "address2": "Bronx", + "card_source": "INTERNET", + "card_holder_name": "Rex Joseph", + "is_default": 0, + "exp_month": "12", + "exp_year": "2022", + "is_verified": 0, + "fingerprint": "1Lv0NN9LyN5Pm105", + "city": "New York", + "state": "New York", + "zip": "10469", + "brand": "V", + "last4digit": "1111", + "first6digit": 411111, + "country": "USA", + "avs_status": null, + "cvc_status": null, + "address_check_passed": 0, + "zip_check_passed": 0, + "customer_id": null, + "created_at": 1622000879, + "updated_at": 1622000896 + } + } + }, + "meta": { + "include": [ + "review" + ], + "custom": [] + } + } + ) + end + + def successful_token_response + %{ + { + "data": { + "object": "Token", + "id": "0mYL8wllq08YwlNE", + "used": false, + "ip": null, + "tokenization_method": null, + "created_at": 1620412546, + "updated_at": 1620412546, + "card": { + "data": { + "object": "Card", + "id": "59P1y0PL1M9L0vML", + "address1": "920 Sunnyslope Ave", + "address2": "Bronx", + "card_source": "INTERNET", + "card_holder_name": "Rex Joseph", + "is_default": 0, + "exp_month": "12", + "exp_year": "2022", + "is_verified": 0, + "fingerprint": "1Lv0NN9LyN5Pm105", + "city": "New York", + "state": "New York", + "zip": "10469", + "brand": "V", + "last4digit": "1111", + "first6digit": 411111, + "country": "USA", + "avs_status": null, + "cvc_status": null, + "address_check_passed": 0, + "zip_check_passed": 0, + "customer_id": null, + "created_at": 1620412546, + "updated_at": 1620412546 + } + } + }, + "meta": { + "include": [], + "custom": [] + } + } + } + end + + def failed_token_response + %{ + { + "status": "error", + "code": 0, + "message": "Invalid Card", + "status_code": 409, + "exception": "App\\Containers\\Card\\Exceptions\\InvalidCardDetailsException", + "file": "/home/deploy/payarc.com/app/Containers/Token/Actions/CreateTokenAction.php", + "line": 45 + } + } + end + + def successful_charge_response + %{ + { + "data": { + "object": "Charge", + "id": "LDoBnOnRnyLyOyWX", + "amount": 1010, + "amount_approved": "1010", + "amount_refunded": 0, + "amount_captured": "1010", + "amount_voided": 0, + "application_fee_amount": 0, + "tip_amount": 0, + "payarc_fees": 29, + "type": "Sale", + "net_amount": 981, + "captured": "1", + "is_refunded": 0, + "status": "submitted_for_settlement", + "auth_code": "TAS353", + "failure_code": null, + "failure_message": null, + "charge_description": null, + "statement_description": "Testing", + "invoice": null, + "under_review": false, + "created_at": 1620473990, + "updated_at": 1620473992, + "email": null, + "phone_number": null, + "card_level": "LEVEL1", + "sales_tax": null, + "purchase_order": null, + "supplier_reference_number": null, + "customer_ref_id": null, + "ship_to_zip": null, + "amex_descriptor": null, + "customer_vat_number": null, + "summary_commodity_code": null, + "shipping_charges": null, + "duty_charges": null, + "ship_from_zip": null, + "destination_country_code": null, + "vat_invoice": null, + "order_date": null, + "tax_category": null, + "tax_type": null, + "tax_rate": null, + "tax_amount": null, + "created_by": "bubbles@eyepaste.com", + "terminal_register": null, + "tip_amount_refunded": null, + "sales_tax_refunded": null, + "shipping_charges_refunded": null, + "duty_charges_refunded": null, + "pax_reference_number": null, + "refund_reason": null, + "refund_description": null, + "surcharge": 0, + "toll_amount": null, + "refund": { + "data": [] + }, + "card": { + "data": { + "object": "Card", + "id": "mP1Lv0NP19mN05MN", + "address1": "920 Sunnyslope Ave", + "address2": "Bronx", + "card_source": "INTERNET", + "card_holder_name": "Rex Joseph", + "is_default": 0, + "exp_month": "12", + "exp_year": "2022", + "is_verified": 0, + "fingerprint": "1Lv0NN9LyN5Pm105", + "city": "New York", + "state": "New York", + "zip": "10469", + "brand": "V", + "last4digit": "1111", + "first6digit": 411111, + "country": "USA", + "avs_status": null, + "cvc_status": null, + "address_check_passed": 0, + "zip_check_passed": 0, + "customer_id": null, + "created_at": 1620473969, + "updated_at": 1620473992 + } + } + }, + "meta": { + "include": [ + "review" + ], + "custom": [] + } + } + } + end + + def failed_capture_response + %{ + { + "status": "error", + "code": 0, + "message": "The given data was invalid.", + "errors": { + "currency": [ + "The selected currency is invalid." + ], + "customer_id": [ + "The customer id field is required when none of token id / cvv / exp year / exp month / card number are present." + ], + "token_id": [ + "The token id field is required when none of customer id / cvv / exp year / exp month / card number are present." + ], + "card_number": [ + "The card number field is required when none of token id / customer id are present." + ], + "exp_month": [ + "The exp month field is required when none of token id / customer id are present." + ], + "exp_year": [ + "The exp year field is required when none of token id / customer id are present." + ] + }, + "status_code": 422, + "exception": "Illuminate\\Validation\\ValidationException", + "file": "/home/deploy/payarc.com/vendor/laravel/framework/src/Illuminate/Foundation/Http/FormRequest.php", + "line": 130 + } + } + end + + def successful_void_response + %{ + { + "data": { + "object": "Charge", + "id": "LDoBnOnRnyLyOyWX", + "amount": 1010, + "amount_approved": 1010, + "amount_refunded": 0, + "amount_captured": 1010, + "amount_voided": 1010, + "application_fee_amount": 0, + "tip_amount": 0, + "payarc_fees": 29, + "type": "Sale", + "net_amount": 0, + "captured": 1, + "is_refunded": 0, + "status": "void", + "auth_code": "TAS353", + "failure_code": null, + "failure_message": null, + "charge_description": null, + "statement_description": "Testing", + "invoice": null, + "under_review": 0, + "created_at": 1620473990, + "updated_at": 1620495791, + "email": null, + "phone_number": null, + "card_level": "LEVEL1", + "sales_tax": null, + "purchase_order": null, + "supplier_reference_number": null, + "customer_ref_id": null, + "ship_to_zip": null, + "amex_descriptor": null, + "customer_vat_number": null, + "summary_commodity_code": null, + "shipping_charges": null, + "duty_charges": null, + "ship_from_zip": null, + "destination_country_code": null, + "vat_invoice": null, + "order_date": null, + "tax_category": null, + "tax_type": null, + "tax_rate": null, + "tax_amount": null, + "created_by": "bubbles@eyepaste.com", + "terminal_register": null, + "tip_amount_refunded": null, + "sales_tax_refunded": null, + "shipping_charges_refunded": null, + "duty_charges_refunded": null, + "pax_reference_number": null, + "refund_reason": null, + "refund_description": null, + "surcharge": 0, + "toll_amount": null, + "refund": { + "data": [] + }, + "card": { + "data": { + "object": "Card", + "id": "mP1Lv0NP19mN05MN", + "address1": "920 Sunnyslope Ave", + "address2": "Bronx", + "card_source": "INTERNET", + "card_holder_name": "Rex Joseph", + "is_default": 0, + "exp_month": "12", + "exp_year": "2022", + "is_verified": 0, + "fingerprint": "1Lv0NN9LyN5Pm105", + "city": "New York", + "state": "New York", + "zip": "10469", + "brand": "V", + "last4digit": "1111", + "first6digit": 411111, + "country": "USA", + "avs_status": null, + "cvc_status": null, + "address_check_passed": 0, + "zip_check_passed": 0, + "customer_id": null, + "created_at": 1620473969, + "updated_at": 1620473992 + } + } + }, + "meta": { + "include": [ + "review" + ], + "custom": [] + } + } + } + end + + def failed_void_response + %{ + { + "status": "error", + "code": 0, + "message": "Property [is_under_review] does not exist on this collection instance.", + "status_code": 500, + "exception": "Exception", + "file": "/home/deploy/payarc.com/vendor/laravel/framework/src/Illuminate/Support/Collection.php", + "line": 2160 + } + } + end + + def successful_authorize_response + %{ + { + "data": { + "object": "Charge", + "id": "BXMbnObLnoDMORoD", + "amount": 1010, + "amount_approved": "1010", + "amount_refunded": 0, + "amount_captured": 0, + "amount_voided": 0, + "application_fee_amount": 0, + "tip_amount": 0, + "payarc_fees": 0, + "type": "Sale", + "net_amount": 0, + "captured": "0", + "is_refunded": 0, + "status": "authorized", + "auth_code": "TAS363", + "failure_code": null, + "failure_message": null, + "charge_description": null, + "statement_description": "Testing", + "invoice": null, + "under_review": false, + "created_at": 1620651112, + "updated_at": 1620651115, + "email": null, + "phone_number": null, + "card_level": "LEVEL1", + "sales_tax": null, + "purchase_order": null, + "supplier_reference_number": null, + "customer_ref_id": null, + "ship_to_zip": null, + "amex_descriptor": null, + "customer_vat_number": null, + "summary_commodity_code": null, + "shipping_charges": null, + "duty_charges": null, + "ship_from_zip": null, + "destination_country_code": null, + "vat_invoice": null, + "order_date": null, + "tax_category": null, + "tax_type": null, + "tax_rate": null, + "tax_amount": null, + "created_by": "bubbles@eyepaste.com", + "terminal_register": null, + "tip_amount_refunded": null, + "sales_tax_refunded": null, + "shipping_charges_refunded": null, + "duty_charges_refunded": null, + "pax_reference_number": null, + "refund_reason": null, + "refund_description": null, + "surcharge": 0, + "toll_amount": null, + "refund": { + "data": [] + }, + "card": { + "data": { + "object": "Card", + "id": "mP1Lv0NP19y105MN", + "address1": "920 Sunnyslope Ave", + "address2": "Bronx", + "card_source": "INTERNET", + "card_holder_name": "Rex Joseph", + "is_default": 0, + "exp_month": "12", + "exp_year": "2022", + "is_verified": 0, + "fingerprint": "1Lv0NN9LyN5Pm105", + "city": "New York", + "state": "New York", + "zip": "10469", + "brand": "V", + "last4digit": "1111", + "first6digit": 411111, + "country": "USA", + "avs_status": null, + "cvc_status": null, + "address_check_passed": 0, + "zip_check_passed": 0, + "customer_id": null, + "created_at": 1620651066, + "updated_at": 1620651115 + } + } + }, + "meta": { + "include": [ + "review" + ], + "custom": [] + } + } + } + end + + def failed_authorize_response + %{ + { + "status": "error", + "code": 0, + "message": "The requested token is not valid or already used", + "status_code": 400, + "exception": "App\\Containers\\Customer\\Exceptions\\InvalidTokenException", + "file": "/home/deploy/payarc.com/app/Containers/Charge/Actions/CreateSaleAction.php", + "line": 260 + } + } + end + + def failed_charge_response + %{ + { + "status": "error", + "code": 0, + "message": "The requested token is not valid or already used", + "status_code": 400, + "exception": "App\\Containers\\Customer\\Exceptions\\InvalidTokenException", + "file": "/home/deploy/payarc.com/app/Containers/Charge/Actions/CreateSaleAction.php", + "line": 260 + } + } + end + + def successful_refund_response + %{ + { + "data": { + "object": "Refund", + "id": "x9bQvpYvxBOYOqyB", + "refund_amount": "1010", + "currency": "usd", + "status": "refunded", + "reason": "requested_by_customer", + "description": "", + "email": null, + "receipt_number": null, + "charge_id": "LnbDBOMMbWXyORXM", + "created_at": 1620734715, + "updated_at": 1620734715 + }, + "meta": { + "include": [], + "custom": [] + } + } + } + end + + def successful_partial_refund_response + %{ + { + "data": { + "object": "Refund", + "id": "Pqy8QxY8vb9YvB1O", + "refund_amount": "500", + "currency": "usd", + "status": "partial_refund", + "reason": "requested_by_customer", + "description": "", + "email": null, + "receipt_number": null, + "charge_id": "RbWLnOyBbyWBODBX", + "created_at": 1620734893, + "updated_at": 1620734893 + }, + "meta": { + "include": [], + "custom": [] + } + } + } + end + + def failed_refund_response + %{ + { + "status": "error", + "code": 0, + "message": "Amount requested is not available for Refund ", + "status_code": 409, + "exception": "Symfony\\Component\\HttpKernel\\Exception\\ConflictHttpException", + "file": "/home/deploy/payarc.com/app/Containers/Refund/Tasks/CheckAmountTask.php", + "line": 39 } + } + end +end diff --git a/test/unit/gateways/pay_conex_test.rb b/test/unit/gateways/pay_conex_test.rb index 0269f29396d..78b62196eb5 100644 --- a/test/unit/gateways/pay_conex_test.rb +++ b/test/unit/gateways/pay_conex_test.rb @@ -140,7 +140,7 @@ def test_failed_store def test_card_present_purchase_passes_track_data stub_comms do @gateway.purchase(@amount, credit_card_with_track_data('4000100011112224')) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/card_tracks/, data) end.respond_with(successful_card_present_purchase_response) end @@ -171,6 +171,11 @@ def test_scrub assert_equal post_scrubbed, @gateway.scrub(pre_scrubbed) end + def test_scrub_check + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed_check), post_scrubbed_check + end + private def pre_scrubbed @@ -197,6 +202,18 @@ def post_scrubbed POST_SCRUBBED end + def pre_scrubbed_check + <<-PRE_SCRUBBED + <- "account_id=220614968961&api_accesskey=69e9c4dd6b8ab9ab47da4e288df78315&tender_type=ACH&first_name=Jim&last_name=Smith&bank_account_number=15378535&bank_routing_number=244183602&check_number=1&ach_account_type=checking&street_address1=456+My+Street&street_address2=Apt+1&city=Ottawa&state=ON&zip=K1C2N6&country=CA&phone=%28555%29555-5555&transaction_description=Store+Purchase&response_format=JSON&transaction_amount=1.00&currency=USD&email=joe%40example.com&transaction_type=SALE" + PRE_SCRUBBED + end + + def post_scrubbed_check + <<-POST_SCRUBBED + <- "account_id=220614968961&api_accesskey=[FILTERED]&tender_type=ACH&first_name=Jim&last_name=Smith&bank_account_number=[FILTERED]&bank_routing_number=[FILTERED]&check_number=1&ach_account_type=checking&street_address1=456+My+Street&street_address2=Apt+1&city=Ottawa&state=ON&zip=K1C2N6&country=CA&phone=%28555%29555-5555&transaction_description=Store+Purchase&response_format=JSON&transaction_amount=1.00&currency=USD&email=joe%40example.com&transaction_type=SALE" + POST_SCRUBBED + end + def successful_purchase_response %({"transaction_id":"000000001681","tender_type":"CARD","transaction_timestamp":"2015-03-04 16:35:52","card_brand":"VISA","transaction_type":"SALE","last4":"2224","card_expiration":"0916","authorization_code":"CVI877","authorization_message":"APPROVED","request_amount":1,"transaction_amount":1,"first_name":"Longbob","last_name":"Longsen","keyed":true,"swiped":false,"transaction_approved":true,"avs_response":"Z","cvv2_response":"U","transaction_description":"Store Purchase","balance":1,"currency":"USD","error":false,"error_code":0,"error_message":null,"error_msg":null}) end diff --git a/test/unit/gateways/pay_gate_xml_test.rb b/test/unit/gateways/pay_gate_xml_test.rb index c8ee14c5e70..39ef9455139 100644 --- a/test/unit/gateways/pay_gate_xml_test.rb +++ b/test/unit/gateways/pay_gate_xml_test.rb @@ -10,11 +10,11 @@ def setup # May need to generate a unique order id as server responds with duplicate order detected @options = { - :order_id => Time.now.getutc, - :billing_address => address, - :email => 'john.doe@example.com', - :ip => '127.0.0.1', - :description => 'Store Purchase', + order_id: Time.now.getutc, + billing_address: address, + email: 'john.doe@example.com', + ip: '127.0.0.1', + description: 'Store Purchase' } end @@ -30,7 +30,6 @@ def test_successful_authorization assert response.test? end - def test_successful_settlement @gateway.expects(:ssl_post).returns(successful_settlement_response) @@ -102,6 +101,4 @@ def successful_refund_response </protocol> ENDOFXML end - - end diff --git a/test/unit/gateways/pay_hub_test.rb b/test/unit/gateways/pay_hub_test.rb index 0226cea3ec0..3f8317e5d75 100644 --- a/test/unit/gateways/pay_hub_test.rb +++ b/test/unit/gateways/pay_hub_test.rb @@ -137,7 +137,7 @@ def test_expired_card end def test_card_declined - ['05', '61', '62', '65', '93'].each do |error_code| + %w[05 61 62 65 93].each do |error_code| @gateway.expects(:ssl_request).returns(response_for_error_codes(error_code)) response = @gateway.purchase(@amount, @credit_card, @options) @@ -147,7 +147,7 @@ def test_card_declined end def test_call_issuer - ['01', '02'].each do |error_code| + %w[01 02].each do |error_code| @gateway.expects(:ssl_request).returns(response_for_error_codes(error_code)) response = @gateway.purchase(@amount, @credit_card, @options) @@ -157,7 +157,7 @@ def test_call_issuer end def test_pickup_card - ['04', '07', '41', '43'].each do |error_code| + %w[04 07 41 43].each do |error_code| @gateway.expects(:ssl_request).returns(response_for_error_codes(error_code)) response = @gateway.purchase(@amount, @credit_card, @options) @@ -190,7 +190,7 @@ def test_cvv_codes def test_unsuccessful_request @gateway.expects(:ssl_request).returns(failed_purchase_or_authorize_response) - @gateway.options.merge!({:mode => 'live', :test => false}) + @gateway.options.merge!({ mode: 'live', test: false }) assert response = @gateway.purchase(@amount, @credit_card, @options) assert_failure response @@ -200,7 +200,7 @@ def test_unsuccessful_request def test_unsuccessful_authorize @gateway.expects(:ssl_request).returns(failed_purchase_or_authorize_response) - @gateway.options.merge!({:mode => 'live', :test => false}) + @gateway.options.merge!({ mode: 'live', test: false }) response = @gateway.authorize(@amount, @credit_card, @options) assert_failure response diff --git a/test/unit/gateways/pay_junction_test.rb b/test/unit/gateways/pay_junction_test.rb index 1a94fd2a77b..045f2221ab2 100644 --- a/test/unit/gateways/pay_junction_test.rb +++ b/test/unit/gateways/pay_junction_test.rb @@ -8,32 +8,31 @@ def setup Base.mode = :test @gateway = PayJunctionGateway.new( - :login => 'pj-ql-01', - :password => 'pj-ql-01p' - ) + login: 'pj-ql-01', + password: 'pj-ql-01p' + ) @credit_card = credit_card @options = { - :billing_address => address, - :description => 'Test purchase' + billing_address: address, + description: 'Test purchase' } @amount = 100 end - def test_detect_test_credentials_when_in_production Base.mode = :production - live_gw = PayJunctionGateway.new( - :login => 'l', - :password => 'p' - ) + live_gw = PayJunctionGateway.new( + login: 'l', + password: 'p' + ) assert_false live_gw.test? test_gw = PayJunctionGateway.new( - :login => 'pj-ql-01', - :password => 'pj-ql-01p' - ) + login: 'pj-ql-01', + password: 'pj-ql-01p' + ) assert test_gw.test? end @@ -85,7 +84,7 @@ def test_add_creditcard_with_track_data @credit_card.track_data = 'Tracking data' stub_comms do @gateway.authorize(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match 'dc_track=Tracking+data', data assert_no_match(/dc_name=/, data) assert_no_match(/dc_number=/, data) @@ -95,111 +94,111 @@ def test_add_creditcard_with_track_data end.respond_with(successful_authorization_response) end - private + def successful_authorization_response - <<-RESPONSE -dc_merchant_name=PayJunction - (demo)dc_merchant_address=3 W. Carrillodc_merchant_city=Santa Barbaradc_merchant_state=CAdc_merchant_zip=93101dc_merchant_phone=800-601-0230dc_device_id=1174dc_transaction_date=2007-11-28 19:22:33.791634dc_transaction_action=chargedc_approval_code=TAS193dc_response_code=00dc_response_message=APPROVAL TAS193 dc_transaction_id=3144302dc_posture=holddc_invoice_number=9f76c4e4bd66a36dc5aeb4bd7b3a02fadc_notes=--START QUICK-LINK DEBUG-- -----Vars Received---- -dc_expiration_month =&gt; * -dc_expiration_year =&gt; * -dc_invoice =&gt; 9f76c4e4bd66a36dc5aeb4bd7b3a02fa -dc_logon =&gt; pj-ql-01 -dc_name =&gt; Cody Fauser -dc_number =&gt; * -dc_password =&gt; * -dc_transaction_amount =&gt; 4.00 -dc_transaction_type =&gt; AUTHORIZATION -dc_verification_number =&gt; * -dc_version =&gt; 1.2 -----End Vars---- - -----Start Response Sent---- -dc_merchant_name=PayJunction - (demo) -dc_merchant_address=3 W. Carrillo -dc_merchant_city=Santa Barbara -dc_merchant_state=CA -dc_merchant_zip=93101 -dc_merchant_phone=800-601-0230 -dc_device_id=1174 -dc_transaction_date=2007-11-28 19:22:33.791634 -dc_transaction_action=charge -dc_approval_code=TAS193 -dc_response_code=00 -dc_response_message=APPROVAL TAS193 -dc_transaction_id=3144302 -dc_posture=hold -dc_invoice_number=9f76c4e4bd66a36dc5aeb4bd7b3a02fa -dc_notes=null -dc_card_name=cody fauser -dc_card_brand=VSA -dc_card_exp=XX/XX -dc_card_number=XXXX-XXXX-XXXX-3344 -dc_card_address= -dc_card_city= -dc_card_zipcode= -dc_card_state= -dc_card_country= -dc_base_amount=4.00 -dc_tax_amount=0.00 -dc_capture_amount=4.00 -dc_cashback_amount=0.00 -dc_shipping_amount=0.00 -----End Response Sent---- -dc_card_name=cody fauserdc_card_brand=VSAdc_card_exp=XX/XXdc_card_number=XXXX-XXXX-XXXX-3344dc_card_address=dc_card_city=dc_card_zipcode=dc_card_state=dc_card_country=dc_base_amount=4.00dc_tax_amount=0.00dc_capture_amount=4.00dc_cashback_amount=0.00dc_shipping_amount=0.00 + <<~RESPONSE + dc_merchant_name=PayJunction - (demo)dc_merchant_address=3 W. Carrillodc_merchant_city=Santa Barbaradc_merchant_state=CAdc_merchant_zip=93101dc_merchant_phone=800-601-0230dc_device_id=1174dc_transaction_date=2007-11-28 19:22:33.791634dc_transaction_action=chargedc_approval_code=TAS193dc_response_code=00dc_response_message=APPROVAL TAS193 dc_transaction_id=3144302dc_posture=holddc_invoice_number=9f76c4e4bd66a36dc5aeb4bd7b3a02fadc_notes=--START QUICK-LINK DEBUG-- + ----Vars Received---- + dc_expiration_month =&gt; * + dc_expiration_year =&gt; * + dc_invoice =&gt; 9f76c4e4bd66a36dc5aeb4bd7b3a02fa + dc_logon =&gt; pj-ql-01 + dc_name =&gt; Cody Fauser + dc_number =&gt; * + dc_password =&gt; * + dc_transaction_amount =&gt; 4.00 + dc_transaction_type =&gt; AUTHORIZATION + dc_verification_number =&gt; * + dc_version =&gt; 1.2 + ----End Vars---- + + ----Start Response Sent---- + dc_merchant_name=PayJunction - (demo) + dc_merchant_address=3 W. Carrillo + dc_merchant_city=Santa Barbara + dc_merchant_state=CA + dc_merchant_zip=93101 + dc_merchant_phone=800-601-0230 + dc_device_id=1174 + dc_transaction_date=2007-11-28 19:22:33.791634 + dc_transaction_action=charge + dc_approval_code=TAS193 + dc_response_code=00 + dc_response_message=APPROVAL TAS193 + dc_transaction_id=3144302 + dc_posture=hold + dc_invoice_number=9f76c4e4bd66a36dc5aeb4bd7b3a02fa + dc_notes=null + dc_card_name=cody fauser + dc_card_brand=VSA + dc_card_exp=XX/XX + dc_card_number=XXXX-XXXX-XXXX-3344 + dc_card_address= + dc_card_city= + dc_card_zipcode= + dc_card_state= + dc_card_country= + dc_base_amount=4.00 + dc_tax_amount=0.00 + dc_capture_amount=4.00 + dc_cashback_amount=0.00 + dc_shipping_amount=0.00 + ----End Response Sent---- + dc_card_name=cody fauserdc_card_brand=VSAdc_card_exp=XX/XXdc_card_number=XXXX-XXXX-XXXX-3344dc_card_address=dc_card_city=dc_card_zipcode=dc_card_state=dc_card_country=dc_base_amount=4.00dc_tax_amount=0.00dc_capture_amount=4.00dc_cashback_amount=0.00dc_shipping_amount=0.00 RESPONSE end def successful_refund_response - <<-RESPONSE -dc_merchant_name=PayJunction - (demo)dc_merchant_address=3 W. Carrillodc_merchant_city=Santa Barbaradc_merchant_state=CAdc_merchant_zip=93101dc_merchant_phone=800-601-0230dc_device_id=1174dc_transaction_date=2007-11-28 19:22:33.791634dc_transaction_action=creditdc_approval_code=TAS193dc_response_code=00dc_response_message=APPROVAL TAS193 dc_transaction_id=3144302dc_posture=holddc_invoice_number=9f76c4e4bd66a36dc5aeb4bd7b3a02fadc_notes=--START QUICK-LINK DEBUG-- -----Vars Received---- -dc_expiration_month =&gt; * -dc_expiration_year =&gt; * -dc_invoice =&gt; 9f76c4e4bd66a36dc5aeb4bd7b3a02fa -dc_logon =&gt; pj-ql-01 -dc_name =&gt; Cody Fauser -dc_number =&gt; * -dc_password =&gt; * -dc_transaction_amount =&gt; 4.00 -dc_transaction_type =&gt; CREDIT -dc_verification_number =&gt; * -dc_version =&gt; 1.2 -----End Vars---- - -----Start Response Sent---- -dc_merchant_name=PayJunction - (demo) -dc_merchant_address=3 W. Carrillo -dc_merchant_city=Santa Barbara -dc_merchant_state=CA -dc_merchant_zip=93101 -dc_merchant_phone=800-601-0230 -dc_device_id=1174 -dc_transaction_date=2007-11-28 19:22:33.791634 -dc_transaction_action=charge -dc_approval_code=TAS193 -dc_response_code=00 -dc_response_message=APPROVAL TAS193 -dc_transaction_id=3144302 -dc_posture=hold -dc_invoice_number=9f76c4e4bd66a36dc5aeb4bd7b3a02fa -dc_notes=null -dc_card_name=cody fauser -dc_card_brand=VSA -dc_card_exp=XX/XX -dc_card_number=XXXX-XXXX-XXXX-3344 -dc_card_address= -dc_card_city= -dc_card_zipcode= -dc_card_state= -dc_card_country= -dc_base_amount=4.00 -dc_tax_amount=0.00 -dc_capture_amount=4.00 -dc_cashback_amount=0.00 -dc_shipping_amount=0.00 -----End Response Sent---- -dc_card_name=cody fauserdc_card_brand=VSAdc_card_exp=XX/XXdc_card_number=XXXX-XXXX-XXXX-3344dc_card_address=dc_card_city=dc_card_zipcode=dc_card_state=dc_card_country=dc_base_amount=4.00dc_tax_amount=0.00dc_capture_amount=4.00dc_cashback_amount=0.00dc_shipping_amount=0.00 + <<~RESPONSE + dc_merchant_name=PayJunction - (demo)dc_merchant_address=3 W. Carrillodc_merchant_city=Santa Barbaradc_merchant_state=CAdc_merchant_zip=93101dc_merchant_phone=800-601-0230dc_device_id=1174dc_transaction_date=2007-11-28 19:22:33.791634dc_transaction_action=creditdc_approval_code=TAS193dc_response_code=00dc_response_message=APPROVAL TAS193 dc_transaction_id=3144302dc_posture=holddc_invoice_number=9f76c4e4bd66a36dc5aeb4bd7b3a02fadc_notes=--START QUICK-LINK DEBUG-- + ----Vars Received---- + dc_expiration_month =&gt; * + dc_expiration_year =&gt; * + dc_invoice =&gt; 9f76c4e4bd66a36dc5aeb4bd7b3a02fa + dc_logon =&gt; pj-ql-01 + dc_name =&gt; Cody Fauser + dc_number =&gt; * + dc_password =&gt; * + dc_transaction_amount =&gt; 4.00 + dc_transaction_type =&gt; CREDIT + dc_verification_number =&gt; * + dc_version =&gt; 1.2 + ----End Vars---- + + ----Start Response Sent---- + dc_merchant_name=PayJunction - (demo) + dc_merchant_address=3 W. Carrillo + dc_merchant_city=Santa Barbara + dc_merchant_state=CA + dc_merchant_zip=93101 + dc_merchant_phone=800-601-0230 + dc_device_id=1174 + dc_transaction_date=2007-11-28 19:22:33.791634 + dc_transaction_action=charge + dc_approval_code=TAS193 + dc_response_code=00 + dc_response_message=APPROVAL TAS193 + dc_transaction_id=3144302 + dc_posture=hold + dc_invoice_number=9f76c4e4bd66a36dc5aeb4bd7b3a02fa + dc_notes=null + dc_card_name=cody fauser + dc_card_brand=VSA + dc_card_exp=XX/XX + dc_card_number=XXXX-XXXX-XXXX-3344 + dc_card_address= + dc_card_city= + dc_card_zipcode= + dc_card_state= + dc_card_country= + dc_base_amount=4.00 + dc_tax_amount=0.00 + dc_capture_amount=4.00 + dc_cashback_amount=0.00 + dc_shipping_amount=0.00 + ----End Response Sent---- + dc_card_name=cody fauserdc_card_brand=VSAdc_card_exp=XX/XXdc_card_number=XXXX-XXXX-XXXX-3344dc_card_address=dc_card_city=dc_card_zipcode=dc_card_state=dc_card_country=dc_base_amount=4.00dc_tax_amount=0.00dc_capture_amount=4.00dc_cashback_amount=0.00dc_shipping_amount=0.00 RESPONSE end diff --git a/test/unit/gateways/pay_junction_v2_test.rb b/test/unit/gateways/pay_junction_v2_test.rb index 99a40a808ec..7e27979f33b 100644 --- a/test/unit/gateways/pay_junction_v2_test.rb +++ b/test/unit/gateways/pay_junction_v2_test.rb @@ -5,9 +5,10 @@ def setup @gateway = PayJunctionV2Gateway.new(api_login: 'api_login', api_password: 'api_password', api_key: 'api_key') @amount = 99 - @credit_card = credit_card('4444333322221111', month: 01, year: 2020, verification_value: 999) + @credit_card = credit_card('4444333322221111', month: 01, year: 2022, verification_value: 999) @options = { - order_id: generate_unique_id + order_id: generate_unique_id, + billing_address: address } end @@ -205,6 +206,20 @@ def test_failed_store assert_match %r{Card Number is not a valid card number}, response.message end + def test_add_address + post = { card: { billingAddress: {} } } + @gateway.send(:add_address, post, @options) + assert_equal @options[:billing_address][:first_name], post[:billingFirstName] + assert_equal @options[:billing_address][:last_name], post[:billingLastName] + assert_equal @options[:billing_address][:company], post[:billingCompanyName] + assert_equal @options[:billing_address][:phone_number], post[:billingPhone] + assert_equal @options[:billing_address][:address1], post[:billingAddress] + assert_equal @options[:billing_address][:city], post[:billingCity] + assert_equal @options[:billing_address][:state], post[:billingState] + assert_equal @options[:billing_address][:country], post[:billingCountry] + assert_equal @options[:billing_address][:zip], post[:billingZip] + end + def test_scrub assert @gateway.supports_scrubbing? assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed diff --git a/test/unit/gateways/pay_secure_test.rb b/test/unit/gateways/pay_secure_test.rb index 78ea5b7b56f..37e3db47250 100644 --- a/test/unit/gateways/pay_secure_test.rb +++ b/test/unit/gateways/pay_secure_test.rb @@ -1,22 +1,21 @@ require 'test_helper' class PaySecureTest < Test::Unit::TestCase - def setup @gateway = PaySecureGateway.new( - :login => 'login', - :password => 'password' - ) + login: 'login', + password: 'password' + ) @credit_card = credit_card - @options = { - :order_id => '1000', - :billing_address => address, - :description => 'Test purchase' + @options = { + order_id: '1000', + billing_address: address, + description: 'Test purchase' } @amount = 100 end - + def test_successful_purchase @gateway.expects(:ssl_post).returns(successful_purchase_response) assert response = @gateway.purchase(@amount, @credit_card, @options) @@ -25,7 +24,7 @@ def test_successful_purchase assert_equal '2778;SimProxy 54041670', response.authorization assert response.test? end - + def test_failed_purchase @gateway.expects(:ssl_post).returns(failure_response) assert response = @gateway.purchase(@amount, @credit_card, @options) @@ -33,39 +32,40 @@ def test_failed_purchase assert_equal "Field value '8f796cb29a1be32af5ce12d4ca7425c2' does not match required format.", response.message assert_failure response end - + def test_avs_result_not_supported @gateway.expects(:ssl_post).returns(successful_purchase_response) - - response = @gateway.purchase(@amount, @credit_card, @options) + + response = @gateway.purchase(@amount, @credit_card, @options) assert_nil response.avs_result['code'] end - + def test_cvv_result_not_supported @gateway.expects(:ssl_post).returns(successful_purchase_response) - + response = @gateway.purchase(@amount, @credit_card, @options) assert_nil response.cvv_result['code'] end - + private + def successful_purchase_response - <<-RESPONSE -Status: Accepted -SettlementDate: 2007-10-09 -AUTHNUM: 2778 -ErrorString: No Error -CardBin: 1 -ERROR: 0 -TransID: SimProxy 54041670 + <<~RESPONSE + Status: Accepted + SettlementDate: 2007-10-09 + AUTHNUM: 2778 + ErrorString: No Error + CardBin: 1 + ERROR: 0 + TransID: SimProxy 54041670 RESPONSE end - + def failure_response - <<-RESPONSE -Status: Declined -ErrorString: Field value '8f796cb29a1be32af5ce12d4ca7425c2' does not match required format. -ERROR: 1 + <<~RESPONSE + Status: Declined + ErrorString: Field value '8f796cb29a1be32af5ce12d4ca7425c2' does not match required format. + ERROR: 1 RESPONSE end end diff --git a/test/unit/gateways/pay_trace_test.rb b/test/unit/gateways/pay_trace_test.rb new file mode 100644 index 00000000000..0f44fb59822 --- /dev/null +++ b/test/unit/gateways/pay_trace_test.rb @@ -0,0 +1,570 @@ +require 'test_helper' + +class PayTraceTest < Test::Unit::TestCase + include CommStub + + def setup + @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 + + @options = { + billing_address: address + } + 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 + 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) + + 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, {}) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_include endpoint, 'checks/sale/by_account' + assert_equal request['amount'], '1.00' + assert_equal request['check']['account_number'], @echeck.account_number + assert_equal request['check']['routing_number'], @echeck.routing_number + assert_equal request['integrator_id'], @gateway.options[:integrator_id] + assert_equal request['billing_address']['name'], @echeck.name + assert_equal request.dig('billing_address', 'street_address'), nil + end.respond_with(successful_ach_processing_response) + + assert_success response + assert_equal response.message, 'Your check was successfully processed.' + end + + def test_successful_purchase_by_customer_with_ach + customer_id = 'customerId121' + response = stub_comms(@gateway) do + @gateway.purchase(@amount, customer_id, { check_transaction: 'true' }) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_include endpoint, 'checks/sale/by_customer' + assert_equal request['amount'], '1.00' + assert_equal request['customer_id'], customer_id + assert_equal request['integrator_id'], @gateway.options[:integrator_id] + end.respond_with(successful_ach_processing_response) + + assert_success response + assert_equal response.message, 'Your check was successfully processed.' + end + + def test_successful_purchase_with_level_3_data + @gateway.expects(:ssl_post).times(2).returns(successful_purchase_response).then.returns(successful_level_3_response) + + options = { + visa_or_mastercard: 'visa', + invoice_id: 'inv12345', + customer_reference_id: '123abcd', + tax_amount: 499, + national_tax_amount: 172, + merchant_tax_id: '3456defg', + customer_tax_id: '3456test', + commodity_code: '4321', + discount_amount: 99, + freight_amount: 75, + duty_amount: 32, + source_address: { + zip: '94947' + }, + shipping_address: { + zip: '94948', + country: 'US' + }, + additional_tax_amount: 4, + additional_tax_rate: 1, + line_items: [ + { + additional_tax_amount: 0, + additional_tax_rate: 8, + amount: 1999, + commodity_code: '123commodity', + description: 'plumbing', + discount_amount: 327, + product_id: 'skucode123', + quantity: 4, + unit_of_measure: 'EACH', + unit_cost: 424 + } + ] + } + + response = @gateway.purchase(100, @credit_card, options) + assert_success response + assert_equal 101, response.params['response_code'] + end + + def test_omitting_level_3_fields_with_nil_values + options = { + visa_or_mastercard: 'mastercard', + additional_tax_included: nil, + line_items: [ + { + description: 'business services', + discount_included: nil + } + ] + } + stub_comms(@gateway) do + @gateway.purchase(100, @credit_card, options) + end.check_request do |endpoint, data, _headers| + next unless endpoint == 'https://api.paytrace.com/v1/level_three/mastercard' + + refute_includes data, 'discount_included' + refute_includes data, 'additional_tax_included' + end.respond_with(successful_level_3_response) + end + + def test_failed_purchase + @gateway.expects(:ssl_post).returns(failed_purchase_response) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_equal PayTraceGateway::STANDARD_ERROR_CODE[:declined], response.error_code + end + + def test_failed_ach_processing + @gateway.expects(:ssl_post).returns(failed_ach_processing_response) + + response = @gateway.purchase(@amount, @echeck, @options) + assert_failure response + assert_equal response.message, 'Your check was NOT successfully processed. ' + end + + def test_failed_bad_request_ach_processing + @gateway.expects(:ssl_post).returns(failed_bad_request_ach_processing_response) + + response = @gateway.authorize(@amount, @echeck, @options) + assert_failure response + assert_include response.message, 'Please provide a valid Checking Account Number.' + end + + def test_successful_authorize + @gateway.expects(:ssl_post).returns(successful_authorize_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + assert_equal true, response.success? + end + + def test_successful_authorize_with_ach + response = stub_comms(@gateway) do + @gateway.authorize(@amount, @echeck, @options) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_include endpoint, 'checks/hold/by_account' + assert_equal request['amount'], '1.00' + assert_equal request['check']['account_number'], @echeck.account_number + assert_equal request['check']['routing_number'], @echeck.routing_number + assert_equal request['integrator_id'], @gateway.options[:integrator_id] + assert_equal request['billing_address']['name'], @options[:billing_address][:name] + assert_equal request['billing_address']['street_address'], @options[:billing_address][:address1] + assert_equal request['billing_address']['city'], @options[:billing_address][:city] + assert_equal request['billing_address']['state'], @options[:billing_address][:state] + assert_equal request['billing_address']['zip'], @options[:billing_address][:zip] + end.respond_with(successful_ach_processing_response) + + assert_success response + assert_equal response.message, 'Your check was successfully processed.' + end + + def test_successful_authorize_by_customer_with_ach + customer_id = 'customerId121' + response = stub_comms(@gateway) do + @gateway.authorize(@amount, customer_id, { check_transaction: 'true' }) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_include endpoint, 'checks/hold/by_customer' + assert_equal request['amount'], '1.00' + assert_equal request['customer_id'], customer_id + assert_equal request['integrator_id'], @gateway.options[:integrator_id] + end.respond_with(successful_ach_processing_response) + + assert_success response + assert_equal response.message, 'Your check was successfully processed.' + end + + def test_failed_authorize + @gateway.expects(:ssl_post).returns(failed_authorize_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert_equal 'Your transaction was not approved. EXPIRED CARD - Expired card', response.message + end + + def test_successful_capture + @gateway.expects(:ssl_post).returns(successful_capture_response) + transaction_id = 10598543 + + response = @gateway.capture(@amount, transaction_id, @options) + assert_success response + assert_equal 'Your transaction was successfully captured.', response.message + end + + def test_successful_capture_with_ach + check_transaction_id = 9981615 + response = stub_comms(@gateway) do + @gateway.capture(@amount, check_transaction_id, { check_transaction: 'true' }) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_include endpoint, 'checks/manage/fund' + assert_equal request['amount'], '1.00' + assert_equal request['check_transaction_id'], check_transaction_id + assert_equal request['integrator_id'], @gateway.options[:integrator_id] + end.respond_with(successful_ach_capture_void_response) + + assert_success response + assert_equal response.message, 'Your check was successfully managed.' + end + + def test_successful_level_3_data_field_mapping + authorization = 123456789 + options = { + visa_or_mastercard: 'visa', + address: { + zip: '99201' + } + } + stub_comms(@gateway) do + @gateway.capture(@amount, authorization, options) + end.check_request do |endpoint, data, _headers| + next unless endpoint == 'https://api.paytrace.com/v1/level_three/visa' + + assert_match(/"source_address":{"zip":"99201"}/, data) + end.respond_with(successful_level_3_visa) + end + + def test_failed_capture + @gateway.expects(:ssl_post).returns(failed_capture_response) + + response = @gateway.capture(@amount, '', @options) + assert_failure response + assert_equal 'Errors- code:58, message:["Please provide a valid Transaction ID."]', response.message + end + + def test_failed_capture_void_response + @gateway.expects(:ssl_post).returns(failed_ach_capture_void_response) + + response = @gateway.capture(@amount, @echeck, @options) + assert_failure response + assert_include response.message, 'The Check ID that you have provided was not found in the PayTrace records. It may already be voided or settled.' + end + + def test_successful_refund + transaction_id = 105968532 + @gateway.expects(:ssl_post).returns(successful_refund_response) + + response = @gateway.refund(100, transaction_id) + assert_success response + assert_equal 'Your transaction successfully refunded.', response.message + end + + def test_successful_refund_with_ach + check_transaction_id = 9981615 + response = stub_comms(@gateway) do + @gateway.refund(@amount, check_transaction_id, { check_transaction: 'true' }) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_include endpoint, 'checks/refund/by_transaction' + assert_equal request['amount'], '1.00' + assert_equal request['check_transaction_id'], check_transaction_id + assert_equal request['integrator_id'], @gateway.options[:integrator_id] + end.respond_with(successful_ach_refund_response) + + assert_success response + assert_equal response.message, 'Your check was successfully refunded.' + end + + def test_failed_refund + @gateway.expects(:ssl_post).returns(failed_refund_response) + + response = @gateway.refund(200, '', @options) + assert_failure response + assert_equal 'Errors- code:981, message:["Log in failed for insufficient permissions."]', response.message + end + + def test_failed_refund_response + @gateway.expects(:ssl_post).returns(failed_ach_refund_response) + + response = @gateway.refund(@amount, @echeck, @options) + assert_failure response + assert_include response.message, 'The Check ID that you provided was not found in the PayTrace record.' + end + + def test_successful_void + transaction_id = 105968551 + @gateway.expects(:ssl_post).returns(successful_void_response) + + assert void = @gateway.void(transaction_id, @options) + assert_success void + assert_equal 'Your transaction was successfully voided.', void.message + end + + def test_successful_void_with_ach + check_transaction_id = 9981615 + response = stub_comms(@gateway) do + @gateway.void(check_transaction_id, { check_transaction: 'true' }) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_include endpoint, 'checks/manage/void' + assert_equal request['check_transaction_id'], check_transaction_id + assert_equal request['integrator_id'], @gateway.options[:integrator_id] + end.respond_with(successful_ach_capture_void_response) + + assert_success response + assert_equal response.message, 'Your check was successfully managed.' + end + + def test_failed_void + @gateway.expects(:ssl_post).returns(failed_void_response) + + response = @gateway.void('') + assert_failure response + assert_equal 'Errors- code:58, message:["Please provide a valid Transaction ID."]', response.message + end + + def test_successful_verify + @gateway.expects(:ssl_post).returns(successful_authorize_response) + + response = @gateway.verify(@credit_card, @options) + assert_success response + end + + def test_failed_verify + @gateway.expects(:ssl_post).returns(failed_authorize_response) + + response = @gateway.verify(@credit_card, @options) + assert_failure response + assert_equal 'Your transaction was not approved. EXPIRED CARD - Expired card', response.message + end + + def test_successful_customer_creation + @gateway.expects(:ssl_post).returns(successful_create_customer_response) + + response = @gateway.store(@credit_card, @options) + assert_success response + assert_equal true, response.success? + end + + def test_duplicate_customer_creation + options = { + customer_id: '7cad678781bf0456d50e1478', + billing_address: { + address1: '8320 This Way Lane', + city: 'Placeville', + state: 'CA', + zip: '85284' + } + } + @gateway.expects(:ssl_post).returns(failed_customer_creation_response) + response = @gateway.store(@credit_card, options) + assert_failure response + assert_equal false, response.success? + assert_match 'Please provide a unique customer ID.', response.params['errors'].to_s + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + private + + def pre_scrubbed + <<-PRE_SCRUBBED + opening connection to api.paytrace.com:443... + opened + 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},\"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" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "Status: 200 OK\r\n" + -> "Cache-Control: max-age=0, private, must-revalidate\r\n" + -> "Referrer-Policy: strict-origin-when-cross-origin\r\n" + -> "X-Permitted-Cross-Domain-Policies: none\r\n" + -> "X-XSS-Protection: 1; mode=block\r\n" + -> "X-Request-Id: f008583e-3755-4eca-b8a0-83d8d82cefca\r\n" + -> "X-Download-Options: noopen\r\n" + -> "ETag: W/\"4edcbabd892d2f033a4cbc7932f26fae\"\r\n" + -> "X-Frame-Options: SAMEORIGIN\r\n" + -> "X-Runtime: 1.984489\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "X-Frame-Options: SAMEORIGIN\r\n" + -> "X-XSS-Protection: 1; mode=block\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "Content-Security-Policy: frame-ancestors 'self';\r\n" + -> "Strict-Transport-Security: max-age=31536000; includeSubDomains\r\n" + -> "\r\n" + -> "142\r\n" + reading 322 bytes... + -> "{\"success\":true,\"response_code\":101,\"status_message\":\"Your transaction was successfully approved.\",\"transaction_id\":395970044,\"approval_code\":\"TAS679\",\"approval_message\":\" NO MATCH - Approved and completed\",\"avs_response\":\"No Match\",\"csc_response\":\"\",\"external_transaction_id\":\"\",\"masked_card_number\":\"xxxxxxxxxxxx5439\"}" + read 322 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.paytrace.com:443... + opened + 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},\"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" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "Status: 200 OK\r\n" + -> "Cache-Control: max-age=0, private, must-revalidate\r\n" + -> "Referrer-Policy: strict-origin-when-cross-origin\r\n" + -> "X-Permitted-Cross-Domain-Policies: none\r\n" + -> "X-XSS-Protection: 1; mode=block\r\n" + -> "X-Request-Id: f008583e-3755-4eca-b8a0-83d8d82cefca\r\n" + -> "X-Download-Options: noopen\r\n" + -> "ETag: W/\"4edcbabd892d2f033a4cbc7932f26fae\"\r\n" + -> "X-Frame-Options: SAMEORIGIN\r\n" + -> "X-Runtime: 1.984489\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "X-Frame-Options: SAMEORIGIN\r\n" + -> "X-XSS-Protection: 1; mode=block\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "Content-Security-Policy: frame-ancestors 'self';\r\n" + -> "Strict-Transport-Security: max-age=31536000; includeSubDomains\r\n" + -> "\r\n" + -> "142\r\n" + reading 322 bytes... + -> "{\"success\":true,\"response_code\":101,\"status_message\":\"Your transaction was successfully approved.\",\"transaction_id\":395970044,\"approval_code\":\"TAS679\",\"approval_message\":\" NO MATCH - Approved and completed\",\"avs_response\":\"No Match\",\"csc_response\":\"\",\"external_transaction_id\":\"\",\"masked_card_number\":\"xxxxxxxxxxxx5439\"}" + read 322 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + POST_SCRUBBED + end + + def successful_purchase_response + '{"success":true,"response_code":101,"status_message":"Your transaction was successfully approved.","transaction_id":392483066,"approval_code":"TAS610","approval_message":" NO MATCH - Approved and completed","avs_response":"No Match","csc_response":"","external_transaction_id":"","masked_card_number":"xxxxxxxxxxxx5439"}' + end + + def successful_level_3_response + '{"success":true,"response_code":170,"status_message":"Visa/MasterCard enhanced data was successfully added to Transaction ID 392483066. 1 line item records were created."}' + end + + def successful_level_3_visa + '{"success":true,"response_code":170,"status_message":"Visa/MasterCard enhanced data was successfully added to Transaction ID 123456789. 2 line item records were created."}' + end + + def failed_purchase_response + '{"success":false,"response_code":102,"status_message":"Your transaction was not approved.","transaction_id":392501201,"approval_code":"","approval_message":" DECLINE - Do not honor","avs_response":"No Match","csc_response":"","external_transaction_id":"","masked_card_number":"xxxxxxxxxxxx5439"}' + end + + def successful_authorize_response + '{"success":true,"response_code":101,"status_message":"Your transaction was successfully approved.","transaction_id":392224547,"approval_code":"TAS161","approval_message":" NO MATCH - Approved and completed","avs_response":"No Match","csc_response":"","external_transaction_id":"","masked_card_number":"xxxxxxxxxxxx2224"}' + end + + def failed_authorize_response + '{"success":false,"response_code":102,"status_message":"Your transaction was not approved.","transaction_id":395971008,"approval_code":"","approval_message":" EXPIRED CARD - Expired card","avs_response":"No Match","csc_response":"","external_transaction_id":"","masked_card_number":"xxxxxxxxxxxx5439"}' + end + + def successful_capture_response + '{"success":true,"response_code":112,"status_message":"Your transaction was successfully captured.","transaction_id":392442990,"external_transaction_id":""}' + end + + def failed_capture_response + '{"success":false,"response_code":1,"status_message":"One or more errors has occurred.","errors":{"58":["Please provide a valid Transaction ID."]},"external_transaction_id":""}' + end + + def successful_refund_response + '{"success":true,"response_code":106,"status_message":"Your transaction successfully refunded.","transaction_id":105968559,"external_transaction_id":""}' + end + + def failed_refund_response + '{"success":false,"response_code":1,"status_message":"One or more errors has occurred.","errors":{"981":["Log in failed for insufficient permissions."]},"external_transaction_id":""}' + end + + def successful_void_response + '{"success":true,"response_code":109,"status_message":"Your transaction was successfully voided.","transaction_id":395971574}' + end + + def failed_void_response + '{"success":false,"response_code":1,"status_message":"One or more errors has occurred.","errors":{"58":["Please provide a valid Transaction ID."]}}' + end + + def successful_create_customer_response + '{"success":true,"response_code":160,"status_message":"The customer profile for customerTest150/Steve Smith was successfully created","customer_id":"customerTest150","masked_card_number":"xxxxxxxxxxxx1111"}' + end + + def failed_customer_creation_response + '{"success":false,"response_code":1,"status_message":"One or more errors has occurred.","errors":{"171":["Please provide a unique customer ID."]},"masked_card_number":"xxxxxxxxxxxx5439"}' + end + + def successful_ach_processing_response + '{ "success":true, "response_code":120, "status_message":"Your check was successfully processed.", "check_transaction_id":981619 }' + end + + def successful_ach_capture_void_response + '{ "success":true, "response_code":124, "status_message":"Your check was successfully managed.", "check_transaction_id":9981614 }' + end + + def successful_ach_refund_response + '{ "success":true, "response_code":122, "status_message":"Your check was successfully refunded.", "check_transaction_id":9981632 }' + end + + def failed_ach_processing_response + '{ "success":false, "response_code":125, "status_message":"Your check was NOT successfully processed.", "check_transaction_id":981610, "ach_code":0, "ach_message":"" }' + end + + def failed_bad_request_ach_processing_response + '{ "success":false, "response_code":1, "status_message":"One or more errors has occurred.", "errors":{ "30":[ "Customer ID, customerid123, was not found or is incomplete." ], "45":[ "Please provide a valid Checking Account Number." ], "46":[ "Please provide a valid Transit Routing Number." ] } }' + end + + def failed_ach_refund_response + '{ "success":false, "response_code":1, "status_message":"One or more errors has occurred.", "errors":{ "80":[ "The Check ID that you provided was not found in the PayTrace record." ] } }' + end + + def failed_ach_capture_void_response + '{ "success":false, "response_code":1, "status_message":"One or more errors has occurred.", "errors":{ "80":[ "The Check ID that you have provided was not found in the PayTrace records. It may already be voided or settled." ] } }' + end +end diff --git a/test/unit/gateways/paybox_direct_test.rb b/test/unit/gateways/paybox_direct_test.rb index 23ec8124b59..0676dc82ef9 100644 --- a/test/unit/gateways/paybox_direct_test.rb +++ b/test/unit/gateways/paybox_direct_test.rb @@ -5,19 +5,17 @@ class PayboxDirectTest < Test::Unit::TestCase def setup @gateway = PayboxDirectGateway.new( - :login => 'l', - :password => 'p' - ) + login: 'l', + password: 'p' + ) - @credit_card = credit_card('1111222233334444', - :brand => 'visa' - ) + @credit_card = credit_card('1111222233334444', brand: 'visa') @amount = 100 @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase' + order_id: '1', + billing_address: address, + description: 'Store Purchase' } end @@ -82,7 +80,7 @@ def test_unsuccessful_request end def test_keep_the_card_code_not_considered_fraudulent - @gateway.expects(:ssl_post).returns(purchase_response('00104')) + @gateway.expects(:ssl_post).returns(purchase_response('00103')) assert response = @gateway.purchase(@amount, @credit_card, @options) assert_failure response @@ -113,7 +111,7 @@ def test_version private # Place raw successful response from gateway here - def purchase_response(code='00000') + def purchase_response(code = '00000') "NUMTRANS=0720248861&NUMAPPEL=0713790302&NUMQUESTION=0000790217&SITE=1999888&RANG=99&AUTORISATION=XXXXXX&CODEREPONSE=#{code}&COMMENTAIRE=Demande trait?e avec succ?s ✔漢" end diff --git a/test/unit/gateways/payeezy_test.rb b/test/unit/gateways/payeezy_test.rb index 08319f927f4..53a7c090975 100644 --- a/test/unit/gateways/payeezy_test.rb +++ b/test/unit/gateways/payeezy_test.rb @@ -12,10 +12,59 @@ def setup @check = check @amount = 100 @options = { - :billing_address => address, - :ta_token => '123' + billing_address: address, + ta_token: '123' + } + @options_stored_credentials = { + cardbrand_original_transaction_id: 'original_transaction_id_abc123', + sequence: 'FIRST', + is_scheduled: true, + initiator: 'MERCHANT', + auth_type_override: 'A' + } + @options_standardized_stored_credentials = { + stored_credential: { + network_transaction_id: 'stored_credential_abc123', + initial_transaction: false, + reason_type: 'recurring', + initiator: 'cardholder' + } } @authorization = 'ET1700|106625152|credit_card|4738' + @reversal_id = SecureRandom.random_number(1000000).to_s + + @options_mdd = { + soft_descriptors: { + dba_name: 'Caddyshack', + street: '1234 Any Street', + city: 'Durham', + region: 'North Carolina', + mid: 'mid_1234', + mcc: 'mcc_5678', + postal_code: '27701', + country_code: 'US', + merchant_contact_info: '8885551212' + } + } + @apple_pay_card = network_tokenization_credit_card( + '4761209980011439', + payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', + month: '11', + year: '2022', + eci: 5, + source: :apple_pay, + verification_value: 569 + ) + @apple_pay_card_amex = network_tokenization_credit_card( + '373953192351004', + brand: 'american_express', + payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', + month: '11', + year: Time.now.year + 1, + eci: 5, + source: :apple_pay, + verification_value: 569 + ) end def test_invalid_credentials @@ -49,14 +98,67 @@ 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? assert_equal 'Transaction Normal - Approved', response.message end + def test_successful_purchase_with_apple_pay + stub_comms do + @gateway.purchase(@amount, @apple_pay_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['method'], '3DS' + assert_equal request['3DS']['type'], 'D' + assert_equal request['3DS']['wallet_provider_id'], 'APPLE_PAY' + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_apple_pay_no_cryptogram + @apple_pay_card.payment_cryptogram = '' + @apple_pay_card.eci = nil + stub_comms do + @gateway.purchase(@amount, @apple_pay_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['eci_indicator'], '5' + assert_nil request['3DS']['xid'] + assert_nil request['3DS']['cavv'] + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_apple_pay_amex + stub_comms do + @gateway.purchase(@amount, @apple_pay_card_amex, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert request['3DS']['cavv'], @apple_pay_card_amex.payment_cryptogram + assert_nil request['3DS']['xid'] + end.respond_with(successful_purchase_response) + end + + def test_failed_purchase_no_name + @apple_pay_card.first_name = nil + @apple_pay_card.last_name = nil + @options[:billing_address] = nil + stub_comms do + @gateway.purchase(@amount, @apple_pay_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal nil, request['cardholder_name'] + end.respond_with(failed_purchase_no_name_response) + end + def test_successful_store response = stub_comms(@gateway, :ssl_request) do @gateway.store(@credit_card, @options.merge(js_security_key: 'js-f4c4b54f08d6c44c8cad3ea80bbf92c4f4c4b54f08d6c44c')) @@ -105,7 +207,7 @@ def test_successful_purchase_defaulting_check_number response = stub_comms do @gateway.purchase(@amount, check_without_number, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/001/, data) end.respond_with(successful_purchase_echeck_response) @@ -115,6 +217,95 @@ def test_successful_purchase_defaulting_check_number assert_equal 'Transaction Normal - Approved', response.message end + def test_successful_purchase_with_customer_ref + options = @options.merge(level2: { customer_ref: 'An important customer' }) + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/"level2":{"customer_ref":"An important customer"}/, data) + end.respond_with(successful_purchase_response) + + 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| + 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_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| + 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 + assert response.test? + assert_equal 'Transaction Normal - Approved', response.message + end + def test_failed_purchase @gateway.expects(:ssl_post).raises(failed_purchase_response) assert response = @gateway.purchase(@amount, @credit_card, @options) @@ -123,6 +314,15 @@ def test_failed_purchase assert_equal response.error_code, 'card_expired' end + def test_failed_purchase_with_insufficient_funds + response = stub_comms do + @gateway.purchase(530200, @credit_card, @options) + end.respond_with(failed_purchase_response_for_insufficient_funds) + + assert_failure response + assert_equal '302', response.error_code + end + def test_successful_authorize @gateway.expects(:ssl_post).returns(successful_authorize_response) assert response = @gateway.authorize(@amount, @credit_card, @options) @@ -167,18 +367,81 @@ def test_successful_refund_with_echeck assert_success response end + def test_successful_refund_with_soft_descriptors + response = stub_comms do + @gateway.refund(@amount, @authorization, @options.merge(@options_mdd)) + end.check_request do |_endpoint, data, _headers| + json = '{"transaction_type":"refund","method":"credit_card","transaction_tag":"106625152","currency_code":"USD","amount":"100","soft_descriptors":{"dba_name":"Caddyshack","street":"1234 Any Street","city":"Durham","region":"North Carolina","mid":"mid_1234","mcc":"mcc_5678","postal_code":"27701","country_code":"US","merchant_contact_info":"8885551212"},"merchant_ref":null}' + assert_match json, data + end.respond_with(successful_refund_response) + + assert_success response + end + + def test_successful_refund_with_order_id + response = stub_comms do + @gateway.refund(@amount, @authorization, @options.merge(order_id: 1234)) + end.check_request do |_endpoint, data, _headers| + json = '{"transaction_type":"refund","method":"credit_card","transaction_tag":"106625152","currency_code":"USD","amount":"100","merchant_ref":1234}' + assert_match json, data + end.respond_with(successful_refund_response) + + assert_success response + end + def test_failed_refund @gateway.expects(:ssl_post).raises(failed_refund_response) assert response = @gateway.refund(@amount, @authorization) assert_failure response end + def test_successful_general_credit + @gateway.expects(:ssl_post).returns(successful_refund_response) + assert response = @gateway.credit(@amount, @credit_card) + assert_success response + end + + def test_successful_general_credit_with_soft_descriptors + response = stub_comms do + @gateway.credit(@amount, @credit_card, @options.merge(@options_mdd)) + end.check_request do |_endpoint, data, _headers| + soft_descriptors_regex = %r("soft_descriptors":{"dba_name":"Caddyshack","street":"1234 Any Street","city":"Durham","region":"North Carolina","mid":"mid_1234","mcc":"mcc_5678","postal_code":"27701","country_code":"US","merchant_contact_info":"8885551212"}) + assert_match soft_descriptors_regex, data + end.respond_with(successful_refund_response) + + assert_success response + end + + def test_successful_general_credit_with_order_id + response = stub_comms do + @gateway.credit(@amount, @credit_card, @options.merge(order_id: 1234)) + end.check_request do |_endpoint, data, _headers| + assert_match(/\"merchant_ref\":1234/, data) + end.respond_with(successful_refund_response) + + assert_success response + end + def test_successful_void - @gateway.expects(:ssl_post).returns(successful_void_response) - assert response = @gateway.void(@authorization, @options) + response = stub_comms do + @gateway.void(@authorization, @options) + end.check_request do |_endpoint, data, _headers| + json = '{"transaction_type":"void","method":"credit_card","transaction_tag":"106625152","currency_code":"USD","amount":"4738"}' + assert_match json, data + end.respond_with(successful_void_response) + assert_success response end + def test_successful_void_with_reversal_id + stub_comms do + @gateway.void(@authorization, @options.merge(reversal_id: @reversal_id)) + end.check_request do |_endpoint, data, _headers| + json = "{\"transaction_type\":\"void\",\"method\":\"credit_card\",\"reversal_id\":\"#{@reversal_id}\",\"currency_code\":\"USD\",\"amount\":\"4738\"}" + assert_match json, data + end.respond_with(successful_void_response) + end + def test_failed_void @gateway.expects(:ssl_post).raises(failed_void_response) assert response = @gateway.void(@authorization, @options) @@ -205,16 +468,18 @@ def test_invalid_transaction_tag assert response = @gateway.capture(@amount, @authorization) assert_instance_of Response, response assert_failure response - assert_equal response.error_code, 'server_error' + error_msg = response.params['Error']['messages'] + error_code = error_msg.map { |x| x.values[0] } + assert_equal error_code[0], 'server_error' assert_equal response.message, 'ProcessedBad Request (69) - Invalid Transaction Tag' end def test_supported_countries - assert_equal ['CA', 'US'].sort, PayeezyGateway.supported_countries.sort + assert_equal %w[CA US].sort, PayeezyGateway.supported_countries.sort end def test_supported_cardtypes - assert_equal [:visa, :master, :american_express, :discover, :jcb, :diners_club], PayeezyGateway.supported_cardtypes + assert_equal %i[visa master american_express discover jcb diners_club], PayeezyGateway.supported_cardtypes end def test_avs_result @@ -234,7 +499,7 @@ def test_cvv_result def test_requests_include_verification_string stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| json_address = '{"street":"456 My Street","city":"Ottawa","state_province":"ON","zip_postal_code":"K1C2N6","country":"CA"}' assert_match json_address, data end.respond_with(successful_purchase_response) @@ -269,497 +534,603 @@ def test_scrub_echeck assert_equal @gateway.scrub(pre_scrubbed_echeck), post_scrubbed_echeck end + def test_scrub_network_token + assert_equal @gateway.scrub(pre_scrubbed_network_token), post_scrubbed_network_token + end + private def pre_scrubbed - <<-TRANSCRIPT - opening connection to api-cert.payeezy.com:443... - opened - starting SSL for api-cert.payeezy.com:443... - SSL established - <- "POST /v1/transactions HTTP/1.1\r\nContent-Type: application/json\r\nApikey: oKB61AAxbN3xwC6gVAH3dp58FmioHSAT\r\nToken: fdoa-a480ce8951daa73262734cf102641994c1e55e7cdf4c02b6\r\nNonce: 5803993876.636232\r\nTimestamp: 1449523748359\r\nAuthorization: NGRlZjJkMWNlMDc5NGI5OTVlYTQxZDRkOGQ4NjRhNmZhNDgwZmIyNTZkMWJhN2M3MDdkNDI0ZWI1OGUwMGExMA==\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: api-cert.payeezy.com\r\nContent-Length: 365\r\n\r\n" - <- "{\"transaction_type\":\"purchase\",\"merchant_ref\":null,\"method\":\"credit_card\",\"credit_card\":{\"type\":\"Visa\",\"cardholder_name\":\"Longbob Longsen\",\"card_number\":\"4242424242424242\",\"exp_date\":\"0916\",\"cvv\":\"123\"},\"billing_address\":{\"street\":\"456 My Street\",\"city\":\"Ottawa\",\"state_province\":\"ON\",\"zip_postal_code\":\"K1C2N6\",\"country\":\"CA\"},\"currency_code\":\"USD\",\"amount\":\"100\"}" - -> "HTTP/1.1 201 Created\r\n" - -> "Access-Control-Allow-Headers: Content-Type, apikey, token\r\n" - -> "Access-Control-Allow-Methods: GET, PUT, POST, DELETE\r\n" - -> "Access-Control-Allow-Origin: http://localhost:8080\r\n" - -> "Access-Control-Max-Age: 3628800\r\n" - -> "Access-Control-Request-Headers: origin, x-requested-with, accept, content-type\r\n" - -> "Content-Language: en-US\r\n" - -> "Content-Type: application/json;charset=UTF-8\r\n" - -> "Date: Mon, 07 Dec 2015 21:29:08 GMT\r\n" - -> "OPTR_CXT: 0100010000e4b64c5c-53c6-4f8b-aab6-b7950e2a40c100000000-0000-0000-0000-000000000000-1 HTTP ;\r\n" - -> "Server: Apigee Router\r\n" - -> "X-Archived-Client-IP: 10.180.205.250\r\n" - -> "X-Backside-Transport: OK OK,OK OK\r\n" - -> "X-Client-IP: 10.180.205.250,54.236.202.5\r\n" - -> "X-Global-Transaction-ID: 74768541\r\n" - -> "X-Powered-By: Servlet/3.0\r\n" - -> "Content-Length: 549\r\n" - -> "Connection: Close\r\n" - -> "\r\n" - reading 549 bytes... - -> "{\"correlation_id\":\"228.1449523748595\",\"transaction_status\":\"approved\",\"validation_status\":\"success\",\"transaction_type\":\"purchase\",\"transaction_id\":\"ET189831\",\"transaction_tag\":\"69607700\",\"method\":\"credit_card\",\"amount\":\"100\",\"currency\":\"USD\",\"avs\":\"4\",\"cvv2\":\"M\",\"token\":{\"token_type\":\"FDToken\",\"token_data\":{\"value\":\"1950935021264242\"}},\"card\":{\"type\":\"Visa\",\"cardholder_name\":\"Longbob Longsen\",\"card_number\":\"4242\",\"exp_date\":\"0916\"},\"bank_resp_code\":\"100\",\"bank_message\":\"Approved\",\"gateway_resp_code\":\"00\",\"gateway_message\":\"Transaction Normal\"}" - read 549 bytes - Conn close + <<~TRANSCRIPT + opening connection to api-cert.payeezy.com:443... + opened + starting SSL for api-cert.payeezy.com:443... + SSL established + <- "POST /v1/transactions HTTP/1.1\r\nContent-Type: application/json\r\nApikey: oKB61AAxbN3xwC6gVAH3dp58FmioHSAT\r\nToken: fdoa-a480ce8951daa73262734cf102641994c1e55e7cdf4c02b6\r\nNonce: 5803993876.636232\r\nTimestamp: 1449523748359\r\nAuthorization: NGRlZjJkMWNlMDc5NGI5OTVlYTQxZDRkOGQ4NjRhNmZhNDgwZmIyNTZkMWJhN2M3MDdkNDI0ZWI1OGUwMGExMA==\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: api-cert.payeezy.com\r\nContent-Length: 365\r\n\r\n" + <- "{\"transaction_type\":\"purchase\",\"merchant_ref\":null,\"method\":\"credit_card\",\"credit_card\":{\"type\":\"Visa\",\"cardholder_name\":\"Longbob Longsen\",\"card_number\":\"4242424242424242\",\"exp_date\":\"0916\",\"cvv\":\"123\"},\"billing_address\":{\"street\":\"456 My Street\",\"city\":\"Ottawa\",\"state_province\":\"ON\",\"zip_postal_code\":\"K1C2N6\",\"country\":\"CA\"},\"currency_code\":\"USD\",\"amount\":\"100\"}" + -> "HTTP/1.1 201 Created\r\n" + -> "Access-Control-Allow-Headers: Content-Type, apikey, token\r\n" + -> "Access-Control-Allow-Methods: GET, PUT, POST, DELETE\r\n" + -> "Access-Control-Allow-Origin: http://localhost:8080\r\n" + -> "Access-Control-Max-Age: 3628800\r\n" + -> "Access-Control-Request-Headers: origin, x-requested-with, accept, content-type\r\n" + -> "Content-Language: en-US\r\n" + -> "Content-Type: application/json;charset=UTF-8\r\n" + -> "Date: Mon, 07 Dec 2015 21:29:08 GMT\r\n" + -> "OPTR_CXT: 0100010000e4b64c5c-53c6-4f8b-aab6-b7950e2a40c100000000-0000-0000-0000-000000000000-1 HTTP ;\r\n" + -> "Server: Apigee Router\r\n" + -> "X-Archived-Client-IP: 10.180.205.250\r\n" + -> "X-Backside-Transport: OK OK,OK OK\r\n" + -> "X-Client-IP: 10.180.205.250,54.236.202.5\r\n" + -> "X-Global-Transaction-ID: 74768541\r\n" + -> "X-Powered-By: Servlet/3.0\r\n" + -> "Content-Length: 549\r\n" + -> "Connection: Close\r\n" + -> "\r\n" + reading 549 bytes... + -> "{\"correlation_id\":\"228.1449523748595\",\"transaction_status\":\"approved\",\"validation_status\":\"success\",\"transaction_type\":\"purchase\",\"transaction_id\":\"ET189831\",\"transaction_tag\":\"69607700\",\"method\":\"credit_card\",\"amount\":\"100\",\"currency\":\"USD\",\"avs\":\"4\",\"cvv2\":\"M\",\"token\":{\"token_type\":\"FDToken\",\"token_data\":{\"value\":\"1950935021264242\"}},\"card\":{\"type\":\"Visa\",\"cardholder_name\":\"Longbob Longsen\",\"card_number\":\"4242\",\"exp_date\":\"0916\"},\"bank_resp_code\":\"100\",\"bank_message\":\"Approved\",\"gateway_resp_code\":\"00\",\"gateway_message\":\"Transaction Normal\"}" + read 549 bytes + Conn close TRANSCRIPT end def post_scrubbed - <<-TRANSCRIPT - opening connection to api-cert.payeezy.com:443... - opened - starting SSL for api-cert.payeezy.com:443... - SSL established - <- "POST /v1/transactions HTTP/1.1\r\nContent-Type: application/json\r\nApikey: [FILTERED]\r\nToken: [FILTERED]\r\nNonce: 5803993876.636232\r\nTimestamp: 1449523748359\r\nAuthorization: NGRlZjJkMWNlMDc5NGI5OTVlYTQxZDRkOGQ4NjRhNmZhNDgwZmIyNTZkMWJhN2M3MDdkNDI0ZWI1OGUwMGExMA==\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: api-cert.payeezy.com\r\nContent-Length: 365\r\n\r\n" - <- "{\"transaction_type\":\"purchase\",\"merchant_ref\":null,\"method\":\"credit_card\",\"credit_card\":{\"type\":\"Visa\",\"cardholder_name\":\"Longbob Longsen\",\"card_number\":\"[FILTERED]\",\"exp_date\":\"0916\",\"cvv\":\"[FILTERED]\"},\"billing_address\":{\"street\":\"456 My Street\",\"city\":\"Ottawa\",\"state_province\":\"ON\",\"zip_postal_code\":\"K1C2N6\",\"country\":\"CA\"},\"currency_code\":\"USD\",\"amount\":\"100\"}" - -> "HTTP/1.1 201 Created\r\n" - -> "Access-Control-Allow-Headers: Content-Type, apikey, token\r\n" - -> "Access-Control-Allow-Methods: GET, PUT, POST, DELETE\r\n" - -> "Access-Control-Allow-Origin: http://localhost:8080\r\n" - -> "Access-Control-Max-Age: 3628800\r\n" - -> "Access-Control-Request-Headers: origin, x-requested-with, accept, content-type\r\n" - -> "Content-Language: en-US\r\n" - -> "Content-Type: application/json;charset=UTF-8\r\n" - -> "Date: Mon, 07 Dec 2015 21:29:08 GMT\r\n" - -> "OPTR_CXT: 0100010000e4b64c5c-53c6-4f8b-aab6-b7950e2a40c100000000-0000-0000-0000-000000000000-1 HTTP ;\r\n" - -> "Server: Apigee Router\r\n" - -> "X-Archived-Client-IP: 10.180.205.250\r\n" - -> "X-Backside-Transport: OK OK,OK OK\r\n" - -> "X-Client-IP: 10.180.205.250,54.236.202.5\r\n" - -> "X-Global-Transaction-ID: 74768541\r\n" - -> "X-Powered-By: Servlet/3.0\r\n" - -> "Content-Length: 549\r\n" - -> "Connection: Close\r\n" - -> "\r\n" - reading 549 bytes... - -> "{\"correlation_id\":\"228.1449523748595\",\"transaction_status\":\"approved\",\"validation_status\":\"success\",\"transaction_type\":\"purchase\",\"transaction_id\":\"ET189831\",\"transaction_tag\":\"69607700\",\"method\":\"credit_card\",\"amount\":\"100\",\"currency\":\"USD\",\"avs\":\"4\",\"cvv2\":\"M\",\"token\":{\"token_type\":\"FDToken\",\"token_data\":{\"value\":\"1950935021264242\"}},\"card\":{\"type\":\"Visa\",\"cardholder_name\":\"Longbob Longsen\",\"card_number\":\"[FILTERED]\",\"exp_date\":\"0916\"},\"bank_resp_code\":\"100\",\"bank_message\":\"Approved\",\"gateway_resp_code\":\"00\",\"gateway_message\":\"Transaction Normal\"}" - read 549 bytes - Conn close + <<~TRANSCRIPT + opening connection to api-cert.payeezy.com:443... + opened + starting SSL for api-cert.payeezy.com:443... + SSL established + <- "POST /v1/transactions HTTP/1.1\r\nContent-Type: application/json\r\nApikey: [FILTERED]\r\nToken: [FILTERED]\r\nNonce: 5803993876.636232\r\nTimestamp: 1449523748359\r\nAuthorization: NGRlZjJkMWNlMDc5NGI5OTVlYTQxZDRkOGQ4NjRhNmZhNDgwZmIyNTZkMWJhN2M3MDdkNDI0ZWI1OGUwMGExMA==\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: api-cert.payeezy.com\r\nContent-Length: 365\r\n\r\n" + <- "{\"transaction_type\":\"purchase\",\"merchant_ref\":null,\"method\":\"credit_card\",\"credit_card\":{\"type\":\"Visa\",\"cardholder_name\":\"Longbob Longsen\",\"card_number\":\"[FILTERED]\",\"exp_date\":\"0916\",\"cvv\":\"[FILTERED]\"},\"billing_address\":{\"street\":\"456 My Street\",\"city\":\"Ottawa\",\"state_province\":\"ON\",\"zip_postal_code\":\"K1C2N6\",\"country\":\"CA\"},\"currency_code\":\"USD\",\"amount\":\"100\"}" + -> "HTTP/1.1 201 Created\r\n" + -> "Access-Control-Allow-Headers: Content-Type, apikey, token\r\n" + -> "Access-Control-Allow-Methods: GET, PUT, POST, DELETE\r\n" + -> "Access-Control-Allow-Origin: http://localhost:8080\r\n" + -> "Access-Control-Max-Age: 3628800\r\n" + -> "Access-Control-Request-Headers: origin, x-requested-with, accept, content-type\r\n" + -> "Content-Language: en-US\r\n" + -> "Content-Type: application/json;charset=UTF-8\r\n" + -> "Date: Mon, 07 Dec 2015 21:29:08 GMT\r\n" + -> "OPTR_CXT: 0100010000e4b64c5c-53c6-4f8b-aab6-b7950e2a40c100000000-0000-0000-0000-000000000000-1 HTTP ;\r\n" + -> "Server: Apigee Router\r\n" + -> "X-Archived-Client-IP: 10.180.205.250\r\n" + -> "X-Backside-Transport: OK OK,OK OK\r\n" + -> "X-Client-IP: 10.180.205.250,54.236.202.5\r\n" + -> "X-Global-Transaction-ID: 74768541\r\n" + -> "X-Powered-By: Servlet/3.0\r\n" + -> "Content-Length: 549\r\n" + -> "Connection: Close\r\n" + -> "\r\n" + reading 549 bytes... + -> "{\"correlation_id\":\"228.1449523748595\",\"transaction_status\":\"approved\",\"validation_status\":\"success\",\"transaction_type\":\"purchase\",\"transaction_id\":\"ET189831\",\"transaction_tag\":\"69607700\",\"method\":\"credit_card\",\"amount\":\"100\",\"currency\":\"USD\",\"avs\":\"4\",\"cvv2\":\"M\",\"token\":{\"token_type\":\"FDToken\",\"token_data\":{\"value\":\"1950935021264242\"}},\"card\":{\"type\":\"Visa\",\"cardholder_name\":\"Longbob Longsen\",\"card_number\":\"[FILTERED]\",\"exp_date\":\"0916\"},\"bank_resp_code\":\"100\",\"bank_message\":\"Approved\",\"gateway_resp_code\":\"00\",\"gateway_message\":\"Transaction Normal\"}" + read 549 bytes + Conn close TRANSCRIPT end def pre_scrubbed_echeck - <<-TRANSCRIPT - {\"transaction_type\":\"purchase\",\"merchant_ref\":null,\"method\":\"tele_check\",\"tele_check\":{\"check_number\":\"1\",\"check_type\":\"P\",\"routing_number\":\"244183602\",\"account_number\":\"15378535\",\"accountholder_name\":\"Jim Smith\"},\"billing_address\":{\"street\":\"456 My Street\",\"city\":\"Ottawa\",\"state_province\":\"ON\",\"zip_postal_code\":\"K1C2N6\",\"country\":\"CA\"},\"currency_code\":\"USD\",\"amount\":\"100\"}" - -> "HTTP/1.1 201 Created\r\n" - -> "Access-Control-Allow-Headers: Content-Type, apikey, token\r\n" - -> "Access-Control-Allow-Methods: GET, PUT, POST, DELETE\r\n" - -> "Access-Control-Allow-Origin: http://localhost:8080\r\n" - -> "Access-Control-Max-Age: 3628800\r\n" - -> "Access-Control-Request-Headers: origin, x-requested-with, accept, content-type\r\n" - -> "Content-Language: en-US\r\n" - -> "Content-Type: application/json;charset=UTF-8\r\n" - -> "Date: Wed, 09 Dec 2015 19:33:14 GMT\r\n" - -> "OPTR_CXT: 0100010000094b4179-bed8-4068-b077-d8679a20046f00000000-0000-0000-0000-000000000000-1 HTTP ;\r\n" - -> "Server: Apigee Router\r\n" - -> "X-Archived-Client-IP: 10.180.205.250\r\n" - -> "X-Backside-Transport: OK OK,OK OK\r\n" - -> "X-Client-IP: 10.180.205.250,107.23.55.229\r\n" - -> "X-Global-Transaction-ID: 97138449\r\n" - -> "X-Powered-By: Servlet/3.0\r\n" - -> "Content-Length: 491\r\n" - -> "Connection: Close\r\n" - -> "\r\n" - reading 491 bytes... - -> "{\"correlation_id\":\"228.1449689594381\",\"transaction_status\":\"approved\",\"validation_status\":\"success\",\"transaction_type\":\"purchase\",\"transaction_id\":\"ET196703\",\"transaction_tag\":\"69865571\",\"method\":\"tele_check\",\"amount\":\"100\",\"currency\":\"USD\",\"bank_resp_code\":\"100\",\"bank_message\":\"Approved\",\"gateway_resp_code\":\"00\",\"gateway_message\":\"Transaction Normal\",\"tele_check\":{\"accountholder_name\":\"Jim Smith\",\"check_number\":\"1\",\"check_type\":\"P\",\"account_number\":\"8535\",\"routing_number\":\"244183602\"}} + <<~TRANSCRIPT + {\"transaction_type\":\"purchase\",\"merchant_ref\":null,\"method\":\"tele_check\",\"tele_check\":{\"check_number\":\"1\",\"check_type\":\"P\",\"routing_number\":\"244183602\",\"account_number\":\"15378535\",\"accountholder_name\":\"Jim Smith\"},\"billing_address\":{\"street\":\"456 My Street\",\"city\":\"Ottawa\",\"state_province\":\"ON\",\"zip_postal_code\":\"K1C2N6\",\"country\":\"CA\"},\"currency_code\":\"USD\",\"amount\":\"100\"}" + -> "HTTP/1.1 201 Created\r\n" + -> "Access-Control-Allow-Headers: Content-Type, apikey, token\r\n" + -> "Access-Control-Allow-Methods: GET, PUT, POST, DELETE\r\n" + -> "Access-Control-Allow-Origin: http://localhost:8080\r\n" + -> "Access-Control-Max-Age: 3628800\r\n" + -> "Access-Control-Request-Headers: origin, x-requested-with, accept, content-type\r\n" + -> "Content-Language: en-US\r\n" + -> "Content-Type: application/json;charset=UTF-8\r\n" + -> "Date: Wed, 09 Dec 2015 19:33:14 GMT\r\n" + -> "OPTR_CXT: 0100010000094b4179-bed8-4068-b077-d8679a20046f00000000-0000-0000-0000-000000000000-1 HTTP ;\r\n" + -> "Server: Apigee Router\r\n" + -> "X-Archived-Client-IP: 10.180.205.250\r\n" + -> "X-Backside-Transport: OK OK,OK OK\r\n" + -> "X-Client-IP: 10.180.205.250,107.23.55.229\r\n" + -> "X-Global-Transaction-ID: 97138449\r\n" + -> "X-Powered-By: Servlet/3.0\r\n" + -> "Content-Length: 491\r\n" + -> "Connection: Close\r\n" + -> "\r\n" + reading 491 bytes... + -> "{\"correlation_id\":\"228.1449689594381\",\"transaction_status\":\"approved\",\"validation_status\":\"success\",\"transaction_type\":\"purchase\",\"transaction_id\":\"ET196703\",\"transaction_tag\":\"69865571\",\"method\":\"tele_check\",\"amount\":\"100\",\"currency\":\"USD\",\"bank_resp_code\":\"100\",\"bank_message\":\"Approved\",\"gateway_resp_code\":\"00\",\"gateway_message\":\"Transaction Normal\",\"tele_check\":{\"accountholder_name\":\"Jim Smith\",\"check_number\":\"1\",\"check_type\":\"P\",\"account_number\":\"8535\",\"routing_number\":\"244183602\"}} TRANSCRIPT end def post_scrubbed_echeck - <<-TRANSCRIPT - {\"transaction_type\":\"purchase\",\"merchant_ref\":null,\"method\":\"tele_check\",\"tele_check\":{\"check_number\":\"1\",\"check_type\":\"P\",\"routing_number\":\"[FILTERED]\",\"account_number\":\"[FILTERED]\",\"accountholder_name\":\"Jim Smith\"},\"billing_address\":{\"street\":\"456 My Street\",\"city\":\"Ottawa\",\"state_province\":\"ON\",\"zip_postal_code\":\"K1C2N6\",\"country\":\"CA\"},\"currency_code\":\"USD\",\"amount\":\"100\"}" - -> "HTTP/1.1 201 Created\r\n" - -> "Access-Control-Allow-Headers: Content-Type, apikey, token\r\n" - -> "Access-Control-Allow-Methods: GET, PUT, POST, DELETE\r\n" - -> "Access-Control-Allow-Origin: http://localhost:8080\r\n" - -> "Access-Control-Max-Age: 3628800\r\n" - -> "Access-Control-Request-Headers: origin, x-requested-with, accept, content-type\r\n" - -> "Content-Language: en-US\r\n" - -> "Content-Type: application/json;charset=UTF-8\r\n" - -> "Date: Wed, 09 Dec 2015 19:33:14 GMT\r\n" - -> "OPTR_CXT: 0100010000094b4179-bed8-4068-b077-d8679a20046f00000000-0000-0000-0000-000000000000-1 HTTP ;\r\n" - -> "Server: Apigee Router\r\n" - -> "X-Archived-Client-IP: 10.180.205.250\r\n" - -> "X-Backside-Transport: OK OK,OK OK\r\n" - -> "X-Client-IP: 10.180.205.250,107.23.55.229\r\n" - -> "X-Global-Transaction-ID: 97138449\r\n" - -> "X-Powered-By: Servlet/3.0\r\n" - -> "Content-Length: 491\r\n" - -> "Connection: Close\r\n" - -> "\r\n" - reading 491 bytes... - -> "{\"correlation_id\":\"228.1449689594381\",\"transaction_status\":\"approved\",\"validation_status\":\"success\",\"transaction_type\":\"purchase\",\"transaction_id\":\"ET196703\",\"transaction_tag\":\"69865571\",\"method\":\"tele_check\",\"amount\":\"100\",\"currency\":\"USD\",\"bank_resp_code\":\"100\",\"bank_message\":\"Approved\",\"gateway_resp_code\":\"00\",\"gateway_message\":\"Transaction Normal\",\"tele_check\":{\"accountholder_name\":\"Jim Smith\",\"check_number\":\"1\",\"check_type\":\"P\",\"account_number\":\"[FILTERED]\",\"routing_number\":\"[FILTERED]\"}} + <<~TRANSCRIPT + {\"transaction_type\":\"purchase\",\"merchant_ref\":null,\"method\":\"tele_check\",\"tele_check\":{\"check_number\":\"1\",\"check_type\":\"P\",\"routing_number\":\"[FILTERED]\",\"account_number\":\"[FILTERED]\",\"accountholder_name\":\"Jim Smith\"},\"billing_address\":{\"street\":\"456 My Street\",\"city\":\"Ottawa\",\"state_province\":\"ON\",\"zip_postal_code\":\"K1C2N6\",\"country\":\"CA\"},\"currency_code\":\"USD\",\"amount\":\"100\"}" + -> "HTTP/1.1 201 Created\r\n" + -> "Access-Control-Allow-Headers: Content-Type, apikey, token\r\n" + -> "Access-Control-Allow-Methods: GET, PUT, POST, DELETE\r\n" + -> "Access-Control-Allow-Origin: http://localhost:8080\r\n" + -> "Access-Control-Max-Age: 3628800\r\n" + -> "Access-Control-Request-Headers: origin, x-requested-with, accept, content-type\r\n" + -> "Content-Language: en-US\r\n" + -> "Content-Type: application/json;charset=UTF-8\r\n" + -> "Date: Wed, 09 Dec 2015 19:33:14 GMT\r\n" + -> "OPTR_CXT: 0100010000094b4179-bed8-4068-b077-d8679a20046f00000000-0000-0000-0000-000000000000-1 HTTP ;\r\n" + -> "Server: Apigee Router\r\n" + -> "X-Archived-Client-IP: 10.180.205.250\r\n" + -> "X-Backside-Transport: OK OK,OK OK\r\n" + -> "X-Client-IP: 10.180.205.250,107.23.55.229\r\n" + -> "X-Global-Transaction-ID: 97138449\r\n" + -> "X-Powered-By: Servlet/3.0\r\n" + -> "Content-Length: 491\r\n" + -> "Connection: Close\r\n" + -> "\r\n" + reading 491 bytes... + -> "{\"correlation_id\":\"228.1449689594381\",\"transaction_status\":\"approved\",\"validation_status\":\"success\",\"transaction_type\":\"purchase\",\"transaction_id\":\"ET196703\",\"transaction_tag\":\"69865571\",\"method\":\"tele_check\",\"amount\":\"100\",\"currency\":\"USD\",\"bank_resp_code\":\"100\",\"bank_message\":\"Approved\",\"gateway_resp_code\":\"00\",\"gateway_message\":\"Transaction Normal\",\"tele_check\":{\"accountholder_name\":\"Jim Smith\",\"check_number\":\"1\",\"check_type\":\"P\",\"account_number\":\"[FILTERED]\",\"routing_number\":\"[FILTERED]\"}} TRANSCRIPT end def pre_scrubbed_store - <<-TRANSCRIPT - opening connection to api-cert.payeezy.com:443... - opened - starting SSL for api-cert.payeezy.com:443... - SSL established - <- "GET /v1/securitytokens?apikey=UyDMTXx6TD9WErF6ynw7xeEfCAn8fcGs&js_security_key=js-f4c4b54f08d6c44c8cad3ea80bbf92c4f4c4b54f08d6c44c&ta_token=120&callback=Payeezy.callback&type=FDToken&credit_card.type=Visa&credit_card.cardholder_name=Longbob+Longsen&credit_card.card_number=4242424242424242&credit_card.exp_date=0919&credit_card.cvv=123 HTTP/1.1\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: api-cert.payeezy.com\r\n\r\n" - -> "HTTP/1.1 200 Success\r\n" - -> "Content-Language: en-US\r\n" - -> "Content-Type: application/json\r\n" - -> "correlation_id: 228.1574930196886\r\n" - -> "Date: Fri, 12 Jan 2018 09:28:22 GMT\r\n" - -> "statuscode: 201\r\n" - -> "X-Archived-Client-IP: 10.180.205.250\r\n" - -> "X-Backside-Transport: OK OK,OK OK\r\n" - -> "X-Client-IP: 10.180.205.250,54.218.45.37\r\n" - -> "X-Global-Transaction-ID: 463881989\r\n" - -> "X-Powered-By: Servlet/3.0\r\n" - -> "Content-Length: 266\r\n" - -> "Connection: Close\r\n" - -> "\r\n" - reading 266 bytes... - -> "\n Payeezy.callback({\n \t\"status\":201,\n \t\"results\":{\"correlation_id\":\"228.1574930196886\",\"status\":\"success\",\"type\":\"FDToken\",\"token\":{\"type\":\"Visa\",\"cardholder_name\":\"Longbob Longsen\",\"exp_date\":\"0919\",\"value\":\"2158545373614242\"}}\n })\n " - read 266 bytes - Conn close + <<~TRANSCRIPT + opening connection to api-cert.payeezy.com:443... + opened + starting SSL for api-cert.payeezy.com:443... + SSL established + <- "GET /v1/securitytokens?apikey=UyDMTXx6TD9WErF6ynw7xeEfCAn8fcGs&js_security_key=js-f4c4b54f08d6c44c8cad3ea80bbf92c4f4c4b54f08d6c44c&ta_token=120&callback=Payeezy.callback&type=FDToken&credit_card.type=Visa&credit_card.cardholder_name=Longbob+Longsen&credit_card.card_number=4242424242424242&credit_card.exp_date=0919&credit_card.cvv=123 HTTP/1.1\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: api-cert.payeezy.com\r\n\r\n" + -> "HTTP/1.1 200 Success\r\n" + -> "Content-Language: en-US\r\n" + -> "Content-Type: application/json\r\n" + -> "correlation_id: 228.1574930196886\r\n" + -> "Date: Fri, 12 Jan 2018 09:28:22 GMT\r\n" + -> "statuscode: 201\r\n" + -> "X-Archived-Client-IP: 10.180.205.250\r\n" + -> "X-Backside-Transport: OK OK,OK OK\r\n" + -> "X-Client-IP: 10.180.205.250,54.218.45.37\r\n" + -> "X-Global-Transaction-ID: 463881989\r\n" + -> "X-Powered-By: Servlet/3.0\r\n" + -> "Content-Length: 266\r\n" + -> "Connection: Close\r\n" + -> "\r\n" + reading 266 bytes... + -> "\n Payeezy.callback({\n \t\"status\":201,\n \t\"results\":{\"correlation_id\":\"228.1574930196886\",\"status\":\"success\",\"type\":\"FDToken\",\"token\":{\"type\":\"Visa\",\"cardholder_name\":\"Longbob Longsen\",\"exp_date\":\"0919\",\"value\":\"2158545373614242\"}}\n })\n " + read 266 bytes + Conn close TRANSCRIPT end def post_scrubbed_store - <<-TRANSCRIPT - opening connection to api-cert.payeezy.com:443... - opened - starting SSL for api-cert.payeezy.com:443... - SSL established - <- "GET /v1/securitytokens?apikey=[FILTERED]js_security_key=js-f4c4b54f08d6c44c8cad3ea80bbf92c4f4c4b54f08d6c44c&ta_token=120&callback=Payeezy.callback&type=FDToken&credit_card.type=Visa&credit_card.cardholder_name=Longbob+Longsen&credit_card.card_number=[FILTERED]credit_card.exp_date=0919&credit_card.cvv=[FILTERED] HTTP/1.1\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: api-cert.payeezy.com\r\n\r\n" - -> "HTTP/1.1 200 Success\r\n" - -> "Content-Language: en-US\r\n" - -> "Content-Type: application/json\r\n" - -> "correlation_id: 228.1574930196886\r\n" - -> "Date: Fri, 12 Jan 2018 09:28:22 GMT\r\n" - -> "statuscode: 201\r\n" - -> "X-Archived-Client-IP: 10.180.205.250\r\n" - -> "X-Backside-Transport: OK OK,OK OK\r\n" - -> "X-Client-IP: 10.180.205.250,54.218.45.37\r\n" - -> "X-Global-Transaction-ID: 463881989\r\n" - -> "X-Powered-By: Servlet/3.0\r\n" - -> "Content-Length: 266\r\n" - -> "Connection: Close\r\n" - -> "\r\n" - reading 266 bytes... - -> "\n Payeezy.callback({\n \t\"status\":201,\n \t\"results\":{\"correlation_id\":\"228.1574930196886\",\"status\":\"success\",\"type\":\"FDToken\",\"token\":{\"type\":\"Visa\",\"cardholder_name\":\"Longbob Longsen\",\"exp_date\":\"0919\",\"value\":\"2158545373614242\"}}\n })\n " - read 266 bytes - Conn close + <<~TRANSCRIPT + opening connection to api-cert.payeezy.com:443... + opened + starting SSL for api-cert.payeezy.com:443... + SSL established + <- "GET /v1/securitytokens?apikey=[FILTERED]js_security_key=js-f4c4b54f08d6c44c8cad3ea80bbf92c4f4c4b54f08d6c44c&ta_token=120&callback=Payeezy.callback&type=FDToken&credit_card.type=Visa&credit_card.cardholder_name=Longbob+Longsen&credit_card.card_number=[FILTERED]credit_card.exp_date=0919&credit_card.cvv=[FILTERED] HTTP/1.1\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: api-cert.payeezy.com\r\n\r\n" + -> "HTTP/1.1 200 Success\r\n" + -> "Content-Language: en-US\r\n" + -> "Content-Type: application/json\r\n" + -> "correlation_id: 228.1574930196886\r\n" + -> "Date: Fri, 12 Jan 2018 09:28:22 GMT\r\n" + -> "statuscode: 201\r\n" + -> "X-Archived-Client-IP: 10.180.205.250\r\n" + -> "X-Backside-Transport: OK OK,OK OK\r\n" + -> "X-Client-IP: 10.180.205.250,54.218.45.37\r\n" + -> "X-Global-Transaction-ID: 463881989\r\n" + -> "X-Powered-By: Servlet/3.0\r\n" + -> "Content-Length: 266\r\n" + -> "Connection: Close\r\n" + -> "\r\n" + reading 266 bytes... + -> "\n Payeezy.callback({\n \t\"status\":201,\n \t\"results\":{\"correlation_id\":\"228.1574930196886\",\"status\":\"success\",\"type\":\"FDToken\",\"token\":{\"type\":\"Visa\",\"cardholder_name\":\"Longbob Longsen\",\"exp_date\":\"0919\",\"value\":\"2158545373614242\"}}\n })\n " + read 266 bytes + Conn close + TRANSCRIPT + end + + def pre_scrubbed_network_token + <<~TRANSCRIPT + opening connection to api-cert.payeezy.com:443... + opened + starting SSL for api-cert.payeezy.com:443... + SSL established + <- "POST /v1/transactions HTTP/1.1\r\nContent-Type: application/json\r\nApikey: oKB61AAxbN3xwC6gVAH3dp58FmioHSAT\r\nToken: fdoa-a480ce8951daa73262734cf102641994c1e55e7cdf4c02b6\r\nNonce: 2713241561.4909368\r\nTimestamp: 1668784714406\r\nAuthorization: NDU2ZWRiNmUwMmUxNGMwOGIwYjMxYTAxMDkzZDcwNWNhM2Y0ODExNmRmMTNjNDVjMTFhODMyNTg4NDdiNzZiNw==\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: api-cert.payeezy.com\r\nContent-Length: 462\r\n\r\n" + <- "{\"transaction_type\":\"purchase\",\"merchant_ref\":null,\"3DS\":{\"type\":\"D\",\"cardholder_name\":\"Longbob\",\"card_number\":\"4761209980011439\",\"exp_date\":\"1122\",\"cvv\":569,\"xid\":\"YwAAAAAABaYcCMX/OhNRQAAAAAA=\",\"cavv\":\"YwAAAAAABaYcCMX/OhNRQAAAAAA=\",\"wallet_provider_id\":\"APPLE_PAY\"},\"method\":\"3DS\",\"eci_indicator\":5,\"billing_address\":{\"street\":\"456 My Street\",\"city\":\"Ottawa\",\"state_province\":\"ON\",\"zip_postal_code\":\"K1C2N6\",\"country\":\"CA\"},\"currency_code\":\"USD\",\"amount\":\"100\"}" + -> "HTTP/1.1 201 Created\r\n" + -> "Date: Fri, 18 Nov 2022 15:18:35 GMT\r\n" + -> "Content-Type: application/json;charset=UTF-8\r\n" + -> "Connection: close\r\n" + -> "X-Backside-Transport: OK OK,OK OK\r\n" + -> "Content-Language: en-US\r\n" + -> "X-Global-Transaction-ID: 7f41427d6377a24aa50b34df\r\n" + -> "Strict-Transport-Security: max-age=31536000; includeSubDomains\r\n" + -> "X-Xss-Protection: 1; mode=block\r\n" + -> "Cache-Control: no-store, no-cache, must-revalidate\r\n" + -> "Pragma: no-cache\r\n" + -> "X-Frame-Options: SAMEORIGIN\r\n" + -> "Referrer-Policy: strict-origin\r\n" + -> "Feature-Policy: vibrate 'self'\r\n" + -> "Content-Security-Policy: default-src 'none'; frame-ancestors 'self'; script-src 'unsafe-inline' 'self' *.googleapis.com *.klarna.com *.masterpass.com *.mastercard.com *.newrelic.com *.npci.org.in *.nr-data.net *.google-analytics.com *.google.com *.getsitecontrol.com *.gstatic.com *.kxcdn.com 'strict-dynamic' 'nonce-6f62fa22a79de4c553d2bbde' 'unsafe-eval' 'unsafe-inline'; connect-src 'self'; img-src 'self'; style-src 'self'; base-uri 'self';\r\n" + -> "Access-Control-Allow-Origin: http://localhost:8080\r\n" + -> "Access-Control-Request-Headers: origin, x-requested-with, accept, content-type\r\n" + -> "Access-Control-Max-Age: 3628800\r\n" + -> "Access-Control-Allow-Methods: GET, PUT, POST, DELETE\r\n" + -> "Access-Control-Allow-Headers: Content-Type, apikey, token\r\n" + -> "Via: 1.1 dca1-bit16021\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "\r\n" + -> "249\r\n" + reading 585 bytes... + -> "{\"correlation_id\":\"134.6878471461658\",\"transaction_status\":\"approved\",\"validation_status\":\"success\",\"transaction_type\":\"purchase\",\"transaction_id\":\"ET188163\",\"transaction_tag\":\"10032826722\",\"method\":\"3ds\",\"amount\":\"100\",\"currency\":\"USD\",\"avs\":\"4\",\"cvv2\":\"U\",\"token\":{\"token_type\":\"FDToken\",\"token_data\":{\"value\":\"9324008290401439\"}},\"card\":{\"type\":\"VISA\",\"cardholder_name\":\"Longbob\",\"card_number\":\"1439\",\"exp_date\":\"1122\"},\"bank_resp_code\":\"100\",\"bank_message\":\"Approved\",\"gateway_resp_code\":\"00\",\"gateway_message\":\"Transaction Normal\",\"eCommerce_flag\":\"5\",\"retrieval_ref_no\":\"221118\"}" + read 585 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + TRANSCRIPT + end + + def post_scrubbed_network_token + <<~TRANSCRIPT + opening connection to api-cert.payeezy.com:443... + opened + starting SSL for api-cert.payeezy.com:443... + SSL established + <- "POST /v1/transactions HTTP/1.1\r\nContent-Type: application/json\r\nApikey: [FILTERED]\r\nToken: [FILTERED]\r\nNonce: 2713241561.4909368\r\nTimestamp: 1668784714406\r\nAuthorization: NDU2ZWRiNmUwMmUxNGMwOGIwYjMxYTAxMDkzZDcwNWNhM2Y0ODExNmRmMTNjNDVjMTFhODMyNTg4NDdiNzZiNw==\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: api-cert.payeezy.com\r\nContent-Length: 462\r\n\r\n" + <- "{\"transaction_type\":\"purchase\",\"merchant_ref\":null,\"3DS\":{\"type\":\"D\",\"cardholder_name\":\"Longbob\",\"card_number\":\"[FILTERED]\",\"exp_date\":\"1122\",\"cvv\":[FILTERED],\"xid\":[FILTERED],\"cavv\":[FILTERED],\"wallet_provider_id\":\"APPLE_PAY\"},\"method\":\"3DS\",\"eci_indicator\":5,\"billing_address\":{\"street\":\"456 My Street\",\"city\":\"Ottawa\",\"state_province\":\"ON\",\"zip_postal_code\":\"K1C2N6\",\"country\":\"CA\"},\"currency_code\":\"USD\",\"amount\":\"100\"}" + -> "HTTP/1.1 201 Created\r\n" + -> "Date: Fri, 18 Nov 2022 15:18:35 GMT\r\n" + -> "Content-Type: application/json;charset=UTF-8\r\n" + -> "Connection: close\r\n" + -> "X-Backside-Transport: OK OK,OK OK\r\n" + -> "Content-Language: en-US\r\n" + -> "X-Global-Transaction-ID: 7f41427d6377a24aa50b34df\r\n" + -> "Strict-Transport-Security: max-age=31536000; includeSubDomains\r\n" + -> "X-Xss-Protection: 1; mode=block\r\n" + -> "Cache-Control: no-store, no-cache, must-revalidate\r\n" + -> "Pragma: no-cache\r\n" + -> "X-Frame-Options: SAMEORIGIN\r\n" + -> "Referrer-Policy: strict-origin\r\n" + -> "Feature-Policy: vibrate 'self'\r\n" + -> "Content-Security-Policy: default-src 'none'; frame-ancestors 'self'; script-src 'unsafe-inline' 'self' *.googleapis.com *.klarna.com *.masterpass.com *.mastercard.com *.newrelic.com *.npci.org.in *.nr-data.net *.google-analytics.com *.google.com *.getsitecontrol.com *.gstatic.com *.kxcdn.com 'strict-dynamic' 'nonce-6f62fa22a79de4c553d2bbde' 'unsafe-eval' 'unsafe-inline'; connect-src 'self'; img-src 'self'; style-src 'self'; base-uri 'self';\r\n" + -> "Access-Control-Allow-Origin: http://localhost:8080\r\n" + -> "Access-Control-Request-Headers: origin, x-requested-with, accept, content-type\r\n" + -> "Access-Control-Max-Age: 3628800\r\n" + -> "Access-Control-Allow-Methods: GET, PUT, POST, DELETE\r\n" + -> "Access-Control-Allow-Headers: Content-Type, apikey, token\r\n" + -> "Via: 1.1 dca1-bit16021\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "\r\n" + -> "249\r\n" + reading 585 bytes... + -> "{\"correlation_id\":\"134.6878471461658\",\"transaction_status\":\"approved\",\"validation_status\":\"success\",\"transaction_type\":\"purchase\",\"transaction_id\":\"ET188163\",\"transaction_tag\":\"10032826722\",\"method\":\"3ds\",\"amount\":\"100\",\"currency\":\"USD\",\"avs\":\"4\",\"cvv2\":\"U\",\"token\":{\"token_type\":\"FDToken\",\"token_data\":{\"value\":\"9324008290401439\"}},\"card\":{\"type\":\"VISA\",\"cardholder_name\":\"Longbob\",\"card_number\":\"[FILTERED]\",\"exp_date\":\"1122\"},\"bank_resp_code\":\"100\",\"bank_message\":\"Approved\",\"gateway_resp_code\":\"00\",\"gateway_message\":\"Transaction Normal\",\"eCommerce_flag\":\"5\",\"retrieval_ref_no\":\"221118\"}" + read 585 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close TRANSCRIPT end def successful_purchase_response - <<-RESPONSE - {\"method\":\"credit_card\",\"amount\":\"1\",\"currency\":\"USD\",\"avs\":\"4\",\"card\":{\"type\":\"Visa\",\"cardholder_name\":\"Bobsen 995\",\"card_number\":\"4242\",\"exp_date\":\"0816\"},\"token\":{\"token_type\":\"transarmor\",\"token_data\":{\"value\":\"0152552999534242\"}},\"transaction_status\":\"approved\",\"validation_status\":\"success\",\"transaction_type\":\"purchase\",\"transaction_id\":\"ET114541\",\"transaction_tag\":\"55083431\",\"bank_resp_code\":\"100\",\"bank_message\":\"Approved\",\"gateway_resp_code\":\"00\",\"gateway_message\":\"Transaction Normal\",\"correlation_id\":\"124.1433862672836\"} + <<~RESPONSE + {\"method\":\"credit_card\",\"amount\":\"1\",\"currency\":\"USD\",\"avs\":\"4\",\"card\":{\"type\":\"Visa\",\"cardholder_name\":\"Bobsen 995\",\"card_number\":\"4242\",\"exp_date\":\"0816\"},\"token\":{\"token_type\":\"transarmor\",\"token_data\":{\"value\":\"0152552999534242\"}},\"transaction_status\":\"approved\",\"validation_status\":\"success\",\"transaction_type\":\"purchase\",\"transaction_id\":\"ET114541\",\"transaction_tag\":\"55083431\",\"bank_resp_code\":\"100\",\"bank_message\":\"Approved\",\"gateway_resp_code\":\"00\",\"gateway_message\":\"Transaction Normal\",\"correlation_id\":\"124.1433862672836\"} RESPONSE end + def successful_purchase_stored_credentials_response + '{"correlation_id":"228.4479800174823","transaction_status":"approved","validation_status":"success","transaction_type":"purchase","transaction_id":"ET117353","transaction_tag":"2309866208","method":"credit_card","amount":"100","currency":"USD","avs":"4","cvv2":"M","token":{"token_type":"FDToken","token_data":{"value":"9091469151414242"}},"card":{"type":"Visa","cardholder_name":"Longbob Longsen","card_number":"4242","exp_date":"0919"},"bank_resp_code":"100","bank_message":"Approved","gateway_resp_code":"00","gateway_message":"Transaction Normal","stored_credentials":{"cardbrand_original_transaction_id":"706838021010062"}}' + end + def successful_purchase_echeck_response - <<-RESPONSE - {\"correlation_id\":\"228.1449688619062\",\"transaction_status\":\"approved\",\"validation_status\":\"success\",\"transaction_type\":\"purchase\",\"transaction_id\":\"ET133078\",\"transaction_tag\":\"69864362\",\"method\":\"tele_check\",\"amount\":\"100\",\"currency\":\"USD\",\"bank_resp_code\":\"100\",\"bank_message\":\"Approved\",\"gateway_resp_code\":\"00\",\"gateway_message\":\"Transaction Normal\",\"tele_check\":{\"accountholder_name\":\"Jim Smith\",\"check_number\":\"1\",\"check_type\":\"P\",\"account_number\":\"8535\",\"routing_number\":\"244183602\"}} + <<~RESPONSE + {\"correlation_id\":\"228.1449688619062\",\"transaction_status\":\"approved\",\"validation_status\":\"success\",\"transaction_type\":\"purchase\",\"transaction_id\":\"ET133078\",\"transaction_tag\":\"69864362\",\"method\":\"tele_check\",\"amount\":\"100\",\"currency\":\"USD\",\"bank_resp_code\":\"100\",\"bank_message\":\"Approved\",\"gateway_resp_code\":\"00\",\"gateway_message\":\"Transaction Normal\",\"tele_check\":{\"accountholder_name\":\"Jim Smith\",\"check_number\":\"1\",\"check_type\":\"P\",\"account_number\":\"8535\",\"routing_number\":\"244183602\"}} RESPONSE end def successful_store_response - <<-RESPONSE - {\"correlation_id\":\"124.1792879391754\",\"status\":\"success\",\"type\":\"FDToken\",\"token\":{\"type\":\"Visa\",\"cardholder_name\":\"Longbob Longsen\",\"exp_date\":\"0919\",\"value\":\"9045348309244242\"}} - RESPONSE + <<~RESPONSE + {\"correlation_id\":\"124.1792879391754\",\"status\":\"success\",\"type\":\"FDToken\",\"token\":{\"type\":\"Visa\",\"cardholder_name\":\"Longbob Longsen\",\"exp_date\":\"0919\",\"value\":\"9045348309244242\"}} + RESPONSE end def failed_store_response - <<-RESPONSE - {\"correlation_id\":\"124.1792940806770\",\"status\":\"failed\",\"Error\":{\"messages\":[{\"code\":\"invalid_card_number\",\"description\":\"The credit card number check failed\"}]},\"type\":\"FDToken\"} + <<~RESPONSE + {\"correlation_id\":\"124.1792940806770\",\"status\":\"failed\",\"Error\":{\"messages\":[{\"code\":\"invalid_card_number\",\"description\":\"The credit card number check failed\"}]},\"type\":\"FDToken\"} RESPONSE end def failed_purchase_response - yamlexcep = <<-RESPONSE ---- !ruby/exception:ActiveMerchant::ResponseError -response: !ruby/object:Net::HTTPBadRequest - http_version: '1.1' - code: '400' - message: Bad Request - header: - content-language: - - en-US - content-type: - - application/json - date: - - Tue, 09 Jun 2015 15:46:44 GMT - optr_cxt: - - 0100010000eb11d301-785c-449b-b060-6d0b4638d54d00000000-0000-0000-0000-000000000000-1 HTTP ; - x-archived-client-ip: - - 10.174.197.250 - x-backside-transport: - - FAIL FAIL,FAIL FAIL - x-client-ip: - - 10.174.197.250,54.236.202.5 - x-powered-by: - - Servlet/3.0 - content-length: - - '384' - connection: - - Close - body: '{"method":"credit_card","amount":"10000000","currency":"USD","card":{"type":"Visa","cvv":"000","cardholder_name":"Bobsen - 5675","card_number":"4242","exp_date":"0810"},"transaction_status":"Not Processed","validation_status":"failed","transaction_type":"purchase","Error":{"messages":[{"code":"card_expired","description":"The - card has expired"}]},"correlation_id":"124.1433864804381"}' - read: true - uri: - decode_content: true - socket: - body_exist: true -message: + yamlexcep = <<~RESPONSE + --- !ruby/exception:ActiveMerchant::ResponseError + response: !ruby/object:Net::HTTPBadRequest + http_version: '1.1' + code: '400' + message: Bad Request + header: + content-language: + - en-US + content-type: + - application/json + date: + - Tue, 09 Jun 2015 15:46:44 GMT + optr_cxt: + - 0100010000eb11d301-785c-449b-b060-6d0b4638d54d00000000-0000-0000-0000-000000000000-1 HTTP ; + x-archived-client-ip: + - 10.174.197.250 + x-backside-transport: + - FAIL FAIL,FAIL FAIL + x-client-ip: + - 10.174.197.250,54.236.202.5 + x-powered-by: + - Servlet/3.0 + content-length: + - '384' + connection: + - Close + body: '{"method":"credit_card","amount":"10000000","currency":"USD","card":{"type":"Visa","cvv":"000","cardholder_name":"Bobsen + 5675","card_number":"4242","exp_date":"0810"},"transaction_status":"Not Processed","validation_status":"failed","transaction_type":"purchase","Error":{"messages":[{"code":"card_expired","description":"The + card has expired"}]},"correlation_id":"124.1433864804381"}' + read: true + uri: + decode_content: true + socket: + body_exist: true + message: RESPONSE - YAML.load(yamlexcep) + YAML.safe_load(yamlexcep, permitted_classes: ['Net::HTTPBadRequest', 'ActiveMerchant::ResponseError']) + end + + def failed_purchase_response_for_insufficient_funds + '{"correlation_id":"124.1342365067332","transaction_status":"declined","validation_status":"success","transaction_type":"purchase","transaction_tag":"4611610442","method":"credit_card","amount":"530200","currency":"USD","avs":"4","cvv2":"M","token":{"token_type":"FDToken", "token_data":{"value":"0788934280684242"}},"card":{"type":"Visa","cardholder_name":"Longbob Longsen","card_number":"4242","exp_date":"0922"},"bank_resp_code":"302","bank_message":"Insufficient Funds","gateway_resp_code":"00","gateway_message":"Transaction Normal"}' end def successful_authorize_response - <<-RESPONSE + <<~RESPONSE {\"correlation_id\":\"228.1449517682800\",\"transaction_status\":\"approved\",\"validation_status\":\"success\",\"transaction_type\":\"authorize\",\"transaction_id\":\"ET156862\",\"transaction_tag\":\"69601979\",\"method\":\"credit_card\",\"amount\":\"100\",\"currency\":\"USD\",\"avs\":\"4\",\"cvv2\":\"M\",\"token\":{\"token_type\":\"FDToken\",\"token_data\":{\"value\":\"1446473518714242\"}},\"card\":{\"type\":\"Visa\",\"cardholder_name\":\"Longbob Longsen\",\"card_number\":\"4242\",\"exp_date\":\"0916\"},\"bank_resp_code\":\"100\",\"bank_message\":\"Approved\",\"gateway_resp_code\":\"00\",\"gateway_message\":\"Transaction Normal\"} RESPONSE end def failed_authorize_response - <<-RESPONSE + <<~RESPONSE {\"correlation_id\":\"228.1449522605561\",\"transaction_status\":\"declined\",\"validation_status\":\"success\",\"transaction_type\":\"authorize\",\"transaction_tag\":\"69607256\",\"method\":\"credit_card\",\"amount\":\"501300\",\"currency\":\"USD\",\"avs\":\"4\",\"cvv2\":\"M\",\"token\":{\"token_type\":\"FDToken\",\"token_data\":{\"value\":\"0843687226934242\"}},\"card\":{\"type\":\"Visa\",\"cardholder_name\":\"Longbob Longsen\",\"card_number\":\"4242\",\"exp_date\":\"0916\"},\"bank_resp_code\":\"013\",\"bank_message\":\"Transaction not approved\",\"gateway_resp_code\":\"00\",\"gateway_message\":\"Transaction Normal\"} RESPONSE end def successful_capture_response - <<-RESPONSE + <<~RESPONSE {\"correlation_id\":\"228.1449517473876\",\"transaction_status\":\"approved\",\"validation_status\":\"success\",\"transaction_type\":\"capture\",\"transaction_id\":\"ET176427\",\"transaction_tag\":\"69601874\",\"method\":\"credit_card\",\"amount\":\"100\",\"currency\":\"USD\",\"cvv2\":\"I\",\"token\":{\"token_type\":\"FDToken\",\"token_data\":{\"value\":\"8129044621504242\"}},\"bank_resp_code\":\"100\",\"bank_message\":\"Approved\",\"gateway_resp_code\":\"00\",\"gateway_message\":\"Transaction Normal\"} RESPONSE end def successful_refund_response - <<-RESPONSE - {\"method\":\"credit_card\",\"amount\":\"1\",\"currency\":\"USD\",\"cvv2\":\"I\",\"token\":{\"token_type\":\"transarmor\",\"token_data\":{\"value\":\"9968749582724242\"}},\"transaction_status\":\"approved\",\"validation_status\":\"success\",\"transaction_type\":\"refund\",\"transaction_id\":\"55084328\",\"transaction_tag\":\"55084328\",\"bank_resp_code\":\"100\",\"bank_message\":\"Approved\",\"gateway_resp_code\":\"00\",\"gateway_message\":\"Transaction Normal\",\"correlation_id\":\"124.1433864648126\"} + <<~RESPONSE + {\"method\":\"credit_card\",\"amount\":\"1\",\"currency\":\"USD\",\"cvv2\":\"I\",\"token\":{\"token_type\":\"transarmor\",\"token_data\":{\"value\":\"9968749582724242\"}},\"transaction_status\":\"approved\",\"validation_status\":\"success\",\"transaction_type\":\"refund\",\"transaction_id\":\"55084328\",\"transaction_tag\":\"55084328\",\"bank_resp_code\":\"100\",\"bank_message\":\"Approved\",\"gateway_resp_code\":\"00\",\"gateway_message\":\"Transaction Normal\",\"correlation_id\":\"124.1433864648126\"} RESPONSE end def successful_refund_echeck_response - <<-RESPONSE - {\"correlation_id\":\"228.1449688783287\",\"transaction_status\":\"approved\",\"validation_status\":\"success\",\"transaction_type\":\"refund\",\"transaction_id\":\"69864710\",\"transaction_tag\":\"69864710\",\"method\":\"tele_check\",\"amount\":\"50\",\"currency\":\"USD\",\"bank_resp_code\":\"100\",\"bank_message\":\"Approved\",\"gateway_resp_code\":\"00\",\"gateway_message\":\"Transaction Normal\"} + <<~RESPONSE + {\"correlation_id\":\"228.1449688783287\",\"transaction_status\":\"approved\",\"validation_status\":\"success\",\"transaction_type\":\"refund\",\"transaction_id\":\"69864710\",\"transaction_tag\":\"69864710\",\"method\":\"tele_check\",\"amount\":\"50\",\"currency\":\"USD\",\"bank_resp_code\":\"100\",\"bank_message\":\"Approved\",\"gateway_resp_code\":\"00\",\"gateway_message\":\"Transaction Normal\"} RESPONSE end def below_minimum_response - <<-RESPONSE - {\"correlation_id\":\"123.1234678982\",\"transaction_status\":\"declined\",\"validation_status\":\"success\",\"transaction_type\":\"authorize\",\"transaction_tag\":\"92384753\",\"method\":\"credit_card\",\"amount\":\"250\",\"currency\":\"USD\",\"card\":{\"type\":\"Mastercard\",\"cardholder_name\":\"Omri Test\",\"card_number\":\"[FILTERED]\",\"exp_date\":\"0123\"},\"gateway_resp_code\":\"36\",\"gateway_message\":\"Below Minimum Sale\"} + <<~RESPONSE + {\"correlation_id\":\"123.1234678982\",\"transaction_status\":\"declined\",\"validation_status\":\"success\",\"transaction_type\":\"authorize\",\"transaction_tag\":\"92384753\",\"method\":\"credit_card\",\"amount\":\"250\",\"currency\":\"USD\",\"card\":{\"type\":\"Mastercard\",\"cardholder_name\":\"Omri Test\",\"card_number\":\"[FILTERED]\",\"exp_date\":\"0123\"},\"gateway_resp_code\":\"36\",\"gateway_message\":\"Below Minimum Sale\"} + RESPONSE + end + + def failed_purchase_no_name_response + <<~RESPONSE + {\"correlation_id\":\"29.7337367613551\",\"transaction_status\":\"approved\",\"validation_status\":\"success\",\"transaction_type\":\"purchase\",\"transaction_id\":\"ET106024\",\"transaction_tag\":\"10049930801\",\"method\":\"3ds\",\"amount\":\"100\",\"currency\":\"USD\",\"avs\":\"4\",\"cvv2\":\"U\",\"token\":{\"token_type\":\"FDToken\",\"token_data\":{\"value\":\"1141044316391439\"}},\"card\":{\"type\":\"VISA\",\"cardholder_name\":\"Jim Smith\",\"card_number\":\"1439\",\"exp_date\":\"1124\"},\"bank_resp_code\":\"100\",\"bank_message\":\"Approved\",\"gateway_resp_code\":\"00\",\"gateway_message\":\"Transaction Normal\",\"eCommerce_flag\":\"5\",\"retrieval_ref_no\":\"230110\"} RESPONSE end def failed_refund_response - yamlexcep = <<-RESPONSE ---- !ruby/exception:ActiveMerchant::ResponseError -response: !ruby/object:Net::HTTPBadRequest - http_version: '1.1' - code: '400' - message: Bad Request - header: - content-language: - - en-US - content-type: - - application/json - date: - - Tue, 09 Jun 2015 15:46:44 GMT - optr_cxt: - - 0100010000eb11d301-785c-449b-b060-6d0b4638d54d00000000-0000-0000-0000-000000000000-1 HTTP ; - x-archived-client-ip: - - 10.174.197.250 - x-backside-transport: - - FAIL FAIL,FAIL FAIL - x-client-ip: - - 10.174.197.250,54.236.202.5 - x-powered-by: - - Servlet/3.0 - content-length: - - '384' - connection: - - Close - body: '{"correlation_id":"228.1449520714925","Error":{"messages":[{"code":"missing_transaction_tag","description":"The transaction tag is not provided"}]},"transaction_status":"Not Processed","validation_status":"failed","transaction_type":"refund","amount":"50","currency":"USD"}' - read: true - uri: - decode_content: true - socket: - body_exist: true -message: + yamlexcep = <<~RESPONSE + --- !ruby/exception:ActiveMerchant::ResponseError + response: !ruby/object:Net::HTTPBadRequest + http_version: '1.1' + code: '400' + message: Bad Request + header: + content-language: + - en-US + content-type: + - application/json + date: + - Tue, 09 Jun 2015 15:46:44 GMT + optr_cxt: + - 0100010000eb11d301-785c-449b-b060-6d0b4638d54d00000000-0000-0000-0000-000000000000-1 HTTP ; + x-archived-client-ip: + - 10.174.197.250 + x-backside-transport: + - FAIL FAIL,FAIL FAIL + x-client-ip: + - 10.174.197.250,54.236.202.5 + x-powered-by: + - Servlet/3.0 + content-length: + - '384' + connection: + - Close + body: '{"correlation_id":"228.1449520714925","Error":{"messages":[{"code":"missing_transaction_tag","description":"The transaction tag is not provided"}]},"transaction_status":"Not Processed","validation_status":"failed","transaction_type":"refund","amount":"50","currency":"USD"}' + read: true + uri: + decode_content: true + socket: + body_exist: true + message: RESPONSE - YAML.load(yamlexcep) + YAML.safe_load(yamlexcep, permitted_classes: ['Net::HTTPBadRequest', 'ActiveMerchant::ResponseError']) end def successful_void_response - <<-RESPONSE - {\"method\":\"credit_card\",\"amount\":\"1\",\"currency\":\"USD\",\"cvv2\":\"I\",\"token\":{\"token_type\":\"transarmor\",\"token_data\":{\"value\":\"9594258319174242\"}},\"transaction_status\":\"approved\",\"validation_status\":\"success\",\"transaction_type\":\"void\",\"transaction_id\":\"ET196233\",\"transaction_tag\":\"55083674\",\"bank_resp_code\":\"100\",\"bank_message\":\"Approved\",\"gateway_resp_code\":\"00\",\"gateway_message\":\"Transaction Normal\",\"correlation_id\":\"124.1433863576596\"} -RESPONSE + <<~RESPONSE + {\"method\":\"credit_card\",\"amount\":\"1\",\"currency\":\"USD\",\"cvv2\":\"I\",\"token\":{\"token_type\":\"transarmor\",\"token_data\":{\"value\":\"9594258319174242\"}},\"transaction_status\":\"approved\",\"validation_status\":\"success\",\"transaction_type\":\"void\",\"transaction_id\":\"ET196233\",\"transaction_tag\":\"55083674\",\"bank_resp_code\":\"100\",\"bank_message\":\"Approved\",\"gateway_resp_code\":\"00\",\"gateway_message\":\"Transaction Normal\",\"correlation_id\":\"124.1433863576596\"} + RESPONSE end def failed_void_response - yamlexcep = <<-RESPONSE ---- !ruby/exception:ActiveMerchant::ResponseError -response: !ruby/object:Net::HTTPBadRequest - http_version: '1.1' - code: '400' - message: Bad Request - header: - content-language: - - en-US - content-type: - - application/json - date: - - Tue, 09 Jun 2015 15:46:44 GMT - optr_cxt: - - 0100010000eb11d301-785c-449b-b060-6d0b4638d54d00000000-0000-0000-0000-000000000000-1 HTTP ; - x-archived-client-ip: - - 10.174.197.250 - x-backside-transport: - - FAIL FAIL,FAIL FAIL - x-client-ip: - - 10.174.197.250,54.236.202.5 - x-powered-by: - - Servlet/3.0 - content-length: - - '384' - connection: - - Close - body: '{"correlation_id":"228.1449520846984","Error":{"messages":[{"code":"missing_transaction_id","description":"The transaction id is not provided"},{"code":"missing_transaction_tag","description":"The transaction tag is not provided"}]},"transaction_status":"Not Processed","validation_status":"failed","transaction_type":"void","amount":"0","currency":"USD"}' - read: true - uri: - decode_content: true - socket: - body_exist: true -message: + yamlexcep = <<~RESPONSE + --- !ruby/exception:ActiveMerchant::ResponseError + response: !ruby/object:Net::HTTPBadRequest + http_version: '1.1' + code: '400' + message: Bad Request + header: + content-language: + - en-US + content-type: + - application/json + date: + - Tue, 09 Jun 2015 15:46:44 GMT + optr_cxt: + - 0100010000eb11d301-785c-449b-b060-6d0b4638d54d00000000-0000-0000-0000-000000000000-1 HTTP ; + x-archived-client-ip: + - 10.174.197.250 + x-backside-transport: + - FAIL FAIL,FAIL FAIL + x-client-ip: + - 10.174.197.250,54.236.202.5 + x-powered-by: + - Servlet/3.0 + content-length: + - '384' + connection: + - Close + body: '{"correlation_id":"228.1449520846984","Error":{"messages":[{"code":"missing_transaction_id","description":"The transaction id is not provided"},{"code":"missing_transaction_tag","description":"The transaction tag is not provided"}]},"transaction_status":"Not Processed","validation_status":"failed","transaction_type":"void","amount":"0","currency":"USD"}' + read: true + uri: + decode_content: true + socket: + body_exist: true + message: RESPONSE - YAML.load(yamlexcep) + YAML.safe_load(yamlexcep, permitted_classes: ['Net::HTTPBadRequest', 'ActiveMerchant::ResponseError']) end def failed_capture_response - yamlexcep = <<-RESPONSE ---- !ruby/exception:ActiveMerchant::ResponseError -response: !ruby/object:Net::HTTPBadRequest - http_version: '1.1' - code: '400' - message: Bad Request - header: - content-language: - - en-US - content-type: - - application/json - date: - - Tue, 09 Jun 2015 17:33:50 GMT - optr_cxt: - - 0100010000d084138f-24f3-4686-8a51-3c17406a572500000000-0000-0000-0000-000000000000-1 HTTP ; - x-archived-client-ip: - - 10.174.197.250 - x-backside-transport: - - FAIL FAIL,FAIL FAIL - x-client-ip: - - 10.174.197.250,107.23.55.229 - x-powered-by: - - Servlet/3.0 - content-length: - - '190' - connection: - - Close - body: '{"transaction_status":"Not Processed","Error":{"messages":[{"code":"server_error","description":"ProcessedBad - Request (69) - Invalid Transaction Tag"}]},"correlation_id":"124.1433871231542"}' - read: true - uri: - decode_content: true - socket: - body_exist: true -message: - RESPONSE - YAML.load(yamlexcep) + yamlexcep = <<~RESPONSE + --- !ruby/exception:ActiveMerchant::ResponseError + response: !ruby/object:Net::HTTPBadRequest + http_version: '1.1' + code: '400' + message: Bad Request + header: + content-language: + - en-US + content-type: + - application/json + date: + - Tue, 09 Jun 2015 17:33:50 GMT + optr_cxt: + - 0100010000d084138f-24f3-4686-8a51-3c17406a572500000000-0000-0000-0000-000000000000-1 HTTP ; + x-archived-client-ip: + - 10.174.197.250 + x-backside-transport: + - FAIL FAIL,FAIL FAIL + x-client-ip: + - 10.174.197.250,107.23.55.229 + x-powered-by: + - Servlet/3.0 + content-length: + - '190' + connection: + - Close + body: '{"transaction_status":"Not Processed","Error":{"messages":[{"code":"server_error","description":"ProcessedBad + Request (69) - Invalid Transaction Tag"}]},"correlation_id":"124.1433871231542"}' + read: true + uri: + decode_content: true + socket: + body_exist: true + message: + RESPONSE + YAML.safe_load(yamlexcep, permitted_classes: ['Net::HTTPBadRequest', 'ActiveMerchant::ResponseError']) end def invalid_token_response - yamlexcep = <<-RESPONSE ---- !ruby/exception:ActiveMerchant::ResponseError -response: !ruby/object:Net::HTTPUnauthorized - http_version: '1.1' - code: '401' - message: Unauthorized - header: - content-language: - - en-US - content-type: - - application/json;charset=utf-8 - date: - - Tue, 23 Jun 2015 15:13:02 GMT - optr_cxt: - - 435543224354-37b2-4369-9cfe-26543635465346346-0000-0000-0000-000000000000-1 HTTP ; - x-archived-client-ip: - - 10.180.205.250 - x-backside-transport: - - FAIL FAIL,FAIL FAIL - x-client-ip: - - 10.180.205.250,107.23.55.229 - x-powered-by: - - Servlet/3.0 - content-length: - - '25' - connection: - - Close - body: '{"error":"Access denied"}' - read: true - uri: - decode_content: true - socket: - body_exist: true -message: + yamlexcep = <<~RESPONSE + --- !ruby/exception:ActiveMerchant::ResponseError + response: !ruby/object:Net::HTTPUnauthorized + http_version: '1.1' + code: '401' + message: Unauthorized + header: + content-language: + - en-US + content-type: + - application/json;charset=utf-8 + date: + - Tue, 23 Jun 2015 15:13:02 GMT + optr_cxt: + - 435543224354-37b2-4369-9cfe-26543635465346346-0000-0000-0000-000000000000-1 HTTP ; + x-archived-client-ip: + - 10.180.205.250 + x-backside-transport: + - FAIL FAIL,FAIL FAIL + x-client-ip: + - 10.180.205.250,107.23.55.229 + x-powered-by: + - Servlet/3.0 + content-length: + - '25' + connection: + - Close + body: '{"error":"Access denied"}' + read: true + uri: + decode_content: true + socket: + body_exist: true + message: RESPONSE - YAML.load(yamlexcep) + YAML.safe_load(yamlexcep, permitted_classes: ['Net::HTTPUnauthorized', 'ActiveMerchant::ResponseError']) end def invalid_token_response_integration - yamlexcep = <<-RESPONSE ---- !ruby/exception:ActiveMerchant::ResponseError -response: !ruby/object:Net::HTTPUnauthorized - http_version: '1.1' - code: '401' - message: Unauthorized - header: - content-type: - - application/json - content-length: - - '125' - connection: - - Close - body: '{\"fault\":{\"faultstring\":\"Invalid ApiKey for given resource\",\"detail\":{\"errorcode\":\"oauth.v2.InvalidApiKeyForGivenResource\"}}}' - read: true - uri: - decode_content: true - socket: - body_exist: true -message: + yamlexcep = <<~RESPONSE + --- !ruby/exception:ActiveMerchant::ResponseError + response: !ruby/object:Net::HTTPUnauthorized + http_version: '1.1' + code: '401' + message: Unauthorized + header: + content-type: + - application/json + content-length: + - '125' + connection: + - Close + body: '{\"fault\":{\"faultstring\":\"Invalid ApiKey for given resource\",\"detail\":{\"errorcode\":\"oauth.v2.InvalidApiKeyForGivenResource\"}}}' + read: true + uri: + decode_content: true + socket: + body_exist: true + message: RESPONSE - YAML.load(yamlexcep) + YAML.safe_load(yamlexcep, permitted_classes: ['Net::HTTPUnauthorized', 'ActiveMerchant::ResponseError']) end def bad_credentials_response - yamlexcep = <<-RESPONSE ---- !ruby/exception:ActiveMerchant::ResponseError -response: !ruby/object:Net::HTTPForbidden - http_version: '1.1' - code: '403' - message: Forbidden - header: - content-type: - - application/json - content-length: - - '51' - connection: - - Close - body: '{"code":"403", "message":"HMAC validation Failure"}' - read: true - uri: - decode_content: true - socket: - body_exist: true -message: + yamlexcep = <<~RESPONSE + --- !ruby/exception:ActiveMerchant::ResponseError + response: !ruby/object:Net::HTTPForbidden + http_version: '1.1' + code: '403' + message: Forbidden + header: + content-type: + - application/json + content-length: + - '51' + connection: + - Close + body: '{"code":"403", "message":"HMAC validation Failure"}' + read: true + uri: + decode_content: true + socket: + body_exist: true + message: RESPONSE - YAML.load(yamlexcep) + YAML.safe_load(yamlexcep, permitted_classes: ['Net::HTTPForbidden', 'ActiveMerchant::ResponseError']) end end diff --git a/test/unit/gateways/payex_test.rb b/test/unit/gateways/payex_test.rb index c948304954a..d3b5e071ed4 100644 --- a/test/unit/gateways/payex_test.rb +++ b/test/unit/gateways/payex_test.rb @@ -3,15 +3,15 @@ class PayexTest < Test::Unit::TestCase def setup @gateway = PayexGateway.new( - :account => 'account', - :encryption_key => 'encryption_key' - ) + account: 'account', + encryption_key: 'encryption_key' + ) @credit_card = credit_card @amount = 1000 @options = { - :order_id => '1234', + order_id: '1234' } end @@ -98,7 +98,7 @@ def test_unsuccessful_refund def test_successful_store @gateway.expects(:ssl_post).times(3).returns(successful_store_response, successful_initialize_response, successful_purchase_response) - assert response = @gateway.store(@credit_card, @options.merge({merchant_ref: '9876'})) + assert response = @gateway.store(@credit_card, @options.merge({ merchant_ref: '9876' })) assert_success response assert_equal 'OK', response.message assert_equal 'bcea4ac8d1f44640bff7a8c93caa249c', response.authorization @@ -115,7 +115,7 @@ def test_successful_unstore def test_successful_purchase_with_stored_card @gateway.expects(:ssl_post).returns(successful_autopay_response) - assert response = @gateway.purchase(@amount, 'fakeauth', @options.merge({order_id: '5678'})) + assert response = @gateway.purchase(@amount, 'fakeauth', @options.merge({ order_id: '5678' })) assert_success response assert_equal 'OK', response.message assert_equal '2624657', response.authorization @@ -125,7 +125,7 @@ def test_successful_purchase_with_stored_card private def successful_initialize_response - %q{<?xml version="1.0" encoding="utf-8"?> + '<?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <Initialize8Response xmlns="http://external.payex.com/PxOrder/"> @@ -133,12 +133,12 @@ def successful_initialize_response </Initialize8Response> </soap:Body> </soap:Envelope> - } + ' end # Place raw successful response from gateway here def successful_purchase_response - %q{<?xml version="1.0" encoding="utf-8"?> + '<?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <PurchaseCCResponse xmlns="http://confined.payex.com/PxOrder/"> @@ -146,11 +146,11 @@ def successful_purchase_response </PurchaseCCResponse> </soap:Body> </soap:Envelope> - } + ' end def failed_purchase_response - %q{<?xml version="1.0" encoding="utf-8"?> + '<?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <PurchaseCCResponse xmlns="http://confined.payex.com/PxOrder/"> @@ -159,11 +159,11 @@ def failed_purchase_response </PurchaseCCResponse> </soap:Body> </soap:Envelope> - } + ' end def successful_authorize_response - %q{<?xml version="1.0" encoding="utf-8"?> + '<?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <PurchaseCCResponse xmlns="http://confined.payex.com/PxOrder/"> @@ -171,11 +171,11 @@ def successful_authorize_response </PurchaseCCResponse> </soap:Body> </soap:Envelope> - } + ' end def successful_capture_response - %q{<?xml version="1.0" encoding="utf-8"?> + '<?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <Capture5Response xmlns="http://external.payex.com/PxOrder/"> @@ -183,11 +183,11 @@ def successful_capture_response </Capture5Response> </soap:Body> </soap:Envelope> - } + ' end def failed_capture_response - %q{<?xml version="1.0" encoding="utf-8"?> + '<?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <Capture5Response xmlns="http://external.payex.com/PxOrder/"> @@ -195,11 +195,11 @@ def failed_capture_response </Capture5Response> </soap:Body> </soap:Envelope> - } + ' end def successful_void_response - %q{<?xml version="1.0" encoding="utf-8"?> + '<?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <Cancel2Response xmlns="http://external.payex.com/PxOrder/"> @@ -207,11 +207,11 @@ def successful_void_response </Cancel2Response> </soap:Body> </soap:Envelope> - } + ' end def unsuccessful_void_response - %q{<?xml version="1.0" encoding="utf-8"?> + '<?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <Cancel2Response xmlns="http://external.payex.com/PxOrder/"> @@ -219,11 +219,11 @@ def unsuccessful_void_response </Cancel2Response> </soap:Body> </soap:Envelope> - } + ' end def successful_refund_response - %q{<?xml version="1.0" encoding="utf-8"?> + '<?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <Credit5Response xmlns="http://external.payex.com/PxOrder/"> @@ -231,11 +231,11 @@ def successful_refund_response </Credit5Response> </soap:Body> </soap:Envelope> - } + ' end def unsuccessful_refund_response - %q{<?xml version="1.0" encoding="utf-8"?> + '<?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <Credit5Response xmlns="http://external.payex.com/PxOrder/"> @@ -243,11 +243,11 @@ def unsuccessful_refund_response </Credit5Response> </soap:Body> </soap:Envelope> - } + ' end def successful_store_response - %q{<?xml version="1.0" encoding="utf-8"?> + '<?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <CreateAgreement3Response xmlns="http://external.payex.com/PxAgreement/"> @@ -255,11 +255,11 @@ def successful_store_response </CreateAgreement3Response> </soap:Body> </soap:Envelope> - } + ' end def successful_unstore_response - %q{<?xml version="1.0" encoding="utf-8"?> + '<?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <DeleteAgreementResponse xmlns="http://external.payex.com/PxAgreement/"> @@ -267,11 +267,11 @@ def successful_unstore_response </DeleteAgreementResponse> </soap:Body> </soap:Envelope> - } + ' end def successful_autopay_response - %q{<?xml version="1.0" encoding="utf-8"?> + '<?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <AutoPay3Response xmlns="http://external.payex.com/PxAgreement/"> @@ -279,6 +279,6 @@ def successful_autopay_response </AutoPay3Response> </soap:Body> </soap:Envelope> - } + ' end end diff --git a/test/unit/gateways/payflow_express_test.rb b/test/unit/gateways/payflow_express_test.rb index 719fd885117..0e74a0aaa87 100644 --- a/test/unit/gateways/payflow_express_test.rb +++ b/test/unit/gateways/payflow_express_test.rb @@ -5,85 +5,84 @@ class PayflowExpressTest < Test::Unit::TestCase TEST_REDIRECT_URL_MOBILE = 'https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout-mobile&token=1234567890' LIVE_REDIRECT_URL = 'https://www.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=1234567890' LIVE_REDIRECT_URL_MOBILE = 'https://www.paypal.com/cgi-bin/webscr?cmd=_express-checkout-mobile&token=1234567890' - + TEST_REDIRECT_URL_WITHOUT_REVIEW = "#{TEST_REDIRECT_URL}&useraction=commit" LIVE_REDIRECT_URL_WITHOUT_REVIEW = "#{LIVE_REDIRECT_URL}&useraction=commit" TEST_REDIRECT_URL_MOBILE_WITHOUT_REVIEW = "#{TEST_REDIRECT_URL_MOBILE}&useraction=commit" LIVE_REDIRECT_URL_MOBILE_WITHOUT_REVIEW = "#{LIVE_REDIRECT_URL_MOBILE}&useraction=commit" - + def setup Base.mode = :test - + @gateway = PayflowExpressGateway.new( - :login => 'LOGIN', - :password => 'PASSWORD' + login: 'LOGIN', + password: 'PASSWORD' ) - @address = { :address1 => '1234 My Street', - :address2 => 'Apt 1', - :company => 'Widgets Inc', - :city => 'Ottawa', - :state => 'ON', - :zip => 'K1C2N6', - :country => 'Canada', - :phone => '(555)555-5555' - } - end - + @address = { address1: '1234 My Street', + address2: 'Apt 1', + company: 'Widgets Inc', + city: 'Ottawa', + state: 'ON', + zip: 'K1C2N6', + country: 'Canada', + phone: '(555)555-5555' } + end + def teardown Base.mode = :test end - + def test_using_test_mode assert @gateway.test? end - + def test_overriding_test_mode Base.mode = :production - + gateway = PayflowExpressGateway.new( - :login => 'LOGIN', - :password => 'PASSWORD', - :test => true + login: 'LOGIN', + password: 'PASSWORD', + test: true ) - + assert gateway.test? end - + def test_using_production_mode Base.mode = :production - + gateway = PayflowExpressGateway.new( - :login => 'LOGIN', - :password => 'PASSWORD' + login: 'LOGIN', + password: 'PASSWORD' ) - + assert !gateway.test? end - + def test_live_redirect_url Base.mode = :production assert_equal LIVE_REDIRECT_URL, @gateway.redirect_url_for('1234567890') - assert_equal LIVE_REDIRECT_URL_MOBILE, @gateway.redirect_url_for('1234567890', :mobile => true) + assert_equal LIVE_REDIRECT_URL_MOBILE, @gateway.redirect_url_for('1234567890', mobile: true) end - + def test_test_redirect_url assert_equal TEST_REDIRECT_URL, @gateway.redirect_url_for('1234567890') - assert_equal TEST_REDIRECT_URL_MOBILE, @gateway.redirect_url_for('1234567890', :mobile => true) + assert_equal TEST_REDIRECT_URL_MOBILE, @gateway.redirect_url_for('1234567890', mobile: true) end - + def test_live_redirect_url_without_review Base.mode = :production - assert_equal LIVE_REDIRECT_URL_WITHOUT_REVIEW, @gateway.redirect_url_for('1234567890', :review => false) - assert_equal LIVE_REDIRECT_URL_MOBILE_WITHOUT_REVIEW, @gateway.redirect_url_for('1234567890', :review => false, :mobile => true) + assert_equal LIVE_REDIRECT_URL_WITHOUT_REVIEW, @gateway.redirect_url_for('1234567890', review: false) + assert_equal LIVE_REDIRECT_URL_MOBILE_WITHOUT_REVIEW, @gateway.redirect_url_for('1234567890', review: false, mobile: true) end - + def test_test_redirect_url_without_review assert_equal :test, Base.mode - assert_equal TEST_REDIRECT_URL_WITHOUT_REVIEW, @gateway.redirect_url_for('1234567890', :review => false) - assert_equal TEST_REDIRECT_URL_MOBILE_WITHOUT_REVIEW, @gateway.redirect_url_for('1234567890', :review => false, :mobile => true) + assert_equal TEST_REDIRECT_URL_WITHOUT_REVIEW, @gateway.redirect_url_for('1234567890', review: false) + assert_equal TEST_REDIRECT_URL_MOBILE_WITHOUT_REVIEW, @gateway.redirect_url_for('1234567890', review: false, mobile: true) end - + def test_invalid_get_express_details_request @gateway.expects(:ssl_post).returns(invalid_get_express_details_response) response = @gateway.details_for('EC-2OPN7UJGFWK9OYFV') @@ -91,20 +90,20 @@ def test_invalid_get_express_details_request assert response.test? assert_equal 'Field format error: Invalid Token', response.message end - + def test_get_express_details @gateway.expects(:ssl_post).returns(successful_get_express_details_response) response = @gateway.details_for('EC-2OPN7UJGFWK9OYFV') assert_instance_of PayflowExpressResponse, response assert_success response assert response.test? - + assert_equal 'EC-2OPN7UJGFWK9OYFV', response.token assert_equal '12345678901234567', response.payer_id assert_equal 'Buyer1@paypal.com', response.email assert_equal 'Joe Smith', response.full_name assert_equal 'US', response.payer_country - + assert address = response.address assert_equal 'Joe Smith', address['name'] assert_nil address['company'] @@ -114,11 +113,36 @@ def test_get_express_details assert_equal 'CA', address['state'] assert_equal '95100', address['zip'] assert_equal 'US', address['country'] - assert_nil address['phone'] + assert_equal '555-555-5555', address['phone'] + end + + def test_get_express_details_with_ship_to_name + @gateway.expects(:ssl_post).returns(successful_get_express_details_response_with_ship_to_name) + response = @gateway.details_for('EC-2OPN7UJGFWK9OYFV') + assert_instance_of PayflowExpressResponse, response + assert_success response + assert response.test? + + assert_equal 'EC-2OPN7UJGFWK9OYFV', response.token + assert_equal '12345678901234567', response.payer_id + assert_equal 'Buyer1@paypal.com', response.email + assert_equal 'Joe Smith', response.full_name + assert_equal 'US', response.payer_country + + assert address = response.address + assert_equal 'John Joseph', address['name'] + assert_nil address['company'] + assert_equal '111 Main St.', address['address1'] + assert_nil address['address2'] + assert_equal 'San Jose', address['city'] + assert_equal 'CA', address['state'] + assert_equal '95100', address['zip'] + assert_equal 'US', address['country'] + assert_equal '555-555-5555', address['phone'] end def test_get_express_details_with_invalid_xml - @gateway.expects(:ssl_post).returns(successful_get_express_details_response(:street => 'Main & Magic')) + @gateway.expects(:ssl_post).returns(successful_get_express_details_response(street: 'Main & Magic')) response = @gateway.details_for('EC-2OPN7UJGFWK9OYFV') assert_instance_of PayflowExpressResponse, response assert_success response @@ -134,59 +158,98 @@ def test_button_source xml_doc = REXML::Document.new(xml.target!) assert_nil REXML::XPath.first(xml_doc, '/PayPal/ButtonSource') end - + private - - def successful_get_express_details_response(options={:street => '111 Main St.'}) - <<-RESPONSE -<XMLPayResponse xmlns='http://www.verisign.com/XMLPay'> - <ResponseData> - <Vendor>TEST</Vendor> - <Partner>verisign</Partner> - <TransactionResults> - <TransactionResult> - <Result>0</Result> - <Message>Approved</Message> - <PayPalResult> - <EMail>Buyer1@paypal.com</EMail> - <PayerID>12345678901234567</PayerID> - <Token>EC-2OPN7UJGFWK9OYFV</Token> - <FeeAmount>0</FeeAmount> - <PayerStatus>verified</PayerStatus> - <Name>Joe</Name> - <ShipTo> - <Address> - <Street>#{options[:street]}</Street> - <City>San Jose</City> - <State>CA</State> - <Zip>95100</Zip> - <Country>US</Country> - </Address> - </ShipTo> - <CorrelationID>9c3706997455e</CorrelationID> - </PayPalResult> - <ExtData Name='LASTNAME' Value='Smith'/> - </TransactionResult> - </TransactionResults> - </ResponseData> - </XMLPayResponse> + + def successful_get_express_details_response(options = { street: '111 Main St.' }) + <<~RESPONSE + <XMLPayResponse xmlns='http://www.verisign.com/XMLPay'> + <ResponseData> + <Vendor>TEST</Vendor> + <Partner>verisign</Partner> + <TransactionResults> + <TransactionResult> + <Result>0</Result> + <Message>Approved</Message> + <PayPalResult> + <EMail>Buyer1@paypal.com</EMail> + <PayerID>12345678901234567</PayerID> + <Token>EC-2OPN7UJGFWK9OYFV</Token> + <FeeAmount>0</FeeAmount> + <PayerStatus>verified</PayerStatus> + <Name>Joe</Name> + <Phone>555-555-5555</Phone> + <ShipTo> + <Address> + <Street>#{options[:street]}</Street> + <City>San Jose</City> + <State>CA</State> + <Zip>95100</Zip> + <Country>US</Country> + </Address> + </ShipTo> + <CorrelationID>9c3706997455e</CorrelationID> + </PayPalResult> + <ExtData Name='LASTNAME' Value='Smith'/> + </TransactionResult> + </TransactionResults> + </ResponseData> + </XMLPayResponse> RESPONSE end - + + def successful_get_express_details_response_with_ship_to_name + <<~RESPONSE + <XMLPayResponse xmlns='http://www.verisign.com/XMLPay'> + <ResponseData> + <Vendor>TEST</Vendor> + <Partner>verisign</Partner> + <TransactionResults> + <TransactionResult> + <Result>0</Result> + <Message>Approved</Message> + <PayPalResult> + <EMail>Buyer1@paypal.com</EMail> + <PayerID>12345678901234567</PayerID> + <Token>EC-2OPN7UJGFWK9OYFV</Token> + <FeeAmount>0</FeeAmount> + <PayerStatus>verified</PayerStatus> + <Name>Joe</Name> + <Phone>555-555-5555</Phone> + <ShipTo> + <Address> + <Street>111 Main St.</Street> + <City>San Jose</City> + <State>CA</State> + <Zip>95100</Zip> + <Country>US</Country> + </Address> + </ShipTo> + <CorrelationID>9c3706997455e</CorrelationID> + </PayPalResult> + <ExtData Name='LASTNAME' Value='Smith'/> + <ExtData Name='SHIPTONAME' Value='John Joseph'/> + </TransactionResult> + </TransactionResults> + </ResponseData> + </XMLPayResponse> + RESPONSE + end + def invalid_get_express_details_response - <<-RESPONSE -<XMLPayResponse xmlns='http://www.verisign.com/XMLPay'> - <ResponseData> - <Vendor>TEST</Vendor> - <Partner>verisign</Partner> - <TransactionResults> - <TransactionResult> - <Result>7</Result> - <Message>Field format error: Invalid Token</Message> - </TransactionResult> - </TransactionResults> - </ResponseData> -</XMLPayResponse> + <<~RESPONSE + <XMLPayResponse xmlns='http://www.verisign.com/XMLPay'> + <ResponseData> + <Vendor>TEST</Vendor> + <Partner>verisign</Partner> + <TransactionResults> + <TransactionResult> + <Result>7</Result> + <Message>Field format error: Invalid Token</Message> + </TransactionResult> + </TransactionResults> + </ResponseData> + </XMLPayResponse> RESPONSE end end diff --git a/test/unit/gateways/payflow_express_uk_test.rb b/test/unit/gateways/payflow_express_uk_test.rb index 25f88820b1e..ef28d6b4d53 100644 --- a/test/unit/gateways/payflow_express_uk_test.rb +++ b/test/unit/gateways/payflow_express_uk_test.rb @@ -3,8 +3,8 @@ class PayflowExpressUkTest < Test::Unit::TestCase def setup @gateway = PayflowExpressUkGateway.new( - :login => 'LOGIN', - :password => 'PASSWORD' + login: 'LOGIN', + password: 'PASSWORD' ) end @@ -37,50 +37,121 @@ def test_get_express_details assert_nil address['phone'] end + def test_get_express_details_with_ship_to_name + @gateway.expects(:ssl_post).returns(successful_get_express_details_response_with_ship_to_name) + response = @gateway.details_for('EC-2OPN7UJGFWK9OYFV') + assert_instance_of PayflowExpressResponse, response + assert_success response + assert response.test? + + assert_equal 'EC-2OPN7UJGFWK9OYFV', response.token + assert_equal 'LYWCMEN4FA7ZQ', response.payer_id + assert_equal 'paul@test.com', response.email + assert_equal 'paul smith', response.full_name + assert_equal 'GB', response.payer_country + + assert address = response.address + assert_equal 'John Joseph', address['name'] + assert_nil address['company'] + assert_equal '10 keyworth avenue', address['address1'] + assert_equal 'grangetown', address['address2'] + assert_equal 'hinterland', address['city'] + assert_equal 'Tyne and Wear', address['state'] + assert_equal 'sr5 2uh', address['zip'] + assert_equal 'GB', address['country'] + assert_nil address['phone'] + end + private + def successful_get_express_details_response - <<-RESPONSE -<?xml version="1.0"?> -<XMLPayResponse xmlns="http://www.paypal.com/XMLPay"> - <ResponseData> - <Vendor>markcoop</Vendor> - <Partner>paypaluk</Partner> - <TransactionResults> - <TransactionResult> - <Result>0</Result> - <AVSResult> - <StreetMatch>Match</StreetMatch> - <ZipMatch>Match</ZipMatch> - </AVSResult> - <Message>Approved</Message> - <PayPalResult> - <EMail>paul@test.com</EMail> - <PayerID>LYWCMEN4FA7ZQ</PayerID> - <Token>EC-2OPN7UJGFWK9OYFV</Token> - <FeeAmount>0</FeeAmount> - <PayerStatus>unverified</PayerStatus> - <Name>paul</Name> - <ShipTo> - <Address> - <Street>10 keyworth avenue</Street> - <City>hinterland</City> - <State>Tyne and Wear</State> - <Zip>sr5 2uh</Zip> - <Country>GB</Country> - </Address> - </ShipTo> - <CorrelationID>1ea22ef3873ba</CorrelationID> - </PayPalResult> - <ExtData Name="LASTNAME" Value="smith"/> - <ExtData Name="SHIPTOSTREET2" Value="grangetown"/> - <ExtData Name="SHIPTONAME" Value="paul smith"/> - <ExtData Name="STREET2" Value="ALLAWAY AVENUE"/> - <ExtData Name="COUNTRYCODE" Value="GB"/> - <ExtData Name="ADDRESSSTATUS" Value="Y"/> - </TransactionResult> - </TransactionResults> - </ResponseData> -</XMLPayResponse> + <<~RESPONSE + <?xml version="1.0"?> + <XMLPayResponse xmlns="http://www.paypal.com/XMLPay"> + <ResponseData> + <Vendor>markcoop</Vendor> + <Partner>paypaluk</Partner> + <TransactionResults> + <TransactionResult> + <Result>0</Result> + <AVSResult> + <StreetMatch>Match</StreetMatch> + <ZipMatch>Match</ZipMatch> + </AVSResult> + <Message>Approved</Message> + <PayPalResult> + <EMail>paul@test.com</EMail> + <PayerID>LYWCMEN4FA7ZQ</PayerID> + <Token>EC-2OPN7UJGFWK9OYFV</Token> + <FeeAmount>0</FeeAmount> + <PayerStatus>unverified</PayerStatus> + <Name>paul</Name> + <ShipTo> + <Address> + <Street>10 keyworth avenue</Street> + <City>hinterland</City> + <State>Tyne and Wear</State> + <Zip>sr5 2uh</Zip> + <Country>GB</Country> + </Address> + </ShipTo> + <CorrelationID>1ea22ef3873ba</CorrelationID> + </PayPalResult> + <ExtData Name="LASTNAME" Value="smith"/> + <ExtData Name="SHIPTOSTREET2" Value="grangetown"/> + <ExtData Name="STREET2" Value="ALLAWAY AVENUE"/> + <ExtData Name="COUNTRYCODE" Value="GB"/> + <ExtData Name="ADDRESSSTATUS" Value="Y"/> + </TransactionResult> + </TransactionResults> + </ResponseData> + </XMLPayResponse> + RESPONSE + end + + def successful_get_express_details_response_with_ship_to_name + <<~RESPONSE + <?xml version="1.0"?> + <XMLPayResponse xmlns="http://www.paypal.com/XMLPay"> + <ResponseData> + <Vendor>markcoop</Vendor> + <Partner>paypaluk</Partner> + <TransactionResults> + <TransactionResult> + <Result>0</Result> + <AVSResult> + <StreetMatch>Match</StreetMatch> + <ZipMatch>Match</ZipMatch> + </AVSResult> + <Message>Approved</Message> + <PayPalResult> + <EMail>paul@test.com</EMail> + <PayerID>LYWCMEN4FA7ZQ</PayerID> + <Token>EC-2OPN7UJGFWK9OYFV</Token> + <FeeAmount>0</FeeAmount> + <PayerStatus>unverified</PayerStatus> + <Name>paul</Name> + <ShipTo> + <Address> + <Street>10 keyworth avenue</Street> + <City>hinterland</City> + <State>Tyne and Wear</State> + <Zip>sr5 2uh</Zip> + <Country>GB</Country> + </Address> + </ShipTo> + <CorrelationID>1ea22ef3873ba</CorrelationID> + </PayPalResult> + <ExtData Name="LASTNAME" Value="smith"/> + <ExtData Name="SHIPTOSTREET2" Value="grangetown"/> + <ExtData Name="SHIPTONAME" Value="John Joseph"/> + <ExtData Name="STREET2" Value="ALLAWAY AVENUE"/> + <ExtData Name="COUNTRYCODE" Value="GB"/> + <ExtData Name="ADDRESSSTATUS" Value="Y"/> + </TransactionResult> + </TransactionResults> + </ResponseData> + </XMLPayResponse> RESPONSE end end diff --git a/test/unit/gateways/payflow_test.rb b/test/unit/gateways/payflow_test.rb index b85956a69b5..40ed4669bfc 100644 --- a/test/unit/gateways/payflow_test.rb +++ b/test/unit/gateways/payflow_test.rb @@ -3,18 +3,40 @@ class PayflowTest < Test::Unit::TestCase include CommStub + # From `BuyerAuthStatusEnum` in https://www.paypalobjects.com/webstatic/en_US/developer/docs/pdf/pp_payflowpro_xmlpay_guide.pdf, page 109 + SUCCESSFUL_AUTHENTICATION_STATUS = 'Y' + CHALLENGE_REQUIRED_AUTHENTICATION_STATUS = 'C' + def setup Base.mode = :test @gateway = PayflowGateway.new( - :login => 'LOGIN', - :password => 'PASSWORD' + login: 'LOGIN', + password: 'PASSWORD' ) @amount = 100 @credit_card = credit_card('4242424242424242') - @options = { :billing_address => address.merge(:first_name => 'Longbob', :last_name => 'Longsen') } - @check = check( :name => 'Jim Smith' ) + @options = { billing_address: address.merge(first_name: 'Longbob', last_name: 'Longsen') } + @check = check(name: 'Jim Smith') + @l2_json = '{ + "Tender": { + "ACH": { + "AcctType": "C", + "AcctNum": "6355059797", + "ABA": "021000021" + } + } + }' + + @l3_json = '{ + "Invoice": { + "Date": "20190104", + "Level3Invoice": { + "CountyTax": {"Amount": "3.23"} + } + } + }' end def test_successful_authorization @@ -28,6 +50,70 @@ def test_successful_authorization refute response.fraud_review? end + def test_successful_purchase_with_stored_credential + @options[:stored_credential] = { + initial_transaction: false, + reason_type: 'recurring', + initiator: 'cardholder', + network_transaction_id: nil + } + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match %r(<CardOnFile>CITR</CardOnFile>), data + end.respond_with(successful_purchase_with_fraud_review_response) + + @options[:stored_credential] = { + initial_transaction: true, + reason_type: 'unscheduled', + initiator: 'cardholder', + network_transaction_id: nil + } + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match %r(<CardOnFile>CITI</CardOnFile>), data + end.respond_with(successful_purchase_with_fraud_review_response) + + @options[:stored_credential] = { + initial_transaction: false, + reason_type: 'unscheduled', + initiator: 'cardholder', + network_transaction_id: nil + } + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match %r(<CardOnFile>CITU</CardOnFile>), data + end.respond_with(successful_purchase_with_fraud_review_response) + + @options[:stored_credential] = { + initial_transaction: false, + reason_type: 'recurring', + initiator: 'merchant', + network_transaction_id: '1234' + } + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match %r(<CardOnFile>MITR</CardOnFile>), data + assert_match %r(<TxnId>1234</TxnId>), data + end.respond_with(successful_purchase_with_fraud_review_response) + + @options[:stored_credential] = { + initial_transaction: false, + reason_type: 'unscheduled', + initiator: 'merchant', + network_transaction_id: '123' + } + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match %r(<CardOnFile>MITU</CardOnFile>), data + assert_match %r(<TxnId>123</TxnId>), data + end.respond_with(successful_purchase_with_fraud_review_response) + end + def test_failed_authorization @gateway.stubs(:ssl_post).returns(failed_authorization_response) @@ -40,7 +126,7 @@ def test_failed_authorization def test_authorization_with_three_d_secure_option response = stub_comms do @gateway.authorize(@amount, @credit_card, @options.merge(three_d_secure_option)) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_three_d_secure REXML::Document.new(data), authorize_buyer_auth_result_path end.respond_with(successful_authorization_response) assert_equal 'Approved', response.message @@ -50,25 +136,88 @@ def test_authorization_with_three_d_secure_option refute response.fraud_review? end + def test_authorization_with_three_d_secure_option_with_version_includes_three_ds_version + expected_version = '1.0.2' + three_d_secure_option = three_d_secure_option(options: { version: expected_version }) + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(three_d_secure_option)) + end.check_request do |_endpoint, data, _headers| + assert_three_d_secure REXML::Document.new(data), authorize_buyer_auth_result_path, expected_version: + end.respond_with(successful_authorization_response) + end + + def test_authorization_with_three_d_secure_option_with_ds_transaction_id_includes_ds_transaction_id + expected_ds_transaction_id = 'any ds_transaction id' + three_d_secure_option = three_d_secure_option(options: { ds_transaction_id: expected_ds_transaction_id }) + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(three_d_secure_option)) + end.check_request do |_endpoint, data, _headers| + assert_three_d_secure REXML::Document.new(data), authorize_buyer_auth_result_path, expected_ds_transaction_id: + end.respond_with(successful_authorization_response) + end + + def test_authorization_with_three_d_secure_option_with_version_2_x_via_mpi + expected_version = '2.2.0' + expected_authentication_status = SUCCESSFUL_AUTHENTICATION_STATUS + expected_ds_transaction_id = 'f38e6948-5388-41a6-bca4-b49723c19437' + + three_d_secure_option = three_d_secure_option(options: { version: expected_version, ds_transaction_id: expected_ds_transaction_id }) + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(three_d_secure_option)) + end.check_request do |_endpoint, data, _headers| + xml = REXML::Document.new(data) + assert_three_d_secure_via_mpi(xml, tx_type: 'Authorization', expected_version:, expected_ds_transaction_id:) + assert_three_d_secure xml, authorize_buyer_auth_result_path, expected_version:, expected_authentication_status:, expected_ds_transaction_id: + end.respond_with(successful_authorization_response) + end + + def test_authorization_with_three_d_secure_option_with_version_2_x_and_authentication_response_status_include_authentication_status + expected_version = '2.2.0' + expected_authentication_status = SUCCESSFUL_AUTHENTICATION_STATUS + three_d_secure_option = three_d_secure_option(options: { version: expected_version }) + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(three_d_secure_option)) + end.check_request do |_endpoint, data, _headers| + assert_three_d_secure REXML::Document.new(data), authorize_buyer_auth_result_path, expected_version:, expected_authentication_status: + end.respond_with(successful_authorization_response) + end + + def test_authorization_with_three_d_secure_option_with_version_1_x_and_authentication_response_status_does_not_include_authentication_status + expected_version = '1.0.2' + expected_authentication_status = nil + three_d_secure_option = three_d_secure_option(options: { version: expected_version }) + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(three_d_secure_option)) + end.check_request do |_endpoint, data, _headers| + assert_three_d_secure REXML::Document.new(data), authorize_buyer_auth_result_path, expected_version:, expected_authentication_status: + end.respond_with(successful_authorization_response) + end + def test_successful_authorization_with_more_options + partner_id = 'partner_id' + PayflowGateway.application_id = partner_id + options = @options.merge( { order_id: '123', description: 'Description string', order_desc: 'OrderDesc string', comment: 'Comment string', - comment2: 'Comment2 string' + comment2: 'Comment2 string', + merch_descr: 'MerchDescr string' } ) response = stub_comms do @gateway.authorize(@amount, @credit_card, options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match %r(<InvNum>123</InvNum>), data assert_match %r(<Description>Description string</Description>), data assert_match %r(<OrderDesc>OrderDesc string</OrderDesc>), data assert_match %r(<Comment>Comment string</Comment>), data assert_match %r(<ExtData Name=\"COMMENT2\" Value=\"Comment2 string\"/>), data + assert_match %r(</PayData><ExtData Name=\"BUTTONSOURCE\" Value=\"partner_id\"/></Authorization>), data + assert_match %r(<MerchDescr>MerchDescr string</MerchDescr>), data end.respond_with(successful_authorization_response) assert_equal 'Approved', response.message assert_success response @@ -89,7 +238,7 @@ def test_successful_purchase_with_fraud_review def test_successful_purchase_with_three_d_secure_option response = stub_comms do @gateway.purchase(@amount, @credit_card, @options.merge(three_d_secure_option)) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_three_d_secure REXML::Document.new(data), purchase_buyer_auth_result_path end.respond_with(successful_purchase_with_fraud_review_response) assert_success response @@ -97,6 +246,76 @@ def test_successful_purchase_with_three_d_secure_option assert response.fraud_review? end + def test_successful_purchase_with_three_d_secure_option_with_version_2_x_via_mpi + expected_version = '2.2.0' + expected_authentication_status = SUCCESSFUL_AUTHENTICATION_STATUS + expected_ds_transaction_id = 'f38e6948-5388-41a6-bca4-b49723c19437' + + three_d_secure_option = three_d_secure_option(options: { version: expected_version, ds_transaction_id: expected_ds_transaction_id }) + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(three_d_secure_option)) + end.check_request do |_endpoint, data, _headers| + xml = REXML::Document.new(data) + assert_three_d_secure_via_mpi xml, tx_type: 'Sale', expected_version: expected_version, expected_ds_transaction_id: expected_ds_transaction_id + + assert_three_d_secure xml, purchase_buyer_auth_result_path, expected_version: expected_version, expected_authentication_status: expected_authentication_status, expected_ds_transaction_id: expected_ds_transaction_id + end.respond_with(successful_purchase_with_3ds_mpi) + assert_success response + + # see https://www.paypalobjects.com/webstatic/en_US/developer/docs/pdf/pp_payflowpro_xmlpay_guide.pdf, page 145, Table C.1 + assert_equal '0', response.params['result'] + refute response.fraud_review? + end + + def test_successful_purchase_with_level_2_fields + options = @options.merge(level_two_fields: @l2_json) + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match %r(<AcctNum>6355059797</AcctNum>), data + assert_match %r(<ACH><AcctType>), data.tr("\n ", '') + end.respond_with(successful_l2_response) + assert_equal 'Approved', response.message + assert_success response + assert_equal 'A1ADADCE9B12', response.authorization + refute response.fraud_review? + end + + def test_successful_purchase_with_level_3_fields + options = @options.merge(level_three_fields: @l3_json) + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match %r(<Date>20190104</Date>), data + assert_match %r(<Amount>3.23</Amount>), data + assert_match %r(<Level3Invoice><CountyTax><Amount>), data.tr("\n ", '') + end.respond_with(successful_l3_response) + assert_equal 'Approved', response.message + assert_success response + assert_equal 'A71AAC3B60A1', response.authorization + refute response.fraud_review? + end + + def test_successful_purchase_with_level_2_3_fields + options = @options.merge(level_two_fields: @l2_json).merge(level_three_fields: @l3_json) + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match %r(<Date>20190104</Date>), data + assert_match %r(<Amount>3.23</Amount>), data + assert_match %r(<AcctNum>6355059797</AcctNum>), data + assert_match %r(<ACH><AcctType>), data.tr("\n ", '') + assert_match %r(<Level3Invoice><CountyTax><Amount>), data.tr("\n ", '') + end.respond_with(successful_l2_response) + assert_equal 'Approved', response.message + assert_success response + assert_equal 'A1ADADCE9B12', response.authorization + refute response.fraud_review? + end + def test_credit @gateway.expects(:ssl_post).with(anything, regexp_matches(/<CardNum>#{@credit_card.number}<\//), anything).returns('') @gateway.expects(:parse).returns({}) @@ -162,9 +381,9 @@ def test_overriding_test_mode Base.mode = :production gateway = PayflowGateway.new( - :login => 'LOGIN', - :password => 'PASSWORD', - :test => true + login: 'LOGIN', + password: 'PASSWORD', + test: true ) assert gateway.test? @@ -174,8 +393,8 @@ def test_using_production_mode Base.mode = :production gateway = PayflowGateway.new( - :login => 'LOGIN', - :password => 'PASSWORD' + login: 'LOGIN', + password: 'PASSWORD' ) refute gateway.test? @@ -183,26 +402,26 @@ def test_using_production_mode def test_partner_class_accessor assert_equal 'PayPal', PayflowGateway.partner - gateway = PayflowGateway.new(:login => 'test', :password => 'test') + gateway = PayflowGateway.new(login: 'test', password: 'test') assert_equal 'PayPal', gateway.options[:partner] end def test_partner_class_accessor_used_when_passed_in_partner_is_blank assert_equal 'PayPal', PayflowGateway.partner - gateway = PayflowGateway.new(:login => 'test', :password => 'test', :partner => '') + gateway = PayflowGateway.new(login: 'test', password: 'test', partner: '') assert_equal 'PayPal', gateway.options[:partner] end def test_passed_in_partner_overrides_class_accessor assert_equal 'PayPal', PayflowGateway.partner - gateway = PayflowGateway.new(:login => 'test', :password => 'test', :partner => 'PayPalUk') + gateway = PayflowGateway.new(login: 'test', password: 'test', partner: 'PayPalUk') assert_equal 'PayPalUk', gateway.options[:partner] end def test_express_instance gateway = PayflowGateway.new( - :login => 'test', - :password => 'password' + login: 'test', + password: 'password' ) express = gateway.express assert_instance_of PayflowExpressGateway, express @@ -216,11 +435,11 @@ def test_default_currency end def test_supported_countries - assert_equal ['US', 'CA', 'NZ', 'AU'], PayflowGateway.supported_countries + assert_equal %w[US CA NZ AU], PayflowGateway.supported_countries end def test_supported_card_types - assert_equal [:visa, :master, :american_express, :jcb, :discover, :diners_club], PayflowGateway.supported_cardtypes + assert_equal %i[visa master american_express jcb discover diners_club], PayflowGateway.supported_cardtypes end def test_successful_verify @@ -238,12 +457,19 @@ def test_unsuccessful_verify assert_equal 'Declined', response.message end + def test_store_returns_error + error = assert_raises(ArgumentError) { @gateway.store(@credit_card, @options) } + assert_equal 'Store is not supported on Payflow gateways', error.message + end + def test_initial_recurring_transaction_missing_parameters assert_raises ArgumentError do assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.recurring(@amount, @credit_card, - :periodicity => :monthly, - :initial_transaction => { } + @gateway.recurring( + @amount, + @credit_card, + periodicity: :monthly, + initial_transaction: {} ) end end @@ -252,9 +478,11 @@ 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, - :periodicity => :monthly, - :initial_transaction => { :amount => :purchase } + @gateway.recurring( + @amount, + @credit_card, + periodicity: :monthly, + initial_transaction: { amount: :purchase } ) end end @@ -280,7 +508,7 @@ def test_successful_recurring_action @gateway.stubs(:ssl_post).returns(successful_recurring_response) response = assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.recurring(@amount, @credit_card, :periodicity => :monthly) + @gateway.recurring(@amount, @credit_card, periodicity: :monthly) end assert_instance_of PayflowResponse, response @@ -294,7 +522,7 @@ def test_successful_recurring_modify_action @gateway.stubs(:ssl_post).returns(successful_recurring_response) response = assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.recurring(@amount, nil, :profile_id => 'RT0000000009', :periodicity => :monthly) + @gateway.recurring(@amount, nil, profile_id: 'RT0000000009', periodicity: :monthly) end assert_instance_of PayflowResponse, response @@ -308,7 +536,7 @@ def test_successful_recurring_modify_action_with_retry_num_days @gateway.stubs(:ssl_post).returns(successful_recurring_response) response = assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.recurring(@amount, nil, :profile_id => 'RT0000000009', :retry_num_days => 3, :periodicity => :monthly) + @gateway.recurring(@amount, nil, profile_id: 'RT0000000009', retry_num_days: 3, periodicity: :monthly) end assert_instance_of PayflowResponse, response @@ -322,7 +550,7 @@ def test_falied_recurring_modify_action_with_starting_at_in_the_past @gateway.stubs(:ssl_post).returns(start_date_error_recurring_response) response = assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.recurring(@amount, nil, :profile_id => 'RT0000000009', :starting_at => Date.yesterday, :periodicity => :monthly) + @gateway.recurring(@amount, nil, profile_id: 'RT0000000009', starting_at: Date.yesterday, periodicity: :monthly) end assert_instance_of PayflowResponse, response @@ -337,7 +565,7 @@ def test_falied_recurring_modify_action_with_starting_at_missing_and_changed_per @gateway.stubs(:ssl_post).returns(start_date_missing_recurring_response) response = assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.recurring(@amount, nil, :profile_id => 'RT0000000009', :periodicity => :yearly) + @gateway.recurring(@amount, nil, profile_id: 'RT0000000009', periodicity: :yearly) end assert_instance_of PayflowResponse, response @@ -352,7 +580,7 @@ def test_recurring_profile_payment_history_inquiry @gateway.stubs(:ssl_post).returns(successful_payment_history_recurring_response) response = assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.recurring_inquiry('RT0000000009', :history => true) + @gateway.recurring_inquiry('RT0000000009', history: true) end assert_equal 1, response.payment_history.size assert_equal '1', response.payment_history.first['payment_num'] @@ -360,32 +588,40 @@ def test_recurring_profile_payment_history_inquiry end def test_recurring_profile_payment_history_inquiry_contains_the_proper_xml - request = @gateway.send( :build_recurring_request, :inquiry, nil, :profile_id => 'RT0000000009', :history => true) + request = @gateway.send(:build_recurring_request, :inquiry, nil, profile_id: 'RT0000000009', history: true) assert_match %r(<PaymentHistory>Y</PaymentHistory), request end - def test_format_issue_number + def test_add_credit_card_with_three_d_secure xml = Builder::XmlMarkup.new - credit_card = credit_card('5641820000000005', - :brand => 'switch', - :issue_number => 1 + credit_card = credit_card( + '5641820000000005', + brand: 'maestro' ) - @gateway.send(:add_credit_card, xml, credit_card) - doc = REXML::Document.new(xml.target!) - node = REXML::XPath.first(doc, '/Card/ExtData') - assert_equal '01', node.attributes['Value'] + @gateway.send(:add_credit_card, xml, credit_card, @options.merge(three_d_secure_option)) + assert_three_d_secure REXML::Document.new(xml.target!), '/Card/BuyerAuthResult' end - def test_add_credit_card_with_three_d_secure + def test_add_credit_card_with_three_d_secure_challenge_required xml = Builder::XmlMarkup.new - credit_card = credit_card('5641820000000005', - :brand => 'switch', - :issue_number => 1 + credit_card = credit_card( + '5641820000000005', + brand: 'maestro' ) + three_d_secure_option = three_d_secure_option( + options: { + authentication_response_status: nil, + directory_response_status: CHALLENGE_REQUIRED_AUTHENTICATION_STATUS + } + ) @gateway.send(:add_credit_card, xml, credit_card, @options.merge(three_d_secure_option)) - assert_three_d_secure REXML::Document.new(xml.target!), '/Card/BuyerAuthResult' + assert_three_d_secure( + REXML::Document.new(xml.target!), + '/Card/BuyerAuthResult', + expected_status: CHALLENGE_REQUIRED_AUTHENTICATION_STATUS + ) end def test_duplicate_response_flag @@ -411,7 +647,7 @@ def test_timeout_is_same_in_header_and_xml end def test_name_field_are_included_instead_of_first_and_last - @gateway.expects(:ssl_post).returns(successful_authorization_response).with do |url, data| + @gateway.expects(:ssl_post).returns(successful_authorization_response).with do |_url, data| data !~ /FirstName/ && data !~ /LastName/ && data =~ /<Name>/ end response = @gateway.authorize(@amount, @credit_card, @options) @@ -419,8 +655,8 @@ def test_name_field_are_included_instead_of_first_and_last end def test_passed_in_verbosity - assert_nil PayflowGateway.new(:login => 'test', :password => 'test').options[:verbosity] - gateway = PayflowGateway.new(:login => 'test', :password => 'test', :verbosity => 'HIGH') + assert_nil PayflowGateway.new(login: 'test', password: 'test').options[:verbosity] + gateway = PayflowGateway.new(login: 'test', password: 'test', verbosity: 'HIGH') assert_equal 'HIGH', gateway.options[:verbosity] @gateway.expects(:ssl_post).returns(verbose_transaction_response) response = @gateway.purchase(100, @credit_card, @options) @@ -448,199 +684,288 @@ def test_scrub private def pre_scrubbed - <<-EOS -opening connection to pilot-payflowpro.paypal.com:443... -opened -starting SSL for pilot-payflowpro.paypal.com:443... -SSL established -<- "POST / HTTP/1.1\r\nContent-Type: text/xml\r\nContent-Length: 1017\r\nX-Vps-Client-Timeout: 60\r\nX-Vps-Vit-Integration-Product: ActiveMerchant\r\nX-Vps-Vit-Runtime-Version: 2.1.7\r\nX-Vps-Request-Id: 3b2f9831949b48b4b0b89a33a60f9b0c\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: pilot-payflowpro.paypal.com\r\n\r\n" -<- "<?xml version=\"1.0\" encoding=\"UTF-8\"?><XMLPayRequest Timeout=\"60\" version=\"2.1\" xmlns=\"http://www.paypal.com/XMLPay\"><RequestData><Vendor>spreedlyIntegrations</Vendor><Partner>PayPal</Partner><Transactions><Transaction CustRef=\"codyexample\"><Verbosity>MEDIUM</Verbosity><Sale><PayData><Invoice><EMail>cody@example.com</EMail><BillTo><Name>Jim Smith</Name><EMail>cody@example.com</EMail><Phone>(555)555-5555</Phone><CustCode>codyexample</CustCode><Address><Street>456 My Street</Street><City>Ottawa</City><State>ON</State><Country>CA</Country><Zip>K1C2N6</Zip></Address></BillTo><TotalAmt Currency=\"USD\"/></Invoice><Tender><Card><CardType>MasterCard</CardType><CardNum>5105105105105100</CardNum><ExpDate>201909</ExpDate><NameOnCard>Longbob</NameOnCard><CVNum>123</CVNum><ExtData Name=\"LASTNAME\" Value=\"Longsen\"/></Card></Tender></PayData></Sale></Transaction></Transactions></RequestData><RequestAuth><UserPass><User>spreedlyIntegrations</User><Password>L9DjqEKjXCkU</Password></UserPass></RequestAuth></XMLPayRequest>" --> "HTTP/1.1 200 OK\r\n" --> "Connection: close\r\n" --> "Server: VPS-3.033.00\r\n" --> "X-VPS-Request-ID: 3b2f9831949b48b4b0b89a33a60f9b0c\r\n" --> "Date: Thu, 01 Mar 2018 15:42:15 GMT\r\n" --> "Content-type: text/xml\r\n" --> "Content-length: 267\r\n" --> "\r\n" -reading 267 bytes... --> "<XMLPayResponse xmlns=\"http://www.paypal.com/XMLPay\"><ResponseData><Vendor></Vendor><Partner></Partner><TransactionResults><TransactionResult><Result>4</Result><Message>Invalid amount</Message></TransactionResult></TransactionResults></ResponseData></XMLPayResponse>" -read 267 bytes -Conn close - EOS + <<~REQUEST + opening connection to pilot-payflowpro.paypal.com:443... + opened + starting SSL for pilot-payflowpro.paypal.com:443... + SSL established + <- "POST / HTTP/1.1\r\nContent-Type: text/xml\r\nContent-Length: 1017\r\nX-Vps-Client-Timeout: 60\r\nX-Vps-Vit-Integration-Product: ActiveMerchant\r\nX-Vps-Vit-Runtime-Version: 2.1.7\r\nX-Vps-Request-Id: 3b2f9831949b48b4b0b89a33a60f9b0c\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: pilot-payflowpro.paypal.com\r\n\r\n" + <- "<?xml version=\"1.0\" encoding=\"UTF-8\"?><XMLPayRequest Timeout=\"60\" version=\"2.1\" xmlns=\"http://www.paypal.com/XMLPay\"><RequestData><Vendor>spreedlyIntegrations</Vendor><Partner>PayPal</Partner><Transactions><Transaction CustRef=\"codyexample\"><Verbosity>MEDIUM</Verbosity><Sale><PayData><Invoice><EMail>cody@example.com</EMail><BillTo><Name>Jim Smith</Name><EMail>cody@example.com</EMail><Phone>(555)555-5555</Phone><CustCode>codyexample</CustCode><Address><Street>456 My Street</Street><City>Ottawa</City><State>ON</State><Country>CA</Country><Zip>K1C2N6</Zip></Address></BillTo><TotalAmt Currency=\"USD\"/></Invoice><Tender><Card><CardType>MasterCard</CardType><CardNum>5105105105105100</CardNum><ExpDate>201909</ExpDate><NameOnCard>Longbob</NameOnCard><CVNum>123</CVNum><ExtData Name=\"LASTNAME\" Value=\"Longsen\"/></Card></Tender></PayData></Sale></Transaction></Transactions></RequestData><RequestAuth><UserPass><User>spreedlyIntegrations</User><Password>L9DjqEKjXCkU</Password></UserPass></RequestAuth></XMLPayRequest>" + -> "HTTP/1.1 200 OK\r\n" + -> "Connection: close\r\n" + -> "Server: VPS-3.033.00\r\n" + -> "X-VPS-Request-ID: 3b2f9831949b48b4b0b89a33a60f9b0c\r\n" + -> "Date: Thu, 01 Mar 2018 15:42:15 GMT\r\n" + -> "Content-type: text/xml\r\n" + -> "Content-length: 267\r\n" + -> "\r\n" + reading 267 bytes... + -> "<XMLPayResponse xmlns=\"http://www.paypal.com/XMLPay\"><ResponseData><Vendor></Vendor><Partner></Partner><TransactionResults><TransactionResult><Result>4</Result><Message>Invalid amount</Message></TransactionResult></TransactionResults></ResponseData></XMLPayResponse>" + read 267 bytes + Conn close + REQUEST end def post_scrubbed - <<-EOS -opening connection to pilot-payflowpro.paypal.com:443... -opened -starting SSL for pilot-payflowpro.paypal.com:443... -SSL established -<- "POST / HTTP/1.1\r\nContent-Type: text/xml\r\nContent-Length: 1017\r\nX-Vps-Client-Timeout: 60\r\nX-Vps-Vit-Integration-Product: ActiveMerchant\r\nX-Vps-Vit-Runtime-Version: 2.1.7\r\nX-Vps-Request-Id: 3b2f9831949b48b4b0b89a33a60f9b0c\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: pilot-payflowpro.paypal.com\r\n\r\n" -<- "<?xml version=\"1.0\" encoding=\"UTF-8\"?><XMLPayRequest Timeout=\"60\" version=\"2.1\" xmlns=\"http://www.paypal.com/XMLPay\"><RequestData><Vendor>spreedlyIntegrations</Vendor><Partner>PayPal</Partner><Transactions><Transaction CustRef=\"codyexample\"><Verbosity>MEDIUM</Verbosity><Sale><PayData><Invoice><EMail>cody@example.com</EMail><BillTo><Name>Jim Smith</Name><EMail>cody@example.com</EMail><Phone>(555)555-5555</Phone><CustCode>codyexample</CustCode><Address><Street>456 My Street</Street><City>Ottawa</City><State>ON</State><Country>CA</Country><Zip>K1C2N6</Zip></Address></BillTo><TotalAmt Currency=\"USD\"/></Invoice><Tender><Card><CardType>MasterCard</CardType><CardNum>[FILTERED]</CardNum><ExpDate>201909</ExpDate><NameOnCard>Longbob</NameOnCard><CVNum>[FILTERED]</CVNum><ExtData Name=\"LASTNAME\" Value=\"Longsen\"/></Card></Tender></PayData></Sale></Transaction></Transactions></RequestData><RequestAuth><UserPass><User>spreedlyIntegrations</User><Password>[FILTERED]</Password></UserPass></RequestAuth></XMLPayRequest>" --> "HTTP/1.1 200 OK\r\n" --> "Connection: close\r\n" --> "Server: VPS-3.033.00\r\n" --> "X-VPS-Request-ID: 3b2f9831949b48b4b0b89a33a60f9b0c\r\n" --> "Date: Thu, 01 Mar 2018 15:42:15 GMT\r\n" --> "Content-type: text/xml\r\n" --> "Content-length: 267\r\n" --> "\r\n" -reading 267 bytes... --> "<XMLPayResponse xmlns=\"http://www.paypal.com/XMLPay\"><ResponseData><Vendor></Vendor><Partner></Partner><TransactionResults><TransactionResult><Result>4</Result><Message>Invalid amount</Message></TransactionResult></TransactionResults></ResponseData></XMLPayResponse>" -read 267 bytes -Conn close - EOS + <<~REQUEST + opening connection to pilot-payflowpro.paypal.com:443... + opened + starting SSL for pilot-payflowpro.paypal.com:443... + SSL established + <- "POST / HTTP/1.1\r\nContent-Type: text/xml\r\nContent-Length: 1017\r\nX-Vps-Client-Timeout: 60\r\nX-Vps-Vit-Integration-Product: ActiveMerchant\r\nX-Vps-Vit-Runtime-Version: 2.1.7\r\nX-Vps-Request-Id: 3b2f9831949b48b4b0b89a33a60f9b0c\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: pilot-payflowpro.paypal.com\r\n\r\n" + <- "<?xml version=\"1.0\" encoding=\"UTF-8\"?><XMLPayRequest Timeout=\"60\" version=\"2.1\" xmlns=\"http://www.paypal.com/XMLPay\"><RequestData><Vendor>spreedlyIntegrations</Vendor><Partner>PayPal</Partner><Transactions><Transaction CustRef=\"codyexample\"><Verbosity>MEDIUM</Verbosity><Sale><PayData><Invoice><EMail>cody@example.com</EMail><BillTo><Name>Jim Smith</Name><EMail>cody@example.com</EMail><Phone>(555)555-5555</Phone><CustCode>codyexample</CustCode><Address><Street>456 My Street</Street><City>Ottawa</City><State>ON</State><Country>CA</Country><Zip>K1C2N6</Zip></Address></BillTo><TotalAmt Currency=\"USD\"/></Invoice><Tender><Card><CardType>MasterCard</CardType><CardNum>[FILTERED]</CardNum><ExpDate>201909</ExpDate><NameOnCard>Longbob</NameOnCard><CVNum>[FILTERED]</CVNum><ExtData Name=\"LASTNAME\" Value=\"Longsen\"/></Card></Tender></PayData></Sale></Transaction></Transactions></RequestData><RequestAuth><UserPass><User>spreedlyIntegrations</User><Password>[FILTERED]</Password></UserPass></RequestAuth></XMLPayRequest>" + -> "HTTP/1.1 200 OK\r\n" + -> "Connection: close\r\n" + -> "Server: VPS-3.033.00\r\n" + -> "X-VPS-Request-ID: 3b2f9831949b48b4b0b89a33a60f9b0c\r\n" + -> "Date: Thu, 01 Mar 2018 15:42:15 GMT\r\n" + -> "Content-type: text/xml\r\n" + -> "Content-length: 267\r\n" + -> "\r\n" + reading 267 bytes... + -> "<XMLPayResponse xmlns=\"http://www.paypal.com/XMLPay\"><ResponseData><Vendor></Vendor><Partner></Partner><TransactionResults><TransactionResult><Result>4</Result><Message>Invalid amount</Message></TransactionResult></TransactionResults></ResponseData></XMLPayResponse>" + read 267 bytes + Conn close + REQUEST end def pre_scrubbed_check - <<-EOS -opening connection to pilot-payflowpro.paypal.com:443... -opened -starting SSL for pilot-payflowpro.paypal.com:443... -SSL established -<- "POST / HTTP/1.1\r\nContent-Type: text/xml\r\nContent-Length: 658\r\nX-Vps-Client-Timeout: 60\r\nX-Vps-Vit-Integration-Product: ActiveMerchant\r\nX-Vps-Vit-Runtime-Version: 2.1.7\r\nX-Vps-Request-Id: 863021e6890a0660238ef22d0a21c5f2\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: pilot-payflowpro.paypal.com\r\n\r\n" -<- "<?xml version=\"1.0\" encoding=\"UTF-8\"?><XMLPayRequest Timeout=\"60\" version=\"2.1\" xmlns=\"http://www.paypal.com/XMLPay\"><RequestData><Vendor>spreedlyIntegrations</Vendor><Partner>PayPal</Partner><Transactions><Transaction CustRef=\"codyexample\"><Verbosity>MEDIUM</Verbosity><Sale><PayData><Invoice><BillTo><Name>Jim Smith</Name></BillTo><TotalAmt Currency=\"USD\"/></Invoice><Tender><ACH><AcctType>C</AcctType><AcctNum>1234567801</AcctNum><ABA>111111118</ABA></ACH></Tender></PayData></Sale></Transaction></Transactions></RequestData><RequestAuth><UserPass><User>spreedlyIntegrations</User><Password>L9DjqEKjXCkU</Password></UserPass></RequestAuth></XMLPayRequest>" --> "HTTP/1.1 200 OK\r\n" --> "Connection: close\r\n" --> "Server: VPS-3.033.00\r\n" --> "X-VPS-Request-ID: 863021e6890a0660238ef22d0a21c5f2\r\n" --> "Date: Thu, 01 Mar 2018 15:45:59 GMT\r\n" --> "Content-type: text/xml\r\n" --> "Content-length: 267\r\n" --> "\r\n" -reading 267 bytes... --> "<XMLPayResponse xmlns=\"http://www.paypal.com/XMLPay\"><ResponseData><Vendor></Vendor><Partner></Partner><TransactionResults><TransactionResult><Result>4</Result><Message>Invalid amount</Message></TransactionResult></TransactionResults></ResponseData></XMLPayResponse>" -read 267 bytes -Conn close - EOS + <<~REQUEST + opening connection to pilot-payflowpro.paypal.com:443... + opened + starting SSL for pilot-payflowpro.paypal.com:443... + SSL established + <- "POST / HTTP/1.1\r\nContent-Type: text/xml\r\nContent-Length: 658\r\nX-Vps-Client-Timeout: 60\r\nX-Vps-Vit-Integration-Product: ActiveMerchant\r\nX-Vps-Vit-Runtime-Version: 2.1.7\r\nX-Vps-Request-Id: 863021e6890a0660238ef22d0a21c5f2\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: pilot-payflowpro.paypal.com\r\n\r\n" + <- "<?xml version=\"1.0\" encoding=\"UTF-8\"?><XMLPayRequest Timeout=\"60\" version=\"2.1\" xmlns=\"http://www.paypal.com/XMLPay\"><RequestData><Vendor>spreedlyIntegrations</Vendor><Partner>PayPal</Partner><Transactions><Transaction CustRef=\"codyexample\"><Verbosity>MEDIUM</Verbosity><Sale><PayData><Invoice><BillTo><Name>Jim Smith</Name></BillTo><TotalAmt Currency=\"USD\"/></Invoice><Tender><ACH><AcctType>C</AcctType><AcctNum>1234567801</AcctNum><ABA>111111118</ABA></ACH></Tender></PayData></Sale></Transaction></Transactions></RequestData><RequestAuth><UserPass><User>spreedlyIntegrations</User><Password>L9DjqEKjXCkU</Password></UserPass></RequestAuth></XMLPayRequest>" + -> "HTTP/1.1 200 OK\r\n" + -> "Connection: close\r\n" + -> "Server: VPS-3.033.00\r\n" + -> "X-VPS-Request-ID: 863021e6890a0660238ef22d0a21c5f2\r\n" + -> "Date: Thu, 01 Mar 2018 15:45:59 GMT\r\n" + -> "Content-type: text/xml\r\n" + -> "Content-length: 267\r\n" + -> "\r\n" + reading 267 bytes... + -> "<XMLPayResponse xmlns=\"http://www.paypal.com/XMLPay\"><ResponseData><Vendor></Vendor><Partner></Partner><TransactionResults><TransactionResult><Result>4</Result><Message>Invalid amount</Message></TransactionResult></TransactionResults></ResponseData></XMLPayResponse>" + read 267 bytes + Conn close + REQUEST end def post_scrubbed_check - <<-EOS -opening connection to pilot-payflowpro.paypal.com:443... -opened -starting SSL for pilot-payflowpro.paypal.com:443... -SSL established -<- "POST / HTTP/1.1\r\nContent-Type: text/xml\r\nContent-Length: 658\r\nX-Vps-Client-Timeout: 60\r\nX-Vps-Vit-Integration-Product: ActiveMerchant\r\nX-Vps-Vit-Runtime-Version: 2.1.7\r\nX-Vps-Request-Id: 863021e6890a0660238ef22d0a21c5f2\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: pilot-payflowpro.paypal.com\r\n\r\n" -<- "<?xml version=\"1.0\" encoding=\"UTF-8\"?><XMLPayRequest Timeout=\"60\" version=\"2.1\" xmlns=\"http://www.paypal.com/XMLPay\"><RequestData><Vendor>spreedlyIntegrations</Vendor><Partner>PayPal</Partner><Transactions><Transaction CustRef=\"codyexample\"><Verbosity>MEDIUM</Verbosity><Sale><PayData><Invoice><BillTo><Name>Jim Smith</Name></BillTo><TotalAmt Currency=\"USD\"/></Invoice><Tender><ACH><AcctType>C</AcctType><AcctNum>[FILTERED]</AcctNum><ABA>111111118</ABA></ACH></Tender></PayData></Sale></Transaction></Transactions></RequestData><RequestAuth><UserPass><User>spreedlyIntegrations</User><Password>[FILTERED]</Password></UserPass></RequestAuth></XMLPayRequest>" --> "HTTP/1.1 200 OK\r\n" --> "Connection: close\r\n" --> "Server: VPS-3.033.00\r\n" --> "X-VPS-Request-ID: 863021e6890a0660238ef22d0a21c5f2\r\n" --> "Date: Thu, 01 Mar 2018 15:45:59 GMT\r\n" --> "Content-type: text/xml\r\n" --> "Content-length: 267\r\n" --> "\r\n" -reading 267 bytes... --> "<XMLPayResponse xmlns=\"http://www.paypal.com/XMLPay\"><ResponseData><Vendor></Vendor><Partner></Partner><TransactionResults><TransactionResult><Result>4</Result><Message>Invalid amount</Message></TransactionResult></TransactionResults></ResponseData></XMLPayResponse>" -read 267 bytes -Conn close - EOS + <<~REQUEST + opening connection to pilot-payflowpro.paypal.com:443... + opened + starting SSL for pilot-payflowpro.paypal.com:443... + SSL established + <- "POST / HTTP/1.1\r\nContent-Type: text/xml\r\nContent-Length: 658\r\nX-Vps-Client-Timeout: 60\r\nX-Vps-Vit-Integration-Product: ActiveMerchant\r\nX-Vps-Vit-Runtime-Version: 2.1.7\r\nX-Vps-Request-Id: 863021e6890a0660238ef22d0a21c5f2\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: pilot-payflowpro.paypal.com\r\n\r\n" + <- "<?xml version=\"1.0\" encoding=\"UTF-8\"?><XMLPayRequest Timeout=\"60\" version=\"2.1\" xmlns=\"http://www.paypal.com/XMLPay\"><RequestData><Vendor>spreedlyIntegrations</Vendor><Partner>PayPal</Partner><Transactions><Transaction CustRef=\"codyexample\"><Verbosity>MEDIUM</Verbosity><Sale><PayData><Invoice><BillTo><Name>Jim Smith</Name></BillTo><TotalAmt Currency=\"USD\"/></Invoice><Tender><ACH><AcctType>C</AcctType><AcctNum>[FILTERED]</AcctNum><ABA>111111118</ABA></ACH></Tender></PayData></Sale></Transaction></Transactions></RequestData><RequestAuth><UserPass><User>spreedlyIntegrations</User><Password>[FILTERED]</Password></UserPass></RequestAuth></XMLPayRequest>" + -> "HTTP/1.1 200 OK\r\n" + -> "Connection: close\r\n" + -> "Server: VPS-3.033.00\r\n" + -> "X-VPS-Request-ID: 863021e6890a0660238ef22d0a21c5f2\r\n" + -> "Date: Thu, 01 Mar 2018 15:45:59 GMT\r\n" + -> "Content-type: text/xml\r\n" + -> "Content-length: 267\r\n" + -> "\r\n" + reading 267 bytes... + -> "<XMLPayResponse xmlns=\"http://www.paypal.com/XMLPay\"><ResponseData><Vendor></Vendor><Partner></Partner><TransactionResults><TransactionResult><Result>4</Result><Message>Invalid amount</Message></TransactionResult></TransactionResults></ResponseData></XMLPayResponse>" + read 267 bytes + Conn close + REQUEST end def successful_recurring_response - <<-XML -<ResponseData> - <Result>0</Result> - <Message>Approved</Message> - <Partner>paypal</Partner> - <RPRef>R7960E739F80</RPRef> - <Vendor>ActiveMerchant</Vendor> - <ProfileId>RT0000000009</ProfileId> -</ResponseData> - XML + <<~XML + <ResponseData> + <Result>0</Result> + <Message>Approved</Message> + <Partner>paypal</Partner> + <RPRef>R7960E739F80</RPRef> + <Vendor>ActiveMerchant</Vendor> + <ProfileId>RT0000000009</ProfileId> + </ResponseData> + XML end def start_date_error_recurring_response - <<-XML - <ResponseData> - <Result>0</Result> - <Message>Field format error: START or NEXTPAYMENTDATE older than last payment date</Message> - <Partner>paypal</Partner> - <RPRef>R7960E739F80</RPRef> - <Vendor>ActiveMerchant</Vendor> - <ProfileId>RT0000000009</ProfileId> - </ResponseData> + <<~XML + <ResponseData> + <Result>0</Result> + <Message>Field format error: START or NEXTPAYMENTDATE older than last payment date</Message> + <Partner>paypal</Partner> + <RPRef>R7960E739F80</RPRef> + <Vendor>ActiveMerchant</Vendor> + <ProfileId>RT0000000009</ProfileId> + </ResponseData> XML end def start_date_missing_recurring_response - <<-XML - <ResponseData> - <Result>0</Result> - <Message>Field format error: START field missing</Message> - <Partner>paypal</Partner> - <RPRef>R7960E739F80</RPRef> - <Vendor>ActiveMerchant</Vendor> - <ProfileId>RT0000000009</ProfileId> - </ResponseData> + <<~XML + <ResponseData> + <Result>0</Result> + <Message>Field format error: START field missing</Message> + <Partner>paypal</Partner> + <RPRef>R7960E739F80</RPRef> + <Vendor>ActiveMerchant</Vendor> + <ProfileId>RT0000000009</ProfileId> + </ResponseData> XML end def successful_payment_history_recurring_response - <<-XML -<ResponseData> - <Result>0</Result> - <Partner>paypal</Partner> - <RPRef>R7960E739F80</RPRef> - <Vendor>ActiveMerchant</Vendor> - <ProfileId>RT0000000009</ProfileId> - <RPPaymentResult> - <PaymentNum>1</PaymentNum> - <PNRef>V18A0D3048AF</PNRef> - <TransTime>12-Jan-08 04:30 AM</TransTime> - <Result>0</Result> - <Tender>C</Tender> - <Amt Currency="7.25"></Amt> - <TransState>6</TransState> - </RPPaymentResult> -</ResponseData> - XML + <<~XML + <ResponseData> + <Result>0</Result> + <Partner>paypal</Partner> + <RPRef>R7960E739F80</RPRef> + <Vendor>ActiveMerchant</Vendor> + <ProfileId>RT0000000009</ProfileId> + <RPPaymentResult> + <PaymentNum>1</PaymentNum> + <PNRef>V18A0D3048AF</PNRef> + <TransTime>12-Jan-08 04:30 AM</TransTime> + <Result>0</Result> + <Tender>C</Tender> + <Amt Currency="7.25"></Amt> + <TransState>6</TransState> + </RPPaymentResult> + </ResponseData> + XML end def successful_authorization_response - <<-XML -<ResponseData> - <Result>0</Result> - <Message>Approved</Message> - <Partner>verisign</Partner> - <HostCode>000</HostCode> - <ResponseText>AP</ResponseText> - <PnRef>VUJN1A6E11D9</PnRef> - <IavsResult>N</IavsResult> - <ZipMatch>Match</ZipMatch> - <AuthCode>094016</AuthCode> - <Vendor>ActiveMerchant</Vendor> - <AvsResult>Y</AvsResult> - <StreetMatch>Match</StreetMatch> - <CvResult>Match</CvResult> -</ResponseData> + <<~XML + <ResponseData> + <Result>0</Result> + <Message>Approved</Message> + <Partner>verisign</Partner> + <HostCode>000</HostCode> + <ResponseText>AP</ResponseText> + <PnRef>VUJN1A6E11D9</PnRef> + <IavsResult>N</IavsResult> + <ZipMatch>Match</ZipMatch> + <AuthCode>094016</AuthCode> + <Vendor>ActiveMerchant</Vendor> + <AvsResult>Y</AvsResult> + <StreetMatch>Match</StreetMatch> + <CvResult>Match</CvResult> + </ResponseData> + XML + end + + def successful_purchase_with_3ds_mpi + <<~XML + <XMLPayResponse xmlns="http://www.paypal.com/XMLPay"> + <ResponseData> + <Vendor>spreedlyIntegrations</Vendor> + <Partner>paypal</Partner> + <TransactionResults> + <TransactionResult> + <Result>0</Result> + <ProcessorResult> + <AVSResult>Z</AVSResult> + <CVResult>M</CVResult> + <HostCode>A</HostCode> + </ProcessorResult> + <FraudPreprocessResult> + <Message>No Rules Triggered</Message> + </FraudPreprocessResult> + <FraudPostprocessResult> + <Message>No Rules Triggered</Message> + </FraudPostprocessResult> + <IAVSResult>N</IAVSResult> + <AVSResult> + <StreetMatch>No Match</StreetMatch> + <ZipMatch>Match</ZipMatch> + </AVSResult> + <CVResult>Match</CVResult> + <Message>Approved</Message> + <PNRef>A11AB1C8156A</PNRef> + <AuthCode>980PNI</AuthCode> + </TransactionResult> + </TransactionResults> + </ResponseData> + </XMLPayResponse> + XML + end + + def successful_l3_response + <<~XML + <ResponseData> + <Vendor>spreedlyIntegrations</Vendor> + <Partner>paypal</Partner> + <TransactionResults> + <TransactionResult> + <Result>0</Result> + <ProcessorResult> + <AVSResult>Z</AVSResult> + <CVResult>M</CVResult> + <HostCode>A</HostCode> + </ProcessorResult> + <FraudPreprocessResult> + <Message>No Rules Triggered</Message> + </FraudPreprocessResult> + <FraudPostprocessResult> + <Message>No Rules Triggered</Message> + </FraudPostprocessResult> + <IAVSResult>N</IAVSResult> + <AVSResult> + <StreetMatch>No Match</StreetMatch> + <ZipMatch>Match</ZipMatch> + </AVSResult> + <CVResult>Match</CVResult> + <Message>Approved</Message> + <PNRef>A71AAC3B60A1</PNRef> + <AuthCode>240PNI</AuthCode> + </TransactionResult> + </TransactionResults> + </ResponseData> + XML + end + + def successful_l2_response + <<~XML + <ResponseData> + <Vendor>spreedlyIntegrations</Vendor> + <Partner>paypal</Partner> + <TransactionResults> + <TransactionResult> + <Result>0</Result> + <ProcessorResult> + <HostCode>A</HostCode> + </ProcessorResult> + <Message>Approved</Message> + <PNRef>A1ADADCE9B12</PNRef> + </TransactionResult> + </TransactionResults> + </ResponseData> XML end def failed_authorization_response - <<-XML -<ResponseData> - <Result>12</Result> - <Message>Declined</Message> - <Partner>verisign</Partner> - <HostCode>000</HostCode> - <ResponseText>AP</ResponseText> - <PnRef>VUJN1A6E11D9</PnRef> - <IavsResult>N</IavsResult> - <ZipMatch>Match</ZipMatch> - <AuthCode>094016</AuthCode> - <Vendor>ActiveMerchant</Vendor> - <AvsResult>Y</AvsResult> - <StreetMatch>Match</StreetMatch> - <CvResult>Match</CvResult> -</ResponseData> + <<~XML + <ResponseData> + <Result>12</Result> + <Message>Declined</Message> + <Partner>verisign</Partner> + <HostCode>000</HostCode> + <ResponseText>AP</ResponseText> + <PnRef>VUJN1A6E11D9</PnRef> + <IavsResult>N</IavsResult> + <ZipMatch>Match</ZipMatch> + <AuthCode>094016</AuthCode> + <Vendor>ActiveMerchant</Vendor> + <AvsResult>Y</AvsResult> + <StreetMatch>Match</StreetMatch> + <CvResult>Match</CvResult> + </ResponseData> XML end def successful_purchase_with_fraud_review_response - <<-XML + <<~XML <XMLPayResponse xmlns="http://www.paypal.com/XMLPay"> <ResponseData> <Vendor>spreedly</Vendor> @@ -680,114 +1005,161 @@ def successful_purchase_with_fraud_review_response end def successful_duplicate_response - <<-XML -<?xml version="1.0"?> -<XMLPayResponse xmlns="http://www.verisign.com/XMLPay"> - <ResponseData> - <Vendor>ActiveMerchant</Vendor> - <Partner>paypal</Partner> - <TransactionResults> - <TransactionResult Duplicate="true"> - <Result>0</Result> - <ProcessorResult> - <AVSResult>A</AVSResult> - <CVResult>M</CVResult> - <HostCode>A</HostCode> - </ProcessorResult> - <IAVSResult>N</IAVSResult> - <AVSResult> - <StreetMatch>Match</StreetMatch> - <ZipMatch>No Match</ZipMatch> - </AVSResult> - <CVResult>Match</CVResult> - <Message>Approved</Message> - <PNRef>V18A0CBB04CF</PNRef> - <AuthCode>692PNI</AuthCode> - <ExtData Name="DATE_TO_SETTLE" Value="2007-11-28 10:53:50"/> - </TransactionResult> - </TransactionResults> - </ResponseData> -</XMLPayResponse> + <<~XML + <?xml version="1.0"?> + <XMLPayResponse xmlns="http://www.verisign.com/XMLPay"> + <ResponseData> + <Vendor>ActiveMerchant</Vendor> + <Partner>paypal</Partner> + <TransactionResults> + <TransactionResult Duplicate="true"> + <Result>0</Result> + <ProcessorResult> + <AVSResult>A</AVSResult> + <CVResult>M</CVResult> + <HostCode>A</HostCode> + </ProcessorResult> + <IAVSResult>N</IAVSResult> + <AVSResult> + <StreetMatch>Match</StreetMatch> + <ZipMatch>No Match</ZipMatch> + </AVSResult> + <CVResult>Match</CVResult> + <Message>Approved</Message> + <PNRef>V18A0CBB04CF</PNRef> + <AuthCode>692PNI</AuthCode> + <ExtData Name="DATE_TO_SETTLE" Value="2007-11-28 10:53:50"/> + </TransactionResult> + </TransactionResults> + </ResponseData> + </XMLPayResponse> XML end def verbose_transaction_response - <<-XML -<?xml version="1.0" encoding="UTF-8"?> -<XMLPayResponse xmlns="http://www.paypal.com/XMLPay"> - <ResponseData> - <Vendor>ActiveMerchant</Vendor> - <Partner>paypal</Partner> - <TransactionResults> - <TransactionResult> - <Result>0</Result> - <ProcessorResult> - <AVSResult>U</AVSResult> - <CVResult>M</CVResult> - <HostCode>A</HostCode> - </ProcessorResult> - <FraudPreprocessResult> - <Message>No Rules Triggered</Message> - </FraudPreprocessResult> - <FraudPostprocessResult> - <Message>No Rules Triggered</Message> - </FraudPostprocessResult> - <IAVSResult>X</IAVSResult> - <AVSResult> - <StreetMatch>Service Not Available</StreetMatch> - <ZipMatch>Service Not Available</ZipMatch> - </AVSResult> - <CVResult>Match</CVResult> - <Message>Approved</Message> - <PNRef>A70A6C93C4C8</PNRef> - <AuthCode>242PNI</AuthCode> - <Amount>1.00</Amount> - <VisaCardLevel>12</VisaCardLevel> - <TransactionTime>2014-06-25 09:33:41</TransactionTime> - <Account>4242</Account> - <ExpirationDate>0714</ExpirationDate> - <CardType>0</CardType> - <PayPalResult> - <FeeAmount>0</FeeAmount> - <Name>Longbob</Name> - <Lastname>Longsen</Lastname> - </PayPalResult> - </TransactionResult> - </TransactionResults> - </ResponseData> -</XMLPayResponse> + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <XMLPayResponse xmlns="http://www.paypal.com/XMLPay"> + <ResponseData> + <Vendor>ActiveMerchant</Vendor> + <Partner>paypal</Partner> + <TransactionResults> + <TransactionResult> + <Result>0</Result> + <ProcessorResult> + <AVSResult>U</AVSResult> + <CVResult>M</CVResult> + <HostCode>A</HostCode> + </ProcessorResult> + <FraudPreprocessResult> + <Message>No Rules Triggered</Message> + </FraudPreprocessResult> + <FraudPostprocessResult> + <Message>No Rules Triggered</Message> + </FraudPostprocessResult> + <IAVSResult>X</IAVSResult> + <AVSResult> + <StreetMatch>Service Not Available</StreetMatch> + <ZipMatch>Service Not Available</ZipMatch> + </AVSResult> + <CVResult>Match</CVResult> + <Message>Approved</Message> + <PNRef>A70A6C93C4C8</PNRef> + <AuthCode>242PNI</AuthCode> + <Amount>1.00</Amount> + <VisaCardLevel>12</VisaCardLevel> + <TransactionTime>2014-06-25 09:33:41</TransactionTime> + <Account>4242</Account> + <ExpirationDate>0714</ExpirationDate> + <CardType>0</CardType> + <PayPalResult> + <FeeAmount>0</FeeAmount> + <Name>Longbob</Name> + <Lastname>Longsen</Lastname> + </PayPalResult> + </TransactionResult> + </TransactionResults> + </ResponseData> + </XMLPayResponse> XML end - def assert_three_d_secure(xml_doc, buyer_auth_result_path) - assert_equal 'Y', REXML::XPath.first(xml_doc, "#{buyer_auth_result_path}/Status").text - assert_equal 'QvDbSAxSiaQs241899E0', REXML::XPath.first(xml_doc, "#{buyer_auth_result_path}/AuthenticationId").text - assert_equal 'pareq block', REXML::XPath.first(xml_doc, "#{buyer_auth_result_path}/PAReq").text - assert_equal 'https://bankacs.bank.com/ascurl', REXML::XPath.first(xml_doc, "#{buyer_auth_result_path}/ACSUrl").text - assert_equal '02', REXML::XPath.first(xml_doc, "#{buyer_auth_result_path}/ECI").text - assert_equal 'jGvQIvG/5UhjAREALGYa6Vu/hto=', REXML::XPath.first(xml_doc, "#{buyer_auth_result_path}/CAVV").text - assert_equal 'UXZEYlNBeFNpYVFzMjQxODk5RTA=', REXML::XPath.first(xml_doc, "#{buyer_auth_result_path}/XID").text + def three_d_secure_option(options: {}) + { + three_d_secure: { + authentication_id: 'QvDbSAxSiaQs241899E0', + authentication_response_status: SUCCESSFUL_AUTHENTICATION_STATUS, + pareq: 'pareq block', + acs_url: 'https://bankacs.bank.com/ascurl', + eci: '02', + cavv: 'jGvQIvG/5UhjAREALGYa6Vu/hto=', + xid: 'UXZEYlNBeFNpYVFzMjQxODk5RTA=' + }. + merge(options). + compact + } end - def authorize_buyer_auth_result_path - '/XMLPayRequest/RequestData/Transactions/Transaction/Authorization/PayData/Tender/Card/BuyerAuthResult' + def assert_three_d_secure_via_mpi(xml_doc, tx_type: 'Authorization', expected_version: nil, expected_ds_transaction_id: nil) + [ + { name: 'AUTHENTICATION_STATUS', expected: SUCCESSFUL_AUTHENTICATION_STATUS }, + { name: 'AUTHENTICATION_ID', expected: 'QvDbSAxSiaQs241899E0' }, + { name: 'ECI', expected: '02' }, + { name: 'CAVV', expected: 'jGvQIvG/5UhjAREALGYa6Vu/hto=' }, + { name: 'XID', expected: 'UXZEYlNBeFNpYVFzMjQxODk5RTA=' }, + { name: 'THREEDSVERSION', expected: expected_version }, + { name: 'DSTRANSACTIONID', expected: expected_ds_transaction_id } + ].each do |item| + assert_equal item[:expected], REXML::XPath.first(xml_doc, threeds_xpath_for_extdata(item[:name], tx_type:)) + end end - def purchase_buyer_auth_result_path - '/XMLPayRequest/RequestData/Transactions/Transaction/Sale/PayData/Tender/Card/BuyerAuthResult' + def assert_three_d_secure( + xml_doc, + buyer_auth_result_path, + expected_status: SUCCESSFUL_AUTHENTICATION_STATUS, + expected_authentication_status: nil, + expected_version: nil, + expected_ds_transaction_id: nil + ) + assert_text_value_or_nil expected_status, REXML::XPath.first(xml_doc, "#{buyer_auth_result_path}/Status") + assert_text_value_or_nil(expected_authentication_status, REXML::XPath.first(xml_doc, "#{buyer_auth_result_path}/AuthenticationStatus")) + assert_text_value_or_nil 'QvDbSAxSiaQs241899E0', REXML::XPath.first(xml_doc, "#{buyer_auth_result_path}/AuthenticationId") + assert_text_value_or_nil 'pareq block', REXML::XPath.first(xml_doc, "#{buyer_auth_result_path}/PAReq") + assert_text_value_or_nil 'https://bankacs.bank.com/ascurl', REXML::XPath.first(xml_doc, "#{buyer_auth_result_path}/ACSUrl") + assert_text_value_or_nil '02', REXML::XPath.first(xml_doc, "#{buyer_auth_result_path}/ECI") + assert_text_value_or_nil 'jGvQIvG/5UhjAREALGYa6Vu/hto=', REXML::XPath.first(xml_doc, "#{buyer_auth_result_path}/CAVV") + assert_text_value_or_nil 'UXZEYlNBeFNpYVFzMjQxODk5RTA=', REXML::XPath.first(xml_doc, "#{buyer_auth_result_path}/XID") + assert_text_value_or_nil(expected_version, REXML::XPath.first(xml_doc, "#{buyer_auth_result_path}/ThreeDSVersion")) + assert_text_value_or_nil(expected_ds_transaction_id, REXML::XPath.first(xml_doc, "#{buyer_auth_result_path}/DSTransactionID")) + end + + def assert_text_value_or_nil(expected_text_value, xml_element) + if expected_text_value + assert_equal expected_text_value, xml_element.text + else + assert_nil xml_element + end end - def three_d_secure_option - { - :three_d_secure => { - :status => 'Y', - :authentication_id => 'QvDbSAxSiaQs241899E0', - :pareq => 'pareq block', - :acs_url => 'https://bankacs.bank.com/ascurl', - :eci => '02', - :cavv => 'jGvQIvG/5UhjAREALGYa6Vu/hto=', - :xid => 'UXZEYlNBeFNpYVFzMjQxODk5RTA=' - } - } + def xpath_prefix_for_transaction_type(tx_type) + return '/XMLPayRequest/RequestData/Transactions/Transaction/Authorization/' unless tx_type == 'Sale' + + '/XMLPayRequest/RequestData/Transactions/Transaction/Sale/' + end + + 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)) + end + + def authorize_buyer_auth_result_path + xpath_prefix = xpath_prefix_for_transaction_type('Authorization') + "#{xpath_prefix}/PayData/Tender/Card/BuyerAuthResult" + end + + def purchase_buyer_auth_result_path + xpath_prefix = xpath_prefix_for_transaction_type('Sale') + "#{xpath_prefix}/PayData/Tender/Card/BuyerAuthResult" end end diff --git a/test/unit/gateways/payflow_uk_test.rb b/test/unit/gateways/payflow_uk_test.rb index 53c41fcfd20..0b6f812d752 100644 --- a/test/unit/gateways/payflow_uk_test.rb +++ b/test/unit/gateways/payflow_uk_test.rb @@ -3,28 +3,28 @@ class PayflowUkTest < Test::Unit::TestCase def setup @gateway = PayflowUkGateway.new( - :login => 'LOGIN', - :password => 'PASSWORD' + login: 'LOGIN', + password: 'PASSWORD' ) end def test_default_currency assert_equal 'GBP', PayflowUkGateway.default_currency end - + def test_express_instance assert_instance_of PayflowExpressUkGateway, @gateway.express end - + def test_default_partner assert_equal 'PayPalUk', PayflowUkGateway.partner end - + def test_supported_countries assert_equal ['GB'], PayflowUkGateway.supported_countries end - + def test_supported_card_types - assert_equal [:visa, :master, :american_express, :discover, :solo, :switch], PayflowUkGateway.supported_cardtypes + assert_equal %i[visa master american_express discover], PayflowUkGateway.supported_cardtypes end end diff --git a/test/unit/gateways/payment_express_test.rb b/test/unit/gateways/payment_express_test.rb index 1dfad68daba..3e8490b83f0 100644 --- a/test/unit/gateways/payment_express_test.rb +++ b/test/unit/gateways/payment_express_test.rb @@ -5,19 +5,19 @@ class PaymentExpressTest < Test::Unit::TestCase def setup @gateway = PaymentExpressGateway.new( - :login => 'LOGIN', - :password => 'PASSWORD' + login: 'LOGIN', + password: 'PASSWORD' ) @visa = credit_card - @solo = credit_card('6334900000000005', :brand => 'solo', :issue_number => '01') + @solo = credit_card('6334900000000005', brand: 'maestro') @options = { - :order_id => generate_unique_id, - :billing_address => address, - :email => 'cody@example.com', - :description => 'Store purchase' + order_id: generate_unique_id, + billing_address: address, + email: 'cody@example.com', + description: 'Store purchase' } @amount = 100 @@ -50,6 +50,24 @@ def test_successful_authorization # assert_equal '00000004011a2478', response.authorization end + def test_pass_currency_code_on_validation + stub_comms do + @gateway.verify(@visa, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<InputCurrency>NZD<\/InputCurrency>/, data) + end.respond_with(successful_validation_response) + end + + def test_successful_validation + @gateway.expects(:ssl_post).returns(successful_validation_response) + + assert response = @gateway.verify(@visa, @options) + assert_success response + assert response.test? + assert_equal 'The Transaction was approved', response.message + assert_equal '0000000c025d2744', response.authorization + end + def test_purchase_request_should_include_cvc2_presence @gateway.expects(:commit).with do |type, request| type == :purchase && request.to_s =~ %r{<Cvc2Presence>1<\/Cvc2Presence>} @@ -58,84 +76,84 @@ def test_purchase_request_should_include_cvc2_presence @gateway.purchase(@amount, @visa, @options) end - # def test_successful_solo_authorization - # @gateway.expects(:ssl_post).returns(successful_authorization_response) - # - # assert response = @gateway.purchase(@amount, @solo, @options) - # assert_success response - # assert response.test? - # assert_equal 'The Transaction was approved', response.message - # assert_equal '00000004011a2478', response.authorization - # end - # - # def test_successful_card_store - # @gateway.expects(:ssl_post).returns(successful_store_response) - # - # assert response = @gateway.store(@visa) - # assert_success response - # assert response.test? - # assert_equal '0000030000141581', response.authorization - # assert_equal response.authorization, response.token - # end - # - # def test_successful_card_store_with_custom_billing_id - # @gateway.expects(:ssl_post).returns(successful_store_response(:billing_id => 'my-custom-id')) - # - # assert response = @gateway.store(@visa, :billing_id => 'my-custom-id') - # assert_success response - # assert response.test? - # assert_equal 'my-custom-id', response.token - # end - # - # def test_unsuccessful_card_store - # @gateway.expects(:ssl_post).returns(unsuccessful_store_response) - # - # @visa.number = 2 - # - # assert response = @gateway.store(@visa) - # assert_failure response - # end - # - # def test_purchase_using_dps_billing_id_token - # @gateway.expects(:ssl_post).returns(successful_store_response) - # - # assert response = @gateway.store(@visa) - # token = response.token - # - # @gateway.expects(:ssl_post).returns(successful_dps_billing_id_token_purchase_response) - # - # assert response = @gateway.purchase(@amount, token, @options) - # assert_success response - # assert_equal 'The Transaction was approved', response.message - # assert_equal '0000000303ace8db', response.authorization - # end - # - # def test_purchase_using_merchant_specified_billing_id_token - # @gateway = PaymentExpressGateway.new( - # :login => 'LOGIN', - # :password => 'PASSWORD', - # :use_custom_payment_token => true - # ) - # - # @gateway.expects(:ssl_post).returns(successful_store_response({:billing_id => 'TEST1234'})) - # - # assert response = @gateway.store(@visa, {:billing_id => 'TEST1234'}) - # assert_equal 'TEST1234', response.token - # - # @gateway.expects(:ssl_post).returns(successful_billing_id_token_purchase_response) - # - # assert response = @gateway.purchase(@amount, 'TEST1234', @options) - # assert_success response - # assert_equal 'The Transaction was approved', response.message - # assert_equal '0000000303ace8db', response.authorization - # end + def test_successful_solo_authorization + @gateway.expects(:ssl_post).returns(successful_authorization_response) + + assert response = @gateway.purchase(@amount, @solo, @options) + assert_success response + assert response.test? + assert_equal 'The Transaction was approved', response.message + assert_equal '00000004011a2478', response.authorization + end + + def test_successful_card_store + @gateway.expects(:ssl_post).returns(successful_store_response) + + assert response = @gateway.store(@visa) + assert_success response + assert response.test? + assert_equal '0000030000141581', response.authorization + assert_equal response.authorization, response.token + end + + def test_successful_card_store_with_custom_billing_id + @gateway.expects(:ssl_post).returns(successful_store_response(billing_id: 'my-custom-id')) + + assert response = @gateway.store(@visa, billing_id: 'my-custom-id') + assert_success response + assert response.test? + assert_equal 'my-custom-id', response.token + end + + def test_unsuccessful_card_store + @gateway.expects(:ssl_post).returns(unsuccessful_store_response) + + @visa.number = 2 + + assert response = @gateway.store(@visa) + assert_failure response + end + + def test_purchase_using_dps_billing_id_token + @gateway.expects(:ssl_post).returns(successful_store_response) + + assert response = @gateway.store(@visa) + token = response.token + + @gateway.expects(:ssl_post).returns(successful_dps_billing_id_token_purchase_response) + + assert response = @gateway.purchase(@amount, token, @options) + assert_success response + assert_equal 'The Transaction was approved', response.message + assert_equal '0000000303ace8db', response.authorization + end + + def test_purchase_using_merchant_specified_billing_id_token + @gateway = PaymentExpressGateway.new( + login: 'LOGIN', + password: 'PASSWORD', + use_custom_payment_token: true + ) + + @gateway.expects(:ssl_post).returns(successful_store_response({ billing_id: 'TEST1234' })) + + assert response = @gateway.store(@visa, { billing_id: 'TEST1234' }) + assert_equal 'TEST1234', response.token + + @gateway.expects(:ssl_post).returns(successful_billing_id_token_purchase_response) + + assert response = @gateway.purchase(@amount, 'TEST1234', @options) + assert_success response + assert_equal 'The Transaction was approved', response.message + assert_equal '0000000303ace8db', response.authorization + end def test_supported_countries assert_equal %w(AU FJ GB HK IE MY NZ PG SG US), PaymentExpressGateway.supported_countries end def test_supported_card_types - assert_equal [ :visa, :master, :american_express, :diners_club, :jcb ], PaymentExpressGateway.supported_cardtypes + assert_equal %i[visa master american_express diners_club jcb], PaymentExpressGateway.supported_cardtypes end def test_avs_result_not_supported @@ -163,9 +181,9 @@ def test_expect_no_optional_fields_by_default def test_pass_optional_txn_data options = { - :txn_data1 => 'Transaction Data 1', - :txn_data2 => 'Transaction Data 2', - :txn_data3 => 'Transaction Data 3' + txn_data1: 'Transaction Data 1', + txn_data2: 'Transaction Data 2', + txn_data3: 'Transaction Data 3' } perform_each_transaction_type_with_request_body_assertions(options) do |body| @@ -177,9 +195,9 @@ def test_pass_optional_txn_data def test_pass_optional_txn_data_truncated_to_255_chars options = { - :txn_data1 => 'Transaction Data 1-01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345-EXTRA', - :txn_data2 => 'Transaction Data 2-01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345-EXTRA', - :txn_data3 => 'Transaction Data 3-01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345-EXTRA' + txn_data1: 'Transaction Data 1-01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345-EXTRA', + txn_data2: 'Transaction Data 2-01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345-EXTRA', + txn_data3: 'Transaction Data 3-01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345-EXTRA' } truncated_addendum = '01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345' @@ -191,8 +209,26 @@ def test_pass_optional_txn_data_truncated_to_255_chars end end + def test_pass_enable_avs_data_and_avs_action + options = { + address: { + address1: '123 Pine Street', + zip: '12345' + }, + enable_avs_data: 0, + avs_action: 3 + } + + stub_comms do + @gateway.purchase(@amount, @visa, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<EnableAvsData>0<\/EnableAvsData>/, data) + assert_match(/<AvsAction>3<\/AvsAction>/, data) + end.respond_with(successful_authorization_response) + end + def test_pass_client_type_as_symbol_for_web - options = {:client_type => :web} + options = { client_type: :web } perform_each_transaction_type_with_request_body_assertions(options) do |body| assert_match(/<ClientType>Web<\/ClientType>/, body) @@ -200,7 +236,7 @@ def test_pass_client_type_as_symbol_for_web end def test_pass_client_type_as_symbol_for_ivr - options = {:client_type => :ivr} + options = { client_type: :ivr } perform_each_transaction_type_with_request_body_assertions(options) do |body| assert_match(/<ClientType>IVR<\/ClientType>/, body) @@ -208,7 +244,7 @@ def test_pass_client_type_as_symbol_for_ivr end def test_pass_client_type_as_symbol_for_moto - options = {:client_type => :moto} + options = { client_type: :moto } perform_each_transaction_type_with_request_body_assertions(options) do |body| assert_match(/<ClientType>MOTO<\/ClientType>/, body) @@ -216,7 +252,7 @@ def test_pass_client_type_as_symbol_for_moto end def test_pass_client_type_as_symbol_for_unattended - options = {:client_type => :unattended} + options = { client_type: :unattended } perform_each_transaction_type_with_request_body_assertions(options) do |body| assert_match(/<ClientType>Unattended<\/ClientType>/, body) @@ -224,7 +260,7 @@ def test_pass_client_type_as_symbol_for_unattended end def test_pass_client_type_as_symbol_for_internet - options = {:client_type => :internet} + options = { client_type: :internet } perform_each_transaction_type_with_request_body_assertions(options) do |body| assert_match(/<ClientType>Internet<\/ClientType>/, body) @@ -232,7 +268,7 @@ def test_pass_client_type_as_symbol_for_internet end def test_pass_client_type_as_symbol_for_recurring - options = {:client_type => :recurring} + options = { client_type: :recurring } perform_each_transaction_type_with_request_body_assertions(options) do |body| assert_match(/<ClientType>Recurring<\/ClientType>/, body) @@ -240,74 +276,82 @@ def test_pass_client_type_as_symbol_for_recurring end def test_pass_client_type_as_symbol_for_unknown_type_omits_element - options = {:client_type => :unknown} + options = { client_type: :unknown } perform_each_transaction_type_with_request_body_assertions(options) do |body| assert_no_match(/<ClientType>/, body) end end + def test_pass_ip_as_client_info + options = { ip: '192.168.0.1' } + + perform_each_transaction_type_with_request_body_assertions(options) do |body| + assert_match(/<ClientInfo>192.168.0.1<\/ClientInfo>/, body) + end + end + def test_purchase_truncates_order_id_to_16_chars stub_comms do - @gateway.purchase(@amount, @visa, {:order_id => '16chars---------EXTRA'}) - end.check_request do |endpoint, data, headers| + @gateway.purchase(@amount, @visa, { order_id: '16chars---------EXTRA' }) + end.check_request do |_endpoint, data, _headers| assert_match(/<TxnId>16chars---------<\/TxnId>/, data) end.respond_with(successful_authorization_response) end def test_authorize_truncates_order_id_to_16_chars stub_comms do - @gateway.authorize(@amount, @visa, {:order_id => '16chars---------EXTRA'}) - end.check_request do |endpoint, data, headers| + @gateway.authorize(@amount, @visa, { order_id: '16chars---------EXTRA' }) + end.check_request do |_endpoint, data, _headers| assert_match(/<TxnId>16chars---------<\/TxnId>/, data) end.respond_with(successful_authorization_response) end def test_capture_truncates_order_id_to_16_chars stub_comms do - @gateway.capture(@amount, 'identification', {:order_id => '16chars---------EXTRA'}) - end.check_request do |endpoint, data, headers| + @gateway.capture(@amount, 'identification', { order_id: '16chars---------EXTRA' }) + end.check_request do |_endpoint, data, _headers| assert_match(/<TxnId>16chars---------<\/TxnId>/, data) end.respond_with(successful_authorization_response) end def test_refund_truncates_order_id_to_16_chars stub_comms do - @gateway.refund(@amount, 'identification', {:description => 'refund', :order_id => '16chars---------EXTRA'}) - end.check_request do |endpoint, data, headers| + @gateway.refund(@amount, 'identification', { description: 'refund', order_id: '16chars---------EXTRA' }) + end.check_request do |_endpoint, data, _headers| assert_match(/<TxnId>16chars---------<\/TxnId>/, data) end.respond_with(successful_authorization_response) end def test_purchase_truncates_description_to_64_chars stub_comms do - @gateway.purchase(@amount, @visa, {:description => '64chars---------------------------------------------------------EXTRA'}) - end.check_request do |endpoint, data, headers| - assert_match(/<MerchantReference>64chars---------------------------------------------------------<\/MerchantReference>/, data) + @gateway.purchase(@amount, @visa, { description: '50chars-------------------------------------------EXTRA' }) + end.check_request do |_endpoint, data, _headers| + assert_match(/<MerchantReference>50chars-------------------------------------------<\/MerchantReference>/, data) end.respond_with(successful_authorization_response) end def test_authorize_truncates_description_to_64_chars stub_comms do - @gateway.authorize(@amount, @visa, {:description => '64chars---------------------------------------------------------EXTRA'}) - end.check_request do |endpoint, data, headers| - assert_match(/<MerchantReference>64chars---------------------------------------------------------<\/MerchantReference>/, data) + @gateway.authorize(@amount, @visa, { description: '50chars-------------------------------------------EXTRA' }) + end.check_request do |_endpoint, data, _headers| + assert_match(/<MerchantReference>50chars-------------------------------------------<\/MerchantReference>/, data) end.respond_with(successful_authorization_response) end def test_capture_truncates_description_to_64_chars stub_comms do - @gateway.capture(@amount, 'identification', {:description => '64chars---------------------------------------------------------EXTRA'}) - end.check_request do |endpoint, data, headers| - assert_match(/<MerchantReference>64chars---------------------------------------------------------<\/MerchantReference>/, data) + @gateway.capture(@amount, 'identification', { description: '50chars-------------------------------------------EXTRA' }) + end.check_request do |_endpoint, data, _headers| + assert_match(/<MerchantReference>50chars-------------------------------------------<\/MerchantReference>/, data) end.respond_with(successful_authorization_response) end def test_refund_truncates_description_to_64_chars stub_comms do - @gateway.capture(@amount, 'identification', {:description => '64chars---------------------------------------------------------EXTRA'}) - end.check_request do |endpoint, data, headers| - assert_match(/<MerchantReference>64chars---------------------------------------------------------<\/MerchantReference>/, data) + @gateway.capture(@amount, 'identification', { description: '50chars-------------------------------------------EXTRA' }) + end.check_request do |_endpoint, data, _headers| + assert_match(/<MerchantReference>50chars-------------------------------------------<\/MerchantReference>/, data) end.respond_with(successful_authorization_response) end @@ -321,35 +365,35 @@ def perform_each_transaction_type_with_request_body_assertions(options = {}) # purchase stub_comms do @gateway.purchase(@amount, @visa, options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| yield data end.respond_with(successful_authorization_response) # authorize stub_comms do @gateway.authorize(@amount, @visa, options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| yield data end.respond_with(successful_authorization_response) # capture stub_comms do @gateway.capture(@amount, 'identification', options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| yield data end.respond_with(successful_authorization_response) # refund stub_comms do - @gateway.refund(@amount, 'identification', {:description => 'description'}.merge(options)) - end.check_request do |endpoint, data, headers| + @gateway.refund(@amount, 'identification', { description: 'description' }.merge(options)) + end.check_request do |_endpoint, data, _headers| yield data end.respond_with(successful_authorization_response) # store stub_comms do @gateway.store(@visa, options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| yield data end.respond_with(successful_store_response) end @@ -363,10 +407,149 @@ def invalid_credentials_response end def successful_authorization_response - <<-RESPONSE - <Request valid="1"> - <URI>https://sec.paymentexpress.com/pxmi3/EF4054F622D6C4C1B4F9AEA59DC91CAD3654CD60ED7ED04110CBC402959AC7CF035878AEB85D87223</URI> - </Request> + <<~RESPONSE + <Txn> + <Transaction success="1" reco="00" responsetext="APPROVED"> + <Authorized>1</Authorized> + <MerchantReference>Test Transaction</MerchantReference> + <Cvc2>M</Cvc2> + <CardName>Visa</CardName> + <Retry>0</Retry> + <StatusRequired>0</StatusRequired> + <AuthCode>015921</AuthCode> + <Amount>1.23</Amount> + <InputCurrencyId>1</InputCurrencyId> + <InputCurrencyName>NZD</InputCurrencyName> + <Acquirer>WestpacTrust</Acquirer> + <CurrencyId>1</CurrencyId> + <CurrencyName>NZD</CurrencyName> + <CurrencyRate>1.00</CurrencyRate> + <Acquirer>WestpacTrust</Acquirer> + <AcquirerDate>30102000</AcquirerDate> + <AcquirerId>1</AcquirerId> + <CardHolderName>DPS</CardHolderName> + <DateSettlement>20050811</DateSettlement> + <TxnType>Purchase</TxnType> + <CardNumber>411111</CardNumber> + <DateExpiry>0807</DateExpiry> + <ProductId></ProductId> + <AcquirerDate>20050811</AcquirerDate> + <AcquirerTime>060039</AcquirerTime> + <AcquirerId>9000</AcquirerId> + <Acquirer>Test</Acquirer> + <TestMode>1</TestMode> + <CardId>2</CardId> + <CardHolderResponseText>APPROVED</CardHolderResponseText> + <CardHolderHelpText>The Transaction was approved</CardHolderHelpText> + <CardHolderResponseDescription>The Transaction was approved</CardHolderResponseDescription> + <MerchantResponseText>APPROVED</MerchantResponseText> + <MerchantHelpText>The Transaction was approved</MerchantHelpText> + <MerchantResponseDescription>The Transaction was approved</MerchantResponseDescription> + <GroupAccount>9997</GroupAccount> + <DpsTxnRef>00000004011a2478</DpsTxnRef> + <AllowRetry>0</AllowRetry> + <DpsBillingId></DpsBillingId> + <BillingId></BillingId> + <TransactionId>011a2478</TransactionId> + </Transaction> + <ReCo>00</ReCo> + <ResponseText>APPROVED</ResponseText> + <HelpText>The Transaction was approved</HelpText> + <Success>1</Success> + <TxnRef>00000004011a2478</TxnRef> + </Txn> + RESPONSE + end + + def successful_validation_response + <<~RESPONSE + <Txn> + <Transaction success="1" reco="00" responseText="APPROVED" pxTxn="true"> + <Authorized>1</Authorized> + <ReCo>00</ReCo> + <RxDate>20210126161020</RxDate> + <RxDateLocal>20210127051020</RxDateLocal> + <LocalTimeZone>NZT</LocalTimeZone> + <MerchantReference>Store purchase</MerchantReference> + <CardName>Visa</CardName> + <Retry>0</Retry> + <StatusRequired>0</StatusRequired> + <AuthCode>051020XXX</AuthCode> + <AmountBalance>0.00</AmountBalance> + <Amount>1.00</Amount> + <CurrencyId>554</CurrencyId> + <CurrencyName>NZD</CurrencyName> + <InputCurrencyId>554</InputCurrencyId> + <InputCurrencyName>NZD</InputCurrencyName> + <CurrencyRate>1.00</CurrencyRate> + <CardHolderName>LONGBOB LONGSEN</CardHolderName> + <DateSettlement>20210127</DateSettlement> + <TxnType>Auth</TxnType> + <CardNumber>411111........11</CardNumber> + <TxnMac>2BC20210</TxnMac> + <DateExpiry>0922</DateExpiry> + <ProductId></ProductId> + <AcquirerDate>20210127</AcquirerDate> + <AcquirerTime>051020</AcquirerTime> + <AcquirerId>9001</AcquirerId> + <Acquirer>Undefined</Acquirer> + <AcquirerReCo>00</AcquirerReCo> + <AcquirerResponseText>APPROVED</AcquirerResponseText> + <TestMode>1</TestMode> + <CardId>2</CardId> + <CardHolderResponseText>APPROVED</CardHolderResponseText> + <CardHolderHelpText>The Transaction was approved</CardHolderHelpText> + <CardHolderResponseDescription>The Transaction was approved</CardHolderResponseDescription> + <MerchantResponseText>APPROVED</MerchantResponseText> + <MerchantHelpText>The Transaction was approved</MerchantHelpText> + <MerchantResponseDescription>The Transaction was approved</MerchantResponseDescription> + <UrlFail></UrlFail> + <UrlSuccess></UrlSuccess> + <EnablePostResponse>0</EnablePostResponse> + <PxPayName></PxPayName> + <PxPayLogoSrc></PxPayLogoSrc> + <PxPayUserId></PxPayUserId> + <PxPayXsl></PxPayXsl> + <PxPayBgColor></PxPayBgColor> + <PxPayOptions></PxPayOptions> + <Cvc2ResultCode>U</Cvc2ResultCode> + <AvsPostCode>K1C2N6</AvsPostCode> + <AvsStreetAddress>456 My Street</AvsStreetAddress> + <AvsResultCode>E</AvsResultCode> + <AvsResultName>E - AVS data is invalid, or AVS not allowed for this card type</AvsResultName> + <AvsActionName>Attempt</AvsActionName> + <AcquirerPort>10000000-10003813</AcquirerPort> + <AcquirerTxnRef>451734</AcquirerTxnRef> + <GroupAccount>9997</GroupAccount> + <DpsTxnRef>0000000c025d2744</DpsTxnRef> + <AllowRetry>0</AllowRetry> + <DpsBillingId></DpsBillingId> + <PhoneNumber></PhoneNumber> + <HomePhoneNumber></HomePhoneNumber> + <AccountInfo></AccountInfo> + <BillingId></BillingId> + <CardNumber2>9310200000000010</CardNumber2> + <EnableCardNumber2UniqueEveryGenerate>0</EnableCardNumber2UniqueEveryGenerate> + <TransactionId>025d2744</TransactionId> + <PxHostId>0000000c</PxHostId> + <RmReason></RmReason> + <RmReasonId>0000000000000000</RmReasonId> + <RiskScore>-1</RiskScore> + <RiskScoreText></RiskScoreText> + </Transaction> + <RuleName></RuleName> + <RmReason></RmReason> + <RmReasonId>0000000000000000</RmReasonId> + <RiskScore>-1</RiskScore> + <RiskScoreText></RiskScoreText> + <ReCo>00</ReCo> + <ResponseText>APPROVED</ResponseText> + <HelpText>Transaction Approved</HelpText> + <Success>1</Success> + <DpsTxnRef>0000000c025d2744</DpsTxnRef> + <ICCResult></ICCResult> + <TxnRef>5f3d2cde30deeef0</TxnRef> + </Txn> RESPONSE # <Request> @@ -422,19 +605,251 @@ def successful_authorization_response end def successful_store_response(options = {}) - %(<GenerateRequest><Transaction valid="1" reco="00" responsetext="APPROVED"><Authorized>1</Authorized><MerchantReference></MerchantReference><CardName>Visa</CardName><Retry>0</Retry><StatusRequired>0</StatusRequired><AuthCode>02381203accf5c00000003</AuthCode><AmountInput>0.01</AmountInput><CurrencyId>554</CurrencyId><InputCurrencyId>554</InputCurrencyId><InputCurrencyName>NZD</InputCurrencyName><CurrencyRate>1.00</CurrencyRate><CurrencyName>NZD</CurrencyName><CardHolderName>BOB BOBSEN</CardHolderName><DateSettlement>20070323</DateSettlement><TxnType>Auth</TxnType><CardNumber>424242........42</CardNumber><DateExpiry>0809</DateExpiry><ProductId></ProductId><AcquirerDate>20070323</AcquirerDate><AcquirerTime>023812</AcquirerTime><AcquirerId>9000</AcquirerId><Acquirer>Test</Acquirer><TestMode>1</TestMode><CardId>2</CardId><CardHolderResponseText>APPROVED</CardHolderResponseText><CardHolderHelpText>The Transaction was approved</CardHolderHelpText><CardHolderResponseDescription>The Transaction was approved</CardHolderResponseDescription><MerchantResponseText>APPROVED</MerchantResponseText><MerchantHelpText>The Transaction was approved</MerchantHelpText><MerchantResponseDescription>The Transaction was approved</MerchantResponseDescription><UrlFail></UrlFail><UrlSuccess></UrlSuccess><EnablePostResponse>0</EnablePostResponse><PxPayName></PxPayName><PxPayLogoSrc></PxPayLogoSrc><PxPayUserId></PxPayUserId><PxPayXsl></PxPayXsl><PxPayBgColor></PxPayBgColor><AcquirerPort>9999999999-99999999</AcquirerPort><AcquirerTxnRef>12835</AcquirerTxnRef><GroupAccount>9997</GroupAccount><DpsTxnRef>0000000303accf5c</DpsTxnRef><AllowRetry>0</AllowRetry><DpsBillingId>0000030000141581</DpsBillingId><BillingId>#{options[:billing_id]}</BillingId><TransactionId>03accf5c</TransactionId><PxHostId>00000003</PxHostId></Transaction><ReCo>00</ReCo><ResponseText>APPROVED</ResponseText><HelpText>The Transaction was approved</HelpText><Success>1</Success><DpsTxnRef>0000000303accf5c</DpsTxnRef><TxnRef></TxnRef></GenerateRequest>) + <<~RESPONSE + <Txn> + <Transaction success="1" reco="00" responsetext="APPROVED"> + <Authorized>1</Authorized> + <MerchantReference></MerchantReference> + <CardName>Visa</CardName> + <Retry>0</Retry> + <StatusRequired>0</StatusRequired> + <AuthCode>02381203accf5c00000003</AuthCode> + <Amount>0.01</Amount> + <CurrencyId>554</CurrencyId> + <InputCurrencyId>554</InputCurrencyId> + <InputCurrencyName>NZD</InputCurrencyName> + <CurrencyRate>1.00</CurrencyRate> + <CurrencyName>NZD</CurrencyName> + <CardHolderName>BOB BOBSEN</CardHolderName> + <DateSettlement>20070323</DateSettlement> + <TxnType>Auth</TxnType> + <CardNumber>424242........42</CardNumber> + <DateExpiry>0809</DateExpiry> + <ProductId></ProductId> + <AcquirerDate>20070323</AcquirerDate> + <AcquirerTime>023812</AcquirerTime> + <AcquirerId>9000</AcquirerId> + <Acquirer>Test</Acquirer> + <TestMode>1</TestMode> + <CardId>2</CardId> + <CardHolderResponseText>APPROVED</CardHolderResponseText> + <CardHolderHelpText>The Transaction was approved</CardHolderHelpText> + <CardHolderResponseDescription>The Transaction was approved</CardHolderResponseDescription> + <MerchantResponseText>APPROVED</MerchantResponseText> + <MerchantHelpText>The Transaction was approved</MerchantHelpText> + <MerchantResponseDescription>The Transaction was approved</MerchantResponseDescription> + <UrlFail></UrlFail> + <UrlSuccess></UrlSuccess> + <EnablePostResponse>0</EnablePostResponse> + <PxPayName></PxPayName> + <PxPayLogoSrc></PxPayLogoSrc> + <PxPayUserId></PxPayUserId> + <PxPayXsl></PxPayXsl> + <PxPayBgColor></PxPayBgColor> + <AcquirerPort>9999999999-99999999</AcquirerPort> + <AcquirerTxnRef>12835</AcquirerTxnRef> + <GroupAccount>9997</GroupAccount> + <DpsTxnRef>0000000303accf5c</DpsTxnRef> + <AllowRetry>0</AllowRetry> + <DpsBillingId>0000030000141581</DpsBillingId> + <BillingId>#{options[:billing_id]}</BillingId> + <TransactionId>03accf5c</TransactionId> + <PxHostId>00000003</PxHostId> + </Transaction> + <ReCo>00</ReCo> + <ResponseText>APPROVED</ResponseText> + <HelpText>The Transaction was approved</HelpText> + <Success>1</Success> + <DpsTxnRef>0000000303accf5c</DpsTxnRef> + <TxnRef></TxnRef> + </Txn> + RESPONSE end def unsuccessful_store_response(options = {}) - %(<GenerateRequest><Transaction valid="0" reco="QK" responsetext="INVALID CARD NUMBER"><Authorized>0</Authorized><MerchantReference></MerchantReference><CardName></CardName><Retry>0</Retry><StatusRequired>0</StatusRequired><AuthCode></AuthCode><AmountInput>0.01</AmountInput><CurrencyId>554</CurrencyId><InputCurrencyId>554</InputCurrencyId><InputCurrencyName>NZD</InputCurrencyName><CurrencyRate>1.00</CurrencyRate><CurrencyName>NZD</CurrencyName><CardHolderName>LONGBOB LONGSEN</CardHolderName><DateSettlement>19800101</DateSettlement><TxnType>Validate</TxnType><CardNumber>000000........00</CardNumber><DateExpiry>0808</DateExpiry><ProductId></ProductId><AcquirerDate></AcquirerDate><AcquirerTime></AcquirerTime><AcquirerId>9000</AcquirerId><Acquirer></Acquirer><TestMode>0</TestMode><CardId>0</CardId><CardHolderResponseText>INVALID CARD NUMBER</CardHolderResponseText><CardHolderHelpText>An Invalid Card Number was entered. Check the card number</CardHolderHelpText><CardHolderResponseDescription>An Invalid Card Number was entered. Check the card number</CardHolderResponseDescription><MerchantResponseText>INVALID CARD NUMBER</MerchantResponseText><MerchantHelpText>An Invalid Card Number was entered. Check the card number</MerchantHelpText><MerchantResponseDescription>An Invalid Card Number was entered. Check the card number</MerchantResponseDescription><UrlFail></UrlFail><UrlSuccess></UrlSuccess><EnablePostResponse>0</EnablePostResponse><PxPayName></PxPayName><PxPayLogoSrc></PxPayLogoSrc><PxPayUserId></PxPayUserId><PxPayXsl></PxPayXsl><PxPayBgColor></PxPayBgColor><AcquirerPort>9999999999-99999999</AcquirerPort><AcquirerTxnRef>0</AcquirerTxnRef><GroupAccount>9997</GroupAccount><DpsTxnRef></DpsTxnRef><AllowRetry>0</AllowRetry><DpsBillingId></DpsBillingId><BillingId></BillingId><TransactionId>00000000</TransactionId><PxHostId>00000003</PxHostId></Transaction><ReCo>QK</ReCo><ResponseText>INVALID CARD NUMBER</ResponseText><HelpText>An Invalid Card Number was entered. Check the card number</HelpText><Success>0</Success><DpsTxnRef></DpsTxnRef><TxnRef></TxnRef></GenerateRequest>) + <<~RESPONSE + <Txn> + <Transaction success="0" reco="QK" responsetext="INVALID CARD NUMBER"> + <Authorized>0</Authorized> + <MerchantReference></MerchantReference> + <CardName></CardName> + <Retry>0</Retry> + <StatusRequired>0</StatusRequired> + <AuthCode></AuthCode> + <Amount>0.01</Amount> + <CurrencyId>554</CurrencyId> + <InputCurrencyId>554</InputCurrencyId> + <InputCurrencyName>NZD</InputCurrencyName> + <CurrencyRate>1.00</CurrencyRate> + <CurrencyName>NZD</CurrencyName> + <CardHolderName>LONGBOB LONGSEN</CardHolderName> + <DateSettlement>19800101</DateSettlement> + <TxnType>Validate</TxnType> + <CardNumber>000000........00</CardNumber> + <DateExpiry>0808</DateExpiry> + <ProductId></ProductId> + <AcquirerDate></AcquirerDate> + <AcquirerTime></AcquirerTime> + <AcquirerId>9000</AcquirerId> + <Acquirer></Acquirer> + <TestMode>0</TestMode> + <CardId>0</CardId> + <CardHolderResponseText>INVALID CARD NUMBER</CardHolderResponseText> + <CardHolderHelpText>An Invalid Card Number was entered. Check the card number</CardHolderHelpText> + <CardHolderResponseDescription>An Invalid Card Number was entered. Check the card number</CardHolderResponseDescription> + <MerchantResponseText>INVALID CARD NUMBER</MerchantResponseText> + <MerchantHelpText>An Invalid Card Number was entered. Check the card number</MerchantHelpText> + <MerchantResponseDescription>An Invalid Card Number was entered. Check the card number</MerchantResponseDescription> + <UrlFail></UrlFail> + <UrlSuccess></UrlSuccess> + <EnablePostResponse>0</EnablePostResponse> + <PxPayName></PxPayName> + <PxPayLogoSrc></PxPayLogoSrc> + <PxPayUserId></PxPayUserId> + <PxPayXsl></PxPayXsl> + <PxPayBgColor></PxPayBgColor> + <AcquirerPort>9999999999-99999999</AcquirerPort> + <AcquirerTxnRef>0</AcquirerTxnRef> + <GroupAccount>9997</GroupAccount> + <DpsTxnRef></DpsTxnRef> + <AllowRetry>0</AllowRetry> + <DpsBillingId></DpsBillingId> + <BillingId></BillingId> + <TransactionId>00000000</TransactionId> + <PxHostId>00000003</PxHostId> + </Transaction> + <ReCo>QK</ReCo> + <ResponseText>INVALID CARD NUMBER</ResponseText> + <HelpText>An Invalid Card Number was entered. Check the card number</HelpText> + <Success>0</Success> + <DpsTxnRef></DpsTxnRef> + <TxnRef></TxnRef> + </Txn> + RESPONSE end def successful_dps_billing_id_token_purchase_response - %(<GenerateRequest><Transaction valid="1" reco="00" responsetext="APPROVED"><Authorized>1</Authorized><MerchantReference></MerchantReference><CardName>Visa</CardName><Retry>0</Retry><StatusRequired>0</StatusRequired><AuthCode>030817</AuthCode><AmountInput>10.00</AmountInput><CurrencyId>554</CurrencyId><InputCurrencyId>554</InputCurrencyId><InputCurrencyName>NZD</InputCurrencyName><CurrencyRate>1.00</CurrencyRate><CurrencyName>NZD</CurrencyName><CardHolderName>LONGBOB LONGSEN</CardHolderName><DateSettlement>20070323</DateSettlement><TxnType>Purchase</TxnType><CardNumber>424242........42</CardNumber><DateExpiry>0808</DateExpiry><ProductId></ProductId><AcquirerDate>20070323</AcquirerDate><AcquirerTime>030817</AcquirerTime><AcquirerId>9000</AcquirerId><Acquirer>Test</Acquirer><TestMode>1</TestMode><CardId>2</CardId><CardHolderResponseText>APPROVED</CardHolderResponseText><CardHolderHelpText>The Transaction was approved</CardHolderHelpText><CardHolderResponseDescription>The Transaction was approved</CardHolderResponseDescription><MerchantResponseText>APPROVED</MerchantResponseText><MerchantHelpText>The Transaction was approved</MerchantHelpText><MerchantResponseDescription>The Transaction was approved</MerchantResponseDescription><UrlFail></UrlFail><UrlSuccess></UrlSuccess><EnablePostResponse>0</EnablePostResponse><PxPayName></PxPayName><PxPayLogoSrc></PxPayLogoSrc><PxPayUserId></PxPayUserId><PxPayXsl></PxPayXsl><PxPayBgColor></PxPayBgColor><AcquirerPort>9999999999-99999999</AcquirerPort><AcquirerTxnRef>12859</AcquirerTxnRef><GroupAccount>9997</GroupAccount><DpsTxnRef>0000000303ace8db</DpsTxnRef><AllowRetry>0</AllowRetry><DpsBillingId>0000030000141581</DpsBillingId><BillingId></BillingId><TransactionId>03ace8db</TransactionId><PxHostId>00000003</PxHostId></Transaction><ReCo>00</ReCo><ResponseText>APPROVED</ResponseText><HelpText>The Transaction was approved</HelpText><Success>1</Success><DpsTxnRef>0000000303ace8db</DpsTxnRef><TxnRef></TxnRef></GenerateRequest>) + <<~RESPONSE + <Txn> + <Transaction success="1" reco="00" responsetext="APPROVED"> + <Authorized>1</Authorized> + <MerchantReference></MerchantReference> + <CardName>Visa</CardName> + <Retry>0</Retry> + <StatusRequired>0</StatusRequired> + <AuthCode>030817</AuthCode> + <Amount>10.00</Amount> + <CurrencyId>554</CurrencyId> + <InputCurrencyId>554</InputCurrencyId> + <InputCurrencyName>NZD</InputCurrencyName> + <CurrencyRate>1.00</CurrencyRate> + <CurrencyName>NZD</CurrencyName> + <CardHolderName>LONGBOB LONGSEN</CardHolderName> + <DateSettlement>20070323</DateSettlement> + <TxnType>Purchase</TxnType> + <CardNumber>424242........42</CardNumber> + <DateExpiry>0808</DateExpiry> + <ProductId></ProductId> + <AcquirerDate>20070323</AcquirerDate> + <AcquirerTime>030817</AcquirerTime> + <AcquirerId>9000</AcquirerId> + <Acquirer>Test</Acquirer> + <TestMode>1</TestMode> + <CardId>2</CardId> + <CardHolderResponseText>APPROVED</CardHolderResponseText> + <CardHolderHelpText>The Transaction was approved</CardHolderHelpText> + <CardHolderResponseDescription>The Transaction was approved</CardHolderResponseDescription> + <MerchantResponseText>APPROVED</MerchantResponseText> + <MerchantHelpText>The Transaction was approved</MerchantHelpText> + <MerchantResponseDescription>The Transaction was approved</MerchantResponseDescription> + <UrlFail></UrlFail> + <UrlSuccess></UrlSuccess> + <EnablePostResponse>0</EnablePostResponse> + <PxPayName></PxPayName> + <PxPayLogoSrc></PxPayLogoSrc> + <PxPayUserId></PxPayUserId> + <PxPayXsl></PxPayXsl> + <PxPayBgColor></PxPayBgColor> + <AcquirerPort>9999999999-99999999</AcquirerPort> + <AcquirerTxnRef>12859</AcquirerTxnRef> + <GroupAccount>9997</GroupAccount> + <DpsTxnRef>0000000303ace8db</DpsTxnRef> + <AllowRetry>0</AllowRetry> + <DpsBillingId>0000030000141581</DpsBillingId> + <BillingId></BillingId> + <TransactionId>03ace8db</TransactionId> + <PxHostId>00000003</PxHostId> + </Transaction> + <ReCo>00</ReCo> + <ResponseText>APPROVED</ResponseText> + <HelpText>The Transaction was approved</HelpText> + <Success>1</Success> + <DpsTxnRef>0000000303ace8db</DpsTxnRef> + <TxnRef></TxnRef> + </Txn> + RESPONSE end def successful_billing_id_token_purchase_response - %(<GenerateRequest><Transaction valid="1" reco="00" responsetext="APPROVED"><Authorized>1</Authorized><MerchantReference></MerchantReference><CardName>Visa</CardName><Retry>0</Retry><StatusRequired>0</StatusRequired><AuthCode>030817</AuthCode><AmountInput>10.00</AmountInput><CurrencyId>554</CurrencyId><InputCurrencyId>554</InputCurrencyId><InputCurrencyName>NZD</InputCurrencyName><CurrencyRate>1.00</CurrencyRate><CurrencyName>NZD</CurrencyName><CardHolderName>LONGBOB LONGSEN</CardHolderName><DateSettlement>20070323</DateSettlement><TxnType>Purchase</TxnType><CardNumber>424242........42</CardNumber><DateExpiry>0808</DateExpiry><ProductId></ProductId><AcquirerDate>20070323</AcquirerDate><AcquirerTime>030817</AcquirerTime><AcquirerId>9000</AcquirerId><Acquirer>Test</Acquirer><TestMode>1</TestMode><CardId>2</CardId><CardHolderResponseText>APPROVED</CardHolderResponseText><CardHolderHelpText>The Transaction was approved</CardHolderHelpText><CardHolderResponseDescription>The Transaction was approved</CardHolderResponseDescription><MerchantResponseText>APPROVED</MerchantResponseText><MerchantHelpText>The Transaction was approved</MerchantHelpText><MerchantResponseDescription>The Transaction was approved</MerchantResponseDescription><UrlFail></UrlFail><UrlSuccess></UrlSuccess><EnablePostResponse>0</EnablePostResponse><PxPayName></PxPayName><PxPayLogoSrc></PxPayLogoSrc><PxPayUserId></PxPayUserId><PxPayXsl></PxPayXsl><PxPayBgColor></PxPayBgColor><AcquirerPort>9999999999-99999999</AcquirerPort><AcquirerTxnRef>12859</AcquirerTxnRef><GroupAccount>9997</GroupAccount><DpsTxnRef>0000000303ace8db</DpsTxnRef><AllowRetry>0</AllowRetry><DpsBillingId></DpsBillingId><BillingId>TEST1234</BillingId><TransactionId>03ace8db</TransactionId><PxHostId>00000003</PxHostId></Transaction><ReCo>00</ReCo><ResponseText>APPROVED</ResponseText><HelpText>The Transaction was approved</HelpText><Success>1</Success><DpsTxnRef>0000000303ace8db</DpsTxnRef><TxnRef></TxnRef></GenerateRequest>) + <<~RESPONSE + <Txn> + <Transaction success="1" reco="00" responsetext="APPROVED"> + <Authorized>1</Authorized> + <MerchantReference></MerchantReference> + <CardName>Visa</CardName> + <Retry>0</Retry> + <StatusRequired>0</StatusRequired> + <AuthCode>030817</AuthCode> + <Amount>10.00</Amount> + <CurrencyId>554</CurrencyId> + <InputCurrencyId>554</InputCurrencyId> + <InputCurrencyName>NZD</InputCurrencyName> + <CurrencyRate>1.00</CurrencyRate> + <CurrencyName>NZD</CurrencyName> + <CardHolderName>LONGBOB LONGSEN</CardHolderName> + <DateSettlement>20070323</DateSettlement> + <TxnType>Purchase</TxnType> + <CardNumber>424242........42</CardNumber> + <DateExpiry>0808</DateExpiry> + <ProductId></ProductId> + <AcquirerDate>20070323</AcquirerDate> + <AcquirerTime>030817</AcquirerTime> + <AcquirerId>9000</AcquirerId> + <Acquirer>Test</Acquirer> + <TestMode>1</TestMode> + <CardId>2</CardId> + <CardHolderResponseText>APPROVED</CardHolderResponseText> + <CardHolderHelpText>The Transaction was approved</CardHolderHelpText> + <CardHolderResponseDescription>The Transaction was approved</CardHolderResponseDescription> + <MerchantResponseText>APPROVED</MerchantResponseText> + <MerchantHelpText>The Transaction was approved</MerchantHelpText> + <MerchantResponseDescription>The Transaction was approved</MerchantResponseDescription> + <UrlFail></UrlFail> + <UrlSuccess></UrlSuccess> + <EnablePostResponse>0</EnablePostResponse> + <PxPayName></PxPayName> + <PxPayLogoSrc></PxPayLogoSrc> + <PxPayUserId></PxPayUserId> + <PxPayXsl></PxPayXsl> + <PxPayBgColor></PxPayBgColor> + <AcquirerPort>9999999999-99999999</AcquirerPort> + <AcquirerTxnRef>12859</AcquirerTxnRef> + <GroupAccount>9997</GroupAccount> + <DpsTxnRef>0000000303ace8db</DpsTxnRef> + <AllowRetry>0</AllowRetry> + <DpsBillingId></DpsBillingId> + <BillingId>TEST1234</BillingId> + <TransactionId>03ace8db</TransactionId> + <PxHostId>00000003</PxHostId> + </Transaction> + <ReCo>00</ReCo> + <ResponseText>APPROVED</ResponseText> + <HelpText>The Transaction was approved</HelpText> + <Success>1</Success> + <DpsTxnRef>0000000303ace8db</DpsTxnRef> + <TxnRef></TxnRef> + </Txn> + RESPONSE end def transcript diff --git a/test/unit/gateways/paymentez_test.rb b/test/unit/gateways/paymentez_test.rb index 05c16c6fd25..c3e303be2e2 100644 --- a/test/unit/gateways/paymentez_test.rb +++ b/test/unit/gateways/paymentez_test.rb @@ -1,9 +1,20 @@ require 'test_helper' class PaymentezTest < Test::Unit::TestCase + include CommStub + def setup @gateway = PaymentezGateway.new(application_code: 'foo', app_key: 'bar') @credit_card = credit_card + @elo_credit_card = credit_card( + '6362970000457013', + month: 10, + year: 2020, + first_name: 'John', + last_name: 'Smith', + verification_value: '737', + brand: 'elo' + ) @amount = 100 @options = { @@ -13,6 +24,29 @@ def setup description: 'Store Purchase', email: 'a@b.com' } + + @cavv = 'example-cavv-value' + @xid = 'three-ds-v1-trans-id' + @eci = '01' + @three_ds_v1_version = '1.0.2' + @three_ds_v2_version = '2.1.0' + @authentication_response_status = 'Y' + @directory_server_transaction_id = 'directory_server_transaction_id' + + @three_ds_v1_mpi = { + cavv: @cavv, + eci: @eci, + version: @three_ds_v1_version, + xid: @xid + } + + @three_ds_v2_mpi = { + cavv: @cavv, + eci: @eci, + version: @three_ds_v2_version, + authentication_response_status: @authentication_response_status, + ds_transaction_id: @directory_server_transaction_id + } end def test_successful_purchase @@ -25,16 +59,91 @@ 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) + + response = @gateway.purchase(@amount, @elo_credit_card, @options) + assert_success response + + assert_equal 'CI-14952', response.authorization + assert response.test? + end + + def test_successful_capture_with_otp + authorization = 'CI-14952' + options = @options.merge({ type: 'BY_OTP', value: '012345' }) + response = stub_comms do + @gateway.capture(nil, authorization, options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal 'BY_OTP', request['type'] + assert_equal '012345', request['value'] + assert_equal authorization, request['transaction']['id'] + assert_equal '123', request['user']['id'] + end.respond_with(successful_otp_capture_response) + assert_success response + end + def test_successful_purchase_with_token @gateway.expects(:ssl_post).returns(successful_purchase_response) response = @gateway.purchase(@amount, '123456789012345678901234567890', @options) assert_success response - assert_equal 'PR-926', response.authorization assert response.test? end + def test_purchase_3ds1_mpi_fields + @options[:three_d_secure] = @three_ds_v1_mpi + + expected_auth_data = { + cavv: @cavv, + xid: @xid, + eci: @eci, + version: @three_ds_v1_version + } + + @gateway.expects(:commit_transaction).with do |_, post_data| + post_data['extra_params'][:auth_data] == expected_auth_data + end + + @gateway.purchase(@amount, @credit_card, @options) + end + + def test_purchase_3ds2_mpi_fields + @options[:three_d_secure] = @three_ds_v2_mpi + + expected_auth_data = { + cavv: @cavv, + eci: @eci, + version: @three_ds_v2_version, + reference_id: @directory_server_transaction_id, + status: @authentication_response_status + } + + @gateway.expects(:commit_transaction).with() do |_, post_data| + post_data['extra_params'][:auth_data] == expected_auth_data + end + + @gateway.purchase(@amount, @credit_card, @options) + end + def test_failed_purchase @gateway.expects(:ssl_post).returns(failed_purchase_response) @@ -61,6 +170,15 @@ def test_successful_authorize assert response.test? end + def test_successful_authorize_with_elo + @gateway.stubs(:ssl_post).returns(successful_authorize_with_elo_response) + + response = @gateway.authorize(@amount, @elo_credit_card, @options) + assert_success response + assert_equal 'CI-14953', response.authorization + assert response.test? + end + def test_successful_authorize_with_token @gateway.stubs(:ssl_post).returns(successful_authorize_response) @@ -70,6 +188,42 @@ def test_successful_authorize_with_token assert response.test? end + def test_authorize_3ds1_mpi_fields + @options[:three_d_secure] = @three_ds_v1_mpi + + expected_auth_data = { + cavv: @cavv, + xid: @xid, + eci: @eci, + version: @three_ds_v1_version + } + + @gateway.expects(:commit_transaction).with() do |_, post_data| + post_data['extra_params'][:auth_data] == expected_auth_data + end + + @gateway.authorize(@amount, @credit_card, @options) + 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: @directory_server_transaction_id, + status: @authentication_response_status + } + + @gateway.expects(:commit_transaction).with() do |_, post_data| + post_data['extra_params'][:auth_data] == expected_auth_data + end + + @gateway.authorize(@amount, @credit_card, @options) + end + def test_failed_authorize @gateway.expects(:ssl_post).returns(failed_authorize_response) @@ -87,6 +241,15 @@ def test_successful_capture assert response.test? end + def test_successful_capture_with_elo + @gateway.expects(:ssl_post).returns(successful_capture_with_elo_response) + + response = @gateway.capture(nil, '1234', @options) + assert_success response + assert_equal 'CI-14953', response.authorization + assert response.test? + end + def test_successful_capture_with_amount @gateway.expects(:ssl_post).returns(successful_capture_response) @@ -107,9 +270,34 @@ def test_failed_capture def test_successful_refund @gateway.expects(:ssl_post).returns(successful_refund_response) - response = @gateway.refund(@amount, '1234', @options) + 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.respond_with(pending_response_current_status_cancelled) + assert_success response + 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 @@ -117,6 +305,7 @@ def test_failed_refund response = @gateway.refund(@amount, '1234', @options) assert_failure response + assert_equal 'Invalid Status', response.message assert response.test? end @@ -125,16 +314,28 @@ def test_successful_void response = @gateway.void('1234', @options) assert_success response + assert_equal 'Completed', response.message assert response.test? end 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 end + def test_successful_void_with_more_info + @gateway.expects(:ssl_post).returns(successful_void_response_with_more_info) + + response = @gateway.void('1234', @options.merge(more_info: true)) + assert_success response + assert_equal 'Completed', response.message + assert_equal '00', response.params['transaction']['carrier_code'] + assert_equal 'Reverse by mock', response.params['transaction']['message'] + assert response.test? + end + def test_simple_store @gateway.expects(:ssl_post).returns(successful_store_response) @@ -143,6 +344,14 @@ def test_simple_store assert_equal '14436664108567261211', response.authorization end + def test_simple_store_with_elo + @gateway.expects(:ssl_post).returns(successful_store_with_elo_response) + + response = @gateway.store(@elo_credit_card, @options) + assert_success response + assert_equal '15550938907932827845', response.authorization + end + def test_complex_store @gateway.stubs(:ssl_post).returns(already_stored_response, successful_unstore_response, successful_store_response) @@ -163,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 @@ -214,10 +435,11 @@ def post_scrubbed end def successful_purchase_response - %q( + ' { "transaction": { "status": "success", + "current_status": "APPROVED", "payment_date": "2017-12-19T20:29:12.715", "amount": 1, "authorization_code": "123456", @@ -237,11 +459,40 @@ def successful_purchase_response "number": "1111" } } - ) + ' + end + + 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", + "installments": 1, + "dev_reference": "Testing", + "message": "Response by mock", + "carrier_code": null, + "id": "CI-14952", + "status_detail": 3 + }, + "card": { + "bin": "636297", + "expiry_year": "2020", + "expiry_month": "10", + "transaction_reference": "CI-14952", + "type": "el", + "number": "7013", + "origin": "Paymentez" + } + } + ' end def failed_purchase_response - %q( + ' { "transaction": { "status": "failure", @@ -264,14 +515,15 @@ def failed_purchase_response "number": "4242" } } - ) + ' end def successful_authorize_response - %q( + ' { "transaction": { "status": "success", + "current_status": "PENDING", "payment_date": "2017-12-21T18:04:42", "amount": 1, "authorization_code": "487897", @@ -293,11 +545,42 @@ def successful_authorize_response "number": "1111" } } - ) + ' + end + + 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", + "installments": 1, + "dev_reference": "Testing", + "message": "Response by mock", + "carrier_code": null, + "id": "CI-14953", + "status_detail": 0 + }, + "card": { + "bin": "636297", + "status": "", + "token": "", + "expiry_year": "2020", + "expiry_month": "10", + "transaction_reference": "CI-14953", + "type": "el", + "number": "7013", + "origin": "Paymentez" + } + } + ' end def failed_authorize_response - %q( + ' { "transaction": { "status": "failure", @@ -323,14 +606,15 @@ def failed_authorize_response "origin": "Paymentez" } } - ) + ' end def successful_capture_response - %q( + ' { "transaction": { "status": "success", + "current_status": "APPROVED", "payment_date": "2017-12-21T18:04:42", "amount": 1, "authorization_code": "487897", @@ -352,7 +636,49 @@ def successful_capture_response "number": "1111" } } - ) + ' + end + + def successful_capture_with_elo_response + ' + { + "transaction": { + "status": "success", + "current_status": "APPROVED", + "payment_date": "2019-03-06T16:53:36", + "amount": 1, + "authorization_code": "TEST00", + "installments": 1, + "dev_reference": "Testing", + "message": "Response by mock", + "carrier_code": null, + "id": "CI-14953", + "status_detail": 3 + }, + "card": { + "bin": "636297", + "status": "", + "token": "", + "expiry_year": "2020", + "expiry_month": "10", + "transaction_reference": "CI-14953", + "type": "el", + "number": "7013", + "origin": "Paymentez" + } + } + ' + end + + def successful_otp_capture_response + '{ + "status": 1, + "payment_date": "2017-09-26T21:16:00", + "amount": 99.0, + "transaction_id": "CI-14952", + "status_detail": 3, + "message": "" + }' end def failed_capture_response @@ -364,11 +690,16 @@ def successful_void_response end def failed_void_response - '{"error": {"type": "Carrier not supported", "help": "", "description": "{}"}}' + '{"status": "failure", "detail": "Invalid Status"}' end - alias_method :successful_refund_response, :successful_void_response - alias_method :failed_refund_response, :failed_void_response + def successful_void_response_with_more_info + '{"status": "success", "detail": "Completed", "transaction": {"carrier_code": "00", "message": "Reverse by mock", "status_detail":7}}' + end + + alias successful_refund_response successful_void_response + alias failed_refund_response failed_void_response + alias successful_refund_response_with_more_info successful_void_response_with_more_info def already_stored_response '{"error": {"type": "Card already added: 14436664108567261211", "help": "If you want to update the card, first delete it", "description": "{}"}}' @@ -382,8 +713,12 @@ def successful_store_response '{"card": {"bin": "411111", "status": "valid", "token": "14436664108567261211", "message": "", "expiry_year": "2018", "expiry_month": "9", "transaction_reference": "PR-959", "type": "vi", "number": "1111"}}' end + def successful_store_with_elo_response + '{"card": {"bin": "636297", "status": "valid", "token": "15550938907932827845", "message": "", "expiry_year": "2020", "expiry_month": "10", "transaction_reference": "CI-14956", "type": "el", "number": "7013", "origin": "Paymentez"}}' + end + def failed_store_response - %q( + ' { "card": { "bin": "424242", @@ -397,11 +732,11 @@ def failed_store_response "number": "4242" } } - ) + ' end def expired_card_response - %q( + ' { "transaction":{ "status":"failure", @@ -425,11 +760,11 @@ def expired_card_response "origin":"Paymentez" } } - ) + ' end def crash_response - %q( + ' <html> <head> <title>Internal Server Error</title> @@ -439,6 +774,26 @@ def crash_response </body> </html> - ) + ' + 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/paymill_test.rb b/test/unit/gateways/paymill_test.rb index 816b6198213..40fd6191ee2 100644 --- a/test/unit/gateways/paymill_test.rb +++ b/test/unit/gateways/paymill_test.rb @@ -2,7 +2,7 @@ class PaymillTest < Test::Unit::TestCase def setup - @gateway = PaymillGateway.new(:public_key => 'PUBLIC', :private_key => 'PRIVATE') + @gateway = PaymillGateway.new(public_key: 'PUBLIC', private_key: 'PRIVATE') @credit_card = credit_card @amount = 100 @@ -181,6 +181,20 @@ def test_successful_store assert response.test? end + def test_store_includes_currency_and_amount + expected_currency = 'USD' + expected_amount = 100 + + @gateway.expects(:raw_ssl_request).with( + :get, + store_endpoint_url(@credit_card, expected_currency, expected_amount), + nil, + {} + ).returns(successful_store_response, successful_purchase_response) + + @gateway.store(@credit_card) + end + def test_failed_store_with_invalid_credit_card @gateway.expects(:raw_ssl_request).returns(failed_store_response) response = @gateway.store(@credit_card) @@ -224,6 +238,11 @@ def test_transcript_scrubbing end private + + def store_endpoint_url(credit_card, currency, amount) + "https://test-token.paymill.com?account.holder=#{credit_card.first_name}+#{credit_card.last_name}&account.number=#{credit_card.number}&account.expiry.month=#{'%02d' % credit_card.month}&account.expiry.year=#{credit_card.year}&account.verification=#{credit_card.verification_value}&presentation.amount3D=#{amount}&presentation.currency3D=#{currency}&channel.id=PUBLIC&jsonPFunction=jsonPFunction&transaction.mode=CONNECTOR_TEST" + end + def successful_store_response MockResponse.new 200, %[jsonPFunction({"transaction":{"mode":"CONNECTOR_TEST","channel":"57313835619696ac361dc591bc973626","response":"SYNC","payment":{"code":"CC.DB"},"processing":{"code":"CC.DB.90.00","reason":{"code":"00","message":"Successful Processing"},"result":"ACK","return":{"code":"000.100.112","message":"Request successfully processed in 'Merchant in Connector Test Mode'"},"timestamp":"2013-02-12 21:33:43"},"identification":{"shortId":"1998.1832.1612","uniqueId":"tok_4f9a571b39bd8d0b4db5"}}})] end @@ -757,5 +776,4 @@ def transcript def scrubbed_transcript 'connection_uri=https://test-token.paymill.com?account.number=[FILTERED]&account.expiry.month=09&account.expiry.year=2016&account.verification=[FILTERED]' 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 7428848a955..2c128c942c2 100644 --- a/test/unit/gateways/paypal/paypal_common_api_test.rb +++ b/test/unit/gateways/paypal/paypal_common_api_test.rb @@ -6,7 +6,9 @@ class CommonPaypalGateway < ActiveMerchant::Billing::Gateway include ActiveMerchant::Billing::PaypalCommonAPI def currency(code); 'USD'; end + def localized_amount(num, code); num; end + def commit(a, b); end end @@ -16,59 +18,94 @@ def setup CommonPaypalGateway.pem_file = nil @gateway = CommonPaypalGateway.new( - :login => 'cody', - :password => 'test', - :pem => 'PEM' + login: 'cody', + password: 'test', + pem: 'PEM' ) - @address = { :address1 => '1234 My Street', - :address2 => 'Apt 1', - :company => 'Widgets Inc', - :city => 'Ottawa', - :state => 'ON', - :zip => 'K1C2N6', - :country => 'Canada', - :phone => '(555)555-5555' - } + @address = { + address1: '1234 My Street', + address2: 'Apt 1', + company: 'Widgets Inc', + city: 'Ottawa', + state: 'ON', + zip: 'K1C2N6', + country: 'Canada', + phone: '(555)555-5555' + } end def xml_builder Builder::XmlMarkup.new end - def wrap_xml(&block) - REXML::Document.new(@gateway.send(:build_request_wrapper, 'Action', &block)) + def wrap_xml(&) + REXML::Document.new(@gateway.send(:build_request_wrapper, 'Action', &)) end def test_add_payment_details_adds_express_only_payment_details_when_necessary - options = {:express_request => true} + options = { express_request: true } @gateway.expects(:add_express_only_payment_details) @gateway.send(:add_payment_details, xml_builder, 100, 'USD', options) end def test_add_payment_details_adds_items_details - options = {:items => [1]} + options = { items: [1] } @gateway.expects(:add_payment_details_items_xml) @gateway.send(:add_payment_details, xml_builder, 100, 'USD', options) end def test_add_payment_details_adds_address - options = {:shipping_address => @address} + options = { shipping_address: @address } @gateway.expects(:add_address) @gateway.send(:add_payment_details, xml_builder, 100, 'USD', options) end def test_add_payment_details_adds_items_details_elements - options = {:items => [{:name => 'foo'}]} + options = { items: [{ name: 'foo' }] } request = wrap_xml do |xml| @gateway.send(:add_payment_details, xml, 100, 'USD', options) end assert_equal 'foo', REXML::XPath.first(request, '//n2:PaymentDetails/n2:PaymentDetailsItem/n2:Name').text end + def test_add_payment_details_adds_order_total_elements + options = { + subtotal: 25, + shipping: 5, + handling: 2, + tax: 1 + } + request = wrap_xml do |xml| + @gateway.send(:add_payment_details, xml, 100, 'USD', options) + end + + assert_equal '25', REXML::XPath.first(request, '//n2:PaymentDetails/n2:ItemTotal').text + assert_equal '5', REXML::XPath.first(request, '//n2:PaymentDetails/n2:ShippingTotal').text + assert_equal '2', REXML::XPath.first(request, '//n2:PaymentDetails/n2:HandlingTotal').text + assert_equal '1', REXML::XPath.first(request, '//n2:PaymentDetails/n2:TaxTotal').text + end + + def test_add_payment_details_does_not_add_order_total_elements_when_any_element_is_nil + options = { + subtotal: nil, + shipping: 5, + handling: 2, + tax: 1 + } + request = wrap_xml do |xml| + @gateway.send(:add_payment_details, xml, 100, 'USD', options) + end + + assert_equal nil, REXML::XPath.first(request, '//n2:PaymentDetails/n2:ItemTotal') + assert_equal nil, REXML::XPath.first(request, '//n2:PaymentDetails/n2:ShippingTotal') + assert_equal nil, REXML::XPath.first(request, '//n2:PaymentDetails/n2:HandlingTotal') + assert_equal nil, REXML::XPath.first(request, '//n2:PaymentDetails/n2:TaxTotal') + end + def test_add_express_only_payment_details_adds_non_blank_fields request = wrap_xml do |xml| - @gateway.send(:add_express_only_payment_details, xml, {:payment_action => 'Sale', :payment_request_id => ''}) + @gateway.send(:add_express_only_payment_details, xml, { payment_action: 'Sale', payment_request_id: '' }) end assert_equal 'Sale', REXML::XPath.first(request, '//n2:PaymentAction').text assert_nil REXML::XPath.first(request, '//n2:PaymentRequestID') @@ -82,8 +119,8 @@ def test_build_request_wrapper_plain end def test_build_request_wrapper_with_request_details - result = @gateway.send(:build_request_wrapper, 'Action', :request_details => true) do |xml| - xml.tag! 'n2:TransactionID', 'baz' + result = @gateway.send(:build_request_wrapper, 'Action', request_details: true) do |xml| + xml.tag! 'n2:TransactionID', 'baz' end assert_equal 'baz', REXML::XPath.first(REXML::Document.new(result), '//ActionReq/ActionRequest/n2:ActionRequestDetails/n2:TransactionID').text end @@ -98,31 +135,30 @@ def test_build_get_balance assert_equal '1', REXML::XPath.first(request, '//GetBalanceReq/GetBalanceRequest/ReturnAllCurrencies').text end - def test_balance_cleans_up_currencies_values_like_1 + def test_balance_cleans_up_currencies_values_like_one @gateway.stubs(:commit) - [1, '1', true].each do |values_like_1| + [1, '1', true].each do |values_like_one| @gateway.expects(:build_get_balance).with('1') - @gateway.balance(values_like_1) + @gateway.balance(values_like_one) end end - def test_balance_cleans_up_currencies_values_like_0 + def test_balance_cleans_up_currencies_values_like_zero @gateway.stubs(:commit) - [0, '0', false, nil, :foo].each do |values_like_0| + [0, '0', false, nil, :foo].each do |values_like_zero| @gateway.expects(:build_get_balance).with('0') - @gateway.balance(values_like_0) + @gateway.balance(values_like_zero) end end def test_build_do_authorize_request - request = REXML::Document.new(@gateway.send(:build_do_authorize,123, 100, :currency => 'USD')) + request = REXML::Document.new(@gateway.send(:build_do_authorize, 123, 100, currency: 'USD')) assert_equal '123', REXML::XPath.first(request, '//DoAuthorizationReq/DoAuthorizationRequest/TransactionID').text assert_equal '1.00', REXML::XPath.first(request, '//DoAuthorizationReq/DoAuthorizationRequest/Amount').text end - def test_build_manage_pending_transaction_status_request - request = REXML::Document.new(@gateway.send(:build_manage_pending_transaction_status,123, 'Accept')) + request = REXML::Document.new(@gateway.send(:build_manage_pending_transaction_status, 123, 'Accept')) assert_equal '123', REXML::XPath.first(request, '//ManagePendingTransactionStatusReq/ManagePendingTransactionStatusRequest/TransactionID').text assert_equal 'Accept', REXML::XPath.first(request, '//ManagePendingTransactionStatusReq/ManagePendingTransactionStatusRequest/Action').text end @@ -134,10 +170,12 @@ def test_transaction_search_requires end def test_build_transaction_search_request - options = {:start_date => DateTime.new(2012, 2, 21, 0), - :end_date => DateTime.new(2012, 3, 21, 0), - :receiver => 'foo@example.com', - :first_name => 'Robert'} + options = { + start_date: DateTime.new(2012, 2, 21, 0), + end_date: DateTime.new(2012, 3, 21, 0), + receiver: 'foo@example.com', + first_name: 'Robert' + } request = REXML::Document.new(@gateway.send(:build_transaction_search, options)) assert_match %r{^2012-02-21T\d{2}:00:00Z$}, REXML::XPath.first(request, '//TransactionSearchReq/TransactionSearchRequest/StartDate').text assert_match %r{^2012-03-21T\d{2}:00:00Z$}, REXML::XPath.first(request, '//TransactionSearchReq/TransactionSearchRequest/EndDate').text @@ -148,17 +186,20 @@ def test_build_reference_transaction_request assert_raise ArgumentError do @gateway.reference_transaction(100) end - @gateway.reference_transaction(100, :reference_id => 'id') + @gateway.reference_transaction(100, reference_id: 'id') 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 end - end diff --git a/test/unit/gateways/paypal_digital_goods_test.rb b/test/unit/gateways/paypal_digital_goods_test.rb index ba8e05f12a8..886253a6673 100644 --- a/test/unit/gateways/paypal_digital_goods_test.rb +++ b/test/unit/gateways/paypal_digital_goods_test.rb @@ -8,9 +8,9 @@ class PaypalDigitalGoodsTest < Test::Unit::TestCase def setup @gateway = PaypalDigitalGoodsGateway.new( - :login => 'cody', - :password => 'test', - :pem => 'PEM' + login: 'cody', + password: 'test', + pem: 'PEM' ) Base.mode = :test @@ -33,94 +33,107 @@ def test_test_redirect_url end def test_setup_request_invalid_requests - assert_raise ArgumentError do - @gateway.setup_purchase(100, - :ip => '127.0.0.1', - :description => 'Test Title', - :return_url => 'http://return.url', - :cancel_return_url => 'http://cancel.url') - end - - assert_raise ArgumentError do - @gateway.setup_purchase(100, - :ip => '127.0.0.1', - :description => 'Test Title', - :return_url => 'http://return.url', - :cancel_return_url => 'http://cancel.url', - :items => [ ]) - end - - assert_raise ArgumentError do - @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 ] ) - end - - assert_raise ArgumentError do - @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' } ] ) - end + assert_raise ArgumentError do + @gateway.setup_purchase( + 100, + ip: '127.0.0.1', + description: 'Test Title', + return_url: 'http://return.url', + cancel_return_url: 'http://cancel.url' + ) + end + + assert_raise ArgumentError do + @gateway.setup_purchase( + 100, + ip: '127.0.0.1', + description: 'Test Title', + return_url: 'http://return.url', + cancel_return_url: 'http://cancel.url', + items: [] + ) + end + + assert_raise ArgumentError do + @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] + ) + end + + assert_raise ArgumentError do + @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' + } + ] + ) + end end - def test_build_setup_request_valid @gateway.expects(:ssl_post).returns(successful_setup_response) - @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' } ] ) - + @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' + } + ] + ) end - private def successful_setup_response -"<?xml version=\"1.0\" encoding=\"UTF-8\"?> -<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:SOAP-ENC=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:cc=\"urn:ebay:apis:CoreComponentTypes\" xmlns:wsu=\"http://schemas.xmlsoap.org/ws/2002/07/utility\" xmlns:saml=\"urn:oasis:names:tc:SAML:1.0:assertion\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:wsse=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xmlns:ed=\"urn:ebay:apis:EnhancedDataTypes\" xmlns:ebl=\"urn:ebay:apis:eBLBaseComponents\" xmlns:ns=\"urn:ebay:api:PayPalAPI\"> - <SOAP-ENV:Header> - <Security xmlns=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xsi:type=\"wsse:SecurityType\"></Security> - <RequesterCredentials xmlns=\"urn:ebay:api:PayPalAPI\" xsi:type=\"ebl:CustomSecurityHeaderType\"> - <Credentials xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:UserIdPasswordType\"> - <Username xsi:type=\"xs:string\"></Username> - <Password xsi:type=\"xs:string\"></Password> - <Signature xsi:type=\"xs:string\">OMGOMGOMGOMGOMG</Signature> - <Subject xsi:type=\"xs:string\"></Subject> - </Credentials> - </RequesterCredentials> - </SOAP-ENV:Header> - <SOAP-ENV:Body id=\"_0\"> - <SetExpressCheckoutResponse xmlns=\"urn:ebay:api:PayPalAPI\"> - <Timestamp xmlns=\"urn:ebay:apis:eBLBaseComponents\">2011-05-19T20:13:30Z</Timestamp> - <Ack xmlns=\"urn:ebay:apis:eBLBaseComponents\">Success</Ack> - <CorrelationID xmlns=\"urn:ebay:apis:eBLBaseComponents\">da0ed6bc90ef1</CorrelationID> - <Version xmlns=\"urn:ebay:apis:eBLBaseComponents\">72</Version> - <Build xmlns=\"urn:ebay:apis:eBLBaseComponents\">1882144</Build> - <Token xsi:type=\"ebl:ExpressCheckoutTokenType\">EC-0XOMGOMGOMG</Token> - </SetExpressCheckoutResponse> - </SOAP-ENV:Body> - </SOAP-ENV:Envelope>" + "<?xml version=\"1.0\" encoding=\"UTF-8\"?> + <SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:SOAP-ENC=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:cc=\"urn:ebay:apis:CoreComponentTypes\" xmlns:wsu=\"http://schemas.xmlsoap.org/ws/2002/07/utility\" xmlns:saml=\"urn:oasis:names:tc:SAML:1.0:assertion\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:wsse=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xmlns:ed=\"urn:ebay:apis:EnhancedDataTypes\" xmlns:ebl=\"urn:ebay:apis:eBLBaseComponents\" xmlns:ns=\"urn:ebay:api:PayPalAPI\"> + <SOAP-ENV:Header> + <Security xmlns=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xsi:type=\"wsse:SecurityType\"></Security> + <RequesterCredentials xmlns=\"urn:ebay:api:PayPalAPI\" xsi:type=\"ebl:CustomSecurityHeaderType\"> + <Credentials xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:UserIdPasswordType\"> + <Username xsi:type=\"xs:string\"></Username> + <Password xsi:type=\"xs:string\"></Password> + <Signature xsi:type=\"xs:string\">OMGOMGOMGOMGOMG</Signature> + <Subject xsi:type=\"xs:string\"></Subject> + </Credentials> + </RequesterCredentials> + </SOAP-ENV:Header> + <SOAP-ENV:Body id=\"_0\"> + <SetExpressCheckoutResponse xmlns=\"urn:ebay:api:PayPalAPI\"> + <Timestamp xmlns=\"urn:ebay:apis:eBLBaseComponents\">2011-05-19T20:13:30Z</Timestamp> + <Ack xmlns=\"urn:ebay:apis:eBLBaseComponents\">Success</Ack> + <CorrelationID xmlns=\"urn:ebay:apis:eBLBaseComponents\">da0ed6bc90ef1</CorrelationID> + <Version xmlns=\"urn:ebay:apis:eBLBaseComponents\">72</Version> + <Build xmlns=\"urn:ebay:apis:eBLBaseComponents\">1882144</Build> + <Token xsi:type=\"ebl:ExpressCheckoutTokenType\">EC-0XOMGOMGOMG</Token> + </SetExpressCheckoutResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope>" end - end - diff --git a/test/unit/gateways/paypal_express_test.rb b/test/unit/gateways/paypal_express_test.rb index 6ac936f3023..5be4717bfed 100644 --- a/test/unit/gateways/paypal_express_test.rb +++ b/test/unit/gateways/paypal_express_test.rb @@ -14,20 +14,21 @@ class PaypalExpressTest < Test::Unit::TestCase def setup @gateway = PaypalExpressGateway.new( - :login => 'cody', - :password => 'test', - :pem => 'PEM' + login: 'cody', + password: 'test', + pem: 'PEM' ) - @address = { :address1 => '1234 My Street', - :address2 => 'Apt 1', - :company => 'Widgets Inc', - :city => 'Ottawa', - :state => 'ON', - :zip => 'K1C2N6', - :country => 'Canada', - :phone => '(555)555-5555' - } + @address = { + address1: '1234 My Street', + address2: 'Apt 1', + company: 'Widgets Inc', + city: 'Ottawa', + state: 'ON', + zip: 'K1C2N6', + country: 'Canada', + phone: '(555)555-5555' + } Base.mode = :test end @@ -39,40 +40,40 @@ def teardown def test_live_redirect_url Base.mode = :production assert_equal LIVE_REDIRECT_URL, @gateway.redirect_url_for('1234567890') - assert_equal LIVE_REDIRECT_URL_MOBILE, @gateway.redirect_url_for('1234567890', :mobile => true) + assert_equal LIVE_REDIRECT_URL_MOBILE, @gateway.redirect_url_for('1234567890', mobile: true) end def test_live_redirect_url_without_review Base.mode = :production - assert_equal LIVE_REDIRECT_URL_WITHOUT_REVIEW, @gateway.redirect_url_for('1234567890', :review => false) - assert_equal LIVE_REDIRECT_URL_MOBILE_WITHOUT_REVIEW, @gateway.redirect_url_for('1234567890', :review => false, :mobile => true) + assert_equal LIVE_REDIRECT_URL_WITHOUT_REVIEW, @gateway.redirect_url_for('1234567890', review: false) + assert_equal LIVE_REDIRECT_URL_MOBILE_WITHOUT_REVIEW, @gateway.redirect_url_for('1234567890', review: false, mobile: true) end def test_force_sandbox_redirect_url Base.mode = :production gateway = PaypalExpressGateway.new( - :login => 'cody', - :password => 'test', - :pem => 'PEM', - :test => true + login: 'cody', + password: 'test', + pem: 'PEM', + test: true ) assert gateway.test? assert_equal TEST_REDIRECT_URL, gateway.redirect_url_for('1234567890') - assert_equal TEST_REDIRECT_URL_MOBILE, gateway.redirect_url_for('1234567890', :mobile => true) + assert_equal TEST_REDIRECT_URL_MOBILE, gateway.redirect_url_for('1234567890', mobile: true) end def test_test_redirect_url assert_equal :test, Base.mode assert_equal TEST_REDIRECT_URL, @gateway.redirect_url_for('1234567890') - assert_equal TEST_REDIRECT_URL_MOBILE, @gateway.redirect_url_for('1234567890', :mobile => true) + assert_equal TEST_REDIRECT_URL_MOBILE, @gateway.redirect_url_for('1234567890', mobile: true) end def test_test_redirect_url_without_review assert_equal :test, Base.mode - assert_equal TEST_REDIRECT_URL_WITHOUT_REVIEW, @gateway.redirect_url_for('1234567890', :review => false) - assert_equal TEST_REDIRECT_URL_MOBILE_WITHOUT_REVIEW, @gateway.redirect_url_for('1234567890', :review => false, :mobile => true) + assert_equal TEST_REDIRECT_URL_WITHOUT_REVIEW, @gateway.redirect_url_for('1234567890', review: false) + assert_equal TEST_REDIRECT_URL_MOBILE_WITHOUT_REVIEW, @gateway.redirect_url_for('1234567890', review: false, mobile: true) end def test_get_express_details @@ -87,6 +88,7 @@ def test_get_express_details assert_equal 'FWRVKNRRZ3WUC', response.payer_id assert_equal 'buyer@jadedpallet.com', response.email assert_equal 'This is a test note', response.note + assert_equal 'PaymentActionNotInitiated', response.checkout_status assert address = response.address assert_equal 'Fred Brooks', address['name'] @@ -110,7 +112,7 @@ def test_express_response_missing_address def test_authorization @gateway.expects(:ssl_post).returns(successful_authorization_response) - response = @gateway.authorize(300, :token => 'EC-6WS104951Y388951L', :payer_id => 'FWRVKNRRZ3WUC') + response = @gateway.authorize(300, token: 'EC-6WS104951Y388951L', payer_id: 'FWRVKNRRZ3WUC') assert response.success? assert_not_nil response.authorization assert response.test? @@ -129,25 +131,25 @@ def test_uk_partner end def test_includes_description - xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 0, { :description => 'a description' })) + xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 0, { description: 'a description' })) assert_equal 'a description', REXML::XPath.first(xml, '//n2:PaymentDetails/n2:OrderDescription').text end def test_includes_order_id - xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 0, { :order_id => '12345' })) + xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 0, { order_id: '12345' })) assert_equal '12345', REXML::XPath.first(xml, '//n2:PaymentDetails/n2:InvoiceID').text end def test_includes_correct_payment_action - xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 0, { })) + xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 0, {})) assert_equal 'SetExpressCheckout', REXML::XPath.first(xml, '//n2:PaymentDetails/n2:PaymentAction').text end def test_includes_custom_tag_if_specified - xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 0, {:custom => 'Foo'})) + xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 0, { custom: 'Foo' })) assert_equal 'Foo', REXML::XPath.first(xml, '//n2:PaymentDetails/n2:Custom').text end @@ -165,10 +167,23 @@ def test_does_not_include_items_if_not_specified end def test_items_are_included_if_specified_in_build_setup_request - xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 0, {:currency => 'GBP', :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_setup_request, 'SetExpressCheckout', 0, { + currency: 'GBP', + 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 @@ -192,7 +207,7 @@ def test_does_not_include_callback_url_if_not_specified end def test_callback_url_is_included_if_specified_in_build_setup_request - xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 0, {:callback_url => 'http://example.com/update_callback'})) + xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 0, { callback_url: 'http://example.com/update_callback' })) assert_equal 'http://example.com/update_callback', REXML::XPath.first(xml, '//n2:CallbackURL').text end @@ -204,7 +219,7 @@ def test_does_not_include_callback_timeout_if_not_specified end def test_callback_timeout_is_included_if_specified_in_build_setup_request - xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 0, {:callback_timeout => 2})) + xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 0, { callback_timeout: 2 })) assert_equal '2', REXML::XPath.first(xml, '//n2:CallbackTimeout').text end @@ -216,7 +231,7 @@ def test_does_not_include_callback_version_if_not_specified end def test_callback_version_is_included_if_specified_in_build_setup_request - xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 0, {:callback_version => '53.0'})) + xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 0, { callback_version: '53.0' })) assert_equal '53.0', REXML::XPath.first(xml, '//n2:CallbackVersion').text end @@ -228,16 +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 + 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 + { + 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 @@ -251,14 +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 @@ -276,21 +313,47 @@ def test_handle_non_zero_amount def test_amount_format_for_jpy_currency @gateway.expects(:ssl_post).with(anything, regexp_matches(/n2:OrderTotal currencyID=.JPY.>1<\/n2:OrderTotal>/), {}).returns(successful_authorization_response) - response = @gateway.authorize(100, :token => 'EC-6WS104951Y388951L', :payer_id => 'FWRVKNRRZ3WUC', :currency => 'JPY') + response = @gateway.authorize(100, token: 'EC-6WS104951Y388951L', payer_id: 'FWRVKNRRZ3WUC', currency: 'JPY') assert response.success? end def test_removes_fractional_amounts_with_twd_currency - xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 150, {:currency => 'TWD'})) + xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 150, { currency: 'TWD' })) assert_equal '1', REXML::XPath.first(xml, '//n2:OrderTotal').text 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 @@ -300,10 +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 @@ -313,10 +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 @@ -326,33 +441,33 @@ def test_fractional_discounts_are_correctly_calculated_with_usd_currency end def test_does_not_add_allow_note_if_not_specified - xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 0, { })) + xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 0, {})) assert_nil REXML::XPath.first(xml, '//n2:AllowNote') end def test_adds_allow_note_if_specified - allow_notes_xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 0, { :allow_note => true })) - do_not_allow_notes_xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 0, { :allow_note => false })) + allow_notes_xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 0, { allow_note: true })) + do_not_allow_notes_xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 0, { allow_note: false })) assert_equal '1', REXML::XPath.first(allow_notes_xml, '//n2:AllowNote').text assert_equal '0', REXML::XPath.first(do_not_allow_notes_xml, '//n2:AllowNote').text end def test_handle_locale_code - xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 0, { :locale => 'GB' })) + xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 0, { locale: 'GB' })) assert_equal 'GB', REXML::XPath.first(xml, '//n2:LocaleCode').text end def test_handle_non_standard_locale_code - xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 0, { :locale => 'IL' })) + xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 0, { locale: 'IL' })) assert_equal 'he_IL', REXML::XPath.first(xml, '//n2:LocaleCode').text end def test_does_not_include_locale_in_request_unless_provided_in_options - xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 0, { :locale => nil })) + xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 0, { locale: nil })) assert_nil REXML::XPath.first(xml, '//n2:LocaleCode') end @@ -369,11 +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 @@ -447,15 +582,23 @@ def test_agreement_details_failure assert_equal '11451', response.params['error_codes'] end - 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' })) + 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 @@ -466,6 +609,36 @@ def test_build_reference_transaction_test assert_equal 'invoice_id', REXML::XPath.first(xml, '//DoReferenceTransactionReq/DoReferenceTransactionRequest/n2:DoReferenceTransactionRequestDetails/n2:PaymentDetails/n2:InvoiceID').text assert_equal 'ActiveMerchant_FOO', REXML::XPath.first(xml, '//DoReferenceTransactionReq/DoReferenceTransactionRequest/n2:DoReferenceTransactionRequestDetails/n2:PaymentDetails/n2:ButtonSource').text assert_equal '127.0.0.1', REXML::XPath.first(xml, '//DoReferenceTransactionReq/DoReferenceTransactionRequest/n2:DoReferenceTransactionRequestDetails/n2:IPAddress').text + assert_equal 'example_merchant_session_id', REXML::XPath.first(xml, '//DoReferenceTransactionReq/DoReferenceTransactionRequest/n2:DoReferenceTransactionRequestDetails/n2:MerchantSessionId').text + end + + 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' + } + ) + ) + + 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 + assert_equal 'Sale', REXML::XPath.first(xml, '//DoReferenceTransactionReq/DoReferenceTransactionRequest/n2:DoReferenceTransactionRequestDetails/n2:PaymentAction').text + assert_equal 'Any', REXML::XPath.first(xml, '//DoReferenceTransactionReq/DoReferenceTransactionRequest/n2:DoReferenceTransactionRequestDetails/n2:PaymentType').text + assert_equal '20.00', REXML::XPath.first(xml, '//DoReferenceTransactionReq/DoReferenceTransactionRequest/n2:DoReferenceTransactionRequestDetails/n2:PaymentDetails/n2:OrderTotal').text + assert_equal 'Description', REXML::XPath.first(xml, '//DoReferenceTransactionReq/DoReferenceTransactionRequest/n2:DoReferenceTransactionRequestDetails/n2:PaymentDetails/n2:OrderDescription').text + assert_equal 'invoice_id', REXML::XPath.first(xml, '//DoReferenceTransactionReq/DoReferenceTransactionRequest/n2:DoReferenceTransactionRequestDetails/n2:PaymentDetails/n2:InvoiceID').text + assert_equal 'ActiveMerchant_FOO', REXML::XPath.first(xml, '//DoReferenceTransactionReq/DoReferenceTransactionRequest/n2:DoReferenceTransactionRequestDetails/n2:PaymentDetails/n2:ButtonSource').text + assert_equal '127.0.0.1', REXML::XPath.first(xml, '//DoReferenceTransactionReq/DoReferenceTransactionRequest/n2:DoReferenceTransactionRequestDetails/n2:IPAddress').text + assert_nil REXML::XPath.first(xml, '//DoReferenceTransactionReq/DoReferenceTransactionRequest/n2:DoReferenceTransactionRequestDetails/n2:MerchantSessionId') end def test_build_details_billing_agreement_request_test @@ -477,22 +650,23 @@ def test_build_details_billing_agreement_request_test def test_authorize_reference_transaction @gateway.expects(:ssl_post).returns(successful_authorize_reference_transaction_response) - response = @gateway.authorize_reference_transaction(2000, { - :reference_id => 'ref_id', - :payment_type => 'Any', - :invoice_id => 'invoice_id', - :description => 'Description', - :ip => '127.0.0.1' }) + response = @gateway.authorize_reference_transaction(2000, reference_id: 'ref_id') assert_equal 'Success', response.params['ack'] assert_equal 'Success', response.message assert_equal '9R43552341412482K', response.authorization end + def test_authorize_reference_transaction_requires_fields + assert_raise ArgumentError do + @gateway.authorize_reference_transaction(2000, {}) + end + end + def test_reference_transaction @gateway.expects(:ssl_post).returns(successful_reference_transaction_response) - response = @gateway.reference_transaction(2000, { :reference_id => 'ref_id' }) + response = @gateway.reference_transaction(2000, { reference_id: 'ref_id' }) assert_equal 'Success', response.params['ack'] assert_equal 'Success', response.message @@ -507,40 +681,38 @@ 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'] + 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 ['10736', '10002'] , response.params['error_codes'].split(',') + assert_equal %w[10736 10002], response.params['error_codes'].split(',') end def test_allow_guest_checkout - xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 10, {:allow_guest_checkout => true})) + xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 10, { allow_guest_checkout: true })) assert_equal 'Sole', REXML::XPath.first(xml, '//n2:SolutionType').text assert_equal 'Billing', REXML::XPath.first(xml, '//n2:LandingPage').text end + def test_paypal_chooses_landing_page + xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 10, { allow_guest_checkout: true, paypal_chooses_landing_page: true })) + + assert_equal 'Sole', REXML::XPath.first(xml, '//n2:SolutionType').text + assert_nil REXML::XPath.first(xml, '//n2:LandingPage') + end + def test_not_adds_brand_name_if_not_specified xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 10, {})) @@ -548,7 +720,7 @@ def test_not_adds_brand_name_if_not_specified end def test_adds_brand_name_if_specified - xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 10, {:brand_name => 'Acme'})) + xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 10, { brand_name: 'Acme' })) assert_equal 'Acme', REXML::XPath.first(xml, '//n2:BrandName').text end @@ -566,15 +738,15 @@ def test_not_adds_buyer_optin_if_not_specified end def test_adds_buyer_optin_if_specified - allow_optin_xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 10, {:allow_buyer_optin => true})) - do_not_allow_optin_xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 10, {:allow_buyer_optin => false})) + allow_optin_xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 10, { allow_buyer_optin: true })) + do_not_allow_optin_xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 10, { allow_buyer_optin: false })) assert_equal '1', REXML::XPath.first(allow_optin_xml, '//n2:BuyerEmailOptInEnable').text assert_equal '0', REXML::XPath.first(do_not_allow_optin_xml, '//n2:BuyerEmailOptInEnable').text end def test_add_total_type_if_specified - total_type_xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 10, {:total_type => 'EstimatedTotal'})) + total_type_xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 10, { total_type: 'EstimatedTotal' })) no_total_type_xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 10, {})) assert_equal 'EstimatedTotal', REXML::XPath.first(total_type_xml, '//n2:TotalType').text @@ -583,48 +755,58 @@ def test_add_total_type_if_specified def test_structure_correct all_options_enabled = { - :allow_guest_checkout => true, - :max_amount => 50, - :locale => 'AU', - :page_style => 'test-gray', - :header_image => 'https://example.com/my_business', - :header_background_color => 'CAFE00', - :header_border_color => 'CAFE00', - :background_color => 'CAFE00', - :email => 'joe@example.com', - :billing_agreement => {:type => 'MerchantInitiatedBilling', :description => '9.99 per month for a year'}, - :allow_note => true, - :allow_buyer_optin => true, - :subtotal => 35, - :shipping => 10, - :handling => 0, - :tax => 5, - :total_type => 'EstimatedTotal', - :items => [{:name => 'item one', - :number => 'number 1', - :quantity => 3, - :amount => 35, - :description => 'one description', - :url => 'http://example.com/number_1'}], - :address => {:name => 'John Doe', - :address1 => 'Apartment 1', - :address2 => '1 Road St', - :city => 'First City', - :state => 'NSW', - :country => 'AU', - :zip => '2000', - :phone => '555 5555'}, - :callback_url => 'http://example.com/update_callback', - :callback_timeout => 2, - :callback_version => '53.0', - :funding_sources => {:source => 'BML'}, - :shipping_options => [{:default => true, - :name => 'first one', - :amount => 10}] + allow_guest_checkout: true, + max_amount: 50, + locale: 'AU', + page_style: 'test-gray', + header_image: 'https://example.com/my_business', + header_background_color: 'CAFE00', + header_border_color: 'CAFE00', + background_color: 'CAFE00', + email: 'joe@example.com', + billing_agreement: { type: 'MerchantInitiatedBilling', description: '9.99 per month for a year' }, + allow_note: true, + allow_buyer_optin: true, + subtotal: 35, + shipping: 10, + handling: 0, + tax: 5, + total_type: 'EstimatedTotal', + items: [ + { + name: 'item one', + number: 'number 1', + quantity: 3, + amount: 35, + description: 'one description', + url: 'http://example.com/number_1' + } + ], + address: { + name: 'John Doe', + address1: 'Apartment 1', + address2: '1 Road St', + city: 'First City', + state: 'NSW', + country: 'AU', + zip: '2000', + phone: '555 5555' + }, + callback_url: 'http://example.com/update_callback', + callback_timeout: 2, + callback_version: '53.0', + funding_sources: { source: 'BML' }, + shipping_options: [ + { + default: true, + name: 'first one', + amount: 10 + } + ] } doc = Nokogiri::XML(@gateway.send(:build_setup_request, 'Sale', 10, all_options_enabled)) - #Strip back to the SetExpressCheckoutRequestDetails element - this is where the base component xsd starts + # Strip back to the SetExpressCheckoutRequestDetails element - this is where the base component xsd starts xml = doc.xpath('//base:SetExpressCheckoutRequestDetails', 'base' => 'urn:ebay:apis:eBLBaseComponents').first sub_doc = Nokogiri::XML::Document.new sub_doc.root = xml @@ -633,485 +815,493 @@ 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 - <<-RESPONSE -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> - <SOAP-ENV:Header> - <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"> - </Security> - <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> - <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> - <Username xsi:type="xs:string"></Username> - <Password xsi:type="xs:string"></Password> - <Signature xsi:type="xs:string">OMGOMGOMG</Signature> - <Subject xsi:type="xs:string"></Subject> - </Credentials> - </RequesterCredentials> -</SOAP-ENV:Header> - <SOAP-ENV:Body id="_0"> - <CreateBillingAgreementResponse xmlns="urn:ebay:api:PayPalAPI"> - <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2013-02-28T16:34:47Z</Timestamp> - <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Success</Ack> - <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">8007ac99c51af</CorrelationID> - <Version xmlns="urn:ebay:apis:eBLBaseComponents">72</Version> - <Build xmlns="urn:ebay:apis:eBLBaseComponents">5331358</Build> - <BillingAgreementID xsi:type="xs:string">B-3R788221G4476823M</BillingAgreementID> - </CreateBillingAgreementResponse> - </SOAP-ENV:Body> -</SOAP-ENV:Envelope> + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> + <SOAP-ENV:Header> + <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"> + </Security> + <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> + <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> + <Username xsi:type="xs:string"></Username> + <Password xsi:type="xs:string"></Password> + <Signature xsi:type="xs:string">OMGOMGOMG</Signature> + <Subject xsi:type="xs:string"></Subject> + </Credentials> + </RequesterCredentials> + </SOAP-ENV:Header> + <SOAP-ENV:Body id="_0"> + <CreateBillingAgreementResponse xmlns="urn:ebay:api:PayPalAPI"> + <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2013-02-28T16:34:47Z</Timestamp> + <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Success</Ack> + <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">8007ac99c51af</CorrelationID> + <Version xmlns="urn:ebay:apis:eBLBaseComponents">72</Version> + <Build xmlns="urn:ebay:apis:eBLBaseComponents">5331358</Build> + <BillingAgreementID xsi:type="xs:string">B-3R788221G4476823M</BillingAgreementID> + </CreateBillingAgreementResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> RESPONSE end - def successful_authorize_reference_transaction_response - <<-RESPONSE -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> - <SOAP-ENV:Header> - <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"></Security> - <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> - <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> - <Username xsi:type="xs:string"></Username> - <Password xsi:type="xs:string"></Password> - <Signature xsi:type="xs:string">OMGOMGOMG</Signature> - <Subject xsi:type="xs:string"></Subject> - </Credentials> - </RequesterCredentials> - </SOAP-ENV:Header> - <SOAP-ENV:Body id="_0"> - <DoReferenceTransactionResponse xmlns="urn:ebay:api:PayPalAPI"> - <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2011-05-23T21:36:32Z</Timestamp> - <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Success</Ack> - <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">4d6d3af55369b</CorrelationID> - <Version xmlns="urn:ebay:apis:eBLBaseComponents">72</Version> - <Build xmlns="urn:ebay:apis:eBLBaseComponents">1863577</Build> - <DoReferenceTransactionResponseDetails xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:DoReferenceTransactionResponseDetailsType"> - <BillingAgreementID xsi:type="xs:string">B-3R788221G4476823M</BillingAgreementID> - <PaymentInfo xsi:type="ebl:PaymentInfoType"> - <TransactionID>9R43552341412482K</TransactionID> - <ParentTransactionID xsi:type="ebl:TransactionId"></ParentTransactionID> - <ReceiptID></ReceiptID> - <TransactionType xsi:type="ebl:PaymentTransactionCodeType">mercht-pmt</TransactionType> - <PaymentType xsi:type="ebl:PaymentCodeType">instant</PaymentType> - <PaymentDate xsi:type="xs:dateTime">2011-05-23T21:36:32Z</PaymentDate> - <GrossAmount xsi:type="cc:BasicAmountType" currencyID="USD">190.00</GrossAmount> - <FeeAmount xsi:type="cc:BasicAmountType" currencyID="USD">5.81</FeeAmount> - <TaxAmount xsi:type="cc:BasicAmountType" currencyID="USD">0.00</TaxAmount> - <ExchangeRate xsi:type="xs:string"></ExchangeRate> - <PaymentStatus xsi:type="ebl:PaymentStatusCodeType">Completed</PaymentStatus> - <PendingReason xsi:type="ebl:PendingStatusCodeType">none</PendingReason> - <ReasonCode xsi:type="ebl:ReversalReasonCodeType">none</ReasonCode> - <ProtectionEligibility xsi:type="xs:string">Ineligible</ProtectionEligibility> - <ProtectionEligibilityType xsi:type="xs:string">None</ProtectionEligibilityType> - </PaymentInfo> - </DoReferenceTransactionResponseDetails> - </DoReferenceTransactionResponse> - </SOAP-ENV:Body> - </SOAP-ENV:Envelope> + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> + <SOAP-ENV:Header> + <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"></Security> + <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> + <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> + <Username xsi:type="xs:string"></Username> + <Password xsi:type="xs:string"></Password> + <Signature xsi:type="xs:string">OMGOMGOMG</Signature> + <Subject xsi:type="xs:string"></Subject> + </Credentials> + </RequesterCredentials> + </SOAP-ENV:Header> + <SOAP-ENV:Body id="_0"> + <DoReferenceTransactionResponse xmlns="urn:ebay:api:PayPalAPI"> + <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2011-05-23T21:36:32Z</Timestamp> + <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Success</Ack> + <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">4d6d3af55369b</CorrelationID> + <Version xmlns="urn:ebay:apis:eBLBaseComponents">72</Version> + <Build xmlns="urn:ebay:apis:eBLBaseComponents">1863577</Build> + <DoReferenceTransactionResponseDetails xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:DoReferenceTransactionResponseDetailsType"> + <BillingAgreementID xsi:type="xs:string">B-3R788221G4476823M</BillingAgreementID> + <PaymentInfo xsi:type="ebl:PaymentInfoType"> + <TransactionID>9R43552341412482K</TransactionID> + <ParentTransactionID xsi:type="ebl:TransactionId"></ParentTransactionID> + <ReceiptID></ReceiptID> + <TransactionType xsi:type="ebl:PaymentTransactionCodeType">mercht-pmt</TransactionType> + <PaymentType xsi:type="ebl:PaymentCodeType">instant</PaymentType> + <PaymentDate xsi:type="xs:dateTime">2011-05-23T21:36:32Z</PaymentDate> + <GrossAmount xsi:type="cc:BasicAmountType" currencyID="USD">190.00</GrossAmount> + <FeeAmount xsi:type="cc:BasicAmountType" currencyID="USD">5.81</FeeAmount> + <TaxAmount xsi:type="cc:BasicAmountType" currencyID="USD">0.00</TaxAmount> + <ExchangeRate xsi:type="xs:string"></ExchangeRate> + <PaymentStatus xsi:type="ebl:PaymentStatusCodeType">Completed</PaymentStatus> + <PendingReason xsi:type="ebl:PendingStatusCodeType">none</PendingReason> + <ReasonCode xsi:type="ebl:ReversalReasonCodeType">none</ReasonCode> + <ProtectionEligibility xsi:type="xs:string">Ineligible</ProtectionEligibility> + <ProtectionEligibilityType xsi:type="xs:string">None</ProtectionEligibilityType> + </PaymentInfo> + </DoReferenceTransactionResponseDetails> + </DoReferenceTransactionResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> RESPONSE end def successful_reference_transaction_response - <<-RESPONSE -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> - <SOAP-ENV:Header> - <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"></Security> - <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> - <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> - <Username xsi:type="xs:string"></Username> - <Password xsi:type="xs:string"></Password> - <Signature xsi:type="xs:string">OMGOMGOMG</Signature> - <Subject xsi:type="xs:string"></Subject> - </Credentials> - </RequesterCredentials> - </SOAP-ENV:Header> - <SOAP-ENV:Body id="_0"> - <DoReferenceTransactionResponse xmlns="urn:ebay:api:PayPalAPI"> - <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2011-05-23T21:36:32Z</Timestamp> - <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Success</Ack> - <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">4d6d3af55369b</CorrelationID> - <Version xmlns="urn:ebay:apis:eBLBaseComponents">72</Version> - <Build xmlns="urn:ebay:apis:eBLBaseComponents">1863577</Build> - <DoReferenceTransactionResponseDetails xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:DoReferenceTransactionResponseDetailsType"> - <BillingAgreementID xsi:type="xs:string">B-3R788221G4476823M</BillingAgreementID> - <PaymentInfo xsi:type="ebl:PaymentInfoType"> - <TransactionID>9R43552341412482K</TransactionID> - <ParentTransactionID xsi:type="ebl:TransactionId"></ParentTransactionID> - <ReceiptID></ReceiptID> - <TransactionType xsi:type="ebl:PaymentTransactionCodeType">mercht-pmt</TransactionType> - <PaymentType xsi:type="ebl:PaymentCodeType">instant</PaymentType> - <PaymentDate xsi:type="xs:dateTime">2011-05-23T21:36:32Z</PaymentDate> - <GrossAmount xsi:type="cc:BasicAmountType" currencyID="USD">190.00</GrossAmount> - <FeeAmount xsi:type="cc:BasicAmountType" currencyID="USD">5.81</FeeAmount> - <TaxAmount xsi:type="cc:BasicAmountType" currencyID="USD">0.00</TaxAmount> - <ExchangeRate xsi:type="xs:string"></ExchangeRate> - <PaymentStatus xsi:type="ebl:PaymentStatusCodeType">Completed</PaymentStatus> - <PendingReason xsi:type="ebl:PendingStatusCodeType">none</PendingReason> - <ReasonCode xsi:type="ebl:ReversalReasonCodeType">none</ReasonCode> - <ProtectionEligibility xsi:type="xs:string">Ineligible</ProtectionEligibility> - <ProtectionEligibilityType xsi:type="xs:string">None</ProtectionEligibilityType> - </PaymentInfo> - </DoReferenceTransactionResponseDetails> - </DoReferenceTransactionResponse> - </SOAP-ENV:Body> - </SOAP-ENV:Envelope> + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> + <SOAP-ENV:Header> + <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"></Security> + <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> + <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> + <Username xsi:type="xs:string"></Username> + <Password xsi:type="xs:string"></Password> + <Signature xsi:type="xs:string">OMGOMGOMG</Signature> + <Subject xsi:type="xs:string"></Subject> + </Credentials> + </RequesterCredentials> + </SOAP-ENV:Header> + <SOAP-ENV:Body id="_0"> + <DoReferenceTransactionResponse xmlns="urn:ebay:api:PayPalAPI"> + <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2011-05-23T21:36:32Z</Timestamp> + <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Success</Ack> + <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">4d6d3af55369b</CorrelationID> + <Version xmlns="urn:ebay:apis:eBLBaseComponents">72</Version> + <Build xmlns="urn:ebay:apis:eBLBaseComponents">1863577</Build> + <DoReferenceTransactionResponseDetails xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:DoReferenceTransactionResponseDetailsType"> + <BillingAgreementID xsi:type="xs:string">B-3R788221G4476823M</BillingAgreementID> + <PaymentInfo xsi:type="ebl:PaymentInfoType"> + <TransactionID>9R43552341412482K</TransactionID> + <ParentTransactionID xsi:type="ebl:TransactionId"></ParentTransactionID> + <ReceiptID></ReceiptID> + <TransactionType xsi:type="ebl:PaymentTransactionCodeType">mercht-pmt</TransactionType> + <PaymentType xsi:type="ebl:PaymentCodeType">instant</PaymentType> + <PaymentDate xsi:type="xs:dateTime">2011-05-23T21:36:32Z</PaymentDate> + <GrossAmount xsi:type="cc:BasicAmountType" currencyID="USD">190.00</GrossAmount> + <FeeAmount xsi:type="cc:BasicAmountType" currencyID="USD">5.81</FeeAmount> + <TaxAmount xsi:type="cc:BasicAmountType" currencyID="USD">0.00</TaxAmount> + <ExchangeRate xsi:type="xs:string"></ExchangeRate> + <PaymentStatus xsi:type="ebl:PaymentStatusCodeType">Completed</PaymentStatus> + <PendingReason xsi:type="ebl:PendingStatusCodeType">none</PendingReason> + <ReasonCode xsi:type="ebl:ReversalReasonCodeType">none</ReasonCode> + <ProtectionEligibility xsi:type="xs:string">Ineligible</ProtectionEligibility> + <ProtectionEligibilityType xsi:type="xs:string">None</ProtectionEligibilityType> + </PaymentInfo> + </DoReferenceTransactionResponseDetails> + </DoReferenceTransactionResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> RESPONSE end - def successful_details_response - <<-RESPONSE -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> - <SOAP-ENV:Header> - <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"/> - <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> - <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> - <Username xsi:type="xs:string"/> - <Password xsi:type="xs:string"/> - <Signature xsi:type="xs:string" /> - <Subject xsi:type="xs:string"/> - </Credentials> - </RequesterCredentials> - </SOAP-ENV:Header> - <SOAP-ENV:Body id="_0"> - <GetExpressCheckoutDetailsResponse xmlns="urn:ebay:api:PayPalAPI"> - <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2011-03-01T20:19:35Z</Timestamp> - <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Success</Ack> - <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">84aff0e17b6f</CorrelationID> - <Version xmlns="urn:ebay:apis:eBLBaseComponents">62.0</Version> - <Build xmlns="urn:ebay:apis:eBLBaseComponents">1741654</Build> - <GetExpressCheckoutDetailsResponseDetails xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:GetExpressCheckoutDetailsResponseDetailsType"> - <Token xsi:type="ebl:ExpressCheckoutTokenType">EC-2XE90996XX9870316</Token> - <PayerInfo xsi:type="ebl:PayerInfoType"> - <Payer xsi:type="ebl:EmailAddressType">buyer@jadedpallet.com</Payer> - <PayerID xsi:type="ebl:UserIDType">FWRVKNRRZ3WUC</PayerID> - <PayerStatus xsi:type="ebl:PayPalUserStatusCodeType">verified</PayerStatus> - <PayerName xsi:type='ebl:PersonNameType'> - <Salutation xmlns='urn:ebay:apis:eBLBaseComponents'/> - <FirstName xmlns='urn:ebay:apis:eBLBaseComponents'>Fred</FirstName> - <MiddleName xmlns='urn:ebay:apis:eBLBaseComponents'/> - <LastName xmlns='urn:ebay:apis:eBLBaseComponents'>Brooks</LastName> - <Suffix xmlns='urn:ebay:apis:eBLBaseComponents'/> - </PayerName> - <PayerCountry xsi:type="ebl:CountryCodeType">US</PayerCountry> - <PayerBusiness xsi:type="xs:string"/> - <Address xsi:type="ebl:AddressType"> - <Name xsi:type="xs:string">Fred Brooks</Name> - <Street1 xsi:type="xs:string">1 Infinite Loop</Street1> - <Street2 xsi:type="xs:string"/> - <CityName xsi:type="xs:string">Cupertino</CityName> - <StateOrProvince xsi:type="xs:string">CA</StateOrProvince> - <Country xsi:type="ebl:CountryCodeType">US</Country> - <CountryName>United States</CountryName> - <PostalCode xsi:type="xs:string">95014</PostalCode> - <AddressOwner xsi:type="ebl:AddressOwnerCodeType">PayPal</AddressOwner> - <AddressStatus xsi:type="ebl:AddressStatusCodeType">Confirmed</AddressStatus> - </Address> - </PayerInfo> - <InvoiceID xsi:type="xs:string">1230123</InvoiceID> - <ContactPhone>416-618-9984</ContactPhone> - <PaymentDetails xsi:type="ebl:PaymentDetailsType"> - <OrderTotal xsi:type="cc:BasicAmountType" currencyID="USD">19.00</OrderTotal> - <ItemTotal xsi:type="cc:BasicAmountType" currencyID="USD">19.00</ItemTotal> - <ShippingTotal xsi:type="cc:BasicAmountType" currencyID="USD">0.00</ShippingTotal> - <HandlingTotal xsi:type="cc:BasicAmountType" currencyID="USD">0.00</HandlingTotal> - <TaxTotal xsi:type="cc:BasicAmountType" currencyID="USD">0.00</TaxTotal> - <ShipToAddress xsi:type="ebl:AddressType"> - <Name xsi:type="xs:string">Fred Brooks</Name> - <Street1 xsi:type="xs:string">1234 Penny Lane</Street1> - <Street2 xsi:type="xs:string"/> - <CityName xsi:type="xs:string">Jonsetown</CityName> - <StateOrProvince xsi:type="xs:string">NC</StateOrProvince> - <Country xsi:type="ebl:CountryCodeType">US</Country> - <CountryName>United States</CountryName> - <Phone xsi:type="xs:string">123-456-7890</Phone> - <PostalCode xsi:type="xs:string">23456</PostalCode> - <AddressID xsi:type="xs:string"/> - <AddressOwner xsi:type="ebl:AddressOwnerCodeType">PayPal</AddressOwner> - <ExternalAddressID xsi:type="xs:string"/> - <AddressStatus xsi:type="ebl:AddressStatusCodeType">Confirmed</AddressStatus> - </ShipToAddress> - <PaymentDetailsItem xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:PaymentDetailsItemType"> - <Name xsi:type="xs:string">Shopify T-Shirt</Name> - <Quantity>1</Quantity> - <Tax xsi:type="cc:BasicAmountType" currencyID="USD">0.00</Tax> - <Amount xsi:type="cc:BasicAmountType" currencyID="USD">19.00</Amount> - <EbayItemPaymentDetailsItem xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:EbayItemPaymentDetailsItemType"/> - </PaymentDetailsItem> - <InsuranceTotal xsi:type="cc:BasicAmountType" currencyID="USD">0.00</InsuranceTotal> - <ShippingDiscount xsi:type="cc:BasicAmountType" currencyID="USD">0.00</ShippingDiscount> - <InsuranceOptionOffered xsi:type="xs:string">false</InsuranceOptionOffered> - <NoteText xsi:type="xs:string">This is a test note</NoteText> - <SellerDetails xsi:type="ebl:SellerDetailsType"/> - <PaymentRequestID xsi:type="xs:string"/> - <OrderURL xsi:type="xs:string"/> - <SoftDescriptor xsi:type="xs:string"/> - </PaymentDetails> - <UserSelectedOptions xsi:type=\"ebl:UserSelectedOptionType\"> - <ShippingCalculationMode xsi:type=\"xs:string\">Callback</ShippingCalculationMode> - <InsuranceOptionSelected xsi:type=\"xs:string\">false</InsuranceOptionSelected> - <ShippingOptionIsDefault xsi:type=\"xs:string\">true</ShippingOptionIsDefault> - <ShippingOptionAmount xsi:type=\"cc:BasicAmountType\" currencyID=\"USD\">2.95</ShippingOptionAmount> - <ShippingOptionName xsi:type=\"xs:string\">default</ShippingOptionName> - </UserSelectedOptions> - <CheckoutStatus xsi:type="xs:string">PaymentActionNotInitiated</CheckoutStatus> - <PaymentRequestInfo xsi:type="ebl:PaymentRequestInfoType" /> - </GetExpressCheckoutDetailsResponseDetails> - </GetExpressCheckoutDetailsResponse> - </SOAP-ENV:Body> -</SOAP-ENV:Envelope> + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> + <SOAP-ENV:Header> + <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"/> + <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> + <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> + <Username xsi:type="xs:string"/> + <Password xsi:type="xs:string"/> + <Signature xsi:type="xs:string" /> + <Subject xsi:type="xs:string"/> + </Credentials> + </RequesterCredentials> + </SOAP-ENV:Header> + <SOAP-ENV:Body id="_0"> + <GetExpressCheckoutDetailsResponse xmlns="urn:ebay:api:PayPalAPI"> + <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2011-03-01T20:19:35Z</Timestamp> + <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Success</Ack> + <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">84aff0e17b6f</CorrelationID> + <Version xmlns="urn:ebay:apis:eBLBaseComponents">62.0</Version> + <Build xmlns="urn:ebay:apis:eBLBaseComponents">1741654</Build> + <GetExpressCheckoutDetailsResponseDetails xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:GetExpressCheckoutDetailsResponseDetailsType"> + <Token xsi:type="ebl:ExpressCheckoutTokenType">EC-2XE90996XX9870316</Token> + <PayerInfo xsi:type="ebl:PayerInfoType"> + <Payer xsi:type="ebl:EmailAddressType">buyer@jadedpallet.com</Payer> + <PayerID xsi:type="ebl:UserIDType">FWRVKNRRZ3WUC</PayerID> + <PayerStatus xsi:type="ebl:PayPalUserStatusCodeType">verified</PayerStatus> + <PayerName xsi:type='ebl:PersonNameType'> + <Salutation xmlns='urn:ebay:apis:eBLBaseComponents'/> + <FirstName xmlns='urn:ebay:apis:eBLBaseComponents'>Fred</FirstName> + <MiddleName xmlns='urn:ebay:apis:eBLBaseComponents'/> + <LastName xmlns='urn:ebay:apis:eBLBaseComponents'>Brooks</LastName> + <Suffix xmlns='urn:ebay:apis:eBLBaseComponents'/> + </PayerName> + <PayerCountry xsi:type="ebl:CountryCodeType">US</PayerCountry> + <PayerBusiness xsi:type="xs:string"/> + <Address xsi:type="ebl:AddressType"> + <Name xsi:type="xs:string">Fred Brooks</Name> + <Street1 xsi:type="xs:string">1 Infinite Loop</Street1> + <Street2 xsi:type="xs:string"/> + <CityName xsi:type="xs:string">Cupertino</CityName> + <StateOrProvince xsi:type="xs:string">CA</StateOrProvince> + <Country xsi:type="ebl:CountryCodeType">US</Country> + <CountryName>United States</CountryName> + <PostalCode xsi:type="xs:string">95014</PostalCode> + <AddressOwner xsi:type="ebl:AddressOwnerCodeType">PayPal</AddressOwner> + <AddressStatus xsi:type="ebl:AddressStatusCodeType">Confirmed</AddressStatus> + </Address> + </PayerInfo> + <InvoiceID xsi:type="xs:string">1230123</InvoiceID> + <ContactPhone>416-618-9984</ContactPhone> + <PaymentDetails xsi:type="ebl:PaymentDetailsType"> + <OrderTotal xsi:type="cc:BasicAmountType" currencyID="USD">19.00</OrderTotal> + <ItemTotal xsi:type="cc:BasicAmountType" currencyID="USD">19.00</ItemTotal> + <ShippingTotal xsi:type="cc:BasicAmountType" currencyID="USD">0.00</ShippingTotal> + <HandlingTotal xsi:type="cc:BasicAmountType" currencyID="USD">0.00</HandlingTotal> + <TaxTotal xsi:type="cc:BasicAmountType" currencyID="USD">0.00</TaxTotal> + <ShipToAddress xsi:type="ebl:AddressType"> + <Name xsi:type="xs:string">Fred Brooks</Name> + <Street1 xsi:type="xs:string">1234 Penny Lane</Street1> + <Street2 xsi:type="xs:string"/> + <CityName xsi:type="xs:string">Jonsetown</CityName> + <StateOrProvince xsi:type="xs:string">NC</StateOrProvince> + <Country xsi:type="ebl:CountryCodeType">US</Country> + <CountryName>United States</CountryName> + <Phone xsi:type="xs:string">123-456-7890</Phone> + <PostalCode xsi:type="xs:string">23456</PostalCode> + <AddressID xsi:type="xs:string"/> + <AddressOwner xsi:type="ebl:AddressOwnerCodeType">PayPal</AddressOwner> + <ExternalAddressID xsi:type="xs:string"/> + <AddressStatus xsi:type="ebl:AddressStatusCodeType">Confirmed</AddressStatus> + </ShipToAddress> + <PaymentDetailsItem xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:PaymentDetailsItemType"> + <Name xsi:type="xs:string">Shopify T-Shirt</Name> + <Quantity>1</Quantity> + <Tax xsi:type="cc:BasicAmountType" currencyID="USD">0.00</Tax> + <Amount xsi:type="cc:BasicAmountType" currencyID="USD">19.00</Amount> + <EbayItemPaymentDetailsItem xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:EbayItemPaymentDetailsItemType"/> + </PaymentDetailsItem> + <InsuranceTotal xsi:type="cc:BasicAmountType" currencyID="USD">0.00</InsuranceTotal> + <ShippingDiscount xsi:type="cc:BasicAmountType" currencyID="USD">0.00</ShippingDiscount> + <InsuranceOptionOffered xsi:type="xs:string">false</InsuranceOptionOffered> + <NoteText xsi:type="xs:string">This is a test note</NoteText> + <SellerDetails xsi:type="ebl:SellerDetailsType"/> + <PaymentRequestID xsi:type="xs:string"/> + <OrderURL xsi:type="xs:string"/> + <SoftDescriptor xsi:type="xs:string"/> + </PaymentDetails> + <UserSelectedOptions xsi:type=\"ebl:UserSelectedOptionType\"> + <ShippingCalculationMode xsi:type=\"xs:string\">Callback</ShippingCalculationMode> + <InsuranceOptionSelected xsi:type=\"xs:string\">false</InsuranceOptionSelected> + <ShippingOptionIsDefault xsi:type=\"xs:string\">true</ShippingOptionIsDefault> + <ShippingOptionAmount xsi:type=\"cc:BasicAmountType\" currencyID=\"USD\">2.95</ShippingOptionAmount> + <ShippingOptionName xsi:type=\"xs:string\">default</ShippingOptionName> + </UserSelectedOptions> + <CheckoutStatus xsi:type="xs:string">PaymentActionNotInitiated</CheckoutStatus> + <PaymentRequestInfo xsi:type="ebl:PaymentRequestInfoType" /> + </GetExpressCheckoutDetailsResponseDetails> + </GetExpressCheckoutDetailsResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> RESPONSE end def successful_authorization_response - <<-RESPONSE -<?xml version='1.0' encoding='UTF-8'?> -<SOAP-ENV:Envelope xmlns:cc='urn:ebay:apis:CoreComponentTypes' xmlns:sizeship='urn:ebay:api:PayPalAPI/sizeship.xsd' xmlns:SOAP-ENV='http://schemas.xmlsoap.org/soap/envelope/' xmlns:SOAP-ENC='http://schemas.xmlsoap.org/soap/encoding/' xmlns:saml='urn:oasis:names:tc:SAML:1.0:assertion' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:wsu='http://schemas.xmlsoap.org/ws/2002/07/utility' xmlns:ebl='urn:ebay:apis:eBLBaseComponents' xmlns:ds='http://www.w3.org/2000/09/xmldsig#' xmlns:xs='http://www.w3.org/2001/XMLSchema' xmlns:ns='urn:ebay:api:PayPalAPI' xmlns:market='urn:ebay:apis:Market' xmlns:ship='urn:ebay:apis:ship' xmlns:auction='urn:ebay:apis:Auction' xmlns:wsse='http://schemas.xmlsoap.org/ws/2002/12/secext' xmlns:xsd='http://www.w3.org/2001/XMLSchema'> - <SOAP-ENV:Header> - <Security xsi:type='wsse:SecurityType' xmlns='http://schemas.xmlsoap.org/ws/2002/12/secext'/> - <RequesterCredentials xsi:type='ebl:CustomSecurityHeaderType' xmlns='urn:ebay:api:PayPalAPI'> - <Credentials xsi:type='ebl:UserIdPasswordType' xmlns='urn:ebay:apis:eBLBaseComponents'> - <Username xsi:type='xs:string'/> - <Password xsi:type='xs:string'/> - <Subject xsi:type='xs:string'/> - </Credentials> - </RequesterCredentials> - </SOAP-ENV:Header> - <SOAP-ENV:Body id='_0'> - <DoExpressCheckoutPaymentResponse xmlns='urn:ebay:api:PayPalAPI'> - <Timestamp xmlns='urn:ebay:apis:eBLBaseComponents'>2007-02-13T00:18:50Z</Timestamp> - <Ack xmlns='urn:ebay:apis:eBLBaseComponents'>Success</Ack> - <CorrelationID xmlns='urn:ebay:apis:eBLBaseComponents'>62450a4266d04</CorrelationID> - <Version xmlns='urn:ebay:apis:eBLBaseComponents'>2.000000</Version> - <Build xmlns='urn:ebay:apis:eBLBaseComponents'>1.0006</Build> - <DoExpressCheckoutPaymentResponseDetails xsi:type='ebl:DoExpressCheckoutPaymentResponseDetailsType' xmlns='urn:ebay:apis:eBLBaseComponents'> - <Token xsi:type='ebl:ExpressCheckoutTokenType'>EC-6WS104951Y388951L</Token> - <PaymentInfo xsi:type='ebl:PaymentInfoType'> - <TransactionID>8B266858CH956410C</TransactionID> - <ParentTransactionID xsi:type='ebl:TransactionId'/> - <ReceiptID/> - <TransactionType xsi:type='ebl:PaymentTransactionCodeType'>express-checkout</TransactionType> - <PaymentType xsi:type='ebl:PaymentCodeType'>instant</PaymentType> - <PaymentDate xsi:type='xs:dateTime'>2007-02-13T00:18:48Z</PaymentDate> - <GrossAmount currencyID='USD' xsi:type='cc:BasicAmountType'>3.00</GrossAmount> - <TaxAmount currencyID='USD' xsi:type='cc:BasicAmountType'>0.00</TaxAmount> - <ExchangeRate xsi:type='xs:string'/> - <PaymentStatus xsi:type='ebl:PaymentStatusCodeType'>Pending</PaymentStatus> - <PendingReason xsi:type='ebl:PendingStatusCodeType'>authorization</PendingReason> - <ReasonCode xsi:type='ebl:ReversalReasonCodeType'>none</ReasonCode> - </PaymentInfo> - </DoExpressCheckoutPaymentResponseDetails> - </DoExpressCheckoutPaymentResponse> - </SOAP-ENV:Body> -</SOAP-ENV:Envelope> + <<~RESPONSE + <?xml version='1.0' encoding='UTF-8'?> + <SOAP-ENV:Envelope xmlns:cc='urn:ebay:apis:CoreComponentTypes' xmlns:sizeship='urn:ebay:api:PayPalAPI/sizeship.xsd' xmlns:SOAP-ENV='http://schemas.xmlsoap.org/soap/envelope/' xmlns:SOAP-ENC='http://schemas.xmlsoap.org/soap/encoding/' xmlns:saml='urn:oasis:names:tc:SAML:1.0:assertion' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:wsu='http://schemas.xmlsoap.org/ws/2002/07/utility' xmlns:ebl='urn:ebay:apis:eBLBaseComponents' xmlns:ds='http://www.w3.org/2000/09/xmldsig#' xmlns:xs='http://www.w3.org/2001/XMLSchema' xmlns:ns='urn:ebay:api:PayPalAPI' xmlns:market='urn:ebay:apis:Market' xmlns:ship='urn:ebay:apis:ship' xmlns:auction='urn:ebay:apis:Auction' xmlns:wsse='http://schemas.xmlsoap.org/ws/2002/12/secext' xmlns:xsd='http://www.w3.org/2001/XMLSchema'> + <SOAP-ENV:Header> + <Security xsi:type='wsse:SecurityType' xmlns='http://schemas.xmlsoap.org/ws/2002/12/secext'/> + <RequesterCredentials xsi:type='ebl:CustomSecurityHeaderType' xmlns='urn:ebay:api:PayPalAPI'> + <Credentials xsi:type='ebl:UserIdPasswordType' xmlns='urn:ebay:apis:eBLBaseComponents'> + <Username xsi:type='xs:string'/> + <Password xsi:type='xs:string'/> + <Subject xsi:type='xs:string'/> + </Credentials> + </RequesterCredentials> + </SOAP-ENV:Header> + <SOAP-ENV:Body id='_0'> + <DoExpressCheckoutPaymentResponse xmlns='urn:ebay:api:PayPalAPI'> + <Timestamp xmlns='urn:ebay:apis:eBLBaseComponents'>2007-02-13T00:18:50Z</Timestamp> + <Ack xmlns='urn:ebay:apis:eBLBaseComponents'>Success</Ack> + <CorrelationID xmlns='urn:ebay:apis:eBLBaseComponents'>62450a4266d04</CorrelationID> + <Version xmlns='urn:ebay:apis:eBLBaseComponents'>2.000000</Version> + <Build xmlns='urn:ebay:apis:eBLBaseComponents'>1.0006</Build> + <DoExpressCheckoutPaymentResponseDetails xsi:type='ebl:DoExpressCheckoutPaymentResponseDetailsType' xmlns='urn:ebay:apis:eBLBaseComponents'> + <Token xsi:type='ebl:ExpressCheckoutTokenType'>EC-6WS104951Y388951L</Token> + <PaymentInfo xsi:type='ebl:PaymentInfoType'> + <TransactionID>8B266858CH956410C</TransactionID> + <ParentTransactionID xsi:type='ebl:TransactionId'/> + <ReceiptID/> + <TransactionType xsi:type='ebl:PaymentTransactionCodeType'>express-checkout</TransactionType> + <PaymentType xsi:type='ebl:PaymentCodeType'>instant</PaymentType> + <PaymentDate xsi:type='xs:dateTime'>2007-02-13T00:18:48Z</PaymentDate> + <GrossAmount currencyID='USD' xsi:type='cc:BasicAmountType'>3.00</GrossAmount> + <TaxAmount currencyID='USD' xsi:type='cc:BasicAmountType'>0.00</TaxAmount> + <ExchangeRate xsi:type='xs:string'/> + <PaymentStatus xsi:type='ebl:PaymentStatusCodeType'>Pending</PaymentStatus> + <PendingReason xsi:type='ebl:PendingStatusCodeType'>authorization</PendingReason> + <ReasonCode xsi:type='ebl:ReversalReasonCodeType'>none</ReasonCode> + </PaymentInfo> + </DoExpressCheckoutPaymentResponseDetails> + </DoExpressCheckoutPaymentResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> RESPONSE end def response_with_error - <<-RESPONSE -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:market="urn:ebay:apis:Market" xmlns:auction="urn:ebay:apis:Auction" xmlns:sizeship="urn:ebay:api:PayPalAPI/sizeship.xsd" xmlns:ship="urn:ebay:apis:ship" xmlns:skype="urn:ebay:apis:skype" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> - <SOAP-ENV:Header> - <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"/> - <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> - <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> - <Username xsi:type="xs:string"/> - <Password xsi:type="xs:string"/> - <Subject xsi:type="xs:string"/> - </Credentials> - </RequesterCredentials> - </SOAP-ENV:Header> - <SOAP-ENV:Body id="_0"> - <SetExpressCheckoutResponse xmlns="urn:ebay:api:PayPalAPI"> - <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2008-04-02T17:38:02Z</Timestamp> - <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Failure</Ack> - <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">cdb720feada30</CorrelationID> - <Errors xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:ErrorType"> - <ShortMessage xsi:type="xs:string">Shipping Address Invalid City State Postal Code</ShortMessage> - <LongMessage xsi:type="xs:string">A match of the Shipping Address City, State, and Postal Code failed.</LongMessage> - <ErrorCode xsi:type="xs:token">10736</ErrorCode> - <SeverityCode xmlns="urn:ebay:apis:eBLBaseComponents">Error</SeverityCode> - </Errors> - <Version xmlns="urn:ebay:apis:eBLBaseComponents">2.000000</Version> - <Build xmlns="urn:ebay:apis:eBLBaseComponents">543066</Build> - </SetExpressCheckoutResponse> - </SOAP-ENV:Body> -</SOAP-ENV:Envelope> + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:market="urn:ebay:apis:Market" xmlns:auction="urn:ebay:apis:Auction" xmlns:sizeship="urn:ebay:api:PayPalAPI/sizeship.xsd" xmlns:ship="urn:ebay:apis:ship" xmlns:skype="urn:ebay:apis:skype" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> + <SOAP-ENV:Header> + <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"/> + <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> + <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> + <Username xsi:type="xs:string"/> + <Password xsi:type="xs:string"/> + <Subject xsi:type="xs:string"/> + </Credentials> + </RequesterCredentials> + </SOAP-ENV:Header> + <SOAP-ENV:Body id="_0"> + <SetExpressCheckoutResponse xmlns="urn:ebay:api:PayPalAPI"> + <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2008-04-02T17:38:02Z</Timestamp> + <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Failure</Ack> + <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">cdb720feada30</CorrelationID> + <Errors xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:ErrorType"> + <ShortMessage xsi:type="xs:string">Shipping Address Invalid City State Postal Code</ShortMessage> + <LongMessage xsi:type="xs:string">A match of the Shipping Address City, State, and Postal Code failed.</LongMessage> + <ErrorCode xsi:type="xs:token">10736</ErrorCode> + <SeverityCode xmlns="urn:ebay:apis:eBLBaseComponents">Error</SeverityCode> + </Errors> + <Version xmlns="urn:ebay:apis:eBLBaseComponents">2.000000</Version> + <Build xmlns="urn:ebay:apis:eBLBaseComponents">543066</Build> + </SetExpressCheckoutResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> RESPONSE end - def response_with_errors - <<-RESPONSE - <?xml version="1.0" encoding="UTF-8"?> - <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:market="urn:ebay:apis:Market" xmlns:auction="urn:ebay:apis:Auction" xmlns:sizeship="urn:ebay:api:PayPalAPI/sizeship.xsd" xmlns:ship="urn:ebay:apis:ship" xmlns:skype="urn:ebay:apis:skype" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> - <SOAP-ENV:Header> - <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"/> - <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> - <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> - <Username xsi:type="xs:string"/> - <Password xsi:type="xs:string"/> - <Subject xsi:type="xs:string"/> - </Credentials> - </RequesterCredentials> - </SOAP-ENV:Header> - <SOAP-ENV:Body id="_0"> - <SetExpressCheckoutResponse xmlns="urn:ebay:api:PayPalAPI"> - <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2008-04-02T17:38:02Z</Timestamp> - <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Failure</Ack> - <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">cdb720feada30</CorrelationID> - <Errors xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:ErrorType"> - <ShortMessage xsi:type="xs:string">Shipping Address Invalid City State Postal Code</ShortMessage> - <LongMessage xsi:type="xs:string">A match of the Shipping Address City, State, and Postal Code failed.</LongMessage> - <ErrorCode xsi:type="xs:token">10736</ErrorCode> - <SeverityCode xmlns="urn:ebay:apis:eBLBaseComponents">Error</SeverityCode> - </Errors> - <Errors xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:ErrorType"> - <ShortMessage xsi:type="xs:string">Authentication/Authorization Failed</ShortMessage> - <LongMessage xsi:type="xs:string">You do not have permissions to make this API call</LongMessage> - <ErrorCode xsi:type="xs:token">10002</ErrorCode> - <SeverityCode xmlns="urn:ebay:apis:eBLBaseComponents">Error</SeverityCode> - </Errors> - <Version xmlns="urn:ebay:apis:eBLBaseComponents">2.000000</Version> - <Build xmlns="urn:ebay:apis:eBLBaseComponents">543066</Build> - </SetExpressCheckoutResponse> - </SOAP-ENV:Body> - </SOAP-ENV:Envelope> - RESPONSE - end + def response_with_errors + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:market="urn:ebay:apis:Market" xmlns:auction="urn:ebay:apis:Auction" xmlns:sizeship="urn:ebay:api:PayPalAPI/sizeship.xsd" xmlns:ship="urn:ebay:apis:ship" xmlns:skype="urn:ebay:apis:skype" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> + <SOAP-ENV:Header> + <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"/> + <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> + <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> + <Username xsi:type="xs:string"/> + <Password xsi:type="xs:string"/> + <Subject xsi:type="xs:string"/> + </Credentials> + </RequesterCredentials> + </SOAP-ENV:Header> + <SOAP-ENV:Body id="_0"> + <SetExpressCheckoutResponse xmlns="urn:ebay:api:PayPalAPI"> + <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2008-04-02T17:38:02Z</Timestamp> + <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Failure</Ack> + <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">cdb720feada30</CorrelationID> + <Errors xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:ErrorType"> + <ShortMessage xsi:type="xs:string">Shipping Address Invalid City State Postal Code</ShortMessage> + <LongMessage xsi:type="xs:string">A match of the Shipping Address City, State, and Postal Code failed.</LongMessage> + <ErrorCode xsi:type="xs:token">10736</ErrorCode> + <SeverityCode xmlns="urn:ebay:apis:eBLBaseComponents">Error</SeverityCode> + </Errors> + <Errors xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:ErrorType"> + <ShortMessage xsi:type="xs:string">Authentication/Authorization Failed</ShortMessage> + <LongMessage xsi:type="xs:string">You do not have permissions to make this API call</LongMessage> + <ErrorCode xsi:type="xs:token">10002</ErrorCode> + <SeverityCode xmlns="urn:ebay:apis:eBLBaseComponents">Error</SeverityCode> + </Errors> + <Version xmlns="urn:ebay:apis:eBLBaseComponents">2.000000</Version> + <Build xmlns="urn:ebay:apis:eBLBaseComponents">543066</Build> + </SetExpressCheckoutResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> + RESPONSE + end - def response_with_duplicate_errors - <<-RESPONSE - <?xml version="1.0" encoding="UTF-8"?> - <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:market="urn:ebay:apis:Market" xmlns:auction="urn:ebay:apis:Auction" xmlns:sizeship="urn:ebay:api:PayPalAPI/sizeship.xsd" xmlns:ship="urn:ebay:apis:ship" xmlns:skype="urn:ebay:apis:skype" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> - <SOAP-ENV:Header> - <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"/> - <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> - <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> - <Username xsi:type="xs:string"/> - <Password xsi:type="xs:string"/> - <Subject xsi:type="xs:string"/> - </Credentials> - </RequesterCredentials> - </SOAP-ENV:Header> - <SOAP-ENV:Body id="_0"> - <SetExpressCheckoutResponse xmlns="urn:ebay:api:PayPalAPI"> - <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2008-04-02T17:38:02Z</Timestamp> - <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Failure</Ack> - <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">cdb720feada30</CorrelationID> - <Errors xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:ErrorType"> - <ShortMessage xsi:type="xs:string">Shipping Address Invalid City State Postal Code</ShortMessage> - <LongMessage xsi:type="xs:string">A match of the Shipping Address City, State, and Postal Code failed.</LongMessage> - <ErrorCode xsi:type="xs:token">10736</ErrorCode> - <SeverityCode xmlns="urn:ebay:apis:eBLBaseComponents">Error</SeverityCode> - </Errors> - <Errors xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:ErrorType"> - <ShortMessage xsi:type="xs:string">Shipping Address Invalid City State Postal Code</ShortMessage> - <LongMessage xsi:type="xs:string">A match of the Shipping Address City, State, and Postal Code failed.</LongMessage> - <ErrorCode xsi:type="xs:token">10736</ErrorCode> - <SeverityCode xmlns="urn:ebay:apis:eBLBaseComponents">Error</SeverityCode> - </Errors> - <Version xmlns="urn:ebay:apis:eBLBaseComponents">2.000000</Version> - <Build xmlns="urn:ebay:apis:eBLBaseComponents">543066</Build> - </SetExpressCheckoutResponse> - </SOAP-ENV:Body> - </SOAP-ENV:Envelope> - RESPONSE - end + def response_with_duplicate_errors + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:market="urn:ebay:apis:Market" xmlns:auction="urn:ebay:apis:Auction" xmlns:sizeship="urn:ebay:api:PayPalAPI/sizeship.xsd" xmlns:ship="urn:ebay:apis:ship" xmlns:skype="urn:ebay:apis:skype" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> + <SOAP-ENV:Header> + <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"/> + <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> + <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> + <Username xsi:type="xs:string"/> + <Password xsi:type="xs:string"/> + <Subject xsi:type="xs:string"/> + </Credentials> + </RequesterCredentials> + </SOAP-ENV:Header> + <SOAP-ENV:Body id="_0"> + <SetExpressCheckoutResponse xmlns="urn:ebay:api:PayPalAPI"> + <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2008-04-02T17:38:02Z</Timestamp> + <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Failure</Ack> + <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">cdb720feada30</CorrelationID> + <Errors xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:ErrorType"> + <ShortMessage xsi:type="xs:string">Shipping Address Invalid City State Postal Code</ShortMessage> + <LongMessage xsi:type="xs:string">A match of the Shipping Address City, State, and Postal Code failed.</LongMessage> + <ErrorCode xsi:type="xs:token">10736</ErrorCode> + <SeverityCode xmlns="urn:ebay:apis:eBLBaseComponents">Error</SeverityCode> + </Errors> + <Errors xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:ErrorType"> + <ShortMessage xsi:type="xs:string">Shipping Address Invalid City State Postal Code</ShortMessage> + <LongMessage xsi:type="xs:string">A match of the Shipping Address City, State, and Postal Code failed.</LongMessage> + <ErrorCode xsi:type="xs:token">10736</ErrorCode> + <SeverityCode xmlns="urn:ebay:apis:eBLBaseComponents">Error</SeverityCode> + </Errors> + <Version xmlns="urn:ebay:apis:eBLBaseComponents">2.000000</Version> + <Build xmlns="urn:ebay:apis:eBLBaseComponents">543066</Build> + </SetExpressCheckoutResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> + RESPONSE + end - def successful_cancel_billing_agreement_response - <<-RESPONSE - <?xml version="1.0" encoding="UTF-8"?><SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" - xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" - xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" - xmlns:ns="urn:ebay:api:PayPalAPI"><SOAP-ENV:Header><Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"></Security><RequesterCredentials - xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"><Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"><Username - xsi:type="xs:string"></Username><Password xsi:type="xs:string"></Password><Signature xsi:type="xs:string"></Signature><Subject - xsi:type="xs:string"></Subject></Credentials></RequesterCredentials></SOAP-ENV:Header><SOAP-ENV:Body id="_0"><BAUpdateResponse xmlns="urn:ebay:api:PayPalAPI"><Timestamp - xmlns="urn:ebay:apis:eBLBaseComponents">2013-06-04T16:24:31Z</Timestamp><Ack xmlns="urn:ebay:apis:eBLBaseComponents">Success</Ack><CorrelationID - xmlns="urn:ebay:apis:eBLBaseComponents">aecbb96bd4658</CorrelationID><Version xmlns="urn:ebay:apis:eBLBaseComponents">72</Version><Build - xmlns="urn:ebay:apis:eBLBaseComponents">6118442</Build><BAUpdateResponseDetails xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:BAUpdateResponseDetailsType"><BillingAgreementID - xsi:type="xs:string">B-3RU433629T663020S</BillingAgreementID><BillingAgreementDescription xsi:type="xs:string">Wow. Amazing.</BillingAgreementDescription><BillingAgreementStatus - xsi:type="ebl:MerchantPullStatusCodeType">Canceled</BillingAgreementStatus><PayerInfo xsi:type="ebl:PayerInfoType"><Payer xsi:type="ebl:EmailAddressType">duff@example.com</Payer><PayerID - xsi:type="ebl:UserIDType">VZNKJ2ZWMYK2E</PayerID><PayerStatus xsi:type="ebl:PayPalUserStatusCodeType">verified</PayerStatus><PayerName xsi:type="ebl:PersonNameType"><Salutation - xmlns="urn:ebay:apis:eBLBaseComponents"></Salutation><FirstName xmlns="urn:ebay:apis:eBLBaseComponents">Duff</FirstName><MiddleName - xmlns="urn:ebay:apis:eBLBaseComponents"></MiddleName><LastName xmlns="urn:ebay:apis:eBLBaseComponents">Jones</LastName><Suffix - xmlns="urn:ebay:apis:eBLBaseComponents"></Suffix></PayerName><PayerCountry xsi:type="ebl:CountryCodeType">US</PayerCountry><PayerBusiness xsi:type="xs:string"></PayerBusiness><Address - xsi:type="ebl:AddressType"><Name xsi:type="xs:string"></Name><Street1 xsi:type="xs:string"></Street1><Street2 xsi:type="xs:string"></Street2><CityName - xsi:type="xs:string"></CityName><StateOrProvince xsi:type="xs:string"></StateOrProvince><CountryName></CountryName><Phone xsi:type="xs:string"></Phone><PostalCode - xsi:type="xs:string"></PostalCode><AddressID xsi:type="xs:string"></AddressID><AddressOwner xsi:type="ebl:AddressOwnerCodeType">PayPal</AddressOwner><ExternalAddressID - xsi:type="xs:string"></ExternalAddressID><AddressStatus - xsi:type="ebl:AddressStatusCodeType">None</AddressStatus></Address></PayerInfo></BAUpdateResponseDetails></BAUpdateResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> - RESPONSE - end + def successful_cancel_billing_agreement_response + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?><SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" + xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" + xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" + xmlns:ns="urn:ebay:api:PayPalAPI"><SOAP-ENV:Header><Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"></Security><RequesterCredentials + xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"><Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"><Username + xsi:type="xs:string"></Username><Password xsi:type="xs:string"></Password><Signature xsi:type="xs:string"></Signature><Subject + xsi:type="xs:string"></Subject></Credentials></RequesterCredentials></SOAP-ENV:Header><SOAP-ENV:Body id="_0"><BAUpdateResponse xmlns="urn:ebay:api:PayPalAPI"><Timestamp + xmlns="urn:ebay:apis:eBLBaseComponents">2013-06-04T16:24:31Z</Timestamp><Ack xmlns="urn:ebay:apis:eBLBaseComponents">Success</Ack><CorrelationID + xmlns="urn:ebay:apis:eBLBaseComponents">aecbb96bd4658</CorrelationID><Version xmlns="urn:ebay:apis:eBLBaseComponents">72</Version><Build + xmlns="urn:ebay:apis:eBLBaseComponents">6118442</Build><BAUpdateResponseDetails xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:BAUpdateResponseDetailsType"><BillingAgreementID + xsi:type="xs:string">B-3RU433629T663020S</BillingAgreementID><BillingAgreementDescription xsi:type="xs:string">Wow. Amazing.</BillingAgreementDescription><BillingAgreementStatus + xsi:type="ebl:MerchantPullStatusCodeType">Canceled</BillingAgreementStatus><PayerInfo xsi:type="ebl:PayerInfoType"><Payer xsi:type="ebl:EmailAddressType">duff@example.com</Payer><PayerID + xsi:type="ebl:UserIDType">VZNKJ2ZWMYK2E</PayerID><PayerStatus xsi:type="ebl:PayPalUserStatusCodeType">verified</PayerStatus><PayerName xsi:type="ebl:PersonNameType"><Salutation + xmlns="urn:ebay:apis:eBLBaseComponents"></Salutation><FirstName xmlns="urn:ebay:apis:eBLBaseComponents">Duff</FirstName><MiddleName + xmlns="urn:ebay:apis:eBLBaseComponents"></MiddleName><LastName xmlns="urn:ebay:apis:eBLBaseComponents">Jones</LastName><Suffix + xmlns="urn:ebay:apis:eBLBaseComponents"></Suffix></PayerName><PayerCountry xsi:type="ebl:CountryCodeType">US</PayerCountry><PayerBusiness xsi:type="xs:string"></PayerBusiness><Address + xsi:type="ebl:AddressType"><Name xsi:type="xs:string"></Name><Street1 xsi:type="xs:string"></Street1><Street2 xsi:type="xs:string"></Street2><CityName + xsi:type="xs:string"></CityName><StateOrProvince xsi:type="xs:string"></StateOrProvince><CountryName></CountryName><Phone xsi:type="xs:string"></Phone><PostalCode + xsi:type="xs:string"></PostalCode><AddressID xsi:type="xs:string"></AddressID><AddressOwner xsi:type="ebl:AddressOwnerCodeType">PayPal</AddressOwner><ExternalAddressID + xsi:type="xs:string"></ExternalAddressID><AddressStatus + xsi:type="ebl:AddressStatusCodeType">None</AddressStatus></Address></PayerInfo></BAUpdateResponseDetails></BAUpdateResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + RESPONSE + end - def failed_cancel_billing_agreement_response - <<-RESPONSE - <?xml version="1.0" encoding="UTF-8"?><SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" - xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" - xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" - xmlns:ns="urn:ebay:api:PayPalAPI"><SOAP-ENV:Header><Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"></Security><RequesterCredentials - xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"><Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"><Username - xsi:type="xs:string"></Username><Password xsi:type="xs:string"></Password><Signature xsi:type="xs:string"></Signature><Subject - xsi:type="xs:string"></Subject></Credentials></RequesterCredentials></SOAP-ENV:Header><SOAP-ENV:Body id="_0"><BAUpdateResponse xmlns="urn:ebay:api:PayPalAPI"><Timestamp - xmlns="urn:ebay:apis:eBLBaseComponents">2013-06-04T16:25:06Z</Timestamp><Ack xmlns="urn:ebay:apis:eBLBaseComponents">Failure</Ack><CorrelationID - xmlns="urn:ebay:apis:eBLBaseComponents">5ec2d55830b40</CorrelationID><Errors xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:ErrorType"><ShortMessage xsi:type="xs:string">Billing - Agreement was cancelled</ShortMessage><LongMessage xsi:type="xs:string">Billing Agreement was cancelled</LongMessage><ErrorCode xsi:type="xs:token">10201</ErrorCode><SeverityCode - xmlns="urn:ebay:apis:eBLBaseComponents">Error</SeverityCode></Errors><Version xmlns="urn:ebay:apis:eBLBaseComponents">72</Version><Build - xmlns="urn:ebay:apis:eBLBaseComponents">6118442</Build><BAUpdateResponseDetails xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:BAUpdateResponseDetailsType"><PayerInfo - xsi:type="ebl:PayerInfoType"><Payer xsi:type="ebl:EmailAddressType"></Payer><PayerID xsi:type="ebl:UserIDType"></PayerID><PayerStatus - xsi:type="ebl:PayPalUserStatusCodeType">unverified</PayerStatus><PayerName xsi:type="ebl:PersonNameType"><Salutation xmlns="urn:ebay:apis:eBLBaseComponents"></Salutation><FirstName - xmlns="urn:ebay:apis:eBLBaseComponents"></FirstName><MiddleName xmlns="urn:ebay:apis:eBLBaseComponents"></MiddleName><LastName xmlns="urn:ebay:apis:eBLBaseComponents"></LastName><Suffix - xmlns="urn:ebay:apis:eBLBaseComponents"></Suffix></PayerName><PayerBusiness xsi:type="xs:string"></PayerBusiness><Address xsi:type="ebl:AddressType"><Name - xsi:type="xs:string"></Name><Street1 xsi:type="xs:string"></Street1><Street2 xsi:type="xs:string"></Street2><CityName xsi:type="xs:string"></CityName><StateOrProvince - xsi:type="xs:string"></StateOrProvince><CountryName></CountryName><Phone xsi:type="xs:string"></Phone><PostalCode xsi:type="xs:string"></PostalCode><AddressID - xsi:type="xs:string"></AddressID><AddressOwner xsi:type="ebl:AddressOwnerCodeType">PayPal</AddressOwner><ExternalAddressID xsi:type="xs:string"></ExternalAddressID><AddressStatus - xsi:type="ebl:AddressStatusCodeType">None</AddressStatus></Address></PayerInfo></BAUpdateResponseDetails></BAUpdateResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> - RESPONSE - end + def failed_cancel_billing_agreement_response + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?><SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" + xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" + xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" + xmlns:ns="urn:ebay:api:PayPalAPI"><SOAP-ENV:Header><Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"></Security><RequesterCredentials + xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"><Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"><Username + xsi:type="xs:string"></Username><Password xsi:type="xs:string"></Password><Signature xsi:type="xs:string"></Signature><Subject + xsi:type="xs:string"></Subject></Credentials></RequesterCredentials></SOAP-ENV:Header><SOAP-ENV:Body id="_0"><BAUpdateResponse xmlns="urn:ebay:api:PayPalAPI"><Timestamp + xmlns="urn:ebay:apis:eBLBaseComponents">2013-06-04T16:25:06Z</Timestamp><Ack xmlns="urn:ebay:apis:eBLBaseComponents">Failure</Ack><CorrelationID + xmlns="urn:ebay:apis:eBLBaseComponents">5ec2d55830b40</CorrelationID><Errors xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:ErrorType"><ShortMessage xsi:type="xs:string">Billing + Agreement was cancelled</ShortMessage><LongMessage xsi:type="xs:string">Billing Agreement was cancelled</LongMessage><ErrorCode xsi:type="xs:token">10201</ErrorCode><SeverityCode + xmlns="urn:ebay:apis:eBLBaseComponents">Error</SeverityCode></Errors><Version xmlns="urn:ebay:apis:eBLBaseComponents">72</Version><Build + xmlns="urn:ebay:apis:eBLBaseComponents">6118442</Build><BAUpdateResponseDetails xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:BAUpdateResponseDetailsType"><PayerInfo + xsi:type="ebl:PayerInfoType"><Payer xsi:type="ebl:EmailAddressType"></Payer><PayerID xsi:type="ebl:UserIDType"></PayerID><PayerStatus + xsi:type="ebl:PayPalUserStatusCodeType">unverified</PayerStatus><PayerName xsi:type="ebl:PersonNameType"><Salutation xmlns="urn:ebay:apis:eBLBaseComponents"></Salutation><FirstName + xmlns="urn:ebay:apis:eBLBaseComponents"></FirstName><MiddleName xmlns="urn:ebay:apis:eBLBaseComponents"></MiddleName><LastName xmlns="urn:ebay:apis:eBLBaseComponents"></LastName><Suffix + xmlns="urn:ebay:apis:eBLBaseComponents"></Suffix></PayerName><PayerBusiness xsi:type="xs:string"></PayerBusiness><Address xsi:type="ebl:AddressType"><Name + xsi:type="xs:string"></Name><Street1 xsi:type="xs:string"></Street1><Street2 xsi:type="xs:string"></Street2><CityName xsi:type="xs:string"></CityName><StateOrProvince + xsi:type="xs:string"></StateOrProvince><CountryName></CountryName><Phone xsi:type="xs:string"></Phone><PostalCode xsi:type="xs:string"></PostalCode><AddressID + xsi:type="xs:string"></AddressID><AddressOwner xsi:type="ebl:AddressOwnerCodeType">PayPal</AddressOwner><ExternalAddressID xsi:type="xs:string"></ExternalAddressID><AddressStatus + xsi:type="ebl:AddressStatusCodeType">None</AddressStatus></Address></PayerInfo></BAUpdateResponseDetails></BAUpdateResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + RESPONSE + end - def successful_billing_agreement_details_response - <<-RESPONSE - <?xml version="1.0" encoding="UTF-8"?><SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" - xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" - xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" - xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" - xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"><SOAP-ENV:Header><Security - xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"></Security><RequesterCredentials - xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"><Credentials xmlns="urn:ebay:apis:eBLBaseComponents" - xsi:type="ebl:UserIdPasswordType"><Username xsi:type="xs:string"></Username><Password xsi:type="xs:string"></Password><Signature - xsi:type="xs:string"></Signature><Subject xsi:type="xs:string"></Subject></Credentials></RequesterCredentials></SOAP-ENV:Header><SOAP-ENV:Body id="_0"> - <BAUpdateResponse xmlns="urn:ebay:api:PayPalAPI"><Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2014-05-08T09:22:03Z</Timestamp> - <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Success</Ack><CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">f84ed24f5bd6d</CorrelationID> - <Version xmlns="urn:ebay:apis:eBLBaseComponents">72</Version><Build xmlns="urn:ebay:apis:eBLBaseComponents">10918103</Build><BAUpdateResponseDetails - xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:BAUpdateResponseDetailsType"> - <BillingAgreementID xsi:type="xs:string">B-6VE21702A47915521</BillingAgreementID><BillingAgreementDescription - xsi:type="xs:string">My active merchant custom description</BillingAgreementDescription> - <BillingAgreementStatus xsi:type="ebl:MerchantPullStatusCodeType">Active</BillingAgreementStatus><PayerInfo xsi:type="ebl:PayerInfoType"><Payer - xsi:type="ebl:EmailAddressType">ivan.rostovsky.xan@gmail.com</Payer><PayerID xsi:type="ebl:UserIDType">SW3AR2WYZ3NJW</PayerID><PayerStatus - xsi:type="ebl:PayPalUserStatusCodeType">verified</PayerStatus><PayerName xsi:type="ebl:PersonNameType"><Salutation - xmlns="urn:ebay:apis:eBLBaseComponents"></Salutation><FirstName xmlns="urn:ebay:apis:eBLBaseComponents">Ivan</FirstName><MiddleName - xmlns="urn:ebay:apis:eBLBaseComponents"></MiddleName><LastName xmlns="urn:ebay:apis:eBLBaseComponents">Rostovsky</LastName> - <Suffix xmlns="urn:ebay:apis:eBLBaseComponents"></Suffix></PayerName><PayerCountry xsi:type="ebl:CountryCodeType">US</PayerCountry> - <PayerBusiness xsi:type="xs:string"></PayerBusiness><Address xsi:type="ebl:AddressType"><Name xsi:type="xs:string"></Name><Street1 - xsi:type="xs:string"></Street1><Street2 xsi:type="xs:string"></Street2><CityName xsi:type="xs:string"></CityName><StateOrProvince - xsi:type="xs:string"></StateOrProvince><CountryName></CountryName><Phone xsi:type="xs:string"></Phone><PostalCode xsi:type="xs:string"> - </PostalCode><AddressID xsi:type="xs:string"></AddressID><AddressOwner xsi:type="ebl:AddressOwnerCodeType">PayPal</AddressOwner> - <ExternalAddressID xsi:type="xs:string"></ExternalAddressID><AddressStatus xsi:type="ebl:AddressStatusCodeType">None</AddressStatus> - </Address></PayerInfo></BAUpdateResponseDetails></BAUpdateResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> - RESPONSE - end + def successful_billing_agreement_details_response + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?><SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" + xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" + xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" + xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" + xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"><SOAP-ENV:Header><Security + xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"></Security><RequesterCredentials + xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"><Credentials xmlns="urn:ebay:apis:eBLBaseComponents" + xsi:type="ebl:UserIdPasswordType"><Username xsi:type="xs:string"></Username><Password xsi:type="xs:string"></Password><Signature + xsi:type="xs:string"></Signature><Subject xsi:type="xs:string"></Subject></Credentials></RequesterCredentials></SOAP-ENV:Header><SOAP-ENV:Body id="_0"> + <BAUpdateResponse xmlns="urn:ebay:api:PayPalAPI"><Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2014-05-08T09:22:03Z</Timestamp> + <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Success</Ack><CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">f84ed24f5bd6d</CorrelationID> + <Version xmlns="urn:ebay:apis:eBLBaseComponents">72</Version><Build xmlns="urn:ebay:apis:eBLBaseComponents">10918103</Build><BAUpdateResponseDetails + xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:BAUpdateResponseDetailsType"> + <BillingAgreementID xsi:type="xs:string">B-6VE21702A47915521</BillingAgreementID><BillingAgreementDescription + xsi:type="xs:string">My active merchant custom description</BillingAgreementDescription> + <BillingAgreementStatus xsi:type="ebl:MerchantPullStatusCodeType">Active</BillingAgreementStatus><PayerInfo xsi:type="ebl:PayerInfoType"><Payer + xsi:type="ebl:EmailAddressType">ivan.rostovsky.xan@gmail.com</Payer><PayerID xsi:type="ebl:UserIDType">SW3AR2WYZ3NJW</PayerID><PayerStatus + xsi:type="ebl:PayPalUserStatusCodeType">verified</PayerStatus><PayerName xsi:type="ebl:PersonNameType"><Salutation + xmlns="urn:ebay:apis:eBLBaseComponents"></Salutation><FirstName xmlns="urn:ebay:apis:eBLBaseComponents">Ivan</FirstName><MiddleName + xmlns="urn:ebay:apis:eBLBaseComponents"></MiddleName><LastName xmlns="urn:ebay:apis:eBLBaseComponents">Rostovsky</LastName> + <Suffix xmlns="urn:ebay:apis:eBLBaseComponents"></Suffix></PayerName><PayerCountry xsi:type="ebl:CountryCodeType">US</PayerCountry> + <PayerBusiness xsi:type="xs:string"></PayerBusiness><Address xsi:type="ebl:AddressType"><Name xsi:type="xs:string"></Name><Street1 + xsi:type="xs:string"></Street1><Street2 xsi:type="xs:string"></Street2><CityName xsi:type="xs:string"></CityName><StateOrProvince + xsi:type="xs:string"></StateOrProvince><CountryName></CountryName><Phone xsi:type="xs:string"></Phone><PostalCode xsi:type="xs:string"> + </PostalCode><AddressID xsi:type="xs:string"></AddressID><AddressOwner xsi:type="ebl:AddressOwnerCodeType">PayPal</AddressOwner> + <ExternalAddressID xsi:type="xs:string"></ExternalAddressID><AddressStatus xsi:type="ebl:AddressStatusCodeType">None</AddressStatus> + </Address></PayerInfo></BAUpdateResponseDetails></BAUpdateResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + RESPONSE + end - def failure_billing_agreement_details_response - <<-RESPONSE + def failure_billing_agreement_details_response + <<~RESPONSE <?xml version="1.0" encoding="UTF-8"?><SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" @@ -1139,58 +1329,58 @@ def failure_billing_agreement_details_response xsi:type="xs:string"></PostalCode><AddressID xsi:type="xs:string"></AddressID><AddressOwner xsi:type="ebl:AddressOwnerCodeType">PayPal</AddressOwner> <ExternalAddressID xsi:type="xs:string"></ExternalAddressID><AddressStatus xsi:type="ebl:AddressStatusCodeType">None</AddressStatus></Address> </PayerInfo></BAUpdateResponseDetails></BAUpdateResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> - RESPONSE - end - - def pre_scrubbed - <<-TRANSCRIPT -<?xml version=\"1.0\" encoding=\"UTF-8\"?><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\"><env:Header><RequesterCredentials xmlns=\"urn:ebay:api:PayPalAPI\" xmlns:n1=\"urn:ebay:apis:eBLBaseComponents\" env:mustUnderstand=\"0\"><n1:Credentials><n1:Username>activemerchant-cert-test_api1.example.com</n1:Username><n1:Password>ERDD3JRFU5H5DQXS</n1:Password><n1:Subject/></n1:Credentials></RequesterCredentials></env:Header><env:Body><SetExpressCheckoutReq xmlns=\"urn:ebay:api:PayPalAPI\"> - <SetExpressCheckoutRequest xmlns:n2=\"urn:ebay:apis:eBLBaseComponents\"> - <n2:Version>124</n2:Version> - <n2:SetExpressCheckoutRequestDetails> - <n2:ReturnURL>http://example.com/return</n2:ReturnURL> - <n2:CancelURL>http://example.com/cancel</n2:CancelURL> - <n2:ReqBillingAddress>0</n2:ReqBillingAddress> - <n2:NoShipping>0</n2:NoShipping> - <n2:AddressOverride>0</n2:AddressOverride> - <n2:BuyerEmail>buyer@jadedpallet.com</n2:BuyerEmail> - <n2:PaymentDetails> - <n2:OrderTotal currencyID=\"USD\">5.00</n2:OrderTotal> - <n2:OrderDescription>Stuff that you purchased, yo!</n2:OrderDescription> - <n2:InvoiceID>230000</n2:InvoiceID> - <n2:PaymentAction>Authorization</n2:PaymentAction> - </n2:PaymentDetails> - </n2:SetExpressCheckoutRequestDetails> - </SetExpressCheckoutRequest> -</SetExpressCheckoutReq> -</env:Body></env:Envelope> -<?xml version=\"1.0\" encoding=\"UTF-8\"?><SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:SOAP-ENC=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:cc=\"urn:ebay:apis:CoreComponentTypes\" xmlns:wsu=\"http://schemas.xmlsoap.org/ws/2002/07/utility\" xmlns:saml=\"urn:oasis:names:tc:SAML:1.0:assertion\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:wsse=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xmlns:ed=\"urn:ebay:apis:EnhancedDataTypes\" xmlns:ebl=\"urn:ebay:apis:eBLBaseComponents\" xmlns:ns=\"urn:ebay:api:PayPalAPI\"><SOAP-ENV:Header><Security xmlns=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xsi:type=\"wsse:SecurityType\"></Security><RequesterCredentials xmlns=\"urn:ebay:api:PayPalAPI\" xsi:type=\"ebl:CustomSecurityHeaderType\"><Credentials xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:UserIdPasswordType\"><Username xsi:type=\"xs:string\"></Username><Password xsi:type=\"xs:string\"></Password><Signature xsi:type=\"xs:string\"></Signature><Subject xsi:type=\"xs:string\"></Subject></Credentials></RequesterCredentials></SOAP-ENV:Header><SOAP-ENV:Body id=\"_0\"><SetExpressCheckoutResponse xmlns=\"urn:ebay:api:PayPalAPI\"><Timestamp xmlns=\"urn:ebay:apis:eBLBaseComponents\">2018-05-24T20:23:54Z</Timestamp><Ack xmlns=\"urn:ebay:apis:eBLBaseComponents\">Success</Ack><CorrelationID xmlns=\"urn:ebay:apis:eBLBaseComponents\">b6dd2a043921b</CorrelationID><Version xmlns=\"urn:ebay:apis:eBLBaseComponents\">124</Version><Build xmlns=\"urn:ebay:apis:eBLBaseComponents\">46549960</Build><Token xsi:type=\"ebl:ExpressCheckoutTokenType\">EC-7KR85820NC734104L</Token></SetExpressCheckoutResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> - TRANSCRIPT - end + RESPONSE + end - def post_scrubbed - <<-TRANSCRIPT -<?xml version=\"1.0\" encoding=\"UTF-8\"?><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\"><env:Header><RequesterCredentials xmlns=\"urn:ebay:api:PayPalAPI\" xmlns:n1=\"urn:ebay:apis:eBLBaseComponents\" env:mustUnderstand=\"0\"><n1:Credentials><n1:Username>[FILTERED]</n1:Username><n1:Password>[FILTERED]</n1:Password><n1:Subject/></n1:Credentials></RequesterCredentials></env:Header><env:Body><SetExpressCheckoutReq xmlns=\"urn:ebay:api:PayPalAPI\"> - <SetExpressCheckoutRequest xmlns:n2=\"urn:ebay:apis:eBLBaseComponents\"> - <n2:Version>124</n2:Version> - <n2:SetExpressCheckoutRequestDetails> - <n2:ReturnURL>http://example.com/return</n2:ReturnURL> - <n2:CancelURL>http://example.com/cancel</n2:CancelURL> - <n2:ReqBillingAddress>0</n2:ReqBillingAddress> - <n2:NoShipping>0</n2:NoShipping> - <n2:AddressOverride>0</n2:AddressOverride> - <n2:BuyerEmail>buyer@jadedpallet.com</n2:BuyerEmail> - <n2:PaymentDetails> - <n2:OrderTotal currencyID=\"USD\">5.00</n2:OrderTotal> - <n2:OrderDescription>Stuff that you purchased, yo!</n2:OrderDescription> - <n2:InvoiceID>230000</n2:InvoiceID> - <n2:PaymentAction>Authorization</n2:PaymentAction> - </n2:PaymentDetails> - </n2:SetExpressCheckoutRequestDetails> - </SetExpressCheckoutRequest> -</SetExpressCheckoutReq> -</env:Body></env:Envelope> -<?xml version=\"1.0\" encoding=\"UTF-8\"?><SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:SOAP-ENC=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:cc=\"urn:ebay:apis:CoreComponentTypes\" xmlns:wsu=\"http://schemas.xmlsoap.org/ws/2002/07/utility\" xmlns:saml=\"urn:oasis:names:tc:SAML:1.0:assertion\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:wsse=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xmlns:ed=\"urn:ebay:apis:EnhancedDataTypes\" xmlns:ebl=\"urn:ebay:apis:eBLBaseComponents\" xmlns:ns=\"urn:ebay:api:PayPalAPI\"><SOAP-ENV:Header><Security xmlns=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xsi:type=\"wsse:SecurityType\"></Security><RequesterCredentials xmlns=\"urn:ebay:api:PayPalAPI\" xsi:type=\"ebl:CustomSecurityHeaderType\"><Credentials xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:UserIdPasswordType\"><Username xsi:type=\"xs:string\"></Username><Password xsi:type=\"xs:string\"></Password><Signature xsi:type=\"xs:string\"></Signature><Subject xsi:type=\"xs:string\"></Subject></Credentials></RequesterCredentials></SOAP-ENV:Header><SOAP-ENV:Body id=\"_0\"><SetExpressCheckoutResponse xmlns=\"urn:ebay:api:PayPalAPI\"><Timestamp xmlns=\"urn:ebay:apis:eBLBaseComponents\">2018-05-24T20:23:54Z</Timestamp><Ack xmlns=\"urn:ebay:apis:eBLBaseComponents\">Success</Ack><CorrelationID xmlns=\"urn:ebay:apis:eBLBaseComponents\">b6dd2a043921b</CorrelationID><Version xmlns=\"urn:ebay:apis:eBLBaseComponents\">124</Version><Build xmlns=\"urn:ebay:apis:eBLBaseComponents\">46549960</Build><Token xsi:type=\"ebl:ExpressCheckoutTokenType\">EC-7KR85820NC734104L</Token></SetExpressCheckoutResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> - TRANSCRIPT - end + def pre_scrubbed + <<~TRANSCRIPT + <?xml version=\"1.0\" encoding=\"UTF-8\"?><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\"><env:Header><RequesterCredentials xmlns=\"urn:ebay:api:PayPalAPI\" xmlns:n1=\"urn:ebay:apis:eBLBaseComponents\" env:mustUnderstand=\"0\"><n1:Credentials><n1:Username>activemerchant-cert-test_api1.example.com</n1:Username><n1:Password>ERDD3JRFU5H5DQXS</n1:Password><n1:Subject/></n1:Credentials></RequesterCredentials></env:Header><env:Body><SetExpressCheckoutReq xmlns=\"urn:ebay:api:PayPalAPI\"> + <SetExpressCheckoutRequest xmlns:n2=\"urn:ebay:apis:eBLBaseComponents\"> + <n2:Version>124</n2:Version> + <n2:SetExpressCheckoutRequestDetails> + <n2:ReturnURL>http://example.com/return</n2:ReturnURL> + <n2:CancelURL>http://example.com/cancel</n2:CancelURL> + <n2:ReqBillingAddress>0</n2:ReqBillingAddress> + <n2:NoShipping>0</n2:NoShipping> + <n2:AddressOverride>0</n2:AddressOverride> + <n2:BuyerEmail>buyer@jadedpallet.com</n2:BuyerEmail> + <n2:PaymentDetails> + <n2:OrderTotal currencyID=\"USD\">5.00</n2:OrderTotal> + <n2:OrderDescription>Stuff that you purchased, yo!</n2:OrderDescription> + <n2:InvoiceID>230000</n2:InvoiceID> + <n2:PaymentAction>Authorization</n2:PaymentAction> + </n2:PaymentDetails> + </n2:SetExpressCheckoutRequestDetails> + </SetExpressCheckoutRequest> + </SetExpressCheckoutReq> + </env:Body></env:Envelope> + <?xml version=\"1.0\" encoding=\"UTF-8\"?><SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:SOAP-ENC=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:cc=\"urn:ebay:apis:CoreComponentTypes\" xmlns:wsu=\"http://schemas.xmlsoap.org/ws/2002/07/utility\" xmlns:saml=\"urn:oasis:names:tc:SAML:1.0:assertion\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:wsse=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xmlns:ed=\"urn:ebay:apis:EnhancedDataTypes\" xmlns:ebl=\"urn:ebay:apis:eBLBaseComponents\" xmlns:ns=\"urn:ebay:api:PayPalAPI\"><SOAP-ENV:Header><Security xmlns=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xsi:type=\"wsse:SecurityType\"></Security><RequesterCredentials xmlns=\"urn:ebay:api:PayPalAPI\" xsi:type=\"ebl:CustomSecurityHeaderType\"><Credentials xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:UserIdPasswordType\"><Username xsi:type=\"xs:string\"></Username><Password xsi:type=\"xs:string\"></Password><Signature xsi:type=\"xs:string\"></Signature><Subject xsi:type=\"xs:string\"></Subject></Credentials></RequesterCredentials></SOAP-ENV:Header><SOAP-ENV:Body id=\"_0\"><SetExpressCheckoutResponse xmlns=\"urn:ebay:api:PayPalAPI\"><Timestamp xmlns=\"urn:ebay:apis:eBLBaseComponents\">2018-05-24T20:23:54Z</Timestamp><Ack xmlns=\"urn:ebay:apis:eBLBaseComponents\">Success</Ack><CorrelationID xmlns=\"urn:ebay:apis:eBLBaseComponents\">b6dd2a043921b</CorrelationID><Version xmlns=\"urn:ebay:apis:eBLBaseComponents\">124</Version><Build xmlns=\"urn:ebay:apis:eBLBaseComponents\">46549960</Build><Token xsi:type=\"ebl:ExpressCheckoutTokenType\">EC-7KR85820NC734104L</Token></SetExpressCheckoutResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + TRANSCRIPT + end + + def post_scrubbed + <<~TRANSCRIPT + <?xml version=\"1.0\" encoding=\"UTF-8\"?><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\"><env:Header><RequesterCredentials xmlns=\"urn:ebay:api:PayPalAPI\" xmlns:n1=\"urn:ebay:apis:eBLBaseComponents\" env:mustUnderstand=\"0\"><n1:Credentials><n1:Username>[FILTERED]</n1:Username><n1:Password>[FILTERED]</n1:Password><n1:Subject/></n1:Credentials></RequesterCredentials></env:Header><env:Body><SetExpressCheckoutReq xmlns=\"urn:ebay:api:PayPalAPI\"> + <SetExpressCheckoutRequest xmlns:n2=\"urn:ebay:apis:eBLBaseComponents\"> + <n2:Version>124</n2:Version> + <n2:SetExpressCheckoutRequestDetails> + <n2:ReturnURL>http://example.com/return</n2:ReturnURL> + <n2:CancelURL>http://example.com/cancel</n2:CancelURL> + <n2:ReqBillingAddress>0</n2:ReqBillingAddress> + <n2:NoShipping>0</n2:NoShipping> + <n2:AddressOverride>0</n2:AddressOverride> + <n2:BuyerEmail>buyer@jadedpallet.com</n2:BuyerEmail> + <n2:PaymentDetails> + <n2:OrderTotal currencyID=\"USD\">5.00</n2:OrderTotal> + <n2:OrderDescription>Stuff that you purchased, yo!</n2:OrderDescription> + <n2:InvoiceID>230000</n2:InvoiceID> + <n2:PaymentAction>Authorization</n2:PaymentAction> + </n2:PaymentDetails> + </n2:SetExpressCheckoutRequestDetails> + </SetExpressCheckoutRequest> + </SetExpressCheckoutReq> + </env:Body></env:Envelope> + <?xml version=\"1.0\" encoding=\"UTF-8\"?><SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:SOAP-ENC=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:cc=\"urn:ebay:apis:CoreComponentTypes\" xmlns:wsu=\"http://schemas.xmlsoap.org/ws/2002/07/utility\" xmlns:saml=\"urn:oasis:names:tc:SAML:1.0:assertion\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:wsse=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xmlns:ed=\"urn:ebay:apis:EnhancedDataTypes\" xmlns:ebl=\"urn:ebay:apis:eBLBaseComponents\" xmlns:ns=\"urn:ebay:api:PayPalAPI\"><SOAP-ENV:Header><Security xmlns=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xsi:type=\"wsse:SecurityType\"></Security><RequesterCredentials xmlns=\"urn:ebay:api:PayPalAPI\" xsi:type=\"ebl:CustomSecurityHeaderType\"><Credentials xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:UserIdPasswordType\"><Username xsi:type=\"xs:string\"></Username><Password xsi:type=\"xs:string\"></Password><Signature xsi:type=\"xs:string\"></Signature><Subject xsi:type=\"xs:string\"></Subject></Credentials></RequesterCredentials></SOAP-ENV:Header><SOAP-ENV:Body id=\"_0\"><SetExpressCheckoutResponse xmlns=\"urn:ebay:api:PayPalAPI\"><Timestamp xmlns=\"urn:ebay:apis:eBLBaseComponents\">2018-05-24T20:23:54Z</Timestamp><Ack xmlns=\"urn:ebay:apis:eBLBaseComponents\">Success</Ack><CorrelationID xmlns=\"urn:ebay:apis:eBLBaseComponents\">b6dd2a043921b</CorrelationID><Version xmlns=\"urn:ebay:apis:eBLBaseComponents\">124</Version><Build xmlns=\"urn:ebay:apis:eBLBaseComponents\">46549960</Build><Token xsi:type=\"ebl:ExpressCheckoutTokenType\">EC-7KR85820NC734104L</Token></SetExpressCheckoutResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + TRANSCRIPT + end end diff --git a/test/unit/gateways/paypal_test.rb b/test/unit/gateways/paypal_test.rb index 0dc0926f0ca..98601c7362a 100644 --- a/test/unit/gateways/paypal_test.rb +++ b/test/unit/gateways/paypal_test.rb @@ -8,19 +8,19 @@ def setup @amount = 100 @gateway = PaypalGateway.new( - :login => 'cody', - :password => 'test', - :pem => 'PEM' + login: 'cody', + password: 'test', + pem: 'PEM' ) @credit_card = credit_card('4242424242424242') - @options = { :billing_address => address, :ip => '127.0.0.1' } - @recurring_required_fields = {:start_date => Date.today, :frequency => :Month, :period => 'Month', :description => 'A description'} + @options = { billing_address: address, ip: '127.0.0.1' } + @recurring_required_fields = { start_date: Date.today, frequency: :Month, period: 'Month', description: 'A description' } end def test_no_ip_address assert_raise(ArgumentError) do - @gateway.purchase(@amount, @credit_card, :billing_address => address) + @gateway.purchase(@amount, @credit_card, billing_address: address) end end @@ -63,7 +63,7 @@ def test_recurring_requires_period def test_update_recurring_requires_profile_id assert_raise(ArgumentError) do assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.update_recurring(:amount => 100) + @gateway.update_recurring(amount: 100) end end end @@ -71,7 +71,7 @@ def test_update_recurring_requires_profile_id def test_cancel_recurring_requires_profile_id assert_raise(ArgumentError) do assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.cancel_recurring(nil, :note => 'Note') + @gateway.cancel_recurring(nil, note: 'Note') end end end @@ -87,7 +87,7 @@ def test_status_recurring_requires_profile_id def test_suspend_recurring_requires_profile_id assert_raise(ArgumentError) do assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.suspend_recurring(nil, :note => 'Note') + @gateway.suspend_recurring(nil, note: 'Note') end end end @@ -95,14 +95,14 @@ def test_suspend_recurring_requires_profile_id def test_reactivate_recurring_requires_profile_id assert_raise(ArgumentError) do assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.reactivate_recurring(nil, :note => 'Note') + @gateway.reactivate_recurring(nil, note: 'Note') end end end def test_successful_purchase_with_auth_signature - @gateway = PaypalGateway.new(:login => 'cody', :password => 'test', :pem => 'PEM', :auth_signature => 123) - expected_header = {'X-PP-AUTHORIZATION' => 123, 'X-PAYPAL-MESSAGE-PROTOCOL' => 'SOAP11'} + @gateway = PaypalGateway.new(login: 'cody', password: 'test', pem: 'PEM', auth_signature: 123) + expected_header = { 'X-PP-AUTHORIZATION' => 123, 'X-PAYPAL-MESSAGE-PROTOCOL' => 'SOAP11' } @gateway.expects(:ssl_post).with(anything, anything, expected_header).returns(successful_purchase_response) @gateway.expects(:add_credentials).never @@ -110,7 +110,7 @@ def test_successful_purchase_with_auth_signature end def test_successful_purchase_without_auth_signature - @gateway = PaypalGateway.new(:login => 'cody', :password => 'test', :pem => 'PEM') + @gateway = PaypalGateway.new(login: 'cody', password: 'test', pem: 'PEM') @gateway.expects(:ssl_post).returns(successful_purchase_response) @gateway.expects(:add_credentials) @@ -136,7 +136,7 @@ def test_successful_reference_purchase ref_id = response.authorization - gateway2 = PaypalGateway.new(:login => 'cody', :password => 'test', :pem => 'PEM') + gateway2 = PaypalGateway.new(login: 'cody', password: 'test', pem: 'PEM') gateway2.expects(:ssl_post).returns(successful_reference_purchase_response) assert response = gateway2.purchase(@amount, ref_id, @options) assert_instance_of Response, response @@ -157,7 +157,7 @@ def test_failed_purchase def test_descriptors_passed stub_comms do @gateway.purchase(@amount, @credit_card, @options.merge(soft_descriptor: 'Eggcellent', soft_descriptor_city: 'New York')) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(%r{<n2:SoftDescriptor>Eggcellent}, data) assert_match(%r{<n2:SoftDescriptorCity>New York}, data) end.respond_with(successful_purchase_response) @@ -198,19 +198,19 @@ def test_paypal_timeout_error def test_pem_file_accessor PaypalGateway.pem_file = '123456' - gateway = PaypalGateway.new(:login => 'test', :password => 'test') + gateway = PaypalGateway.new(login: 'test', password: 'test') assert_equal '123456', gateway.options[:pem] end def test_passed_in_pem_overrides_class_accessor PaypalGateway.pem_file = '123456' - gateway = PaypalGateway.new(:login => 'test', :password => 'test', :pem => 'Clobber') + gateway = PaypalGateway.new(login: 'test', password: 'test', pem: 'Clobber') assert_equal 'Clobber', gateway.options[:pem] end def test_ensure_options_are_transferred_to_express_instance PaypalGateway.pem_file = '123456' - gateway = PaypalGateway.new(:login => 'test', :password => 'password') + gateway = PaypalGateway.new(login: 'test', password: 'password') express = gateway.express assert_instance_of PaypalExpressGateway, express assert_equal 'test', express.options[:login] @@ -219,11 +219,11 @@ def test_ensure_options_are_transferred_to_express_instance end def test_supported_countries - assert_equal ['US'], PaypalGateway.supported_countries + assert_equal %w[CA NZ GB US], PaypalGateway.supported_countries end def test_supported_card_types - assert_equal [:visa, :master, :american_express, :discover], PaypalGateway.supported_cardtypes + assert_equal %i[visa master american_express discover], PaypalGateway.supported_cardtypes end def test_button_source @@ -260,10 +260,12 @@ 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, - :tax => @amount, - :shipping => @amount, - :handling => @amount + xml = @gateway.send( + :build_sale_or_authorization_request, + 'Authorization', @amount, @credit_card, + tax: @amount, + shipping: @amount, + handling: @amount ) doc = REXML::Document.new(xml) @@ -271,11 +273,15 @@ def test_item_total_shipping_handling_and_tax_not_included_unless_all_are_presen end def test_item_total_shipping_handling_and_tax - xml = @gateway.send(:build_sale_or_authorization_request, 'Authorization', @amount, @credit_card, - :tax => @amount, - :shipping => @amount, - :handling => @amount, - :subtotal => 200 + xml = @gateway.send( + :build_sale_or_authorization_request, + 'Authorization', + @amount, + @credit_card, + tax: @amount, + shipping: @amount, + handling: @amount, + subtotal: 200 ) doc = REXML::Document.new(xml) @@ -284,19 +290,19 @@ def test_item_total_shipping_handling_and_tax def test_should_use_test_certificate_endpoint gateway = PaypalGateway.new( - :login => 'cody', - :password => 'test', - :pem => 'PEM' - ) + login: 'cody', + password: 'test', + pem: 'PEM' + ) assert_equal PaypalGateway::URLS[:test][:certificate], gateway.send(:endpoint_url) end def test_should_use_live_certificate_endpoint gateway = PaypalGateway.new( - :login => 'cody', - :password => 'test', - :pem => 'PEM' - ) + login: 'cody', + password: 'test', + pem: 'PEM' + ) gateway.expects(:test?).returns(false) assert_equal PaypalGateway::URLS[:live][:certificate], gateway.send(:endpoint_url) @@ -304,20 +310,20 @@ def test_should_use_live_certificate_endpoint def test_should_use_test_signature_endpoint gateway = PaypalGateway.new( - :login => 'cody', - :password => 'test', - :signature => 'SIG' - ) + login: 'cody', + password: 'test', + signature: 'SIG' + ) assert_equal PaypalGateway::URLS[:test][:signature], gateway.send(:endpoint_url) end def test_should_use_live_signature_endpoint gateway = PaypalGateway.new( - :login => 'cody', - :password => 'test', - :signature => 'SIG' - ) + login: 'cody', + password: 'test', + signature: 'SIG' + ) gateway.expects(:test?).returns(false) assert_equal PaypalGateway::URLS[:live][:signature], gateway.send(:endpoint_url) @@ -325,7 +331,7 @@ def test_should_use_live_signature_endpoint def test_should_raise_argument_when_credentials_not_present assert_raises(ArgumentError) do - PaypalGateway.new(:login => 'cody', :password => 'test') + PaypalGateway.new(login: 'cody', password: 'test') end end @@ -372,14 +378,14 @@ def test_authentication_failed_response def test_amount_format_for_jpy_currency @gateway.expects(:ssl_post).with(anything, regexp_matches(/n2:OrderTotal currencyID=.JPY.>1<\/n2:OrderTotal>/), {}).returns(successful_purchase_response) - response = @gateway.purchase(100, @credit_card, @options.merge(:currency => 'JPY')) + response = @gateway.purchase(100, @credit_card, @options.merge(currency: 'JPY')) assert response.success? end def test_successful_create_profile @gateway.expects(:ssl_post).returns(successful_create_profile_paypal_response) response = assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.recurring(@amount, @credit_card, :description => 'some description', :start_date => Time.now, :frequency => 12, :period => 'Month') + @gateway.recurring(@amount, @credit_card, description: 'some description', start_date: Time.now, frequency: 12, period: 'Month') end assert_instance_of Response, response assert response.success? @@ -391,7 +397,7 @@ def test_successful_create_profile def test_failed_create_profile @gateway.expects(:ssl_post).returns(failed_create_profile_paypal_response) response = assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.recurring(@amount, @credit_card, :description => 'some description', :start_date => Time.now, :frequency => 12, :period => 'Month') + @gateway.recurring(@amount, @credit_card, description: 'some description', start_date: Time.now, frequency: 12, period: 'Month') end assert_instance_of Response, response assert !response.success? @@ -401,42 +407,42 @@ def test_failed_create_profile end def test_update_recurring_delegation - @gateway.expects(:build_change_profile_request).with('I-G7A2FF8V75JY', :amount => 200) + @gateway.expects(:build_change_profile_request).with('I-G7A2FF8V75JY', amount: 200) @gateway.stubs(:commit) assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.update_recurring(:profile_id => 'I-G7A2FF8V75JY', :amount => 200) + @gateway.update_recurring(profile_id: 'I-G7A2FF8V75JY', amount: 200) end end def test_update_recurring_response @gateway.expects(:ssl_post).returns(successful_update_recurring_payment_profile_response) response = assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.update_recurring(:profile_id => 'I-G7A2FF8V75JY', :amount => 200) + @gateway.update_recurring(profile_id: 'I-G7A2FF8V75JY', amount: 200) end assert response.success? end def test_cancel_recurring_delegation - @gateway.expects(:build_manage_profile_request).with('I-G7A2FF8V75JY', 'Cancel', :note => 'A Note').returns(:cancel_request) + @gateway.expects(:build_manage_profile_request).with('I-G7A2FF8V75JY', 'Cancel', note: 'A Note').returns(:cancel_request) @gateway.expects(:commit).with('ManageRecurringPaymentsProfileStatus', :cancel_request) assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.cancel_recurring('I-G7A2FF8V75JY', :note => 'A Note') + @gateway.cancel_recurring('I-G7A2FF8V75JY', note: 'A Note') end end def test_suspend_recurring_delegation - @gateway.expects(:build_manage_profile_request).with('I-G7A2FF8V75JY', 'Suspend', :note => 'A Note').returns(:request) + @gateway.expects(:build_manage_profile_request).with('I-G7A2FF8V75JY', 'Suspend', note: 'A Note').returns(:request) @gateway.expects(:commit).with('ManageRecurringPaymentsProfileStatus', :request) assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.suspend_recurring('I-G7A2FF8V75JY', :note => 'A Note') + @gateway.suspend_recurring('I-G7A2FF8V75JY', note: 'A Note') end end def test_reactivate_recurring_delegation - @gateway.expects(:build_manage_profile_request).with('I-G7A2FF8V75JY', 'Reactivate', :note => 'A Note').returns(:request) + @gateway.expects(:build_manage_profile_request).with('I-G7A2FF8V75JY', 'Reactivate', note: 'A Note').returns(:request) @gateway.expects(:commit).with('ManageRecurringPaymentsProfileStatus', :request) assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.reactivate_recurring('I-G7A2FF8V75JY', :note => 'A Note') + @gateway.reactivate_recurring('I-G7A2FF8V75JY', note: 'A Note') end end @@ -458,38 +464,38 @@ def test_status_recurring_response end def test_bill_outstanding_amoung_delegation - @gateway.expects(:build_bill_outstanding_amount).with('I-G7A2FF8V75JY', :amount => 400).returns(:request) + @gateway.expects(:build_bill_outstanding_amount).with('I-G7A2FF8V75JY', amount: 400).returns(:request) @gateway.expects(:commit).with('BillOutstandingAmount', :request) assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.bill_outstanding_amount('I-G7A2FF8V75JY', :amount => 400) + @gateway.bill_outstanding_amount('I-G7A2FF8V75JY', amount: 400) end end def test_bill_outstanding_amoung_response @gateway.expects(:ssl_post).returns(successful_bill_outstanding_amount) response = assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.bill_outstanding_amount('I-G7A2FF8V75JY', :amount => 400) + @gateway.bill_outstanding_amount('I-G7A2FF8V75JY', amount: 400) end assert response.success? end def test_mass_pay_transfer_recipient_types - response = stub_comms do + stub_comms do @gateway.transfer 1000, 'fred@example.com' - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_no_match %r{ReceiverType}, data end.respond_with(successful_purchase_response) - response = stub_comms do - @gateway.transfer 1000, 'fred@example.com', :receiver_type => 'EmailAddress' - end.check_request do |endpoint, data, headers| + stub_comms do + @gateway.transfer 1000, 'fred@example.com', receiver_type: 'EmailAddress' + end.check_request do |_endpoint, data, _headers| assert_match %r{<ReceiverType>EmailAddress</ReceiverType>}, data assert_match %r{<ReceiverEmail>fred@example\.com</ReceiverEmail>}, data end.respond_with(successful_purchase_response) - response = stub_comms do - @gateway.transfer 1000, 'fred@example.com', :receiver_type => 'UserID' - end.check_request do |endpoint, data, headers| + stub_comms do + @gateway.transfer 1000, 'fred@example.com', receiver_type: 'UserID' + end.check_request do |_endpoint, data, _headers| assert_match %r{<ReceiverType>UserID</ReceiverType>}, data assert_match %r{<ReceiverID>fred@example\.com</ReceiverID>}, data end.respond_with(successful_purchase_response) @@ -553,7 +559,7 @@ def test_supports_scrubbing? def test_includes_cvv_tag stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(%r{CVV2}, data) end.respond_with(successful_purchase_response) end @@ -562,20 +568,20 @@ def test_blank_cvv_not_sent @credit_card.verification_value = nil stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_no_match(%r{CVV2}, data) end.respond_with(successful_purchase_response) @credit_card.verification_value = ' ' stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_no_match(%r{CVV2}, data) end.respond_with(successful_purchase_response) end def test_card_declined - ['15005', '10754', '10752', '10759', '10761', '15002', '11084'].each do |error_code| + %w[15005 10754 10752 10759 10761 15002 11084].each do |error_code| @gateway.expects(:ssl_request).returns(response_with_error_code(error_code)) response = @gateway.purchase(@amount, @credit_card, @options) @@ -611,10 +617,35 @@ def test_error_code_with_no_mapping_returns_standardized_processing_error assert_equal(:processing_error, response.error_code) end + def test_3ds_version_1_request + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(three_d_secure_option(version: '1.0.2', xid: 'xid'))) + end.check_request do |_endpoint, data, _headers| + assert_match %r{<n2:Version>124</n2:Version>}, data + assert_match %r{<AuthStatus3ds>Y</AuthStatus3ds>}, data + assert_match %r{<Cavv>cavv</Cavv>}, data + assert_match %r{<Eci3ds>eci</Eci3ds>}, data + assert_match %r{<Xid>xid</Xid>}, data + end.respond_with(successful_purchase_response) + end + + def test_3ds_version_2_request + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(three_d_secure_option(version: '2.1.0', ds_transaction_id: 'ds_transaction_id'))) + end.check_request do |_endpoint, data, _headers| + assert_match %r{<n2:Version>214.0</n2:Version>}, data + assert_match %r{<AuthStatus3ds>Y</AuthStatus3ds>}, data + assert_match %r{<Cavv>cavv</Cavv>}, data + assert_match %r{<Eci3ds>eci</Eci3ds>}, data + assert_match %r{<ThreeDSVersion>2.1.0</ThreeDSVersion>}, data + assert_match %r{<DSTransactionId>ds_transaction_id</DSTransactionId>}, data + end.respond_with(successful_purchase_response) + end + private def pre_scrubbed - <<-PRE_SCRUBBED + <<~PRE_SCRUBBED opening connection to api-3t.sandbox.paypal.com:443... opened starting SSL for api-3t.sandbox.paypal.com:443... @@ -636,7 +667,7 @@ def pre_scrubbed end def post_scrubbed - <<-POST_SCRUBBED + <<~POST_SCRUBBED opening connection to api-3t.sandbox.paypal.com:443... opened starting SSL for api-3t.sandbox.paypal.com:443... @@ -658,763 +689,775 @@ def post_scrubbed end def successful_purchase_response - <<-RESPONSE -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:market="urn:ebay:apis:Market" xmlns:auction="urn:ebay:apis:Auction" xmlns:sizeship="urn:ebay:api:PayPalAPI/sizeship.xsd" xmlns:ship="urn:ebay:apis:ship" xmlns:skype="urn:ebay:apis:skype" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> - <SOAP-ENV:Header> - <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"/> - <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> - <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> - <Username xsi:type="xs:string"/> - <Password xsi:type="xs:string"/> - <Subject xsi:type="xs:string"/> - </Credentials> - </RequesterCredentials> - </SOAP-ENV:Header> - <SOAP-ENV:Body id="_0"> - <DoDirectPaymentResponse xmlns="urn:ebay:api:PayPalAPI"> - <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2008-01-06T23:41:25Z</Timestamp> - <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Success</Ack> - <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">fee61882e6f47</CorrelationID> - <Version xmlns="urn:ebay:apis:eBLBaseComponents">2.000000</Version> - <Build xmlns="urn:ebay:apis:eBLBaseComponents">1.0006</Build> - <Amount xsi:type="cc:BasicAmountType" currencyID="USD">3.00</Amount> - <AVSCode xsi:type="xs:string">X</AVSCode> - <CVV2Code xsi:type="xs:string">M</CVV2Code> - <TransactionID>62U664727W5914806</TransactionID> - </DoDirectPaymentResponse> - </SOAP-ENV:Body> -</SOAP-ENV:Envelope> + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:market="urn:ebay:apis:Market" xmlns:auction="urn:ebay:apis:Auction" xmlns:sizeship="urn:ebay:api:PayPalAPI/sizeship.xsd" xmlns:ship="urn:ebay:apis:ship" xmlns:skype="urn:ebay:apis:skype" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> + <SOAP-ENV:Header> + <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"/> + <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> + <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> + <Username xsi:type="xs:string"/> + <Password xsi:type="xs:string"/> + <Subject xsi:type="xs:string"/> + </Credentials> + </RequesterCredentials> + </SOAP-ENV:Header> + <SOAP-ENV:Body id="_0"> + <DoDirectPaymentResponse xmlns="urn:ebay:api:PayPalAPI"> + <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2008-01-06T23:41:25Z</Timestamp> + <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Success</Ack> + <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">fee61882e6f47</CorrelationID> + <Version xmlns="urn:ebay:apis:eBLBaseComponents">2.000000</Version> + <Build xmlns="urn:ebay:apis:eBLBaseComponents">1.0006</Build> + <Amount xsi:type="cc:BasicAmountType" currencyID="USD">3.00</Amount> + <AVSCode xsi:type="xs:string">X</AVSCode> + <CVV2Code xsi:type="xs:string">M</CVV2Code> + <TransactionID>62U664727W5914806</TransactionID> + </DoDirectPaymentResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> RESPONSE end def successful_reference_purchase_response - <<-RESPONSE -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:market="urn:ebay:apis:Market" xmlns:auction="urn:ebay:apis:Auction" xmlns:sizeship="urn:ebay:api:PayPalAPI/sizeship.xsd" xmlns:ship="urn:ebay:apis:ship" xmlns:skype="urn:ebay:apis:skype" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> - <SOAP-ENV:Header> - <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"/> - <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> - <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> - <Username xsi:type="xs:string"/> - <Password xsi:type="xs:string"/> - <Subject xsi:type="xs:string"/> - </Credentials> - </RequesterCredentials> - </SOAP-ENV:Header> - <SOAP-ENV:Body id="_0"> - <DoReferenceTransactionResponse xmlns="urn:ebay:api:PayPalAPI"> - <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2008-01-06T23:41:25Z</Timestamp> - <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Success</Ack> - <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">fee61882e6f47</CorrelationID> - <Version xmlns="urn:ebay:apis:eBLBaseComponents">2.000000</Version> - <Build xmlns="urn:ebay:apis:eBLBaseComponents">1.0006</Build> - <Amount xsi:type="cc:BasicAmountType" currencyID="USD">3.00</Amount> - <AVSCode xsi:type="xs:string">X</AVSCode> - <CVV2Code xsi:type="xs:string">M</CVV2Code> - <TransactionID>62U664727W5915049</TransactionID> - </DoReferenceTransactionResponse> - </SOAP-ENV:Body> -</SOAP-ENV:Envelope> + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:market="urn:ebay:apis:Market" xmlns:auction="urn:ebay:apis:Auction" xmlns:sizeship="urn:ebay:api:PayPalAPI/sizeship.xsd" xmlns:ship="urn:ebay:apis:ship" xmlns:skype="urn:ebay:apis:skype" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> + <SOAP-ENV:Header> + <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"/> + <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> + <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> + <Username xsi:type="xs:string"/> + <Password xsi:type="xs:string"/> + <Subject xsi:type="xs:string"/> + </Credentials> + </RequesterCredentials> + </SOAP-ENV:Header> + <SOAP-ENV:Body id="_0"> + <DoReferenceTransactionResponse xmlns="urn:ebay:api:PayPalAPI"> + <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2008-01-06T23:41:25Z</Timestamp> + <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Success</Ack> + <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">fee61882e6f47</CorrelationID> + <Version xmlns="urn:ebay:apis:eBLBaseComponents">2.000000</Version> + <Build xmlns="urn:ebay:apis:eBLBaseComponents">1.0006</Build> + <Amount xsi:type="cc:BasicAmountType" currencyID="USD">3.00</Amount> + <AVSCode xsi:type="xs:string">X</AVSCode> + <CVV2Code xsi:type="xs:string">M</CVV2Code> + <TransactionID>62U664727W5915049</TransactionID> + </DoReferenceTransactionResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> RESPONSE end def failed_purchase_response - <<-RESPONSE -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:market="urn:ebay:apis:Market" xmlns:auction="urn:ebay:apis:Auction" xmlns:sizeship="urn:ebay:api:PayPalAPI/sizeship.xsd" xmlns:ship="urn:ebay:apis:ship" xmlns:skype="urn:ebay:apis:skype" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> - <SOAP-ENV:Header> - <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"/> - <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> - <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> - <Username xsi:type="xs:string"/> - <Password xsi:type="xs:string"/> - <Subject xsi:type="xs:string"/> - </Credentials> - </RequesterCredentials> - </SOAP-ENV:Header> - <SOAP-ENV:Body id="_0"> - <DoDirectPaymentResponse xmlns="urn:ebay:api:PayPalAPI"> - <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2008-01-06T23:41:25Z</Timestamp> - <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Failure</Ack> - <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">fee61882e6f47</CorrelationID> - <Version xmlns="urn:ebay:apis:eBLBaseComponents">2.000000</Version> - <Build xmlns="urn:ebay:apis:eBLBaseComponents">1.0006</Build> - <Amount xsi:type="cc:BasicAmountType" currencyID="USD">3.00</Amount> - <AVSCode xsi:type="xs:string">X</AVSCode> - <CVV2Code xsi:type="xs:string">M</CVV2Code> - </DoDirectPaymentResponse> - </SOAP-ENV:Body> -</SOAP-ENV:Envelope> + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:market="urn:ebay:apis:Market" xmlns:auction="urn:ebay:apis:Auction" xmlns:sizeship="urn:ebay:api:PayPalAPI/sizeship.xsd" xmlns:ship="urn:ebay:apis:ship" xmlns:skype="urn:ebay:apis:skype" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> + <SOAP-ENV:Header> + <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"/> + <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> + <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> + <Username xsi:type="xs:string"/> + <Password xsi:type="xs:string"/> + <Subject xsi:type="xs:string"/> + </Credentials> + </RequesterCredentials> + </SOAP-ENV:Header> + <SOAP-ENV:Body id="_0"> + <DoDirectPaymentResponse xmlns="urn:ebay:api:PayPalAPI"> + <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2008-01-06T23:41:25Z</Timestamp> + <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Failure</Ack> + <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">fee61882e6f47</CorrelationID> + <Version xmlns="urn:ebay:apis:eBLBaseComponents">2.000000</Version> + <Build xmlns="urn:ebay:apis:eBLBaseComponents">1.0006</Build> + <Amount xsi:type="cc:BasicAmountType" currencyID="USD">3.00</Amount> + <AVSCode xsi:type="xs:string">X</AVSCode> + <CVV2Code xsi:type="xs:string">M</CVV2Code> + </DoDirectPaymentResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> RESPONSE end def successful_zero_dollar_auth_response - <<-RESPONSE -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> - <SOAP-ENV:Header> - <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"></Security> - <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> - <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> - <Username xsi:type="xs:string"> </Username> - <Password xsi:type="xs:string"></Password> - <Signature xsi:type="xs:string"> </Signature> - <Subject xsi:type="xs:string"> </Subject> - </Credentials> - </RequesterCredentials> - </SOAP-ENV:Header> - <SOAP-ENV:Body id="_0"> - <DoDirectPaymentResponse xmlns="urn:ebay:api:PayPalAPI"> - <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2014-06-27T18:14:48Z</Timestamp> - <Ack xmlns="urn:ebay:apis:eBLBaseComponents">SuccessWithWarning</Ack> - <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">e33ce283dd3d3</CorrelationID> - <Errors xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:ErrorType"> - <ShortMessage xsi:type="xs:string">Credit card verified.</ShortMessage> - <LongMessage xsi:type="xs:string">This card authorization verification is not a payment transaction.</LongMessage> - <ErrorCode xsi:type="xs:token">10574</ErrorCode> - <SeverityCode xmlns="urn:ebay:apis:eBLBaseComponents">Warning</SeverityCode> - </Errors> - <Version xmlns="urn:ebay:apis:eBLBaseComponents">72</Version> - <Build xmlns="urn:ebay:apis:eBLBaseComponents">11660982</Build> - <Amount xsi:type="cc:BasicAmountType" currencyID="USD">0.00</Amount> - <AVSCode xsi:type="xs:string">X</AVSCode><CVV2Code xsi:type="xs:string">M</CVV2Code> - <TransactionID>86D41672SH9764158</TransactionID> - </DoDirectPaymentResponse> - </SOAP-ENV:Body> -</SOAP-ENV:Envelope> + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> + <SOAP-ENV:Header> + <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"></Security> + <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> + <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> + <Username xsi:type="xs:string"> </Username> + <Password xsi:type="xs:string"></Password> + <Signature xsi:type="xs:string"> </Signature> + <Subject xsi:type="xs:string"> </Subject> + </Credentials> + </RequesterCredentials> + </SOAP-ENV:Header> + <SOAP-ENV:Body id="_0"> + <DoDirectPaymentResponse xmlns="urn:ebay:api:PayPalAPI"> + <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2014-06-27T18:14:48Z</Timestamp> + <Ack xmlns="urn:ebay:apis:eBLBaseComponents">SuccessWithWarning</Ack> + <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">e33ce283dd3d3</CorrelationID> + <Errors xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:ErrorType"> + <ShortMessage xsi:type="xs:string">Credit card verified.</ShortMessage> + <LongMessage xsi:type="xs:string">This card authorization verification is not a payment transaction.</LongMessage> + <ErrorCode xsi:type="xs:token">10574</ErrorCode> + <SeverityCode xmlns="urn:ebay:apis:eBLBaseComponents">Warning</SeverityCode> + </Errors> + <Version xmlns="urn:ebay:apis:eBLBaseComponents">72</Version> + <Build xmlns="urn:ebay:apis:eBLBaseComponents">11660982</Build> + <Amount xsi:type="cc:BasicAmountType" currencyID="USD">0.00</Amount> + <AVSCode xsi:type="xs:string">X</AVSCode><CVV2Code xsi:type="xs:string">M</CVV2Code> + <TransactionID>86D41672SH9764158</TransactionID> + </DoDirectPaymentResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> RESPONSE end def failed_zero_dollar_auth_response - <<-RESPONSE -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" xmlns:ns="urn:ebay:api:PayPalAPI" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> - <SOAP-ENV:Header> - <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType" /> - <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> - <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> - <Username xsi:type="xs:string" /> - <Password xsi:type="xs:string" /> - <Signature xsi:type="xs:string" /> - <Subject xsi:type="xs:string" /> - </Credentials> - </RequesterCredentials> - </SOAP-ENV:Header> - <SOAP-ENV:Body id="_0"> - <DoDirectPaymentResponse xmlns="urn:ebay:api:PayPalAPI"> - <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2014-06-27T18:25:51Z</Timestamp> - <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Failure</Ack> - <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">5dda14853a55d</CorrelationID> - <Errors xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:ErrorType"> - <ShortMessage xsi:type="xs:string">Invalid Data</ShortMessage> - <LongMessage xsi:type="xs:string">This transaction cannot be processed. Please enter a valid credit card number and type.</LongMessage> - <ErrorCode xsi:type="xs:token">10527</ErrorCode> - <SeverityCode>Error</SeverityCode> - </Errors> - <Version xmlns="urn:ebay:apis:eBLBaseComponents">72</Version> - <Build xmlns="urn:ebay:apis:eBLBaseComponents">11660982</Build> - <Amount xsi:type="cc:BasicAmountType" currencyID="USD">0.00</Amount> - </DoDirectPaymentResponse> - </SOAP-ENV:Body> -</SOAP-ENV:Envelope> + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" xmlns:ns="urn:ebay:api:PayPalAPI" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <SOAP-ENV:Header> + <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType" /> + <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> + <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> + <Username xsi:type="xs:string" /> + <Password xsi:type="xs:string" /> + <Signature xsi:type="xs:string" /> + <Subject xsi:type="xs:string" /> + </Credentials> + </RequesterCredentials> + </SOAP-ENV:Header> + <SOAP-ENV:Body id="_0"> + <DoDirectPaymentResponse xmlns="urn:ebay:api:PayPalAPI"> + <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2014-06-27T18:25:51Z</Timestamp> + <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Failure</Ack> + <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">5dda14853a55d</CorrelationID> + <Errors xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:ErrorType"> + <ShortMessage xsi:type="xs:string">Invalid Data</ShortMessage> + <LongMessage xsi:type="xs:string">This transaction cannot be processed. Please enter a valid credit card number and type.</LongMessage> + <ErrorCode xsi:type="xs:token">10527</ErrorCode> + <SeverityCode>Error</SeverityCode> + </Errors> + <Version xmlns="urn:ebay:apis:eBLBaseComponents">72</Version> + <Build xmlns="urn:ebay:apis:eBLBaseComponents">11660982</Build> + <Amount xsi:type="cc:BasicAmountType" currencyID="USD">0.00</Amount> + </DoDirectPaymentResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> RESPONSE end def successful_one_dollar_auth_response - <<-RESPONSE -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" xmlns:ns="urn:ebay:api:PayPalAPI" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> - <SOAP-ENV:Header> - <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType" /> - <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> - <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> - <Username xsi:type="xs:string" /> - <Password xsi:type="xs:string" /> - <Signature xsi:type="xs:string" /> - <Subject xsi:type="xs:string" /> - </Credentials> - </RequesterCredentials> - </SOAP-ENV:Header> - <SOAP-ENV:Body id="_0"> - <DoDirectPaymentResponse xmlns="urn:ebay:api:PayPalAPI"> - <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2014-06-27T18:39:40Z</Timestamp> - <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Success</Ack> - <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">814bcb1ced3d</CorrelationID> - <Version xmlns="urn:ebay:apis:eBLBaseComponents">72</Version> - <Build xmlns="urn:ebay:apis:eBLBaseComponents">11660982</Build> - <Amount xsi:type="cc:BasicAmountType" currencyID="USD">1.00</Amount> - <AVSCode xsi:type="xs:string">X</AVSCode> - <CVV2Code xsi:type="xs:string">M</CVV2Code> - <TransactionID>521683708W7313256</TransactionID> - </DoDirectPaymentResponse> - </SOAP-ENV:Body> -</SOAP-ENV:Envelope> + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" xmlns:ns="urn:ebay:api:PayPalAPI" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <SOAP-ENV:Header> + <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType" /> + <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> + <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> + <Username xsi:type="xs:string" /> + <Password xsi:type="xs:string" /> + <Signature xsi:type="xs:string" /> + <Subject xsi:type="xs:string" /> + </Credentials> + </RequesterCredentials> + </SOAP-ENV:Header> + <SOAP-ENV:Body id="_0"> + <DoDirectPaymentResponse xmlns="urn:ebay:api:PayPalAPI"> + <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2014-06-27T18:39:40Z</Timestamp> + <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Success</Ack> + <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">814bcb1ced3d</CorrelationID> + <Version xmlns="urn:ebay:apis:eBLBaseComponents">72</Version> + <Build xmlns="urn:ebay:apis:eBLBaseComponents">11660982</Build> + <Amount xsi:type="cc:BasicAmountType" currencyID="USD">1.00</Amount> + <AVSCode xsi:type="xs:string">X</AVSCode> + <CVV2Code xsi:type="xs:string">M</CVV2Code> + <TransactionID>521683708W7313256</TransactionID> + </DoDirectPaymentResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> RESPONSE end def failed_one_dollar_auth_response - <<-RESPONSE -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" xmlns:ns="urn:ebay:api:PayPalAPI" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> - <SOAP-ENV:Header> - <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType" /> - <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> - <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> - <Username xsi:type="xs:string" /> - <Password xsi:type="xs:string" /> - <Signature xsi:type="xs:string" /> - <Subject xsi:type="xs:string" /> - </Credentials> - </RequesterCredentials> - </SOAP-ENV:Header> - <SOAP-ENV:Body id="_0"> - <DoDirectPaymentResponse xmlns="urn:ebay:api:PayPalAPI"> - <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2014-06-27T18:47:18Z</Timestamp> - <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Failure</Ack> - <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">f3ab2d6fc76e4</CorrelationID> - <Errors xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:ErrorType"> - <ShortMessage xsi:type="xs:string">Invalid Data</ShortMessage> - <LongMessage xsi:type="xs:string">This transaction cannot be processed. Please enter a valid credit card number and type.</LongMessage> - <ErrorCode xsi:type="xs:token">10527</ErrorCode> - <SeverityCode>Error</SeverityCode> - </Errors> - <Version xmlns="urn:ebay:apis:eBLBaseComponents">72</Version> - <Build xmlns="urn:ebay:apis:eBLBaseComponents">11660982</Build> - <Amount xsi:type="cc:BasicAmountType" currencyID="USD">1.00</Amount> - </DoDirectPaymentResponse> - </SOAP-ENV:Body> -</SOAP-ENV:Envelope> + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" xmlns:ns="urn:ebay:api:PayPalAPI" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <SOAP-ENV:Header> + <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType" /> + <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> + <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> + <Username xsi:type="xs:string" /> + <Password xsi:type="xs:string" /> + <Signature xsi:type="xs:string" /> + <Subject xsi:type="xs:string" /> + </Credentials> + </RequesterCredentials> + </SOAP-ENV:Header> + <SOAP-ENV:Body id="_0"> + <DoDirectPaymentResponse xmlns="urn:ebay:api:PayPalAPI"> + <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2014-06-27T18:47:18Z</Timestamp> + <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Failure</Ack> + <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">f3ab2d6fc76e4</CorrelationID> + <Errors xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:ErrorType"> + <ShortMessage xsi:type="xs:string">Invalid Data</ShortMessage> + <LongMessage xsi:type="xs:string">This transaction cannot be processed. Please enter a valid credit card number and type.</LongMessage> + <ErrorCode xsi:type="xs:token">10527</ErrorCode> + <SeverityCode>Error</SeverityCode> + </Errors> + <Version xmlns="urn:ebay:apis:eBLBaseComponents">72</Version> + <Build xmlns="urn:ebay:apis:eBLBaseComponents">11660982</Build> + <Amount xsi:type="cc:BasicAmountType" currencyID="USD">1.00</Amount> + </DoDirectPaymentResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> RESPONSE end def response_with_error_code(error_code) - <<-RESPONSE - <?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" xmlns:ns="urn:ebay:api:PayPalAPI" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> - <SOAP-ENV:Header> - <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType" /> - <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> - <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> - <Username xsi:type="xs:string" /> - <Password xsi:type="xs:string" /> - <Signature xsi:type="xs:string" /> - <Subject xsi:type="xs:string" /> - </Credentials> - </RequesterCredentials> - </SOAP-ENV:Header> - <SOAP-ENV:Body id="_0"> - <DoDirectPaymentResponse xmlns="urn:ebay:api:PayPalAPI"> - <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2014-06-27T18:47:18Z</Timestamp> - <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Failure</Ack> - <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">f3ab2d6fc76e4</CorrelationID> - <Errors xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:ErrorType"> - <ShortMessage xsi:type="xs:string">Invalid Data</ShortMessage> - <LongMessage xsi:type="xs:string">This transaction cannot be processed. Please enter a valid credit card number and type.</LongMessage> - <ErrorCode xsi:type="xs:token">#{error_code}</ErrorCode> - <SeverityCode>Error</SeverityCode> - </Errors> - <Version xmlns="urn:ebay:apis:eBLBaseComponents">72</Version> - <Build xmlns="urn:ebay:apis:eBLBaseComponents">11660982</Build> - <Amount xsi:type="cc:BasicAmountType" currencyID="USD">1.00</Amount> - </DoDirectPaymentResponse> - </SOAP-ENV:Body> -</SOAP-ENV:Envelope> + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" xmlns:ns="urn:ebay:api:PayPalAPI" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <SOAP-ENV:Header> + <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType" /> + <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> + <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> + <Username xsi:type="xs:string" /> + <Password xsi:type="xs:string" /> + <Signature xsi:type="xs:string" /> + <Subject xsi:type="xs:string" /> + </Credentials> + </RequesterCredentials> + </SOAP-ENV:Header> + <SOAP-ENV:Body id="_0"> + <DoDirectPaymentResponse xmlns="urn:ebay:api:PayPalAPI"> + <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2014-06-27T18:47:18Z</Timestamp> + <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Failure</Ack> + <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">f3ab2d6fc76e4</CorrelationID> + <Errors xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:ErrorType"> + <ShortMessage xsi:type="xs:string">Invalid Data</ShortMessage> + <LongMessage xsi:type="xs:string">This transaction cannot be processed. Please enter a valid credit card number and type.</LongMessage> + <ErrorCode xsi:type="xs:token">#{error_code}</ErrorCode> + <SeverityCode>Error</SeverityCode> + </Errors> + <Version xmlns="urn:ebay:apis:eBLBaseComponents">72</Version> + <Build xmlns="urn:ebay:apis:eBLBaseComponents">11660982</Build> + <Amount xsi:type="cc:BasicAmountType" currencyID="USD">1.00</Amount> + </DoDirectPaymentResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> RESPONSE end def successful_void_response - <<-RESPONSE -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" xmlns:ns="urn:ebay:api:PayPalAPI" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> - <SOAP-ENV:Header> - <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType" /> - <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> - <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> - <Username xsi:type="xs:string" /> - <Password xsi:type="xs:string" /> - <Signature xsi:type="xs:string" /> - <Subject xsi:type="xs:string" /> - </Credentials> - </RequesterCredentials> - </SOAP-ENV:Header> - <SOAP-ENV:Body id="_0"> - <DoVoidResponse xmlns="urn:ebay:api:PayPalAPI"> - <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2014-06-27T18:39:41Z</Timestamp> - <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Success</Ack> - <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">5c184c86a25bc</CorrelationID> - <Version xmlns="urn:ebay:apis:eBLBaseComponents">72</Version> - <Build xmlns="urn:ebay:apis:eBLBaseComponents">11624049</Build> - <AuthorizationID xsi:type="xs:string">521683708W7313256</AuthorizationID> - </DoVoidResponse> - </SOAP-ENV:Body> -</SOAP-ENV:Envelope> + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" xmlns:ns="urn:ebay:api:PayPalAPI" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <SOAP-ENV:Header> + <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType" /> + <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> + <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> + <Username xsi:type="xs:string" /> + <Password xsi:type="xs:string" /> + <Signature xsi:type="xs:string" /> + <Subject xsi:type="xs:string" /> + </Credentials> + </RequesterCredentials> + </SOAP-ENV:Header> + <SOAP-ENV:Body id="_0"> + <DoVoidResponse xmlns="urn:ebay:api:PayPalAPI"> + <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2014-06-27T18:39:41Z</Timestamp> + <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Success</Ack> + <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">5c184c86a25bc</CorrelationID> + <Version xmlns="urn:ebay:apis:eBLBaseComponents">72</Version> + <Build xmlns="urn:ebay:apis:eBLBaseComponents">11624049</Build> + <AuthorizationID xsi:type="xs:string">521683708W7313256</AuthorizationID> + </DoVoidResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> RESPONSE end def failed_void_response - <<-RESPONSE -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" xmlns:ns="urn:ebay:api:PayPalAPI" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> - <SOAP-ENV:Header> - <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType" /> - <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> - <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> - <Username xsi:type="xs:string" /> - <Password xsi:type="xs:string" /> - <Signature xsi:type="xs:string" /> - <Subject xsi:type="xs:string" /> - </Credentials> - </RequesterCredentials> - </SOAP-ENV:Header> - <SOAP-ENV:Body id="_0"> - <DoVoidResponse xmlns="urn:ebay:api:PayPalAPI"> - <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2014-06-27T18:50:11Z</Timestamp> - <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Failure</Ack> - <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">e99444d222eaf</CorrelationID> - <Errors xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:ErrorType"> - <ShortMessage xsi:type="xs:string">Transaction refused because of an invalid argument. See additional error messages for details.</ShortMessage> - <LongMessage xsi:type="xs:string">The transaction id is not valid</LongMessage> - <ErrorCode xsi:type="xs:token">10004</ErrorCode> - <SeverityCode>Error</SeverityCode> - </Errors> - <Version xmlns="urn:ebay:apis:eBLBaseComponents">72</Version> - <Build xmlns="urn:ebay:apis:eBLBaseComponents">11624049</Build> - <AuthorizationID xsi:type="xs:string" /> - </DoVoidResponse> - </SOAP-ENV:Body> -</SOAP-ENV:Envelope> + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" xmlns:ns="urn:ebay:api:PayPalAPI" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <SOAP-ENV:Header> + <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType" /> + <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> + <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> + <Username xsi:type="xs:string" /> + <Password xsi:type="xs:string" /> + <Signature xsi:type="xs:string" /> + <Subject xsi:type="xs:string" /> + </Credentials> + </RequesterCredentials> + </SOAP-ENV:Header> + <SOAP-ENV:Body id="_0"> + <DoVoidResponse xmlns="urn:ebay:api:PayPalAPI"> + <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2014-06-27T18:50:11Z</Timestamp> + <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Failure</Ack> + <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">e99444d222eaf</CorrelationID> + <Errors xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:ErrorType"> + <ShortMessage xsi:type="xs:string">Transaction refused because of an invalid argument. See additional error messages for details.</ShortMessage> + <LongMessage xsi:type="xs:string">The transaction id is not valid</LongMessage> + <ErrorCode xsi:type="xs:token">10004</ErrorCode> + <SeverityCode>Error</SeverityCode> + </Errors> + <Version xmlns="urn:ebay:apis:eBLBaseComponents">72</Version> + <Build xmlns="urn:ebay:apis:eBLBaseComponents">11624049</Build> + <AuthorizationID xsi:type="xs:string" /> + </DoVoidResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> RESPONSE end def paypal_timeout_error_response - <<-RESPONSE -<?xml version='1.0' encoding='UTF-8'?> -<SOAP-ENV:Envelope xmlns:cc='urn:ebay:apis:CoreComponentTypes' xmlns:sizeship='urn:ebay:api:PayPalAPI/sizeship.xsd' xmlns:SOAP-ENV='http://schemas.xmlsoap.org/soap/envelope/' xmlns:SOAP-ENC='http://schemas.xmlsoap.org/soap/encoding/' xmlns:saml='urn:oasis:names:tc:SAML:1.0:assertion' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:wsu='http://schemas.xmlsoap.org/ws/2002/07/utility' xmlns:ebl='urn:ebay:apis:eBLBaseComponents' xmlns:ds='http://www.w3.org/2000/09/xmldsig#' xmlns:xs='http://www.w3.org/2001/XMLSchema' xmlns:ns='urn:ebay:api:PayPalAPI' xmlns:market='urn:ebay:apis:Market' xmlns:ship='urn:ebay:apis:ship' xmlns:auction='urn:ebay:apis:Auction' xmlns:wsse='http://schemas.xmlsoap.org/ws/2002/12/secext' xmlns:xsd='http://www.w3.org/2001/XMLSchema'> - <SOAP-ENV:Header> - <Security xsi:type='wsse:SecurityType' xmlns='http://schemas.xmlsoap.org/ws/2002/12/secext'/> - <RequesterCredentials xsi:type='ebl:CustomSecurityHeaderType' xmlns='urn:ebay:api:PayPalAPI'> - <Credentials xsi:type='ebl:UserIdPasswordType' xmlns='urn:ebay:apis:eBLBaseComponents'> - <Username xsi:type='xs:string'/> - <Password xsi:type='xs:string'/> - <Subject xsi:type='xs:string'/> - </Credentials> - </RequesterCredentials> - </SOAP-ENV:Header> - <SOAP-ENV:Body id='_0'> - <SOAP-ENV:Fault> - <faultcode>SOAP-ENV:Server</faultcode> - <faultstring>Internal error</faultstring> - <detail>Timeout processing request</detail> - </SOAP-ENV:Fault> - </SOAP-ENV:Body> -</SOAP-ENV:Envelope> + <<~RESPONSE + <?xml version='1.0' encoding='UTF-8'?> + <SOAP-ENV:Envelope xmlns:cc='urn:ebay:apis:CoreComponentTypes' xmlns:sizeship='urn:ebay:api:PayPalAPI/sizeship.xsd' xmlns:SOAP-ENV='http://schemas.xmlsoap.org/soap/envelope/' xmlns:SOAP-ENC='http://schemas.xmlsoap.org/soap/encoding/' xmlns:saml='urn:oasis:names:tc:SAML:1.0:assertion' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:wsu='http://schemas.xmlsoap.org/ws/2002/07/utility' xmlns:ebl='urn:ebay:apis:eBLBaseComponents' xmlns:ds='http://www.w3.org/2000/09/xmldsig#' xmlns:xs='http://www.w3.org/2001/XMLSchema' xmlns:ns='urn:ebay:api:PayPalAPI' xmlns:market='urn:ebay:apis:Market' xmlns:ship='urn:ebay:apis:ship' xmlns:auction='urn:ebay:apis:Auction' xmlns:wsse='http://schemas.xmlsoap.org/ws/2002/12/secext' xmlns:xsd='http://www.w3.org/2001/XMLSchema'> + <SOAP-ENV:Header> + <Security xsi:type='wsse:SecurityType' xmlns='http://schemas.xmlsoap.org/ws/2002/12/secext'/> + <RequesterCredentials xsi:type='ebl:CustomSecurityHeaderType' xmlns='urn:ebay:api:PayPalAPI'> + <Credentials xsi:type='ebl:UserIdPasswordType' xmlns='urn:ebay:apis:eBLBaseComponents'> + <Username xsi:type='xs:string'/> + <Password xsi:type='xs:string'/> + <Subject xsi:type='xs:string'/> + </Credentials> + </RequesterCredentials> + </SOAP-ENV:Header> + <SOAP-ENV:Body id='_0'> + <SOAP-ENV:Fault> + <faultcode>SOAP-ENV:Server</faultcode> + <faultstring>Internal error</faultstring> + <detail>Timeout processing request</detail> + </SOAP-ENV:Fault> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> RESPONSE end def successful_reauthorization_response - <<-RESPONSE -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope - xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" - xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema" - xmlns:xs="http://www.w3.org/2001/XMLSchema" - xmlns:cc="urn:ebay:apis:CoreComponentTypes" - xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" - xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" - xmlns:ds="http://www.w3.org/2000/09/xmldsig#" - xmlns:market="urn:ebay:apis:Market" - xmlns:auction="urn:ebay:apis:Auction" - xmlns:sizeship="urn:ebay:api:PayPalAPI/sizeship.xsd" - xmlns:ship="urn:ebay:apis:ship" - xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" - xmlns:ebl="urn:ebay:apis:eBLBaseComponents" - xmlns:ns="urn:ebay:api:PayPalAPI"> - <SOAP-ENV:Header> - <Security - xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" - xsi:type="wsse:SecurityType"> - </Security> - <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" - xsi:type="ebl:CustomSecurityHeaderType"> - <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" - xsi:type="ebl:UserIdPasswordType"> - <Username xsi:type="xs:string"></Username> - <Password xsi:type="xs:string"></Password> - <Subject xsi:type="xs:string"></Subject> - </Credentials> - </RequesterCredentials> - </SOAP-ENV:Header> - <SOAP-ENV:Body id="_0"> - <DoReauthorizationResponse xmlns="urn:ebay:api:PayPalAPI"> - <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2007-03-04T23:34:42Z</Timestamp> - <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Success</Ack> - <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">e444ddb7b3ed9</CorrelationID> - <Version xmlns="urn:ebay:apis:eBLBaseComponents">2.000000</Version> - <Build xmlns="urn:ebay:apis:eBLBaseComponents">1.0006</Build> - <AuthorizationID xsi:type="ebl:AuthorizationId">1TX27389GX108740X</AuthorizationID> - </DoReauthorizationResponse> - </SOAP-ENV:Body> -</SOAP-ENV:Envelope> + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope + xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" + xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:xsd="http://www.w3.org/2001/XMLSchema" + xmlns:xs="http://www.w3.org/2001/XMLSchema" + xmlns:cc="urn:ebay:apis:CoreComponentTypes" + xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" + xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" + xmlns:ds="http://www.w3.org/2000/09/xmldsig#" + xmlns:market="urn:ebay:apis:Market" + xmlns:auction="urn:ebay:apis:Auction" + xmlns:sizeship="urn:ebay:api:PayPalAPI/sizeship.xsd" + xmlns:ship="urn:ebay:apis:ship" + xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" + xmlns:ebl="urn:ebay:apis:eBLBaseComponents" + xmlns:ns="urn:ebay:api:PayPalAPI"> + <SOAP-ENV:Header> + <Security + xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" + xsi:type="wsse:SecurityType"> + </Security> + <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" + xsi:type="ebl:CustomSecurityHeaderType"> + <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" + xsi:type="ebl:UserIdPasswordType"> + <Username xsi:type="xs:string"></Username> + <Password xsi:type="xs:string"></Password> + <Subject xsi:type="xs:string"></Subject> + </Credentials> + </RequesterCredentials> + </SOAP-ENV:Header> + <SOAP-ENV:Body id="_0"> + <DoReauthorizationResponse xmlns="urn:ebay:api:PayPalAPI"> + <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2007-03-04T23:34:42Z</Timestamp> + <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Success</Ack> + <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">e444ddb7b3ed9</CorrelationID> + <Version xmlns="urn:ebay:apis:eBLBaseComponents">2.000000</Version> + <Build xmlns="urn:ebay:apis:eBLBaseComponents">1.0006</Build> + <AuthorizationID xsi:type="ebl:AuthorizationId">1TX27389GX108740X</AuthorizationID> + </DoReauthorizationResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> RESPONSE end def successful_with_warning_reauthorization_response - <<-RESPONSE -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope - xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" - xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema" - xmlns:xs="http://www.w3.org/2001/XMLSchema" - xmlns:cc="urn:ebay:apis:CoreComponentTypes" - xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" - xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" - xmlns:ds="http://www.w3.org/2000/09/xmldsig#" - xmlns:market="urn:ebay:apis:Market" - xmlns:auction="urn:ebay:apis:Auction" - xmlns:sizeship="urn:ebay:api:PayPalAPI/sizeship.xsd" - xmlns:ship="urn:ebay:apis:ship" - xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" - xmlns:ebl="urn:ebay:apis:eBLBaseComponents" - xmlns:ns="urn:ebay:api:PayPalAPI"> - <SOAP-ENV:Header> - <Security - xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" - xsi:type="wsse:SecurityType"> - </Security> - <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" - xsi:type="ebl:CustomSecurityHeaderType"> - <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" - xsi:type="ebl:UserIdPasswordType"> - <Username xsi:type="xs:string"></Username> - <Password xsi:type="xs:string"></Password> - <Subject xsi:type="xs:string"></Subject> - </Credentials> - </RequesterCredentials> - </SOAP-ENV:Header> - <SOAP-ENV:Body id="_0"> - <DoReauthorizationResponse xmlns="urn:ebay:api:PayPalAPI"> - <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2007-03-04T23:34:42Z</Timestamp> - <Ack xmlns="urn:ebay:apis:eBLBaseComponents">SuccessWithWarning</Ack> - <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">e444ddb7b3ed9</CorrelationID> - <Version xmlns="urn:ebay:apis:eBLBaseComponents">2.000000</Version> - <Build xmlns="urn:ebay:apis:eBLBaseComponents">1.0006</Build> - <AuthorizationID xsi:type="ebl:AuthorizationId">1TX27389GX108740X</AuthorizationID> - </DoReauthorizationResponse> - </SOAP-ENV:Body> -</SOAP-ENV:Envelope> + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope + xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" + xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:xsd="http://www.w3.org/2001/XMLSchema" + xmlns:xs="http://www.w3.org/2001/XMLSchema" + xmlns:cc="urn:ebay:apis:CoreComponentTypes" + xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" + xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" + xmlns:ds="http://www.w3.org/2000/09/xmldsig#" + xmlns:market="urn:ebay:apis:Market" + xmlns:auction="urn:ebay:apis:Auction" + xmlns:sizeship="urn:ebay:api:PayPalAPI/sizeship.xsd" + xmlns:ship="urn:ebay:apis:ship" + xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" + xmlns:ebl="urn:ebay:apis:eBLBaseComponents" + xmlns:ns="urn:ebay:api:PayPalAPI"> + <SOAP-ENV:Header> + <Security + xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" + xsi:type="wsse:SecurityType"> + </Security> + <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" + xsi:type="ebl:CustomSecurityHeaderType"> + <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" + xsi:type="ebl:UserIdPasswordType"> + <Username xsi:type="xs:string"></Username> + <Password xsi:type="xs:string"></Password> + <Subject xsi:type="xs:string"></Subject> + </Credentials> + </RequesterCredentials> + </SOAP-ENV:Header> + <SOAP-ENV:Body id="_0"> + <DoReauthorizationResponse xmlns="urn:ebay:api:PayPalAPI"> + <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2007-03-04T23:34:42Z</Timestamp> + <Ack xmlns="urn:ebay:apis:eBLBaseComponents">SuccessWithWarning</Ack> + <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">e444ddb7b3ed9</CorrelationID> + <Version xmlns="urn:ebay:apis:eBLBaseComponents">2.000000</Version> + <Build xmlns="urn:ebay:apis:eBLBaseComponents">1.0006</Build> + <AuthorizationID xsi:type="ebl:AuthorizationId">1TX27389GX108740X</AuthorizationID> + </DoReauthorizationResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> RESPONSE end def fraud_review_response - <<-RESPONSE - <?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> - <SOAP-ENV:Header> - <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"/> - <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> - <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> - <Username xsi:type="xs:string"/> - <Password xsi:type="xs:string"/> - <Signature xsi:type="xs:string">An5ns1Kso7MWUdW4ErQKJJJ4qi4-Azffuo82oMt-Cv9I8QTOs-lG5sAv</Signature> - <Subject xsi:type="xs:string"/> - </Credentials> - </RequesterCredentials> - </SOAP-ENV:Header> - <SOAP-ENV:Body id="_0"> - <DoDirectPaymentResponse xmlns="urn:ebay:api:PayPalAPI"> - <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2008-07-04T19:27:39Z</Timestamp> - <Ack xmlns="urn:ebay:apis:eBLBaseComponents">SuccessWithWarning</Ack> - <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">205d8397e7ed</CorrelationID> - <Errors xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:ErrorType"> - <ShortMessage xsi:type="xs:string">Payment Pending your review in Fraud Management Filters</ShortMessage> - <LongMessage xsi:type="xs:string">Payment Pending your review in Fraud Management Filters</LongMessage> - <ErrorCode xsi:type="xs:token">11610</ErrorCode> - <SeverityCode xmlns="urn:ebay:apis:eBLBaseComponents">Warning</SeverityCode> - </Errors> - <Version xmlns="urn:ebay:apis:eBLBaseComponents">50.0</Version> - <Build xmlns="urn:ebay:apis:eBLBaseComponents">623197</Build> - <Amount xsi:type="cc:BasicAmountType" currencyID="USD">1500.00</Amount> - <AVSCode xsi:type="xs:string">X</AVSCode> - <CVV2Code xsi:type="xs:string">M</CVV2Code> - <TransactionID>5V117995ER6796022</TransactionID> - </DoDirectPaymentResponse> - </SOAP-ENV:Body> -</SOAP-ENV:Envelope> + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> + <SOAP-ENV:Header> + <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"/> + <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> + <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> + <Username xsi:type="xs:string"/> + <Password xsi:type="xs:string"/> + <Signature xsi:type="xs:string">An5ns1Kso7MWUdW4ErQKJJJ4qi4-Azffuo82oMt-Cv9I8QTOs-lG5sAv</Signature> + <Subject xsi:type="xs:string"/> + </Credentials> + </RequesterCredentials> + </SOAP-ENV:Header> + <SOAP-ENV:Body id="_0"> + <DoDirectPaymentResponse xmlns="urn:ebay:api:PayPalAPI"> + <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2008-07-04T19:27:39Z</Timestamp> + <Ack xmlns="urn:ebay:apis:eBLBaseComponents">SuccessWithWarning</Ack> + <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">205d8397e7ed</CorrelationID> + <Errors xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:ErrorType"> + <ShortMessage xsi:type="xs:string">Payment Pending your review in Fraud Management Filters</ShortMessage> + <LongMessage xsi:type="xs:string">Payment Pending your review in Fraud Management Filters</LongMessage> + <ErrorCode xsi:type="xs:token">11610</ErrorCode> + <SeverityCode xmlns="urn:ebay:apis:eBLBaseComponents">Warning</SeverityCode> + </Errors> + <Version xmlns="urn:ebay:apis:eBLBaseComponents">50.0</Version> + <Build xmlns="urn:ebay:apis:eBLBaseComponents">623197</Build> + <Amount xsi:type="cc:BasicAmountType" currencyID="USD">1500.00</Amount> + <AVSCode xsi:type="xs:string">X</AVSCode> + <CVV2Code xsi:type="xs:string">M</CVV2Code> + <TransactionID>5V117995ER6796022</TransactionID> + </DoDirectPaymentResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> RESPONSE end def failed_capture_due_to_pending_fraud_review - <<-RESPONSE -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:market="urn:ebay:apis:Market" xmlns:auction="urn:ebay:apis:Auction" xmlns:sizeship="urn:ebay:api:PayPalAPI/sizeship.xsd" xmlns:ship="urn:ebay:apis:ship" xmlns:skype="urn:ebay:apis:skype" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> - <SOAP-ENV:Header> - <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"/> - <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> - <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> - <Username xsi:type="xs:string"/> - <Password xsi:type="xs:string"/> - <Subject xsi:type="xs:string"/> - </Credentials> - </RequesterCredentials> - </SOAP-ENV:Header> - <SOAP-ENV:Body id="_0"> - <DoCaptureResponse xmlns="urn:ebay:api:PayPalAPI"> - <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2008-07-04T21:45:35Z</Timestamp> - <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Failure</Ack> - <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">32a3855bd35b7</CorrelationID> - <Errors xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:ErrorType"> - <ShortMessage xsi:type="xs:string">Transaction must be accepted in Fraud Management Filters before capture.</ShortMessage> - <LongMessage xsi:type="xs:string"/> - <ErrorCode xsi:type="xs:token">11612</ErrorCode> - <SeverityCode xmlns="urn:ebay:apis:eBLBaseComponents">Error</SeverityCode> - </Errors> - <Version xmlns="urn:ebay:apis:eBLBaseComponents">52.000000</Version> - <Build xmlns="urn:ebay:apis:eBLBaseComponents">588340</Build> - <DoCaptureResponseDetails xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:DoCaptureResponseDetailsType"> - <PaymentInfo xsi:type="ebl:PaymentInfoType"> - <TransactionType xsi:type="ebl:PaymentTransactionCodeType">none</TransactionType> - <PaymentType xsi:type="ebl:PaymentCodeType">none</PaymentType> - <PaymentStatus xsi:type="ebl:PaymentStatusCodeType">None</PaymentStatus> - <PendingReason xsi:type="ebl:PendingStatusCodeType">none</PendingReason> - <ReasonCode xsi:type="ebl:ReversalReasonCodeType">none</ReasonCode> - </PaymentInfo> - </DoCaptureResponseDetails> - </DoCaptureResponse> - </SOAP-ENV:Body> -</SOAP-ENV:Envelope> + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:market="urn:ebay:apis:Market" xmlns:auction="urn:ebay:apis:Auction" xmlns:sizeship="urn:ebay:api:PayPalAPI/sizeship.xsd" xmlns:ship="urn:ebay:apis:ship" xmlns:skype="urn:ebay:apis:skype" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> + <SOAP-ENV:Header> + <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"/> + <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> + <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> + <Username xsi:type="xs:string"/> + <Password xsi:type="xs:string"/> + <Subject xsi:type="xs:string"/> + </Credentials> + </RequesterCredentials> + </SOAP-ENV:Header> + <SOAP-ENV:Body id="_0"> + <DoCaptureResponse xmlns="urn:ebay:api:PayPalAPI"> + <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2008-07-04T21:45:35Z</Timestamp> + <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Failure</Ack> + <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">32a3855bd35b7</CorrelationID> + <Errors xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:ErrorType"> + <ShortMessage xsi:type="xs:string">Transaction must be accepted in Fraud Management Filters before capture.</ShortMessage> + <LongMessage xsi:type="xs:string"/> + <ErrorCode xsi:type="xs:token">11612</ErrorCode> + <SeverityCode xmlns="urn:ebay:apis:eBLBaseComponents">Error</SeverityCode> + </Errors> + <Version xmlns="urn:ebay:apis:eBLBaseComponents">52.000000</Version> + <Build xmlns="urn:ebay:apis:eBLBaseComponents">588340</Build> + <DoCaptureResponseDetails xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:DoCaptureResponseDetailsType"> + <PaymentInfo xsi:type="ebl:PaymentInfoType"> + <TransactionType xsi:type="ebl:PaymentTransactionCodeType">none</TransactionType> + <PaymentType xsi:type="ebl:PaymentCodeType">none</PaymentType> + <PaymentStatus xsi:type="ebl:PaymentStatusCodeType">None</PaymentStatus> + <PendingReason xsi:type="ebl:PendingStatusCodeType">none</PendingReason> + <ReasonCode xsi:type="ebl:ReversalReasonCodeType">none</ReasonCode> + </PaymentInfo> + </DoCaptureResponseDetails> + </DoCaptureResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> RESPONSE end def authentication_failed_response - <<-RESPONSE -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:market="urn:ebay:apis:Market" xmlns:auction="urn:ebay:apis:Auction" xmlns:sizeship="urn:ebay:api:PayPalAPI/sizeship.xsd" xmlns:ship="urn:ebay:apis:ship" xmlns:skype="urn:ebay:apis:skype" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> - <SOAP-ENV:Header> - <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"/> - <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> - <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> - <Username xsi:type="xs:string"/> - <Password xsi:type="xs:string"/> - <Subject xsi:type="xs:string"/> - </Credentials> - </RequesterCredentials> - </SOAP-ENV:Header> - <SOAP-ENV:Body id="_0"> - <DoDirectPaymentResponse xmlns="urn:ebay:api:PayPalAPI"> - <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2008-08-12T19:40:59Z</Timestamp> - <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Failure</Ack> - <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">b874109bfd11</CorrelationID> - <Errors xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:ErrorType"> - <ShortMessage xsi:type="xs:string">Authentication/Authorization Failed</ShortMessage> - <LongMessage xsi:type="xs:string">You do not have permissions to make this API call</LongMessage> - <ErrorCode xsi:type="xs:token">10002</ErrorCode> - <SeverityCode xmlns="urn:ebay:apis:eBLBaseComponents">Error</SeverityCode> - </Errors> - <Version xmlns="urn:ebay:apis:eBLBaseComponents">52.000000</Version> - <Build xmlns="urn:ebay:apis:eBLBaseComponents">628921</Build> - </DoDirectPaymentResponse> - </SOAP-ENV:Body> -</SOAP-ENV:Envelope> + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:market="urn:ebay:apis:Market" xmlns:auction="urn:ebay:apis:Auction" xmlns:sizeship="urn:ebay:api:PayPalAPI/sizeship.xsd" xmlns:ship="urn:ebay:apis:ship" xmlns:skype="urn:ebay:apis:skype" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> + <SOAP-ENV:Header> + <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"/> + <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> + <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> + <Username xsi:type="xs:string"/> + <Password xsi:type="xs:string"/> + <Subject xsi:type="xs:string"/> + </Credentials> + </RequesterCredentials> + </SOAP-ENV:Header> + <SOAP-ENV:Body id="_0"> + <DoDirectPaymentResponse xmlns="urn:ebay:api:PayPalAPI"> + <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2008-08-12T19:40:59Z</Timestamp> + <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Failure</Ack> + <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">b874109bfd11</CorrelationID> + <Errors xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:ErrorType"> + <ShortMessage xsi:type="xs:string">Authentication/Authorization Failed</ShortMessage> + <LongMessage xsi:type="xs:string">You do not have permissions to make this API call</LongMessage> + <ErrorCode xsi:type="xs:token">10002</ErrorCode> + <SeverityCode xmlns="urn:ebay:apis:eBLBaseComponents">Error</SeverityCode> + </Errors> + <Version xmlns="urn:ebay:apis:eBLBaseComponents">52.000000</Version> + <Build xmlns="urn:ebay:apis:eBLBaseComponents">628921</Build> + </DoDirectPaymentResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> RESPONSE end def successful_create_profile_paypal_response - <<-RESPONSE - <?xml version=\"1.0\" encoding=\"UTF-8\"?> - <SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:SOAP-ENC=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:cc=\"urn:ebay:apis:CoreComponentTypes\" xmlns:wsu=\"http://schemas.xmlsoap.org/ws/2002/07/utility\" xmlns:saml=\"urn:oasis:names:tc:SAML:1.0:assertion\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:wsse=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xmlns:ed=\"urn:ebay:apis:EnhancedDataTypes\" xmlns:ebl=\"urn:ebay:apis:eBLBaseComponents\" xmlns:ns=\"urn:ebay:api:PayPalAPI\"> - <SOAP-ENV:Header> - <Security xmlns=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xsi:type=\"wsse:SecurityType\"></Security> - <RequesterCredentials xmlns=\"urn:ebay:api:PayPalAPI\" xsi:type=\"ebl:CustomSecurityHeaderType\"> - <Credentials xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:UserIdPasswordType\"> - <Username xsi:type=\"xs:string\"></Username> - <Password xsi:type=\"xs:string\"></Password> - <Signature xsi:type=\"xs:string\"></Signature> - <Subject xsi:type=\"xs:string\"></Subject></Credentials> - </RequesterCredentials></SOAP-ENV:Header><SOAP-ENV:Body id=\"_0\"> - <CreateRecurringPaymentsProfileResponse xmlns=\"urn:ebay:api:PayPalAPI\"> - <Timestamp xmlns=\"urn:ebay:apis:eBLBaseComponents\">2011-08-28T18:59:40Z</Timestamp> - <Ack xmlns=\"urn:ebay:apis:eBLBaseComponents\">Success</Ack> - <CorrelationID xmlns=\"urn:ebay:apis:eBLBaseComponents\">4b8eaecc084b</CorrelationID> - <Version xmlns=\"urn:ebay:apis:eBLBaseComponents\">59.0</Version> - <Build xmlns=\"urn:ebay:apis:eBLBaseComponents\">2085867</Build> - <CreateRecurringPaymentsProfileResponseDetails xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:CreateRecurringPaymentsProfileResponseDetailsType\"> - <ProfileID xsi:type=\"xs:string\">I-G7A2FF8V75JY</ProfileID> - <ProfileStatus xsi:type=\"ebl:RecurringPaymentsProfileStatusType\">ActiveProfile</ProfileStatus> - <TransactionID xsi:type=\"xs:string\"></TransactionID></CreateRecurringPaymentsProfileResponseDetails> - </CreateRecurringPaymentsProfileResponse> - </SOAP-ENV:Body> - </SOAP-ENV:Envelope> + <<~RESPONSE + <?xml version=\"1.0\" encoding=\"UTF-8\"?> + <SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:SOAP-ENC=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:cc=\"urn:ebay:apis:CoreComponentTypes\" xmlns:wsu=\"http://schemas.xmlsoap.org/ws/2002/07/utility\" xmlns:saml=\"urn:oasis:names:tc:SAML:1.0:assertion\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:wsse=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xmlns:ed=\"urn:ebay:apis:EnhancedDataTypes\" xmlns:ebl=\"urn:ebay:apis:eBLBaseComponents\" xmlns:ns=\"urn:ebay:api:PayPalAPI\"> + <SOAP-ENV:Header> + <Security xmlns=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xsi:type=\"wsse:SecurityType\"></Security> + <RequesterCredentials xmlns=\"urn:ebay:api:PayPalAPI\" xsi:type=\"ebl:CustomSecurityHeaderType\"> + <Credentials xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:UserIdPasswordType\"> + <Username xsi:type=\"xs:string\"></Username> + <Password xsi:type=\"xs:string\"></Password> + <Signature xsi:type=\"xs:string\"></Signature> + <Subject xsi:type=\"xs:string\"></Subject></Credentials> + </RequesterCredentials></SOAP-ENV:Header><SOAP-ENV:Body id=\"_0\"> + <CreateRecurringPaymentsProfileResponse xmlns=\"urn:ebay:api:PayPalAPI\"> + <Timestamp xmlns=\"urn:ebay:apis:eBLBaseComponents\">2011-08-28T18:59:40Z</Timestamp> + <Ack xmlns=\"urn:ebay:apis:eBLBaseComponents\">Success</Ack> + <CorrelationID xmlns=\"urn:ebay:apis:eBLBaseComponents\">4b8eaecc084b</CorrelationID> + <Version xmlns=\"urn:ebay:apis:eBLBaseComponents\">59.0</Version> + <Build xmlns=\"urn:ebay:apis:eBLBaseComponents\">2085867</Build> + <CreateRecurringPaymentsProfileResponseDetails xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:CreateRecurringPaymentsProfileResponseDetailsType\"> + <ProfileID xsi:type=\"xs:string\">I-G7A2FF8V75JY</ProfileID> + <ProfileStatus xsi:type=\"ebl:RecurringPaymentsProfileStatusType\">ActiveProfile</ProfileStatus> + <TransactionID xsi:type=\"xs:string\"></TransactionID></CreateRecurringPaymentsProfileResponseDetails> + </CreateRecurringPaymentsProfileResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> RESPONSE end def failed_create_profile_paypal_response - <<-RESPONSE - <?xml version=\"1.0\" encoding=\"UTF-8\"?> - <SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:SOAP-ENC=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:cc=\"urn:ebay:apis:CoreComponentTypes\" xmlns:wsu=\"http://schemas.xmlsoap.org/ws/2002/07/utility\" xmlns:saml=\"urn:oasis:names:tc:SAML:1.0:assertion\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:wsse=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xmlns:ed=\"urn:ebay:apis:EnhancedDataTypes\" xmlns:ebl=\"urn:ebay:apis:eBLBaseComponents\" xmlns:ns=\"urn:ebay:api:PayPalAPI\"> - <SOAP-ENV:Header> - <Security xmlns=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xsi:type=\"wsse:SecurityType\"></Security> - <RequesterCredentials xmlns=\"urn:ebay:api:PayPalAPI\" xsi:type=\"ebl:CustomSecurityHeaderType\"> - <Credentials xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:UserIdPasswordType\"> - <Username xsi:type=\"xs:string\"></Username> - <Password xsi:type=\"xs:string\"></Password> - <Signature xsi:type=\"xs:string\"></Signature> - <Subject xsi:type=\"xs:string\"></Subject> - </Credentials> - </RequesterCredentials> - </SOAP-ENV:Header> - <SOAP-ENV:Body id=\"_0\"> - <CreateRecurringPaymentsProfileResponse xmlns=\"urn:ebay:api:PayPalAPI\"> - <Timestamp xmlns=\"urn:ebay:apis:eBLBaseComponents\">2011-08-28T18:59:40Z</Timestamp> - <Ack xmlns=\"urn:ebay:apis:eBLBaseComponents\">This is a test failure</Ack> - <CorrelationID xmlns=\"urn:ebay:apis:eBLBaseComponents\">4b8eaecc084b</CorrelationID> - <Version xmlns=\"urn:ebay:apis:eBLBaseComponents\">59.0</Version> - <Build xmlns=\"urn:ebay:apis:eBLBaseComponents\">2085867</Build> - <CreateRecurringPaymentsProfileResponseDetails xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:CreateRecurringPaymentsProfileResponseDetailsType\"> - <ProfileID xsi:type=\"xs:string\">I-G7A2FF8V75JY</ProfileID> - <ProfileStatus xsi:type=\"ebl:RecurringPaymentsProfileStatusType\">ActiveProfile</ProfileStatus> - <TransactionID xsi:type=\"xs:string\"></TransactionID> - </CreateRecurringPaymentsProfileResponseDetails> - </CreateRecurringPaymentsProfileResponse> - </SOAP-ENV:Body> - </SOAP-ENV:Envelope>" - RESPONSE + <<~RESPONSE + <?xml version=\"1.0\" encoding=\"UTF-8\"?> + <SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:SOAP-ENC=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:cc=\"urn:ebay:apis:CoreComponentTypes\" xmlns:wsu=\"http://schemas.xmlsoap.org/ws/2002/07/utility\" xmlns:saml=\"urn:oasis:names:tc:SAML:1.0:assertion\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:wsse=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xmlns:ed=\"urn:ebay:apis:EnhancedDataTypes\" xmlns:ebl=\"urn:ebay:apis:eBLBaseComponents\" xmlns:ns=\"urn:ebay:api:PayPalAPI\"> + <SOAP-ENV:Header> + <Security xmlns=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xsi:type=\"wsse:SecurityType\"></Security> + <RequesterCredentials xmlns=\"urn:ebay:api:PayPalAPI\" xsi:type=\"ebl:CustomSecurityHeaderType\"> + <Credentials xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:UserIdPasswordType\"> + <Username xsi:type=\"xs:string\"></Username> + <Password xsi:type=\"xs:string\"></Password> + <Signature xsi:type=\"xs:string\"></Signature> + <Subject xsi:type=\"xs:string\"></Subject> + </Credentials> + </RequesterCredentials> + </SOAP-ENV:Header> + <SOAP-ENV:Body id=\"_0\"> + <CreateRecurringPaymentsProfileResponse xmlns=\"urn:ebay:api:PayPalAPI\"> + <Timestamp xmlns=\"urn:ebay:apis:eBLBaseComponents\">2011-08-28T18:59:40Z</Timestamp> + <Ack xmlns=\"urn:ebay:apis:eBLBaseComponents\">This is a test failure</Ack> + <CorrelationID xmlns=\"urn:ebay:apis:eBLBaseComponents\">4b8eaecc084b</CorrelationID> + <Version xmlns=\"urn:ebay:apis:eBLBaseComponents\">59.0</Version> + <Build xmlns=\"urn:ebay:apis:eBLBaseComponents\">2085867</Build> + <CreateRecurringPaymentsProfileResponseDetails xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:CreateRecurringPaymentsProfileResponseDetailsType\"> + <ProfileID xsi:type=\"xs:string\">I-G7A2FF8V75JY</ProfileID> + <ProfileStatus xsi:type=\"ebl:RecurringPaymentsProfileStatusType\">ActiveProfile</ProfileStatus> + <TransactionID xsi:type=\"xs:string\"></TransactionID> + </CreateRecurringPaymentsProfileResponseDetails> + </CreateRecurringPaymentsProfileResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> + RESPONSE end def successful_details_response - <<-RESPONSE -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> - <SOAP-ENV:Header> - <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"/> - <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> - <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> - <Username xsi:type="xs:string"/> - <Password xsi:type="xs:string"/> - <Subject xsi:type="xs:string"/> - </Credentials> - </RequesterCredentials> - </SOAP-ENV:Header> - <SOAP-ENV:Body id="_0"> - <GetExpressCheckoutDetailsResponse xmlns="urn:ebay:api:PayPalAPI"> - <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2011-03-01T20:19:35Z</Timestamp> - <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Success</Ack> - <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">84aff0e17b6f</CorrelationID> - <Version xmlns="urn:ebay:apis:eBLBaseComponents">62.0</Version> - <Build xmlns="urn:ebay:apis:eBLBaseComponents">1741654</Build> - <GetExpressCheckoutDetailsResponseDetails xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:GetExpressCheckoutDetailsResponseDetailsType"> - <Token xsi:type="ebl:ExpressCheckoutTokenType">EC-2XE90996XX9870316</Token> - <PayerInfo xsi:type="ebl:PayerInfoType"> - <Payer xsi:type="ebl:EmailAddressType">buyer@jadedpallet.com</Payer> - <PayerID xsi:type="ebl:UserIDType">FWRVKNRRZ3WUC</PayerID> - <PayerStatus xsi:type="ebl:PayPalUserStatusCodeType">verified</PayerStatus> - <PayerName xsi:type='ebl:PersonNameType'> - <Salutation xmlns='urn:ebay:apis:eBLBaseComponents'/> - <FirstName xmlns='urn:ebay:apis:eBLBaseComponents'>Fred</FirstName> - <MiddleName xmlns='urn:ebay:apis:eBLBaseComponents'/> - <LastName xmlns='urn:ebay:apis:eBLBaseComponents'>Brooks</LastName> - <Suffix xmlns='urn:ebay:apis:eBLBaseComponents'/> - </PayerName> - <PayerCountry xsi:type="ebl:CountryCodeType">US</PayerCountry> - <PayerBusiness xsi:type="xs:string"/> - <Address xsi:type="ebl:AddressType"> - <Name xsi:type="xs:string">Fred Brooks</Name> - <Street1 xsi:type="xs:string">1 Infinite Loop</Street1> - <Street2 xsi:type="xs:string"/> - <CityName xsi:type="xs:string">Cupertino</CityName> - <StateOrProvince xsi:type="xs:string">CA</StateOrProvince> - <Country xsi:type="ebl:CountryCodeType">US</Country> - <CountryName>United States</CountryName> - <PostalCode xsi:type="xs:string">95014</PostalCode> - <AddressOwner xsi:type="ebl:AddressOwnerCodeType">PayPal</AddressOwner> - <AddressStatus xsi:type="ebl:AddressStatusCodeType">Confirmed</AddressStatus> - </Address> - </PayerInfo> - <InvoiceID xsi:type="xs:string">1230123</InvoiceID> - <ContactPhone>416-618-9984</ContactPhone> - <PaymentDetails xsi:type="ebl:PaymentDetailsType"> - <OrderTotal xsi:type="cc:BasicAmountType" currencyID="USD">19.00</OrderTotal> - <ItemTotal xsi:type="cc:BasicAmountType" currencyID="USD">19.00</ItemTotal> - <ShippingTotal xsi:type="cc:BasicAmountType" currencyID="USD">0.00</ShippingTotal> - <HandlingTotal xsi:type="cc:BasicAmountType" currencyID="USD">0.00</HandlingTotal> - <TaxTotal xsi:type="cc:BasicAmountType" currencyID="USD">0.00</TaxTotal> - <ShipToAddress xsi:type="ebl:AddressType"> - <Name xsi:type="xs:string">Fred Brooks</Name> - <Street1 xsi:type="xs:string">1234 Penny Lane</Street1> - <Street2 xsi:type="xs:string"/> - <CityName xsi:type="xs:string">Jonsetown</CityName> - <StateOrProvince xsi:type="xs:string">NC</StateOrProvince> - <Country xsi:type="ebl:CountryCodeType">US</Country> - <CountryName>United States</CountryName> - <Phone xsi:type="xs:string">123-456-7890</Phone> - <PostalCode xsi:type="xs:string">23456</PostalCode> - <AddressID xsi:type="xs:string"/> - <AddressOwner xsi:type="ebl:AddressOwnerCodeType">PayPal</AddressOwner> - <ExternalAddressID xsi:type="xs:string"/> - <AddressStatus xsi:type="ebl:AddressStatusCodeType">Confirmed</AddressStatus> - </ShipToAddress> - <PaymentDetailsItem xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:PaymentDetailsItemType"> - <Name xsi:type="xs:string">Shopify T-Shirt</Name> - <Quantity>1</Quantity> - <Tax xsi:type="cc:BasicAmountType" currencyID="USD">0.00</Tax> - <Amount xsi:type="cc:BasicAmountType" currencyID="USD">19.00</Amount> - <EbayItemPaymentDetailsItem xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:EbayItemPaymentDetailsItemType"/> - </PaymentDetailsItem> - <InsuranceTotal xsi:type="cc:BasicAmountType" currencyID="USD">0.00</InsuranceTotal> - <ShippingDiscount xsi:type="cc:BasicAmountType" currencyID="USD">0.00</ShippingDiscount> - <InsuranceOptionOffered xsi:type="xs:string">false</InsuranceOptionOffered> - <SellerDetails xsi:type="ebl:SellerDetailsType"/> - <PaymentRequestID xsi:type="xs:string"/> - <OrderURL xsi:type="xs:string"/> - <SoftDescriptor xsi:type="xs:string"/> - </PaymentDetails> - <CheckoutStatus xsi:type="xs:string">PaymentActionNotInitiated</CheckoutStatus> - </GetExpressCheckoutDetailsResponseDetails> - </GetExpressCheckoutDetailsResponse> - </SOAP-ENV:Body> -</SOAP-ENV:Envelope> + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cc="urn:ebay:apis:CoreComponentTypes" xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext" xmlns:ed="urn:ebay:apis:EnhancedDataTypes" xmlns:ebl="urn:ebay:apis:eBLBaseComponents" xmlns:ns="urn:ebay:api:PayPalAPI"> + <SOAP-ENV:Header> + <Security xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext" xsi:type="wsse:SecurityType"/> + <RequesterCredentials xmlns="urn:ebay:api:PayPalAPI" xsi:type="ebl:CustomSecurityHeaderType"> + <Credentials xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:UserIdPasswordType"> + <Username xsi:type="xs:string"/> + <Password xsi:type="xs:string"/> + <Subject xsi:type="xs:string"/> + </Credentials> + </RequesterCredentials> + </SOAP-ENV:Header> + <SOAP-ENV:Body id="_0"> + <GetExpressCheckoutDetailsResponse xmlns="urn:ebay:api:PayPalAPI"> + <Timestamp xmlns="urn:ebay:apis:eBLBaseComponents">2011-03-01T20:19:35Z</Timestamp> + <Ack xmlns="urn:ebay:apis:eBLBaseComponents">Success</Ack> + <CorrelationID xmlns="urn:ebay:apis:eBLBaseComponents">84aff0e17b6f</CorrelationID> + <Version xmlns="urn:ebay:apis:eBLBaseComponents">62.0</Version> + <Build xmlns="urn:ebay:apis:eBLBaseComponents">1741654</Build> + <GetExpressCheckoutDetailsResponseDetails xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:GetExpressCheckoutDetailsResponseDetailsType"> + <Token xsi:type="ebl:ExpressCheckoutTokenType">EC-2XE90996XX9870316</Token> + <PayerInfo xsi:type="ebl:PayerInfoType"> + <Payer xsi:type="ebl:EmailAddressType">buyer@jadedpallet.com</Payer> + <PayerID xsi:type="ebl:UserIDType">FWRVKNRRZ3WUC</PayerID> + <PayerStatus xsi:type="ebl:PayPalUserStatusCodeType">verified</PayerStatus> + <PayerName xsi:type='ebl:PersonNameType'> + <Salutation xmlns='urn:ebay:apis:eBLBaseComponents'/> + <FirstName xmlns='urn:ebay:apis:eBLBaseComponents'>Fred</FirstName> + <MiddleName xmlns='urn:ebay:apis:eBLBaseComponents'/> + <LastName xmlns='urn:ebay:apis:eBLBaseComponents'>Brooks</LastName> + <Suffix xmlns='urn:ebay:apis:eBLBaseComponents'/> + </PayerName> + <PayerCountry xsi:type="ebl:CountryCodeType">US</PayerCountry> + <PayerBusiness xsi:type="xs:string"/> + <Address xsi:type="ebl:AddressType"> + <Name xsi:type="xs:string">Fred Brooks</Name> + <Street1 xsi:type="xs:string">1 Infinite Loop</Street1> + <Street2 xsi:type="xs:string"/> + <CityName xsi:type="xs:string">Cupertino</CityName> + <StateOrProvince xsi:type="xs:string">CA</StateOrProvince> + <Country xsi:type="ebl:CountryCodeType">US</Country> + <CountryName>United States</CountryName> + <PostalCode xsi:type="xs:string">95014</PostalCode> + <AddressOwner xsi:type="ebl:AddressOwnerCodeType">PayPal</AddressOwner> + <AddressStatus xsi:type="ebl:AddressStatusCodeType">Confirmed</AddressStatus> + </Address> + </PayerInfo> + <InvoiceID xsi:type="xs:string">1230123</InvoiceID> + <ContactPhone>416-618-9984</ContactPhone> + <PaymentDetails xsi:type="ebl:PaymentDetailsType"> + <OrderTotal xsi:type="cc:BasicAmountType" currencyID="USD">19.00</OrderTotal> + <ItemTotal xsi:type="cc:BasicAmountType" currencyID="USD">19.00</ItemTotal> + <ShippingTotal xsi:type="cc:BasicAmountType" currencyID="USD">0.00</ShippingTotal> + <HandlingTotal xsi:type="cc:BasicAmountType" currencyID="USD">0.00</HandlingTotal> + <TaxTotal xsi:type="cc:BasicAmountType" currencyID="USD">0.00</TaxTotal> + <ShipToAddress xsi:type="ebl:AddressType"> + <Name xsi:type="xs:string">Fred Brooks</Name> + <Street1 xsi:type="xs:string">1234 Penny Lane</Street1> + <Street2 xsi:type="xs:string"/> + <CityName xsi:type="xs:string">Jonsetown</CityName> + <StateOrProvince xsi:type="xs:string">NC</StateOrProvince> + <Country xsi:type="ebl:CountryCodeType">US</Country> + <CountryName>United States</CountryName> + <Phone xsi:type="xs:string">123-456-7890</Phone> + <PostalCode xsi:type="xs:string">23456</PostalCode> + <AddressID xsi:type="xs:string"/> + <AddressOwner xsi:type="ebl:AddressOwnerCodeType">PayPal</AddressOwner> + <ExternalAddressID xsi:type="xs:string"/> + <AddressStatus xsi:type="ebl:AddressStatusCodeType">Confirmed</AddressStatus> + </ShipToAddress> + <PaymentDetailsItem xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:PaymentDetailsItemType"> + <Name xsi:type="xs:string">Shopify T-Shirt</Name> + <Quantity>1</Quantity> + <Tax xsi:type="cc:BasicAmountType" currencyID="USD">0.00</Tax> + <Amount xsi:type="cc:BasicAmountType" currencyID="USD">19.00</Amount> + <EbayItemPaymentDetailsItem xmlns="urn:ebay:apis:eBLBaseComponents" xsi:type="ebl:EbayItemPaymentDetailsItemType"/> + </PaymentDetailsItem> + <InsuranceTotal xsi:type="cc:BasicAmountType" currencyID="USD">0.00</InsuranceTotal> + <ShippingDiscount xsi:type="cc:BasicAmountType" currencyID="USD">0.00</ShippingDiscount> + <InsuranceOptionOffered xsi:type="xs:string">false</InsuranceOptionOffered> + <SellerDetails xsi:type="ebl:SellerDetailsType"/> + <PaymentRequestID xsi:type="xs:string"/> + <OrderURL xsi:type="xs:string"/> + <SoftDescriptor xsi:type="xs:string"/> + </PaymentDetails> + <CheckoutStatus xsi:type="xs:string">PaymentActionNotInitiated</CheckoutStatus> + </GetExpressCheckoutDetailsResponseDetails> + </GetExpressCheckoutDetailsResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> RESPONSE end - def successful_update_recurring_payment_profile_response - <<-RESPONSE - <?xml version=\"1.0\" encoding=\"UTF-8\"?><SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:SOAP-ENC=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:cc=\"urn:ebay:apis:CoreComponentTypes\" xmlns:wsu=\"http://schemas.xmlsoap.org/ws/2002/07/utility\" xmlns:saml=\"urn:oasis:names:tc:SAML:1.0:assertion\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:wsse=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xmlns:ed=\"urn:ebay:apis:EnhancedDataTypes\" xmlns:ebl=\"urn:ebay:apis:eBLBaseComponents\" xmlns:ns=\"urn:ebay:api:PayPalAPI\"><SOAP-ENV:Header><Security xmlns=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xsi:type=\"wsse:SecurityType\"></Security><RequesterCredentials xmlns=\"urn:ebay:api:PayPalAPI\" xsi:type=\"ebl:CustomSecurityHeaderType\"><Credentials xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:UserIdPasswordType\"><Username xsi:type=\"xs:string\"></Username><Password xsi:type=\"xs:string\"></Password><Signature xsi:type=\"xs:string\"></Signature><Subject xsi:type=\"xs:string\"></Subject></Credentials></RequesterCredentials></SOAP-ENV:Header><SOAP-ENV:Body id=\"_0\"> - <UpdateRecurringPaymentsProfileResponse xmlns=\"urn:ebay:api:PayPalAPI\"> - <Timestamp xmlns=\"urn:ebay:apis:eBLBaseComponents\">2012-03-19T20:30:02Z</Timestamp> - <Ack xmlns=\"urn:ebay:apis:eBLBaseComponents\">Success</Ack> - <CorrelationID xmlns=\"urn:ebay:apis:eBLBaseComponents\">9ad0f67c1127c</CorrelationID> - <Version xmlns=\"urn:ebay:apis:eBLBaseComponents\">72</Version> - <Build xmlns=\"urn:ebay:apis:eBLBaseComponents\">2649250</Build> - <UpdateRecurringPaymentsProfileResponseDetails xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:UpdateRecurringPaymentsProfileResponseDetailsType\"> - <ProfileID xsi:type=\"xs:string\">I-M1L3RX91DPDD</ProfileID> - </UpdateRecurringPaymentsProfileResponseDetails> - </UpdateRecurringPaymentsProfileResponse> - </SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~RESPONSE + <?xml version=\"1.0\" encoding=\"UTF-8\"?><SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:SOAP-ENC=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:cc=\"urn:ebay:apis:CoreComponentTypes\" xmlns:wsu=\"http://schemas.xmlsoap.org/ws/2002/07/utility\" xmlns:saml=\"urn:oasis:names:tc:SAML:1.0:assertion\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:wsse=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xmlns:ed=\"urn:ebay:apis:EnhancedDataTypes\" xmlns:ebl=\"urn:ebay:apis:eBLBaseComponents\" xmlns:ns=\"urn:ebay:api:PayPalAPI\"><SOAP-ENV:Header><Security xmlns=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xsi:type=\"wsse:SecurityType\"></Security><RequesterCredentials xmlns=\"urn:ebay:api:PayPalAPI\" xsi:type=\"ebl:CustomSecurityHeaderType\"><Credentials xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:UserIdPasswordType\"><Username xsi:type=\"xs:string\"></Username><Password xsi:type=\"xs:string\"></Password><Signature xsi:type=\"xs:string\"></Signature><Subject xsi:type=\"xs:string\"></Subject></Credentials></RequesterCredentials></SOAP-ENV:Header><SOAP-ENV:Body id=\"_0\"> + <UpdateRecurringPaymentsProfileResponse xmlns=\"urn:ebay:api:PayPalAPI\"> + <Timestamp xmlns=\"urn:ebay:apis:eBLBaseComponents\">2012-03-19T20:30:02Z</Timestamp> + <Ack xmlns=\"urn:ebay:apis:eBLBaseComponents\">Success</Ack> + <CorrelationID xmlns=\"urn:ebay:apis:eBLBaseComponents\">9ad0f67c1127c</CorrelationID> + <Version xmlns=\"urn:ebay:apis:eBLBaseComponents\">72</Version> + <Build xmlns=\"urn:ebay:apis:eBLBaseComponents\">2649250</Build> + <UpdateRecurringPaymentsProfileResponseDetails xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:UpdateRecurringPaymentsProfileResponseDetailsType\"> + <ProfileID xsi:type=\"xs:string\">I-M1L3RX91DPDD</ProfileID> + </UpdateRecurringPaymentsProfileResponseDetails> + </UpdateRecurringPaymentsProfileResponse> + </SOAP-ENV:Body></SOAP-ENV:Envelope> RESPONSE end def successful_manage_recurring_payment_profile_response - <<-RESPONSE - <?xml version=\"1.0\" encoding=\"UTF-8\"?><SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:SOAP-ENC=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:cc=\"urn:ebay:apis:CoreComponentTypes\" xmlns:wsu=\"http://schemas.xmlsoap.org/ws/2002/07/utility\" xmlns:saml=\"urn:oasis:names:tc:SAML:1.0:assertion\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:wsse=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xmlns:ed=\"urn:ebay:apis:EnhancedDataTypes\" xmlns:ebl=\"urn:ebay:apis:eBLBaseComponents\" xmlns:ns=\"urn:ebay:api:PayPalAPI\"><SOAP-ENV:Header><Security xmlns=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xsi:type=\"wsse:SecurityType\"></Security><RequesterCredentials xmlns=\"urn:ebay:api:PayPalAPI\" xsi:type=\"ebl:CustomSecurityHeaderType\"><Credentials xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:UserIdPasswordType\"><Username xsi:type=\"xs:string\"></Username><Password xsi:type=\"xs:string\"></Password><Signature xsi:type=\"xs:string\"></Signature><Subject xsi:type=\"xs:string\"></Subject></Credentials></RequesterCredentials></SOAP-ENV:Header> - <SOAP-ENV:Body id=\"_0\"> - <ManageRecurringPaymentsProfileStatusResponse xmlns=\"urn:ebay:api:PayPalAPI\"><Timestamp xmlns=\"urn:ebay:apis:eBLBaseComponents\">2012-03-19T20:41:03Z</Timestamp><Ack xmlns=\"urn:ebay:apis:eBLBaseComponents\">Success</Ack><CorrelationID xmlns=\"urn:ebay:apis:eBLBaseComponents\">3c02ea62138c4</CorrelationID><Version xmlns=\"urn:ebay:apis:eBLBaseComponents\">72</Version><Build xmlns=\"urn:ebay:apis:eBLBaseComponents\">2649250</Build><ManageRecurringPaymentsProfileStatusResponseDetails xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:ManageRecurringPaymentsProfileStatusResponseDetailsType\"><ProfileID xsi:type=\"xs:string\">I-M1L3RX91DPDD</ProfileID></ManageRecurringPaymentsProfileStatusResponseDetails></ManageRecurringPaymentsProfileStatusResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~RESPONSE + <?xml version=\"1.0\" encoding=\"UTF-8\"?><SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:SOAP-ENC=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:cc=\"urn:ebay:apis:CoreComponentTypes\" xmlns:wsu=\"http://schemas.xmlsoap.org/ws/2002/07/utility\" xmlns:saml=\"urn:oasis:names:tc:SAML:1.0:assertion\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:wsse=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xmlns:ed=\"urn:ebay:apis:EnhancedDataTypes\" xmlns:ebl=\"urn:ebay:apis:eBLBaseComponents\" xmlns:ns=\"urn:ebay:api:PayPalAPI\"><SOAP-ENV:Header><Security xmlns=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xsi:type=\"wsse:SecurityType\"></Security><RequesterCredentials xmlns=\"urn:ebay:api:PayPalAPI\" xsi:type=\"ebl:CustomSecurityHeaderType\"><Credentials xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:UserIdPasswordType\"><Username xsi:type=\"xs:string\"></Username><Password xsi:type=\"xs:string\"></Password><Signature xsi:type=\"xs:string\"></Signature><Subject xsi:type=\"xs:string\"></Subject></Credentials></RequesterCredentials></SOAP-ENV:Header> + <SOAP-ENV:Body id=\"_0\"> + <ManageRecurringPaymentsProfileStatusResponse xmlns=\"urn:ebay:api:PayPalAPI\"><Timestamp xmlns=\"urn:ebay:apis:eBLBaseComponents\">2012-03-19T20:41:03Z</Timestamp><Ack xmlns=\"urn:ebay:apis:eBLBaseComponents\">Success</Ack><CorrelationID xmlns=\"urn:ebay:apis:eBLBaseComponents\">3c02ea62138c4</CorrelationID><Version xmlns=\"urn:ebay:apis:eBLBaseComponents\">72</Version><Build xmlns=\"urn:ebay:apis:eBLBaseComponents\">2649250</Build><ManageRecurringPaymentsProfileStatusResponseDetails xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:ManageRecurringPaymentsProfileStatusResponseDetailsType\"><ProfileID xsi:type=\"xs:string\">I-M1L3RX91DPDD</ProfileID></ManageRecurringPaymentsProfileStatusResponseDetails></ManageRecurringPaymentsProfileStatusResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> RESPONSE end def successful_bill_outstanding_amount - <<-RESPONSE - <?xml version=\"1.0\" encoding=\"UTF-8\"?><SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:SOAP-ENC=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:cc=\"urn:ebay:apis:CoreComponentTypes\" xmlns:wsu=\"http://schemas.xmlsoap.org/ws/2002/07/utility\" xmlns:saml=\"urn:oasis:names:tc:SAML:1.0:assertion\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:wsse=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xmlns:ed=\"urn:ebay:apis:EnhancedDataTypes\" xmlns:ebl=\"urn:ebay:apis:eBLBaseComponents\" xmlns:ns=\"urn:ebay:api:PayPalAPI\"><SOAP-ENV:Header><Security xmlns=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xsi:type=\"wsse:SecurityType\"></Security><RequesterCredentials xmlns=\"urn:ebay:api:PayPalAPI\" xsi:type=\"ebl:CustomSecurityHeaderType\"><Credentials xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:UserIdPasswordType\"><Username xsi:type=\"xs:string\"></Username><Password xsi:type=\"xs:string\"></Password><Signature xsi:type=\"xs:string\"></Signature><Subject xsi:type=\"xs:string\"></Subject></Credentials></RequesterCredentials></SOAP-ENV:Header><SOAP-ENV:Body id=\"_0\"><BillOutstandingAmountResponse xmlns=\"urn:ebay:api:PayPalAPI\"><Timestamp xmlns=\"urn:ebay:apis:eBLBaseComponents\">2012-03-19T20:50:49Z</Timestamp><Ack xmlns=\"urn:ebay:apis:eBLBaseComponents\">Success</Ack><CorrelationID xmlns=\"urn:ebay:apis:eBLBaseComponents\">2c1cbe06d718e</CorrelationID><Version xmlns=\"urn:ebay:apis:eBLBaseComponents\">72</Version><Build xmlns=\"urn:ebay:apis:eBLBaseComponents\">2649250</Build><BillOutstandingAmountResponseDetails xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:BillOutstandingAmountResponseDetailsType\"><ProfileID xsi:type=\"xs:string\">I-M1L3RX91DPDD</ProfileID></BillOutstandingAmountResponseDetails></BillOutstandingAmountResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~RESPONSE + <?xml version=\"1.0\" encoding=\"UTF-8\"?><SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:SOAP-ENC=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:cc=\"urn:ebay:apis:CoreComponentTypes\" xmlns:wsu=\"http://schemas.xmlsoap.org/ws/2002/07/utility\" xmlns:saml=\"urn:oasis:names:tc:SAML:1.0:assertion\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:wsse=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xmlns:ed=\"urn:ebay:apis:EnhancedDataTypes\" xmlns:ebl=\"urn:ebay:apis:eBLBaseComponents\" xmlns:ns=\"urn:ebay:api:PayPalAPI\"><SOAP-ENV:Header><Security xmlns=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xsi:type=\"wsse:SecurityType\"></Security><RequesterCredentials xmlns=\"urn:ebay:api:PayPalAPI\" xsi:type=\"ebl:CustomSecurityHeaderType\"><Credentials xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:UserIdPasswordType\"><Username xsi:type=\"xs:string\"></Username><Password xsi:type=\"xs:string\"></Password><Signature xsi:type=\"xs:string\"></Signature><Subject xsi:type=\"xs:string\"></Subject></Credentials></RequesterCredentials></SOAP-ENV:Header><SOAP-ENV:Body id=\"_0\"><BillOutstandingAmountResponse xmlns=\"urn:ebay:api:PayPalAPI\"><Timestamp xmlns=\"urn:ebay:apis:eBLBaseComponents\">2012-03-19T20:50:49Z</Timestamp><Ack xmlns=\"urn:ebay:apis:eBLBaseComponents\">Success</Ack><CorrelationID xmlns=\"urn:ebay:apis:eBLBaseComponents\">2c1cbe06d718e</CorrelationID><Version xmlns=\"urn:ebay:apis:eBLBaseComponents\">72</Version><Build xmlns=\"urn:ebay:apis:eBLBaseComponents\">2649250</Build><BillOutstandingAmountResponseDetails xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:BillOutstandingAmountResponseDetailsType\"><ProfileID xsi:type=\"xs:string\">I-M1L3RX91DPDD</ProfileID></BillOutstandingAmountResponseDetails></BillOutstandingAmountResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> RESPONSE end def successful_get_recurring_payments_profile_response - <<-RESPONSE - <?xml version=\"1.0\" encoding=\"UTF-8\"?><SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:SOAP-ENC=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:cc=\"urn:ebay:apis:CoreComponentTypes\" xmlns:wsu=\"http://schemas.xmlsoap.org/ws/2002/07/utility\" xmlns:saml=\"urn:oasis:names:tc:SAML:1.0:assertion\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:wsse=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xmlns:ed=\"urn:ebay:apis:EnhancedDataTypes\" xmlns:ebl=\"urn:ebay:apis:eBLBaseComponents\" xmlns:ns=\"urn:ebay:api:PayPalAPI\"><SOAP-ENV:Header><Security xmlns=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xsi:type=\"wsse:SecurityType\"></Security><RequesterCredentials xmlns=\"urn:ebay:api:PayPalAPI\" xsi:type=\"ebl:CustomSecurityHeaderType\"><Credentials xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:UserIdPasswordType\"><Username xsi:type=\"xs:string\"></Username><Password xsi:type=\"xs:string\"></Password><Signature xsi:type=\"xs:string\"></Signature><Subject xsi:type=\"xs:string\"></Subject></Credentials></RequesterCredentials></SOAP-ENV:Header><SOAP-ENV:Body id=\"_0\"><GetRecurringPaymentsProfileDetailsResponse xmlns=\"urn:ebay:api:PayPalAPI\"><Timestamp xmlns=\"urn:ebay:apis:eBLBaseComponents\">2012-03-19T21:34:40Z</Timestamp><Ack xmlns=\"urn:ebay:apis:eBLBaseComponents\">Success</Ack><CorrelationID xmlns=\"urn:ebay:apis:eBLBaseComponents\">6f24b53c49232</CorrelationID><Version xmlns=\"urn:ebay:apis:eBLBaseComponents\">72</Version><Build xmlns=\"urn:ebay:apis:eBLBaseComponents\">2649250</Build><GetRecurringPaymentsProfileDetailsResponseDetails xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:GetRecurringPaymentsProfileDetailsResponseDetailsType\"><ProfileID xsi:type=\"xs:string\">I-M1L3RX91DPDD</ProfileID><ProfileStatus xsi:type=\"ebl:RecurringPaymentsProfileStatusType\">CancelledProfile</ProfileStatus><Description xsi:type=\"xs:string\">A description</Description><AutoBillOutstandingAmount xsi:type=\"ebl:AutoBillType\">NoAutoBill</AutoBillOutstandingAmount><MaxFailedPayments>0</MaxFailedPayments><RecurringPaymentsProfileDetails xsi:type=\"ebl:RecurringPaymentsProfileDetailsType\"><SubscriberName xsi:type=\"xs:string\">Ryan Bates</SubscriberName><SubscriberShippingAddress xsi:type=\"ebl:AddressType\"><Name xsi:type=\"xs:string\"></Name><Street1 xsi:type=\"xs:string\"></Street1><Street2 xsi:type=\"xs:string\"></Street2><CityName xsi:type=\"xs:string\"></CityName><StateOrProvince xsi:type=\"xs:string\"></StateOrProvince><CountryName></CountryName><Phone xsi:type=\"xs:string\"></Phone><PostalCode xsi:type=\"xs:string\"></PostalCode><AddressID xsi:type=\"xs:string\"></AddressID><AddressOwner xsi:type=\"ebl:AddressOwnerCodeType\">PayPal</AddressOwner><ExternalAddressID xsi:type=\"xs:string\"></ExternalAddressID><AddressStatus xsi:type=\"ebl:AddressStatusCodeType\">Unconfirmed</AddressStatus></SubscriberShippingAddress><BillingStartDate xsi:type=\"xs:dateTime\">2012-03-19T11:00:00Z</BillingStartDate></RecurringPaymentsProfileDetails><CurrentRecurringPaymentsPeriod xsi:type=\"ebl:BillingPeriodDetailsType\"><BillingPeriod xsi:type=\"ebl:BillingPeriodTypeType\">Month</BillingPeriod><BillingFrequency>1</BillingFrequency><TotalBillingCycles>0</TotalBillingCycles><Amount xsi:type=\"cc:BasicAmountType\" currencyID=\"USD\">1.23</Amount><ShippingAmount xsi:type=\"cc:BasicAmountType\" currencyID=\"USD\">0.00</ShippingAmount><TaxAmount xsi:type=\"cc:BasicAmountType\" currencyID=\"USD\">0.00</TaxAmount></CurrentRecurringPaymentsPeriod><RecurringPaymentsSummary xsi:type=\"ebl:RecurringPaymentsSummaryType\"><NumberCyclesCompleted>1</NumberCyclesCompleted><NumberCyclesRemaining>-1</NumberCyclesRemaining><OutstandingBalance xsi:type=\"cc:BasicAmountType\" currencyID=\"USD\">1.23</OutstandingBalance><FailedPaymentCount>1</FailedPaymentCount></RecurringPaymentsSummary><CreditCard xsi:type=\"ebl:CreditCardDetailsType\"><CreditCardType xsi:type=\"ebl:CreditCardTypeType\">Visa</CreditCardType><CreditCardNumber xsi:type=\"xs:string\">3576</CreditCardNumber><ExpMonth>1</ExpMonth><ExpYear>2013</ExpYear><CardOwner xsi:type=\"ebl:PayerInfoType\"><PayerStatus xsi:type=\"ebl:PayPalUserStatusCodeType\">unverified</PayerStatus><PayerName xsi:type=\"ebl:PersonNameType\"><FirstName xmlns=\"urn:ebay:apis:eBLBaseComponents\">Ryan</FirstName><LastName xmlns=\"urn:ebay:apis:eBLBaseComponents\">Bates</LastName></PayerName><Address xsi:type=\"ebl:AddressType\"><AddressOwner xsi:type=\"ebl:AddressOwnerCodeType\">PayPal</AddressOwner><AddressStatus xsi:type=\"ebl:AddressStatusCodeType\">Unconfirmed</AddressStatus></Address></CardOwner><StartMonth>0</StartMonth><StartYear>0</StartYear><ThreeDSecureRequest xsi:type=\"ebl:ThreeDSecureRequestType\"></ThreeDSecureRequest></CreditCard><RegularRecurringPaymentsPeriod xsi:type=\"ebl:BillingPeriodDetailsType\"><BillingPeriod xsi:type=\"ebl:BillingPeriodTypeType\">Month</BillingPeriod><BillingFrequency>1</BillingFrequency><TotalBillingCycles>0</TotalBillingCycles><Amount xsi:type=\"cc:BasicAmountType\" currencyID=\"USD\">1.23</Amount><ShippingAmount xsi:type=\"cc:BasicAmountType\" currencyID=\"USD\">0.00</ShippingAmount><TaxAmount xsi:type=\"cc:BasicAmountType\" currencyID=\"USD\">0.00</TaxAmount></RegularRecurringPaymentsPeriod><TrialAmountPaid xsi:type=\"cc:BasicAmountType\" currencyID=\"USD\">0.00</TrialAmountPaid><RegularAmountPaid xsi:type=\"cc:BasicAmountType\" currencyID=\"USD\">0.00</RegularAmountPaid><AggregateAmount xsi:type=\"cc:BasicAmountType\" currencyID=\"USD\">0.00</AggregateAmount><AggregateOptionalAmount xsi:type=\"cc:BasicAmountType\" currencyID=\"USD\">0.00</AggregateOptionalAmount><FinalPaymentDueDate xsi:type=\"xs:dateTime\">1970-01-01T00:00:00Z</FinalPaymentDueDate></GetRecurringPaymentsProfileDetailsResponseDetails></GetRecurringPaymentsProfileDetailsResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~RESPONSE + <?xml version=\"1.0\" encoding=\"UTF-8\"?><SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:SOAP-ENC=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:cc=\"urn:ebay:apis:CoreComponentTypes\" xmlns:wsu=\"http://schemas.xmlsoap.org/ws/2002/07/utility\" xmlns:saml=\"urn:oasis:names:tc:SAML:1.0:assertion\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:wsse=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xmlns:ed=\"urn:ebay:apis:EnhancedDataTypes\" xmlns:ebl=\"urn:ebay:apis:eBLBaseComponents\" xmlns:ns=\"urn:ebay:api:PayPalAPI\"><SOAP-ENV:Header><Security xmlns=\"http://schemas.xmlsoap.org/ws/2002/12/secext\" xsi:type=\"wsse:SecurityType\"></Security><RequesterCredentials xmlns=\"urn:ebay:api:PayPalAPI\" xsi:type=\"ebl:CustomSecurityHeaderType\"><Credentials xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:UserIdPasswordType\"><Username xsi:type=\"xs:string\"></Username><Password xsi:type=\"xs:string\"></Password><Signature xsi:type=\"xs:string\"></Signature><Subject xsi:type=\"xs:string\"></Subject></Credentials></RequesterCredentials></SOAP-ENV:Header><SOAP-ENV:Body id=\"_0\"><GetRecurringPaymentsProfileDetailsResponse xmlns=\"urn:ebay:api:PayPalAPI\"><Timestamp xmlns=\"urn:ebay:apis:eBLBaseComponents\">2012-03-19T21:34:40Z</Timestamp><Ack xmlns=\"urn:ebay:apis:eBLBaseComponents\">Success</Ack><CorrelationID xmlns=\"urn:ebay:apis:eBLBaseComponents\">6f24b53c49232</CorrelationID><Version xmlns=\"urn:ebay:apis:eBLBaseComponents\">72</Version><Build xmlns=\"urn:ebay:apis:eBLBaseComponents\">2649250</Build><GetRecurringPaymentsProfileDetailsResponseDetails xmlns=\"urn:ebay:apis:eBLBaseComponents\" xsi:type=\"ebl:GetRecurringPaymentsProfileDetailsResponseDetailsType\"><ProfileID xsi:type=\"xs:string\">I-M1L3RX91DPDD</ProfileID><ProfileStatus xsi:type=\"ebl:RecurringPaymentsProfileStatusType\">CancelledProfile</ProfileStatus><Description xsi:type=\"xs:string\">A description</Description><AutoBillOutstandingAmount xsi:type=\"ebl:AutoBillType\">NoAutoBill</AutoBillOutstandingAmount><MaxFailedPayments>0</MaxFailedPayments><RecurringPaymentsProfileDetails xsi:type=\"ebl:RecurringPaymentsProfileDetailsType\"><SubscriberName xsi:type=\"xs:string\">Ryan Bates</SubscriberName><SubscriberShippingAddress xsi:type=\"ebl:AddressType\"><Name xsi:type=\"xs:string\"></Name><Street1 xsi:type=\"xs:string\"></Street1><Street2 xsi:type=\"xs:string\"></Street2><CityName xsi:type=\"xs:string\"></CityName><StateOrProvince xsi:type=\"xs:string\"></StateOrProvince><CountryName></CountryName><Phone xsi:type=\"xs:string\"></Phone><PostalCode xsi:type=\"xs:string\"></PostalCode><AddressID xsi:type=\"xs:string\"></AddressID><AddressOwner xsi:type=\"ebl:AddressOwnerCodeType\">PayPal</AddressOwner><ExternalAddressID xsi:type=\"xs:string\"></ExternalAddressID><AddressStatus xsi:type=\"ebl:AddressStatusCodeType\">Unconfirmed</AddressStatus></SubscriberShippingAddress><BillingStartDate xsi:type=\"xs:dateTime\">2012-03-19T11:00:00Z</BillingStartDate></RecurringPaymentsProfileDetails><CurrentRecurringPaymentsPeriod xsi:type=\"ebl:BillingPeriodDetailsType\"><BillingPeriod xsi:type=\"ebl:BillingPeriodTypeType\">Month</BillingPeriod><BillingFrequency>1</BillingFrequency><TotalBillingCycles>0</TotalBillingCycles><Amount xsi:type=\"cc:BasicAmountType\" currencyID=\"USD\">1.23</Amount><ShippingAmount xsi:type=\"cc:BasicAmountType\" currencyID=\"USD\">0.00</ShippingAmount><TaxAmount xsi:type=\"cc:BasicAmountType\" currencyID=\"USD\">0.00</TaxAmount></CurrentRecurringPaymentsPeriod><RecurringPaymentsSummary xsi:type=\"ebl:RecurringPaymentsSummaryType\"><NumberCyclesCompleted>1</NumberCyclesCompleted><NumberCyclesRemaining>-1</NumberCyclesRemaining><OutstandingBalance xsi:type=\"cc:BasicAmountType\" currencyID=\"USD\">1.23</OutstandingBalance><FailedPaymentCount>1</FailedPaymentCount></RecurringPaymentsSummary><CreditCard xsi:type=\"ebl:CreditCardDetailsType\"><CreditCardType xsi:type=\"ebl:CreditCardTypeType\">Visa</CreditCardType><CreditCardNumber xsi:type=\"xs:string\">3576</CreditCardNumber><ExpMonth>1</ExpMonth><ExpYear>2013</ExpYear><CardOwner xsi:type=\"ebl:PayerInfoType\"><PayerStatus xsi:type=\"ebl:PayPalUserStatusCodeType\">unverified</PayerStatus><PayerName xsi:type=\"ebl:PersonNameType\"><FirstName xmlns=\"urn:ebay:apis:eBLBaseComponents\">Ryan</FirstName><LastName xmlns=\"urn:ebay:apis:eBLBaseComponents\">Bates</LastName></PayerName><Address xsi:type=\"ebl:AddressType\"><AddressOwner xsi:type=\"ebl:AddressOwnerCodeType\">PayPal</AddressOwner><AddressStatus xsi:type=\"ebl:AddressStatusCodeType\">Unconfirmed</AddressStatus></Address></CardOwner><StartMonth>0</StartMonth><StartYear>0</StartYear><ThreeDSecureRequest xsi:type=\"ebl:ThreeDSecureRequestType\"></ThreeDSecureRequest></CreditCard><RegularRecurringPaymentsPeriod xsi:type=\"ebl:BillingPeriodDetailsType\"><BillingPeriod xsi:type=\"ebl:BillingPeriodTypeType\">Month</BillingPeriod><BillingFrequency>1</BillingFrequency><TotalBillingCycles>0</TotalBillingCycles><Amount xsi:type=\"cc:BasicAmountType\" currencyID=\"USD\">1.23</Amount><ShippingAmount xsi:type=\"cc:BasicAmountType\" currencyID=\"USD\">0.00</ShippingAmount><TaxAmount xsi:type=\"cc:BasicAmountType\" currencyID=\"USD\">0.00</TaxAmount></RegularRecurringPaymentsPeriod><TrialAmountPaid xsi:type=\"cc:BasicAmountType\" currencyID=\"USD\">0.00</TrialAmountPaid><RegularAmountPaid xsi:type=\"cc:BasicAmountType\" currencyID=\"USD\">0.00</RegularAmountPaid><AggregateAmount xsi:type=\"cc:BasicAmountType\" currencyID=\"USD\">0.00</AggregateAmount><AggregateOptionalAmount xsi:type=\"cc:BasicAmountType\" currencyID=\"USD\">0.00</AggregateOptionalAmount><FinalPaymentDueDate xsi:type=\"xs:dateTime\">1970-01-01T00:00:00Z</FinalPaymentDueDate></GetRecurringPaymentsProfileDetailsResponseDetails></GetRecurringPaymentsProfileDetailsResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> RESPONSE end + + def three_d_secure_option(version:, xid: nil, ds_transaction_id: nil) + { + three_d_secure: { + authentication_response_status: 'Y', + eci: 'eci', + cavv: 'cavv', + xid:, + ds_transaction_id:, + version: + } + } + end end diff --git a/test/unit/gateways/paysafe_test.rb b/test/unit/gateways/paysafe_test.rb new file mode 100644 index 00000000000..cfc15c1fef2 --- /dev/null +++ b/test/unit/gateways/paysafe_test.rb @@ -0,0 +1,421 @@ +require 'test_helper' + +class PaysafeTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = PaysafeGateway.new(username: 'username', password: 'password', account_id: 'account_id') + @credit_card = credit_card + @mastercard = credit_card('5454545454545454', brand: 'master') + @amount = 100 + + @options = { + billing_address: address, + merchant_descriptor: { + dynamic_descriptor: 'Store Purchase' + } + } + end + + def test_successful_purchase + @gateway.expects(:ssl_request).returns(successful_purchase_response) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + + assert_equal 'cddbd29d-4983-4719-983a-c6a862895781', response.authorization + assert response.test? + end + + def test_successful_purchase_with_mastercard_3ds2 + mc_three_d_secure_2_options = { + currency: 'EUR', + three_d_secure: { + eci: 0, + cavv: 'AAABBhkXYgAAAAACBxdiENhf7A+=', + version: '2.1.0', + ds_transaction_id: 'a3a721f3-b6fa-4cb5-84ea-c7b5c39890a2' + } + } + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @mastercard, mc_three_d_secure_2_options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"threeDSecureVersion":"2.1.0"/, data) + assert_match(/"directoryServerTransactionId":"a3a721f3-b6fa-4cb5-84ea-c7b5c39890a2"/, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_airline_details + airline_details = { + airline_travel_details: { + passenger_name: 'Joe Smith', + departure_date: '2021-11-30', + origin: 'SXF', + computerized_reservation_system: 'DATS', + ticket: { + ticket_number: 9876789, + is_restricted_ticket: false + }, + customer_reference_number: 107854099, + travel_agency: { + name: 'Sally Travel', + code: 'AGENTS' + }, + trip_legs: { + leg1: { + flight: { + carrier_code: 'LH', + flight_number: '344' + }, + service_class: 'F', + is_stop_over_allowed: true, + departure_date: '2021-11-30' + }, + leg2: { + flight: { + flight_number: '999' + }, + destination: 'SOF', + fare_basis: 'VMAY' + } + } + } + } + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, airline_details) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"airlineTravelDetails"/, data) + assert_match(/"computerizedReservationSystem":"DATS"/, data) + assert_match(/"tripLegs":{"leg1":{"flight":{"carrierCode":"LH"/, data) + assert_match(/"leg2":{"flight":{"flightNumber":"999"/, data) + assert_match(/"departureDate":"2021-11-30"/, data) + assert_match(/"travelAgency":{"name":"Sally Travel","code":"AGENTS"}/, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_stored_credentials + stored_credential_options = { + stored_credential: { + initial_transaction: true, + reason_type: 'installment', + initiator: 'merchant' + } + } + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential_options)) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(%r{"type":"RECURRING"}, data) + assert_match(%r{"occurrence":"INITIAL"}, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_external_initial_transaction_id + stored_credential_options = { + external_initial_transaction_id: 'abc123', + stored_credential: { + initial_transaction: false, + reason_type: 'unscheduled', + initiator: 'merchant' + } + } + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential_options)) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(%r{"type":"TOPUP"}, data) + assert_match(%r{"occurrence":"SUBSEQUENT"}, data) + assert_match(%r{"externalInitialTransactionId":"abc123"}, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_funding_transaction + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge({ funding_transaction: 'SDW_WALLET_TRANSFER' })) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(%r("fundingTransaction":{"type":"SDW_WALLET_TRANSFER"}), data) + assert_match(%r("profile":{.+"merchantCustomerId"), data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_failed_purchase + @gateway.expects(:ssl_request).returns(failed_purchase_response) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_equal '3022', response.error_code + end + + def test_successful_authorize + @gateway.expects(:ssl_request).returns(successful_authorize_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + + assert_equal '3155d89c-dfff-49a2-9352-b531e69102f7', response.authorization + assert_equal 'COMPLETED', response.message + 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 '3009', response.error_code + end + + def test_successful_capture + @gateway.expects(:ssl_request).returns(successful_capture_response) + auth = '3155d89c-dfff-49a2-9352-b531e69102f7' + + response = @gateway.capture(@amount, auth) + assert_success response + + assert_equal '6ee71dc2-00c0-4891-b226-ab741e63f43a', response.authorization + assert_equal 'PENDING', response.message + end + + def test_failed_capture + @gateway.expects(:ssl_request).returns(failed_capture_response) + + response = @gateway.capture(@amount, '') + assert_failure response + + assert_equal '5023', response.error_code + end + + def test_successful_refund + @gateway.expects(:ssl_request).returns(successful_refund_response) + auth = 'originaltransactionsauthorization' + + response = @gateway.refund(@amount, auth) + assert_success response + + assert_equal 'e86fe7c3-9d92-4149-89a9-fd2b3da95b05', response.authorization + assert_equal 'PENDING', response.message + end + + def test_failed_refund + @gateway.expects(:ssl_request).returns(failed_refund_response) + auth = 'invalidauthorizationid' + + response = @gateway.refund(@amount, auth) + assert_failure response + + assert_equal '3407', response.error_code + assert_equal 'Error(s)- code:3407, message:The settlement referred to by the transaction response ID you provided cannot be found.', response.message + end + + def test_successful_void + @gateway.expects(:ssl_request).returns(successful_void_response) + auth = '3155d89c-dfff-49a2-9352-b531e69102f7' + + response = @gateway.void(auth) + assert_success response + + assert_equal 'eb4d45ac-35ef-49e8-93d0-58b20a4c470e', response.authorization + assert_equal 'COMPLETED', response.message + end + + def test_failed_void + @gateway.expects(:ssl_request).returns(failed_void_response) + + response = @gateway.void('') + assert_failure response + + assert_equal '5023', response.error_code + end + + def test_successful_verify + @gateway.expects(:ssl_request).returns(successful_verify_response) + + response = @gateway.verify(@credit_card, @options) + assert_success response + + assert_equal '493936', response.params['authCode'] + end + + def test_successful_store + profile_options = { + phone: '111-222-3456', + email: 'profile@memail.com', + date_of_birth: { + month: 1, + year: 1979, + day: 1 + } + } + response = stub_comms(@gateway, :ssl_request) do + @gateway.store(@credit_card, profile_options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"holderName":"Longbob Longsen"/, data) + assert_match(/"dateOfBirth":{"year":1979,"month":1,"day":1}/, data) + end.respond_with(successful_store_response) + + assert_success response + end + + def test_successful_credit + stub_comms(@gateway, :ssl_request) do + @gateway.credit(100, @credit_card, @options.merge({ email: 'profile@memail.com', customer_id: SecureRandom.hex(16) })) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(%r("profile":{.+"merchantCustomerId"), data) + assert_match(%r("profile":{.+"email"), data) + assert_match(%r("profile":{"firstName":"Longbob".+}), data) + assert_match(%r("profile":{.+"lastName"), data) + assert_match(%r("merchantDescriptor":{"dynamicDescriptor"), data) + end.respond_with(successful_credit_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 + end + + private + + def pre_scrubbed + ' + <- "POST /cardpayments/v1/accounts/1002158490/auths HTTP/1.1\r\nContent-Type: application/json\r\nAuthorization: Basic cG1sZS03MTA1MjA6Qi1xYTItMC02MGY1YTg5MS0wLTMwMmMwMjE0NDkwZTdlYjliM2IxOWRlOTRlM2FkNjVhOTcxMGM4MTFmYjc4NzhiZTAyMTQxNzQwM2FiYjgyNmQ1NDg2MDdhZGQ3NTNjNmZhMjE0YjYxYmU5YTdj\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.test.paysafe.com\r\nContent-Length: 443\r\n\r\n" + <- "{\"amount\":100,\"card\":{\"cardExpiry\":{\"month\":9,\"year\":2022},\"cardNum\":\"4107857757053670\",\"cvv\":\"123\"},\"billingDetails\":{\"street\":\"999 This Way Lane\",\"city\":\"Hereville\",\"state\":\"NC\",\"country\":\"FR\",\"zip\":\"98989\",\"phone\":\"999-9999999\"},\"profile\":{\"firstName\":\"Longbob\",\"lastName\":\"Longsen\"},\"merchantDescriptor\":{\"dynamicDescriptor\":\"Store Purchase\",\"phone\":\"999-8887777\"},\"settleWithAuth\":true,\"merchantRefNum\":\"08498355c7f86bf096dc5f3fe77bd1da\"}" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: envoy\r\n" + -> "Content-Length: 1324\r\n" + -> "X-Applicationuid: GUID=f26c8e32-e8a4-435d-a288-703750d8a941\r\n" + -> "Content-Type: application/json\r\n" + -> "X-Envoy-Upstream-Service-Time: 144\r\n" + -> "Expires: Tue, 10 Aug 2021 16:56:06 GMT\r\n" + -> "Cache-Control: max-age=0, no-cache, no-store\r\n" + -> "Pragma: no-cache\r\n" + -> "Date: Tue, 10 Aug 2021 16:56:06 GMT\r\n" + -> "Connection: close\r\n" + -> "Set-Cookie: WLSESSIONID=g3Ew_iMEkM_6zDo4AisqhBlyuLi5UbyaVrkLVx3hmj-gOgZeKDl9!-2065395402!6582410; path=/; secure; HttpOnly\r\n" + -> "\r\n" + reading 1324 bytes... + -> "{\"id\":\"f26c8e32-e8a4-435d-a288-703750d8a941\",\"merchantRefNum\":\"08498355c7f86bf096dc5f3fe77bd1da\",\"txnTime\":\"2021-08-10T16:56:06Z\",\"status\":\"COMPLETED\",\"amount\":100,\"settleWithAuth\":true,\"preAuth\":false,\"availableToSettle\":0,\"card\":{\"type\":\"VI\",\"lastDigits\":\"3670\",\"cardExpiry\":{\"month\":9,\"year\":2022}},\"authCode\":\"976920\",\"profile\":{\"firstName\":\"Longbob\",\"lastName\":\"Longsen\"},\"billingDetails\":{\"street\":\"999 This Way Lane\",\"city\":\"Hereville\",\"state\":\"NC\",\"country\":\"FR\",\"zip\":\"98989\",\"phone\":\"999-9999999\"},\"merchantDescriptor\":{\"dynamicDescriptor\":\"Store Purchase\",\"phone\":\"999-8887777\"},\"visaAdditionalAuthData\":{},\"currencyCode\":\"EUR\",\"avsResponse\":\"MATCH\",\"cvvVerification\":\"MATCH\",\"settlements\":[{\"id\":\"f26c8e32-e8a4-435d-a288-703750d8a941\",\"merchantRefNum\":\"08498355c7f86bf096dc5f3fe77bd1da\",\"txnTime\":\"2021-08-10T16:56:06Z\",\"status\":\"PENDING\",\"amount\":100,\"availableToRefund\":100,\"links\":[{\"rel\":\"self\",\"href\":\"https://api.test.paysafe.com/cardpayments/v1/accounts/1002158490/settlements/f26c8e32-e8a4-435d-a288-703750d8a941\"}]}],\"links\":[{\"rel\":\"settlement\",\"href\":\"https://api.test.paysafe.com/cardpayments/v1/accounts/1002158490/settlements/f26c8e32-e8a4-435d-a288-703750d8a941\"},{\"rel\":\"self\",\"href\":\"https://api.test.paysafe.com/cardpayments/v1/accounts/1002158490/auths/f26c8e32-e8a4-435d-a288-703750d8a941\"}]}" + ' + end + + def post_scrubbed + ' + <- "POST /cardpayments/v1/accounts/1002158490/auths HTTP/1.1\r\nContent-Type: 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\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: api.test.paysafe.com\r\nContent-Length: 443\r\n\r\n" + <- "{\"amount\":100,\"card\":{\"cardExpiry\":{\"month\":9,\"year\":2022},\"cardNum\":\"[FILTERED]\",\"cvv\":\"[FILTERED]\"},\"billingDetails\":{\"street\":\"999 This Way Lane\",\"city\":\"Hereville\",\"state\":\"NC\",\"country\":\"FR\",\"zip\":\"98989\",\"phone\":\"999-9999999\"},\"profile\":{\"firstName\":\"Longbob\",\"lastName\":\"Longsen\"},\"merchantDescriptor\":{\"dynamicDescriptor\":\"Store Purchase\",\"phone\":\"999-8887777\"},\"settleWithAuth\":true,\"merchantRefNum\":\"08498355c7f86bf096dc5f3fe77bd1da\"}" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: envoy\r\n" + -> "Content-Length: 1324\r\n" + -> "X-Applicationuid: GUID=f26c8e32-e8a4-435d-a288-703750d8a941\r\n" + -> "Content-Type: application/json\r\n" + -> "X-Envoy-Upstream-Service-Time: 144\r\n" + -> "Expires: Tue, 10 Aug 2021 16:56:06 GMT\r\n" + -> "Cache-Control: max-age=0, no-cache, no-store\r\n" + -> "Pragma: no-cache\r\n" + -> "Date: Tue, 10 Aug 2021 16:56:06 GMT\r\n" + -> "Connection: close\r\n" + -> "Set-Cookie: WLSESSIONID=g3Ew_iMEkM_6zDo4AisqhBlyuLi5UbyaVrkLVx3hmj-gOgZeKDl9!-2065395402!6582410; path=/; secure; HttpOnly\r\n" + -> "\r\n" + reading 1324 bytes... + -> "{\"id\":\"f26c8e32-e8a4-435d-a288-703750d8a941\",\"merchantRefNum\":\"08498355c7f86bf096dc5f3fe77bd1da\",\"txnTime\":\"2021-08-10T16:56:06Z\",\"status\":\"COMPLETED\",\"amount\":100,\"settleWithAuth\":true,\"preAuth\":false,\"availableToSettle\":0,\"card\":{\"type\":\"VI\",\"lastDigits\":\"3670\",\"cardExpiry\":{\"month\":9,\"year\":2022}},\"authCode\":\"976920\",\"profile\":{\"firstName\":\"Longbob\",\"lastName\":\"Longsen\"},\"billingDetails\":{\"street\":\"999 This Way Lane\",\"city\":\"Hereville\",\"state\":\"NC\",\"country\":\"FR\",\"zip\":\"98989\",\"phone\":\"999-9999999\"},\"merchantDescriptor\":{\"dynamicDescriptor\":\"Store Purchase\",\"phone\":\"999-8887777\"},\"visaAdditionalAuthData\":{},\"currencyCode\":\"EUR\",\"avsResponse\":\"MATCH\",\"cvvVerification\":\"MATCH\",\"settlements\":[{\"id\":\"f26c8e32-e8a4-435d-a288-703750d8a941\",\"merchantRefNum\":\"08498355c7f86bf096dc5f3fe77bd1da\",\"txnTime\":\"2021-08-10T16:56:06Z\",\"status\":\"PENDING\",\"amount\":100,\"availableToRefund\":100,\"links\":[{\"rel\":\"self\",\"href\":\"https://api.test.paysafe.com/cardpayments/v1/accounts/1002158490/settlements/f26c8e32-e8a4-435d-a288-703750d8a941\"}]}],\"links\":[{\"rel\":\"settlement\",\"href\":\"https://api.test.paysafe.com/cardpayments/v1/accounts/1002158490/settlements/f26c8e32-e8a4-435d-a288-703750d8a941\"},{\"rel\":\"self\",\"href\":\"https://api.test.paysafe.com/cardpayments/v1/accounts/1002158490/auths/f26c8e32-e8a4-435d-a288-703750d8a941\"}]}" + ' + end + + def successful_purchase_response + '{"id":"cddbd29d-4983-4719-983a-c6a862895781","merchantRefNum":"c9b2ad852a1a37c1cc5c39b741be7484","txnTime":"2021-08-10T18:25:40Z","status":"COMPLETED","amount":100,"settleWithAuth":true,"preAuth":false,"availableToSettle":0,"card":{"type":"VI","lastDigits":"3670","cardExpiry":{"month":9,"year":2022}},"authCode":"544454","profile":{"firstName":"Longbob","lastName":"Longsen"},"billingDetails":{"street":"999 This Way Lane","city":"Hereville","state":"NC","country":"FR","zip":"98989","phone":"999-9999999"},"merchantDescriptor":{"dynamicDescriptor":"Store Purchase","phone":"999-8887777"},"visaAdditionalAuthData":{},"currencyCode":"EUR","avsResponse":"MATCH","cvvVerification":"MATCH","settlements":[{"id":"cddbd29d-4983-4719-983a-c6a862895781","merchantRefNum":"c9b2ad852a1a37c1cc5c39b741be7484","txnTime":"2021-08-10T18:25:40Z","status":"PENDING","amount":100,"availableToRefund":100,"links":[{"rel":"self","href":"https://api.test.paysafe.com/cardpayments/v1/accounts/1002158490/settlements/cddbd29d-4983-4719-983a-c6a862895781"}]}],"links":[{"rel":"settlement","href":"https://api.test.paysafe.com/cardpayments/v1/accounts/1002158490/settlements/cddbd29d-4983-4719-983a-c6a862895781"},{"rel":"self","href":"https://api.test.paysafe.com/cardpayments/v1/accounts/1002158490/auths/cddbd29d-4983-4719-983a-c6a862895781"}]}' + end + + def failed_purchase_response + '{"id":"c671d488-3f27-46f1-b0a7-2123e4e68f35","merchantRefNum":"12b616e548d7b866c6a61e6d585a762b","error":{"code":"3022","message":"The card has been declined due to insufficient funds.","links":[{"rel":"errorinfo","href":"https://developer.paysafe.com/en/rest-api/cards/test-and-go-live/card-errors/#ErrorCode3022"}]},"riskReasonCode":[1059],"settleWithAuth":true,"cvvVerification":"MATCH","links":[{"rel":"self","href":"https://api.test.paysafe.com/cardpayments/v1/accounts/1002158490/auths/c671d488-3f27-46f1-b0a7-2123e4e68f35"}]}' + end + + def successful_authorize_response + '{"id":"3155d89c-dfff-49a2-9352-b531e69102f7","merchantRefNum":"8b3c5142fdce91299e76a39d89e32bc1","txnTime":"2021-08-10T18:31:26Z","status":"COMPLETED","amount":100,"settleWithAuth":false,"preAuth":false,"availableToSettle":100,"card":{"type":"VI","lastDigits":"3670","cardExpiry":{"month":9,"year":2022}},"authCode":"659078","profile":{"firstName":"Longbob","lastName":"Longsen"},"billingDetails":{"street":"999 This Way Lane","city":"Hereville","state":"NC","country":"FR","zip":"98989","phone":"999-9999999"},"merchantDescriptor":{"dynamicDescriptor":"Store Purchase","phone":"999-8887777"},"visaAdditionalAuthData":{},"currencyCode":"EUR","avsResponse":"MATCH","cvvVerification":"MATCH","links":[{"rel":"self","href":"https://api.test.paysafe.com/cardpayments/v1/accounts/1002158490/auths/3155d89c-dfff-49a2-9352-b531e69102f7"}]}' + end + + def failed_authorize_response + '{"id":"bde4a254-7df9-462e-8de1-bfaa205d299a","merchantRefNum":"939b24ab14825b7d365842957dbda683","error":{"code":"3009","message":"Your request has been declined by the issuing bank.","links":[{"rel":"errorinfo","href":"https://developer.paysafe.com/en/rest-api/cards/test-and-go-live/card-errors/#ErrorCode3009"}]},"riskReasonCode":[1100],"settleWithAuth":false,"links":[{"rel":"self","href":"https://api.test.paysafe.com/cardpayments/v1/accounts/1002158490/auths/bde4a254-7df9-462e-8de1-bfaa205d299a"}]}' + end + + def successful_capture_response + '{"id":"6ee71dc2-00c0-4891-b226-ab741e63f43a","merchantRefNum":"09bf1e741aa1485ceae9b779e550f929","txnTime":"2021-08-10T18:31:26Z","status":"PENDING","amount":100,"availableToRefund":100,"links":[{"rel":"self","href":"https://api.test.paysafe.com/cardpayments/v1/accounts/1002158490/settlements/6ee71dc2-00c0-4891-b226-ab741e63f43a"}]}' + end + + def failed_capture_response + '{"error":{"code":"5023","message":"Request method POST not supported","links":[{"rel":"errorinfo","href":"https://developer.paysafe.com/en/rest-api/cards/test-and-go-live/card-errors/#ErrorCode5023"}]}}' + end + + def successful_verify_response + '{"id":"2b48475a-e3e7-47b0-8d84-66a331db9945","merchantRefNum":"fe95dee377466d9a54550c228227c5be","txnTime":"2021-08-18T20:06:55Z","status":"COMPLETED","card":{"type":"VI","lastDigits":"3670","cardExpiry":{"month":9,"year":2022}},"authCode":"493936","profile":{"firstName":"Longbob","lastName":"Longsen"},"billingDetails":{"street":"999 This Way Lane","city":"Hereville","state":"NC","country":"FR","zip":"98989","phone":"999-9999999"},"merchantDescriptor":{"dynamicDescriptor":"Test","phone":"123-1234123"},"visaAdditionalAuthData":{},"currencyCode":"EUR","avsResponse":"NOT_PROCESSED","cvvVerification":"MATCH","links":[{"rel":"self","href":"https://api.test.paysafe.com/cardpayments/v1/accounts/1002158490/verifications/2b48475a-e3e7-47b0-8d84-66a331db9945"}]}' + end + + def successful_refund_response + '{"id":"e86fe7c3-9d92-4149-89a9-fd2b3da95b05","merchantRefNum":"b8e04a4ff196b20f8aea42558aec8cbd","txnTime":"2021-08-11T13:40:59Z","status":"PENDING","amount":100,"links":[{"rel":"self","href":"https://api.test.paysafe.com/cardpayments/v1/accounts/1002158490/refunds/e86fe7c3-9d92-4149-89a9-fd2b3da95b05"}]}' + end + + def failed_refund_response + '{"id":"0c498606-3690-4b24-a083-0f8a75b8e043","merchantRefNum":"2becb71485cb38c862d2589decce99df","error":{"code":"3407","message":"The settlement referred to by the transaction response ID you provided cannot be found.","links":[{"rel":"errorinfo","href":"https://developer.paysafe.com/en/rest-api/cards/test-and-go-live/card-errors/#ErrorCode3407"}]},"links":[{"rel":"self","href":"https://api.test.paysafe.com/cardpayments/v1/accounts/1002158490/refunds/0c498606-3690-4b24-a083-0f8a75b8e043"}]}' + end + + def successful_void_response + '{"id":"eb4d45ac-35ef-49e8-93d0-58b20a4c470e","merchantRefNum":"dbeb1095b191c16715052d4bcc98b42d","txnTime":"2021-08-10T18:35:05Z","status":"COMPLETED","amount":100,"links":[{"rel":"self","href":"https://api.test.paysafe.com/cardpayments/v1/accounts/1002158490/voidauths/eb4d45ac-35ef-49e8-93d0-58b20a4c470e"}]}' + end + + def failed_void_response + '{"error":{"code":"5023","message":"Request method POST not supported","links":[{"rel":"errorinfo","href":"https://developer.paysafe.com/en/rest-api/cards/test-and-go-live/card-errors/#ErrorCode5023"}]}}' + end + + def successful_store_response + '{"id":"bd4e8c66-b023-4b38-b499-bc6d447d1466","status":"ACTIVE","merchantCustomerId":"965d3aff71fb93343ee48513","locale":"en_US","firstName":"Longbob","lastName":"Longsen","dateOfBirth":{"year":1979,"month":1,"day":1},"paymentToken":"PnCQ1xyGCB4sOEq","phone":"111-222-3456","email":"profile@memail.com","addresses":[],"cards":[{"status":"ACTIVE","id":"77685b40-e953-4999-a161-d13b46a8232a","cardBin":"411111","lastDigits":"1111","cardExpiry":{"year":2022,"month":9},"holderName":"Longbob Longsen","cardType":"VI","cardCategory":"CREDIT","paymentToken":"Ct0RrnyIs4lizeH","defaultCardIndicator":true}]}' + end + + def successful_credit_response + '{"id":"b40c327e-92d7-4026-a043-be1f1b03c08a","merchantRefNum":"b0ca10f1ab6b782e6bf8a43e17ff41f8","txnTime":"2024-10-02T19:00:27Z","status":"PENDING","gatewayReconciliationId":"2309329680","amount":100,"card":{"type":"VI", "lastDigits":"0000", "cardExpiry":{"month":9, "year":2025}, "issuingCountry":"US"},"profile":{"firstName":"Longbob", "lastName":"Longsen", "email":"profile@memail.com"},"billingDetails":{"street":"456 My Street","street2":"Apt 1","city":"Ottawa","state":"ON","country":"CA","zip":"K1C2N6","phone":"(555)555-5555"},"currencyCode":"USD","merchantDescriptor":{"dynamicDescriptor":"Store Purchase"},"links":[{"rel":"self","href":"https://api.test.paysafe.com/cardpayments/v1/accounts/1002179730/standalonecredits/b40c327e-92d7-4026-a043-be1f1b03c08a"}]}' + end +end diff --git a/test/unit/gateways/payscout_test.rb b/test/unit/gateways/payscout_test.rb index 7e84b6e4b29..6fc666505f1 100644 --- a/test/unit/gateways/payscout_test.rb +++ b/test/unit/gateways/payscout_test.rb @@ -3,17 +3,17 @@ class PayscoutTest < Test::Unit::TestCase def setup @gateway = PayscoutGateway.new( - :username => 'xxx', - :password => 'xxx' - ) + username: 'xxx', + password: 'xxx' + ) @credit_card = credit_card @amount = 100 @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase' + order_id: '1', + billing_address: address, + description: 'Store Purchase' } end @@ -214,7 +214,6 @@ def test_shipping_address assert_equal address[:email], post[:shipping_email] end - def test_add_currency_from_options post = {} @gateway.send(:add_currency, post, 100, { currency: 'CAD' }) @@ -231,7 +230,7 @@ def test_add_currency_from_money def test_add_invoice post = {} - options = {description: 'Order Description', order_id: '123'} + options = { description: 'Order Description', order_id: '123' } @gateway.send(:add_invoice, post, options) assert_equal 'Order Description', post[:orderdescription] @@ -260,15 +259,15 @@ def test_add_creditcard def test_parse data = @gateway.send(:parse, approved_authorization_response) - assert data.keys.include?('response') - assert data.keys.include?('responsetext') - assert data.keys.include?('authcode') - assert data.keys.include?('transactionid') - assert data.keys.include?('avsresponse') - assert data.keys.include?('cvvresponse') - assert data.keys.include?('orderid') - assert data.keys.include?('type') - assert data.keys.include?('response_code') + assert data.key?('response') + assert data.key?('responsetext') + assert data.key?('authcode') + assert data.key?('transactionid') + assert data.key?('avsresponse') + assert data.key?('cvvresponse') + assert data.key?('orderid') + assert data.key?('type') + assert data.key?('response_code') assert_equal '1', data['response'] assert_equal 'SUCCESS', data['responsetext'] @@ -282,25 +281,25 @@ def test_parse end def test_message_from_for_approved_response - assert_equal 'The transaction has been approved', @gateway.send(:message_from, {'response' => '1'}) + assert_equal 'The transaction has been approved', @gateway.send(:message_from, { 'response' => '1' }) end def test_message_from_for_declined_response - assert_equal 'The transaction has been declined', @gateway.send(:message_from, {'response' => '2'}) + assert_equal 'The transaction has been declined', @gateway.send(:message_from, { 'response' => '2' }) end def test_message_from_for_failed_response - assert_equal 'Error message', @gateway.send(:message_from, {'response' => '3', 'responsetext' => 'Error message'}) + assert_equal 'Error message', @gateway.send(:message_from, { 'response' => '3', 'responsetext' => 'Error message' }) end def test_success - assert @gateway.send(:success?, {'response' => '1'}) - refute @gateway.send(:success?, {'response' => '2'}) - refute @gateway.send(:success?, {'response' => '3'}) + assert @gateway.send(:success?, { 'response' => '1' }) + refute @gateway.send(:success?, { 'response' => '2' }) + refute @gateway.send(:success?, { 'response' => '3' }) end def test_post_data - parameters = {param1: 'value1', param2: 'value2'} + parameters = { param1: 'value1', param2: 'value2' } result = @gateway.send(:post_data, 'auth', parameters) assert_match 'username=xxx', result diff --git a/test/unit/gateways/paystation_test.rb b/test/unit/gateways/paystation_test.rb index f49d013b5ce..ccb6a3525a1 100644 --- a/test/unit/gateways/paystation_test.rb +++ b/test/unit/gateways/paystation_test.rb @@ -3,19 +3,18 @@ class PaystationTest < Test::Unit::TestCase include CommStub def setup - @gateway = PaystationGateway.new( - :paystation_id => 'some_id_number', - :gateway_id => 'another_id_number' - ) + paystation_id: 'some_id_number', + gateway_id: 'another_id_number' + ) @credit_card = credit_card @amount = 100 @options = { - :order_id => '1', - :customer => 'Joe Bloggs, Customer ID #56', - :description => 'Store Purchase' + order_id: '1', + customer: 'Joe Bloggs, Customer ID #56', + description: 'Store Purchase' } end @@ -42,7 +41,7 @@ def test_unsuccessful_request def test_successful_store @gateway.expects(:ssl_post).returns(successful_store_response) - assert response = @gateway.store(@credit_card, @options.merge(:token => 'justatest1310263135')) + assert response = @gateway.store(@credit_card, @options.merge(token: 'justatest1310263135')) assert_success response assert response.test? @@ -75,12 +74,11 @@ def test_successful_authorization def test_successful_capture @gateway.expects(:ssl_post).returns(successful_capture_response) - assert response = @gateway.capture(@amount, '0009062250-01', @options.merge(:credit_card_verification => 123)) + assert response = @gateway.capture(@amount, '0009062250-01', @options.merge(credit_card_verification: 123)) assert_success response end def test_successful_refund - response = stub_comms do @gateway.purchase(@amount, @credit_card, @options) end.respond_with(successful_purchase_response) @@ -91,7 +89,7 @@ def test_successful_refund refund = stub_comms do @gateway.refund(@amount, response.authorization, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/0008813023-01/, data) end.respond_with(successful_refund_response) @@ -120,312 +118,311 @@ def test_scrub private - def successful_purchase_response - %(<?xml version="1.0" standalone="yes"?> - <response> - <ec>0</ec> - <em>Transaction successful</em> - <ti>0006713018-01</ti> - <ct>mastercard</ct> - <merchant_ref>Store Purchase</merchant_ref> - <tm>T</tm> - <MerchantSession>1</MerchantSession> - <UsedAcquirerMerchantID>123456</UsedAcquirerMerchantID> - <TransactionID>0008813023-01</TransactionID> - <PurchaseAmount>10000</PurchaseAmount> - <Locale/> - <ReturnReceiptNumber>8813023</ReturnReceiptNumber> - <ShoppingTransactionNumber/> - <AcqResponseCode>00</AcqResponseCode> - <QSIResponseCode>0</QSIResponseCode> - <CSCResultCode/> - <AVSResultCode/> - <TransactionTime>2011-06-22 00:05:52</TransactionTime> - <PaystationErrorCode>0</PaystationErrorCode> - <PaystationErrorMessage>Transaction successful</PaystationErrorMessage> - <MerchantReference>Store Purchase</MerchantReference> - <TransactionMode>T</TransactionMode> - <BatchNumber>0622</BatchNumber> - <AuthorizeID/> - <Cardtype>MC</Cardtype> - <Username>12345</Username> - <RequestIP>192.168.0.1</RequestIP> - <RequestUserAgent/> - <RequestHttpReferrer/> - <PaymentRequestTime>2011-06-22 00:05:52</PaymentRequestTime> - <DigitalOrderTime/> - <DigitalReceiptTime>2011-06-22 00:05:52</DigitalReceiptTime> - <PaystationTransactionID>0008813023-01</PaystationTransactionID> - <IssuerName>unknown</IssuerName> - <IssuerCountry>unknown</IssuerCountry> - </response>) - end - - def failed_purchase_response - %(<?xml version="1.0" standalone="yes"?> - <response> - <ec>5</ec> - <em>Insufficient Funds</em> - <ti>0006713018-01</ti> - <ct>mastercard</ct> - <merchant_ref>Store Purchase</merchant_ref> - <tm>T</tm> - <MerchantSession>1</MerchantSession> - <UsedAcquirerMerchantID>123456</UsedAcquirerMerchantID> - <TransactionID>0008813018-01</TransactionID> - <PurchaseAmount>10051</PurchaseAmount> - <Locale/> - <ReturnReceiptNumber>8813018</ReturnReceiptNumber> - <ShoppingTransactionNumber/> - <AcqResponseCode>51</AcqResponseCode> - <QSIResponseCode>5</QSIResponseCode> - <CSCResultCode/> - <AVSResultCode/> - <TransactionTime>2011-06-22 00:05:46</TransactionTime> - <PaystationErrorCode>5</PaystationErrorCode> - <PaystationErrorMessage>Insufficient Funds</PaystationErrorMessage> - <MerchantReference>Store Purchase</MerchantReference> - <TransactionMode>T</TransactionMode> - <BatchNumber>0622</BatchNumber> - <AuthorizeID/> - <Cardtype>MC</Cardtype> - <Username>123456</Username> - <RequestIP>192.168.0.1</RequestIP> - <RequestUserAgent/> - <RequestHttpReferrer/> - <PaymentRequestTime>2011-06-22 00:05:46</PaymentRequestTime> - <DigitalOrderTime/> - <DigitalReceiptTime>2011-06-22 00:05:46</DigitalReceiptTime> - <PaystationTransactionID>0008813018-01</PaystationTransactionID> - <IssuerName>unknown</IssuerName> - <IssuerCountry>unknown</IssuerCountry> - </response>) - end - - def successful_store_response - %(<?xml version="1.0" standalone="yes"?> - <PaystationFuturePaymentResponse> - <ec>34</ec> - <em>Future Payment Saved Ok</em> - <ti/> - <ct/> - <merchant_ref>Store Purchase</merchant_ref> - <tm>T</tm> - <MerchantSession>3e48fa9a6b0fe36177adf7269db7a3c4</MerchantSession> - <UsedAcquirerMerchantID/> - <TransactionID/> - <PurchaseAmount>0</PurchaseAmount> - <Locale/> - <ReturnReceiptNumber/> - <ShoppingTransactionNumber/> - <AcqResponseCode/> - <QSIResponseCode/> - <CSCResultCode/> - <AVSResultCode/> - <TransactionTime>2011-07-10 13:58:55</TransactionTime> - <PaystationErrorCode>34</PaystationErrorCode> - <PaystationErrorMessage>Future Payment Saved Ok</PaystationErrorMessage> - <MerchantReference>Store Purchase</MerchantReference> - <TransactionMode>T</TransactionMode> - <BatchNumber/> - <AuthorizeID/> - <Cardtype/> - <Username>123456</Username> - <RequestIP>192.168.0.1</RequestIP> - <RequestUserAgent/> - <RequestHttpReferrer/> - <PaymentRequestTime>2011-07-10 13:58:55</PaymentRequestTime> - <DigitalOrderTime/> - <DigitalReceiptTime>2011-07-10 13:58:55</DigitalReceiptTime> - <PaystationTransactionID>0009062177-01</PaystationTransactionID> - <FuturePaymentToken>justatest1310263135</FuturePaymentToken> - <IssuerName>unknown</IssuerName> - <IssuerCountry>unknown</IssuerCountry> - </PaystationFuturePaymentResponse>) - end - - def successful_stored_purchase_response - %(<?xml version="1.0" standalone="yes"?> - <PaystationFuturePaymentResponse> - <ec>0</ec> - <em>Transaction successful</em> - <ti>0006713018-01</ti> - <ct>visa</ct> - <merchant_ref>Store Purchase</merchant_ref> - <tm>T</tm> - <MerchantSession>0fc70a577f19ae63f651f53c7044640a</MerchantSession> - <UsedAcquirerMerchantID>123456</UsedAcquirerMerchantID> - <TransactionID>0009062149-01</TransactionID> - <PurchaseAmount>10000</PurchaseAmount> - <Locale/> - <ReturnReceiptNumber>9062149</ReturnReceiptNumber> - <ShoppingTransactionNumber/> - <AcqResponseCode>00</AcqResponseCode> - <QSIResponseCode>0</QSIResponseCode> - <CSCResultCode/> - <AVSResultCode/> - <TransactionTime>2011-07-10 13:55:00</TransactionTime> - <PaystationErrorCode>0</PaystationErrorCode> - <PaystationErrorMessage>Transaction successful</PaystationErrorMessage> - <MerchantReference>Store Purchase</MerchantReference> - <TransactionMode>T</TransactionMode> - <BatchNumber>0710</BatchNumber> - <AuthorizeID/> - <Cardtype>VC</Cardtype> - <Username>123456</Username> - <RequestIP>192.168.0.1</RequestIP> - <RequestUserAgent/> - <RequestHttpReferrer/> - <PaymentRequestTime>2011-07-10 13:55:00</PaymentRequestTime> - <DigitalOrderTime/> - <DigitalReceiptTime>2011-07-10 13:55:00</DigitalReceiptTime> - <PaystationTransactionID>0009062149-01</PaystationTransactionID> - <FuturePaymentToken>u09fxli14afpnd6022x0z82317beqe9e2w048l9it8286k6lpvz9x27hdal9bl95</FuturePaymentToken> - <IssuerName>unknown</IssuerName> - <IssuerCountry>unknown</IssuerCountry> - </PaystationFuturePaymentResponse>) - end - - def successful_authorization_response - %(<?xml version="1.0" standalone="yes"?> - <response> - <ec>0</ec> - <em>Transaction successful</em> - <ti>0009062250-01</ti> - <ct>visa</ct> - <merchant_ref>Store Purchase</merchant_ref> - <tm>T</tm> - <MerchantSession>b2168af96076522466af4e3d61e5ba0c</MerchantSession> - <UsedAcquirerMerchantID>123456</UsedAcquirerMerchantID> - <TransactionID>0009062250-01</TransactionID> - <PurchaseAmount>10000</PurchaseAmount> - <Locale/> - <ReturnReceiptNumber>9062250</ReturnReceiptNumber> - <ShoppingTransactionNumber/> - <AcqResponseCode>00</AcqResponseCode> - <QSIResponseCode>0</QSIResponseCode> - <CSCResultCode/> - <AVSResultCode/> - <TransactionTime>2011-07-10 14:11:00</TransactionTime> - <PaystationErrorCode>0</PaystationErrorCode> - <PaystationErrorMessage>Transaction successful</PaystationErrorMessage> - <MerchantReference>Store Purchase</MerchantReference> - <TransactionMode>T</TransactionMode> - <BatchNumber>0710</BatchNumber> - <AuthorizeID/> - <Cardtype>VC</Cardtype> - <Username>123456</Username> - <RequestIP>192.168.0.1</RequestIP> - <RequestUserAgent/> - <RequestHttpReferrer/> - <PaymentRequestTime>2011-07-10 14:11:00</PaymentRequestTime> - <DigitalOrderTime/> - <DigitalReceiptTime>2011-07-10 14:11:00</DigitalReceiptTime> - <PaystationTransactionID>0009062250-01</PaystationTransactionID> - <IssuerName>unknown</IssuerName> - <IssuerCountry>unknown</IssuerCountry> - </response>) - end - - def successful_capture_response - %(<?xml version="1.0" standalone="yes"?> - <PaystationCaptureResponse> - <ec>0</ec> - <em>Transaction successful</em> - <ti>0009062289-01</ti> - <ct/> - <merchant_ref>Store Purchase</merchant_ref> - <tm>T</tm> - <MerchantSession>485fdedc81dc83848dd799cd10a869db</MerchantSession> - <UsedAcquirerMerchantID>123456</UsedAcquirerMerchantID> - <TransactionID>0009062289-01</TransactionID> - <CaptureAmount>10000</CaptureAmount> - <Locale/> - <ReturnReceiptNumber>9062289</ReturnReceiptNumber> - <ShoppingTransactionNumber/> - <AcqResponseCode>00</AcqResponseCode> - <QSIResponseCode>0</QSIResponseCode> - <CSCResultCode/> - <AVSResultCode/> - <TransactionTime>2011-07-10 14:17:36</TransactionTime> - <PaystationErrorCode>0</PaystationErrorCode> - <PaystationErrorMessage>Transaction successful</PaystationErrorMessage> - <MerchantReference>Store Purchase</MerchantReference> - <TransactionMode>T</TransactionMode> - <BatchNumber>0710</BatchNumber> - <AuthorizeID/> - <Cardtype/> - <Username>123456</Username> - <RequestIP>192.168.0.1</RequestIP> - <RequestUserAgent/> - <RequestHttpReferrer/> - <PaymentRequestTime>2011-07-10 14:17:36</PaymentRequestTime> - <DigitalOrderTime>2011-07-10 14:17:36</DigitalOrderTime> - <DigitalReceiptTime>2011-07-10 14:17:36</DigitalReceiptTime> - <PaystationTransactionID/> - <RefundedAmount/> - <CapturedAmount>10000</CapturedAmount> - <AuthorisedAmount/> - </PaystationCaptureResponse>) - end - - def successful_refund_response - %(<?xml version="1.0" standalone="yes"?> - <PaystationRefundResponse> - <ec>0</ec> - <em>Transaction successful</em> - <ti>0008813023-01</ti> - <ct>mastercard</ct> - <merchant_ref>Store Purchase</merchant_ref> - <tm>T</tm> - <MerchantSession>70ceae1b3f069e41ca7f4350a1180cb1</MerchantSession> - <UsedAcquirerMerchantID>924518</UsedAcquirerMerchantID> - <TransactionID>0008813023-01</TransactionID> - <RefundAmount>10000</RefundAmount> - <SurchargeAmount/> - <Locale>en</Locale> - <ReturnReceiptNumber>58160420</ReturnReceiptNumber> - <ShoppingTransactionNumber/> - <AcqResponseCode>00</AcqResponseCode> - <QSIResponseCode>0</QSIResponseCode> - <CSCResultCode/> - <AVSResultCode/> - <TransactionTime>2015-06-25 03:23:24</TransactionTime> - <PaystationErrorCode>0</PaystationErrorCode> - <PaystationErrorMessage>Transaction successful</PaystationErrorMessage> - <PaystationExtendedErrorMessage/> - <MerchantReference>Store Purchase</MerchantReference> - <CardNo>512345XXXXXXX346</CardNo> - <CardExpiry>1305</CardExpiry> - <TransactionProcess>refund</TransactionProcess> - <TransactionMode>T</TransactionMode> - <BatchNumber>0625</BatchNumber> - <AuthorizeID/> - <Cardtype>MC</Cardtype> - <Username>609035</Username> - <RequestIP>173.95.131.239</RequestIP> - <RequestUserAgent>Ruby</RequestUserAgent> - <RequestHttpReferrer/> - <PaymentRequestTime>2015-06-25 03:23:24</PaymentRequestTime> - <DigitalOrderTime>2015-06-25 03:23:24</DigitalOrderTime> - <DigitalReceiptTime/> - <PaystationTransactionID/> - <RefundedAmount>10000</RefundedAmount> - <CapturedAmount/> - </PaystationRefundResponse>) - end - - def failed_refund_response - %(<?xml version="1.0" standalone="yes"?> - <FONT FACE="Arial" SIZE="2"><strong>Error 11:</strong> Not enough input parameters.</FONT>) - end - - def pre_scrubbed - 'pstn_pi=609035&pstn_gi=PUSHPAY&pstn_2p=t&pstn_nr=t&pstn_df=yymm&pstn_ms=a755b9c84a530aee91dc3077f57294b0&pstn_mo=Store+Purchase&pstn_mr=&pstn_am=&pstn_cu=NZD&pstn_cn=5123456789012346&pstn_ct=visa&pstn_ex=1305&pstn_cc=123&pstn_tm=T&paystation=_empty' - end - - def post_scrubbed - 'pstn_pi=609035&pstn_gi=PUSHPAY&pstn_2p=t&pstn_nr=t&pstn_df=yymm&pstn_ms=a755b9c84a530aee91dc3077f57294b0&pstn_mo=Store+Purchase&pstn_mr=&pstn_am=&pstn_cu=NZD&pstn_cn=[FILTERED]&pstn_ct=visa&pstn_ex=1305&pstn_cc=[FILTERED]&pstn_tm=T&paystation=_empty' - end + def successful_purchase_response + %(<?xml version="1.0" standalone="yes"?> + <response> + <ec>0</ec> + <em>Transaction successful</em> + <ti>0006713018-01</ti> + <ct>mastercard</ct> + <merchant_ref>Store Purchase</merchant_ref> + <tm>T</tm> + <MerchantSession>1</MerchantSession> + <UsedAcquirerMerchantID>123456</UsedAcquirerMerchantID> + <TransactionID>0008813023-01</TransactionID> + <PurchaseAmount>10000</PurchaseAmount> + <Locale/> + <ReturnReceiptNumber>8813023</ReturnReceiptNumber> + <ShoppingTransactionNumber/> + <AcqResponseCode>00</AcqResponseCode> + <QSIResponseCode>0</QSIResponseCode> + <CSCResultCode/> + <AVSResultCode/> + <TransactionTime>2011-06-22 00:05:52</TransactionTime> + <PaystationErrorCode>0</PaystationErrorCode> + <PaystationErrorMessage>Transaction successful</PaystationErrorMessage> + <MerchantReference>Store Purchase</MerchantReference> + <TransactionMode>T</TransactionMode> + <BatchNumber>0622</BatchNumber> + <AuthorizeID/> + <Cardtype>MC</Cardtype> + <Username>12345</Username> + <RequestIP>192.168.0.1</RequestIP> + <RequestUserAgent/> + <RequestHttpReferrer/> + <PaymentRequestTime>2011-06-22 00:05:52</PaymentRequestTime> + <DigitalOrderTime/> + <DigitalReceiptTime>2011-06-22 00:05:52</DigitalReceiptTime> + <PaystationTransactionID>0008813023-01</PaystationTransactionID> + <IssuerName>unknown</IssuerName> + <IssuerCountry>unknown</IssuerCountry> + </response>) + end + + def failed_purchase_response + %(<?xml version="1.0" standalone="yes"?> + <response> + <ec>5</ec> + <em>Insufficient Funds</em> + <ti>0006713018-01</ti> + <ct>mastercard</ct> + <merchant_ref>Store Purchase</merchant_ref> + <tm>T</tm> + <MerchantSession>1</MerchantSession> + <UsedAcquirerMerchantID>123456</UsedAcquirerMerchantID> + <TransactionID>0008813018-01</TransactionID> + <PurchaseAmount>10051</PurchaseAmount> + <Locale/> + <ReturnReceiptNumber>8813018</ReturnReceiptNumber> + <ShoppingTransactionNumber/> + <AcqResponseCode>51</AcqResponseCode> + <QSIResponseCode>5</QSIResponseCode> + <CSCResultCode/> + <AVSResultCode/> + <TransactionTime>2011-06-22 00:05:46</TransactionTime> + <PaystationErrorCode>5</PaystationErrorCode> + <PaystationErrorMessage>Insufficient Funds</PaystationErrorMessage> + <MerchantReference>Store Purchase</MerchantReference> + <TransactionMode>T</TransactionMode> + <BatchNumber>0622</BatchNumber> + <AuthorizeID/> + <Cardtype>MC</Cardtype> + <Username>123456</Username> + <RequestIP>192.168.0.1</RequestIP> + <RequestUserAgent/> + <RequestHttpReferrer/> + <PaymentRequestTime>2011-06-22 00:05:46</PaymentRequestTime> + <DigitalOrderTime/> + <DigitalReceiptTime>2011-06-22 00:05:46</DigitalReceiptTime> + <PaystationTransactionID>0008813018-01</PaystationTransactionID> + <IssuerName>unknown</IssuerName> + <IssuerCountry>unknown</IssuerCountry> + </response>) + end + + def successful_store_response + %(<?xml version="1.0" standalone="yes"?> + <PaystationFuturePaymentResponse> + <ec>34</ec> + <em>Future Payment Saved Ok</em> + <ti/> + <ct/> + <merchant_ref>Store Purchase</merchant_ref> + <tm>T</tm> + <MerchantSession>3e48fa9a6b0fe36177adf7269db7a3c4</MerchantSession> + <UsedAcquirerMerchantID/> + <TransactionID/> + <PurchaseAmount>0</PurchaseAmount> + <Locale/> + <ReturnReceiptNumber/> + <ShoppingTransactionNumber/> + <AcqResponseCode/> + <QSIResponseCode/> + <CSCResultCode/> + <AVSResultCode/> + <TransactionTime>2011-07-10 13:58:55</TransactionTime> + <PaystationErrorCode>34</PaystationErrorCode> + <PaystationErrorMessage>Future Payment Saved Ok</PaystationErrorMessage> + <MerchantReference>Store Purchase</MerchantReference> + <TransactionMode>T</TransactionMode> + <BatchNumber/> + <AuthorizeID/> + <Cardtype/> + <Username>123456</Username> + <RequestIP>192.168.0.1</RequestIP> + <RequestUserAgent/> + <RequestHttpReferrer/> + <PaymentRequestTime>2011-07-10 13:58:55</PaymentRequestTime> + <DigitalOrderTime/> + <DigitalReceiptTime>2011-07-10 13:58:55</DigitalReceiptTime> + <PaystationTransactionID>0009062177-01</PaystationTransactionID> + <FuturePaymentToken>justatest1310263135</FuturePaymentToken> + <IssuerName>unknown</IssuerName> + <IssuerCountry>unknown</IssuerCountry> + </PaystationFuturePaymentResponse>) + end + + def successful_stored_purchase_response + %(<?xml version="1.0" standalone="yes"?> + <PaystationFuturePaymentResponse> + <ec>0</ec> + <em>Transaction successful</em> + <ti>0006713018-01</ti> + <ct>visa</ct> + <merchant_ref>Store Purchase</merchant_ref> + <tm>T</tm> + <MerchantSession>0fc70a577f19ae63f651f53c7044640a</MerchantSession> + <UsedAcquirerMerchantID>123456</UsedAcquirerMerchantID> + <TransactionID>0009062149-01</TransactionID> + <PurchaseAmount>10000</PurchaseAmount> + <Locale/> + <ReturnReceiptNumber>9062149</ReturnReceiptNumber> + <ShoppingTransactionNumber/> + <AcqResponseCode>00</AcqResponseCode> + <QSIResponseCode>0</QSIResponseCode> + <CSCResultCode/> + <AVSResultCode/> + <TransactionTime>2011-07-10 13:55:00</TransactionTime> + <PaystationErrorCode>0</PaystationErrorCode> + <PaystationErrorMessage>Transaction successful</PaystationErrorMessage> + <MerchantReference>Store Purchase</MerchantReference> + <TransactionMode>T</TransactionMode> + <BatchNumber>0710</BatchNumber> + <AuthorizeID/> + <Cardtype>VC</Cardtype> + <Username>123456</Username> + <RequestIP>192.168.0.1</RequestIP> + <RequestUserAgent/> + <RequestHttpReferrer/> + <PaymentRequestTime>2011-07-10 13:55:00</PaymentRequestTime> + <DigitalOrderTime/> + <DigitalReceiptTime>2011-07-10 13:55:00</DigitalReceiptTime> + <PaystationTransactionID>0009062149-01</PaystationTransactionID> + <FuturePaymentToken>u09fxli14afpnd6022x0z82317beqe9e2w048l9it8286k6lpvz9x27hdal9bl95</FuturePaymentToken> + <IssuerName>unknown</IssuerName> + <IssuerCountry>unknown</IssuerCountry> + </PaystationFuturePaymentResponse>) + end + + def successful_authorization_response + %(<?xml version="1.0" standalone="yes"?> + <response> + <ec>0</ec> + <em>Transaction successful</em> + <ti>0009062250-01</ti> + <ct>visa</ct> + <merchant_ref>Store Purchase</merchant_ref> + <tm>T</tm> + <MerchantSession>b2168af96076522466af4e3d61e5ba0c</MerchantSession> + <UsedAcquirerMerchantID>123456</UsedAcquirerMerchantID> + <TransactionID>0009062250-01</TransactionID> + <PurchaseAmount>10000</PurchaseAmount> + <Locale/> + <ReturnReceiptNumber>9062250</ReturnReceiptNumber> + <ShoppingTransactionNumber/> + <AcqResponseCode>00</AcqResponseCode> + <QSIResponseCode>0</QSIResponseCode> + <CSCResultCode/> + <AVSResultCode/> + <TransactionTime>2011-07-10 14:11:00</TransactionTime> + <PaystationErrorCode>0</PaystationErrorCode> + <PaystationErrorMessage>Transaction successful</PaystationErrorMessage> + <MerchantReference>Store Purchase</MerchantReference> + <TransactionMode>T</TransactionMode> + <BatchNumber>0710</BatchNumber> + <AuthorizeID/> + <Cardtype>VC</Cardtype> + <Username>123456</Username> + <RequestIP>192.168.0.1</RequestIP> + <RequestUserAgent/> + <RequestHttpReferrer/> + <PaymentRequestTime>2011-07-10 14:11:00</PaymentRequestTime> + <DigitalOrderTime/> + <DigitalReceiptTime>2011-07-10 14:11:00</DigitalReceiptTime> + <PaystationTransactionID>0009062250-01</PaystationTransactionID> + <IssuerName>unknown</IssuerName> + <IssuerCountry>unknown</IssuerCountry> + </response>) + end + + def successful_capture_response + %(<?xml version="1.0" standalone="yes"?> + <PaystationCaptureResponse> + <ec>0</ec> + <em>Transaction successful</em> + <ti>0009062289-01</ti> + <ct/> + <merchant_ref>Store Purchase</merchant_ref> + <tm>T</tm> + <MerchantSession>485fdedc81dc83848dd799cd10a869db</MerchantSession> + <UsedAcquirerMerchantID>123456</UsedAcquirerMerchantID> + <TransactionID>0009062289-01</TransactionID> + <CaptureAmount>10000</CaptureAmount> + <Locale/> + <ReturnReceiptNumber>9062289</ReturnReceiptNumber> + <ShoppingTransactionNumber/> + <AcqResponseCode>00</AcqResponseCode> + <QSIResponseCode>0</QSIResponseCode> + <CSCResultCode/> + <AVSResultCode/> + <TransactionTime>2011-07-10 14:17:36</TransactionTime> + <PaystationErrorCode>0</PaystationErrorCode> + <PaystationErrorMessage>Transaction successful</PaystationErrorMessage> + <MerchantReference>Store Purchase</MerchantReference> + <TransactionMode>T</TransactionMode> + <BatchNumber>0710</BatchNumber> + <AuthorizeID/> + <Cardtype/> + <Username>123456</Username> + <RequestIP>192.168.0.1</RequestIP> + <RequestUserAgent/> + <RequestHttpReferrer/> + <PaymentRequestTime>2011-07-10 14:17:36</PaymentRequestTime> + <DigitalOrderTime>2011-07-10 14:17:36</DigitalOrderTime> + <DigitalReceiptTime>2011-07-10 14:17:36</DigitalReceiptTime> + <PaystationTransactionID/> + <RefundedAmount/> + <CapturedAmount>10000</CapturedAmount> + <AuthorisedAmount/> + </PaystationCaptureResponse>) + end + + def successful_refund_response + %(<?xml version="1.0" standalone="yes"?> + <PaystationRefundResponse> + <ec>0</ec> + <em>Transaction successful</em> + <ti>0008813023-01</ti> + <ct>mastercard</ct> + <merchant_ref>Store Purchase</merchant_ref> + <tm>T</tm> + <MerchantSession>70ceae1b3f069e41ca7f4350a1180cb1</MerchantSession> + <UsedAcquirerMerchantID>924518</UsedAcquirerMerchantID> + <TransactionID>0008813023-01</TransactionID> + <RefundAmount>10000</RefundAmount> + <SurchargeAmount/> + <Locale>en</Locale> + <ReturnReceiptNumber>58160420</ReturnReceiptNumber> + <ShoppingTransactionNumber/> + <AcqResponseCode>00</AcqResponseCode> + <QSIResponseCode>0</QSIResponseCode> + <CSCResultCode/> + <AVSResultCode/> + <TransactionTime>2015-06-25 03:23:24</TransactionTime> + <PaystationErrorCode>0</PaystationErrorCode> + <PaystationErrorMessage>Transaction successful</PaystationErrorMessage> + <PaystationExtendedErrorMessage/> + <MerchantReference>Store Purchase</MerchantReference> + <CardNo>512345XXXXXXX346</CardNo> + <CardExpiry>1305</CardExpiry> + <TransactionProcess>refund</TransactionProcess> + <TransactionMode>T</TransactionMode> + <BatchNumber>0625</BatchNumber> + <AuthorizeID/> + <Cardtype>MC</Cardtype> + <Username>609035</Username> + <RequestIP>173.95.131.239</RequestIP> + <RequestUserAgent>Ruby</RequestUserAgent> + <RequestHttpReferrer/> + <PaymentRequestTime>2015-06-25 03:23:24</PaymentRequestTime> + <DigitalOrderTime>2015-06-25 03:23:24</DigitalOrderTime> + <DigitalReceiptTime/> + <PaystationTransactionID/> + <RefundedAmount>10000</RefundedAmount> + <CapturedAmount/> + </PaystationRefundResponse>) + end + def failed_refund_response + %(<?xml version="1.0" standalone="yes"?> + <FONT FACE="Arial" SIZE="2"><strong>Error 11:</strong> Not enough input parameters.</FONT>) + end + + def pre_scrubbed + 'pstn_pi=609035&pstn_gi=PUSHPAY&pstn_2p=t&pstn_nr=t&pstn_df=yymm&pstn_ms=a755b9c84a530aee91dc3077f57294b0&pstn_mo=Store+Purchase&pstn_mr=&pstn_am=&pstn_cu=NZD&pstn_cn=5123456789012346&pstn_ct=visa&pstn_ex=1305&pstn_cc=123&pstn_tm=T&paystation=_empty' + end + + def post_scrubbed + 'pstn_pi=609035&pstn_gi=PUSHPAY&pstn_2p=t&pstn_nr=t&pstn_df=yymm&pstn_ms=a755b9c84a530aee91dc3077f57294b0&pstn_mo=Store+Purchase&pstn_mr=&pstn_am=&pstn_cu=NZD&pstn_cn=[FILTERED]&pstn_ct=visa&pstn_ex=1305&pstn_cc=[FILTERED]&pstn_tm=T&paystation=_empty' + end end diff --git a/test/unit/gateways/payu_in_test.rb b/test/unit/gateways/payu_in_test.rb index 876bae34162..b2d3c981cc9 100644 --- a/test/unit/gateways/payu_in_test.rb +++ b/test/unit/gateways/payu_in_test.rb @@ -16,7 +16,7 @@ def setup } end - def assert_parameter(parameter, expected_value, data, options={}) + def assert_parameter(parameter, expected_value, data, options = {}) assert (data =~ %r{(?:^|&)#{parameter}=([^&]*)(?:&|$)}), "Unable to find #{parameter} in #{data}" value = CGI.unescape($1 || '') case expected_value @@ -25,9 +25,7 @@ def assert_parameter(parameter, expected_value, data, options={}) else assert_equal expected_value.to_s, value, "#{parameter} value does not match expected" end - if options[:length] - assert_equal options[:length], value.length, "#{parameter} value of #{value} is the wrong length" - end + assert_equal options[:length], value.length, "#{parameter} value of #{value} is the wrong length" if options[:length] end def test_successful_purchase @@ -205,7 +203,7 @@ def test_input_constraint_cleanup phone: ('a-' + ('1' * 51)) } ) - end.check_request do |endpoint, data, headers| + end.check_request do |endpoint, data, _headers| case endpoint when /_payment/ assert_parameter('txnid', /^a/, data, length: 30) @@ -293,7 +291,7 @@ def test_failed_purchase def test_successful_refund response = stub_comms do @gateway.refund(100, 'abc') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_parameter('command', 'cancel_refund_transaction', data) assert_parameter('var1', 'abc', data) assert_parameter('var2', /./, data) @@ -345,108 +343,108 @@ def test_invalid_json private def pre_scrubbed - <<-PRE_SCRUBBED -opening connection to test.payu.in:443... -opened -starting SSL for test.payu.in:443... -SSL established -<- "POST /_payment HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept-Encoding: identity\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: test.payu.in\r\nContent-Length: 460\r\n\r\n" -<- "amount=1.00&txnid=19ceaa9a230d3057dba07b78ad5c7d46&productinfo=Store+Purchase&surl=http%3A%2F%2Fexample.com&furl=http%3A%2F%2Fexample.com&pg=CC&firstname=Longbob&bankcode=VISA&ccnum=5123456789012346&ccvv=123&ccname=Longbob+Longsen&ccexpmon=5&ccexpyr=2017&email=unknown%40example.com&phone=11111111111&key=Gzv04m&txn_s2s_flow=1&hash=a255c1b5107556b7f00b7c5bbebf92392ec4d2c0675253ca20ef459d4259775efbeae039b59357ddd42374d278dedb432f2e9c238acc6358afe9b22cf908fbb3" --> "HTTP/1.1 200 OK\r\n" --> "Date: Fri, 08 May 2015 15:41:17 GMT\r\n" --> "Server: Apache\r\n" --> "P3P: CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\"\r\n" --> "Set-Cookie: PHPSESSID=ud24vi12os6m7f7g0lpmked4a0; path=/; secure; HttpOnly\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" --> "Vary: Accept-Encoding\r\n" --> "Content-Length: 691\r\n" --> "Connection: close\r\n" --> "Content-Type: text/html; charset=UTF-8\r\n" --> "\r\n" -reading 691 bytes... --> "" --> "{\"status\":\"success\",\"response\":{\"form_post_vars\":{\"transactionId\":\"b84436e889cf6864a9fa2ab267f3f76a766ad6437b017ccb5093e8217996b814\",\"pgId\":\"8\",\"eci\":\"7\",\"nonEnrolled\":1,\"nonDomestic\":0,\"bank\":\"VISA\",\"cccat\":\"creditcard\",\"ccnum\":\"4b5c9002295c6cd8e5289e2f9c312dc737810a747b84e71665cf077c78fe245a\",\"ccname\":\"53ab689fdb1b025c7e9c53c6b4a6e27f51e0d627579e7c12af2cb6cbc4944cc0\",\"ccvv\":\"f31c6a1d6582f44ee1be4a3e1126b9cb08d1e7006f7afe083d7270b00dcb933f\",\"ccexpmon\":\"5ddf3702e74f473ec89762f6efece025737c2ab999e695cf10496e6fa3946079\",\"ccexpyr\":\"5da83563fcaa945063dc4c2094c48e800badf7c8246c9d13b43757fe99d63e6d\",\"is_seamless\":\"1\"},\"post_uri\":\"https:\\/\\/test.payu.in\\/hdfc_not_enrolled\",\"enrolled\":\"0\"}}" -read 691 bytes -Conn close -opening connection to test.payu.in:443... -opened -starting SSL for test.payu.in:443... -SSL established -<- "POST /hdfc_not_enrolled HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept-Encoding: identity\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: test.payu.in\r\nContent-Length: 520\r\n\r\n" -<- "transactionId=b84436e889cf6864a9fa2ab267f3f76a766ad6437b017ccb5093e8217996b814&pgId=8&eci=7&nonEnrolled=1&nonDomestic=0&bank=VISA&cccat=creditcard&ccnum=4b5c9002295c6cd8e5289e2f9c312dc737810a747b84e71665cf077c78fe245a&ccname=53ab689fdb1b025c7e9c53c6b4a6e27f51e0d627579e7c12af2cb6cbc4944cc0&ccvv=f31c6a1d6582f44ee1be4a3e1126b9cb08d1e7006f7afe083d7270b00dcb933f&ccexpmon=5ddf3702e74f473ec89762f6efece025737c2ab999e695cf10496e6fa3946079&ccexpyr=5da83563fcaa945063dc4c2094c48e800badf7c8246c9d13b43757fe99d63e6d&is_seamless=1" --> "HTTP/1.1 200 OK\r\n" --> "Date: Fri, 08 May 2015 15:41:27 GMT\r\n" --> "Server: Apache\r\n" --> "P3P: CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\"\r\n" --> "Set-Cookie: PHPSESSID=n717g1mr5lvht96ukdobu6m344; path=/; secure; HttpOnly\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" --> "Vary: Accept-Encoding\r\n" --> "Content-Length: 1012\r\n" --> "Connection: close\r\n" --> "Content-Type: text/html; charset=UTF-8\r\n" --> "\r\n" -reading 1012 bytes... --> "" --> "{\"status\":\"success\",\"result\":\"mihpayid=403993715511983692&mode=CC&status=success&key=Gzv04m&txnid=19ceaa9a230d3057dba07b78ad5c7d46&amount=1.00&addedon=2015-05-08+21%3A11%3A17&productinfo=Store+Purchase&firstname=Longbob&lastname=&address1=&address2=&city=&state=&country=&zipcode=&email=unknown%40example.com&phone=11111111111&udf1=&udf2=&udf3=&udf4=&udf5=&udf6=&udf7=&udf8=&udf9=&udf10=&card_token=&card_no=512345XXXXXX2346&field0=&field1=512816420000&field2=999999&field3=6816991112151281&field4=-1&field5=&field6=&field7=&field8=&field9=SUCCESS&PG_TYPE=HDFCPG&error=E000&error_Message=No+Error&net_amount_debit=1&unmappedstatus=success&hash=c0d3e5346c37ddd32bb3b386ed1d0709a612d304180e7a25dcbf047cc1c3a4e9de9940af0179c6169c0038b2a826d7ea4b868fcbc4e435928e8cbd25da3c1e56&bank_ref_no=6816991112151281&bank_ref_num=6816991112151281&bankcode=VISA&surl=http%3A%2F%2Fexample.com&curl=http%3A%2F%2Fexample.com&furl=http%3A%2F%2Fexample.com&card_hash=f25e4f9ea802050c23423966d35adc54046f651f0d9a2b837b49c75f964d1fa7\"}" -read 1012 bytes -Conn close + <<~PRE_SCRUBBED + opening connection to test.payu.in:443... + opened + starting SSL for test.payu.in:443... + SSL established + <- "POST /_payment HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept-Encoding: identity\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: test.payu.in\r\nContent-Length: 460\r\n\r\n" + <- "amount=1.00&txnid=19ceaa9a230d3057dba07b78ad5c7d46&productinfo=Store+Purchase&surl=http%3A%2F%2Fexample.com&furl=http%3A%2F%2Fexample.com&pg=CC&firstname=Longbob&bankcode=VISA&ccnum=5123456789012346&ccvv=123&ccname=Longbob+Longsen&ccexpmon=5&ccexpyr=2017&email=unknown%40example.com&phone=11111111111&key=Gzv04m&txn_s2s_flow=1&hash=a255c1b5107556b7f00b7c5bbebf92392ec4d2c0675253ca20ef459d4259775efbeae039b59357ddd42374d278dedb432f2e9c238acc6358afe9b22cf908fbb3" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Fri, 08 May 2015 15:41:17 GMT\r\n" + -> "Server: Apache\r\n" + -> "P3P: CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\"\r\n" + -> "Set-Cookie: PHPSESSID=ud24vi12os6m7f7g0lpmked4a0; path=/; secure; HttpOnly\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" + -> "Vary: Accept-Encoding\r\n" + -> "Content-Length: 691\r\n" + -> "Connection: close\r\n" + -> "Content-Type: text/html; charset=UTF-8\r\n" + -> "\r\n" + reading 691 bytes... + -> "" + -> "{\"status\":\"success\",\"response\":{\"form_post_vars\":{\"transactionId\":\"b84436e889cf6864a9fa2ab267f3f76a766ad6437b017ccb5093e8217996b814\",\"pgId\":\"8\",\"eci\":\"7\",\"nonEnrolled\":1,\"nonDomestic\":0,\"bank\":\"VISA\",\"cccat\":\"creditcard\",\"ccnum\":\"4b5c9002295c6cd8e5289e2f9c312dc737810a747b84e71665cf077c78fe245a\",\"ccname\":\"53ab689fdb1b025c7e9c53c6b4a6e27f51e0d627579e7c12af2cb6cbc4944cc0\",\"ccvv\":\"f31c6a1d6582f44ee1be4a3e1126b9cb08d1e7006f7afe083d7270b00dcb933f\",\"ccexpmon\":\"5ddf3702e74f473ec89762f6efece025737c2ab999e695cf10496e6fa3946079\",\"ccexpyr\":\"5da83563fcaa945063dc4c2094c48e800badf7c8246c9d13b43757fe99d63e6d\",\"is_seamless\":\"1\"},\"post_uri\":\"https:\\/\\/test.payu.in\\/hdfc_not_enrolled\",\"enrolled\":\"0\"}}" + read 691 bytes + Conn close + opening connection to test.payu.in:443... + opened + starting SSL for test.payu.in:443... + SSL established + <- "POST /hdfc_not_enrolled HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept-Encoding: identity\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: test.payu.in\r\nContent-Length: 520\r\n\r\n" + <- "transactionId=b84436e889cf6864a9fa2ab267f3f76a766ad6437b017ccb5093e8217996b814&pgId=8&eci=7&nonEnrolled=1&nonDomestic=0&bank=VISA&cccat=creditcard&ccnum=4b5c9002295c6cd8e5289e2f9c312dc737810a747b84e71665cf077c78fe245a&ccname=53ab689fdb1b025c7e9c53c6b4a6e27f51e0d627579e7c12af2cb6cbc4944cc0&ccvv=f31c6a1d6582f44ee1be4a3e1126b9cb08d1e7006f7afe083d7270b00dcb933f&ccexpmon=5ddf3702e74f473ec89762f6efece025737c2ab999e695cf10496e6fa3946079&ccexpyr=5da83563fcaa945063dc4c2094c48e800badf7c8246c9d13b43757fe99d63e6d&is_seamless=1" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Fri, 08 May 2015 15:41:27 GMT\r\n" + -> "Server: Apache\r\n" + -> "P3P: CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\"\r\n" + -> "Set-Cookie: PHPSESSID=n717g1mr5lvht96ukdobu6m344; path=/; secure; HttpOnly\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" + -> "Vary: Accept-Encoding\r\n" + -> "Content-Length: 1012\r\n" + -> "Connection: close\r\n" + -> "Content-Type: text/html; charset=UTF-8\r\n" + -> "\r\n" + reading 1012 bytes... + -> "" + -> "{\"status\":\"success\",\"result\":\"mihpayid=403993715511983692&mode=CC&status=success&key=Gzv04m&txnid=19ceaa9a230d3057dba07b78ad5c7d46&amount=1.00&addedon=2015-05-08+21%3A11%3A17&productinfo=Store+Purchase&firstname=Longbob&lastname=&address1=&address2=&city=&state=&country=&zipcode=&email=unknown%40example.com&phone=11111111111&udf1=&udf2=&udf3=&udf4=&udf5=&udf6=&udf7=&udf8=&udf9=&udf10=&card_token=&card_no=512345XXXXXX2346&field0=&field1=512816420000&field2=999999&field3=6816991112151281&field4=-1&field5=&field6=&field7=&field8=&field9=SUCCESS&PG_TYPE=HDFCPG&error=E000&error_Message=No+Error&net_amount_debit=1&unmappedstatus=success&hash=c0d3e5346c37ddd32bb3b386ed1d0709a612d304180e7a25dcbf047cc1c3a4e9de9940af0179c6169c0038b2a826d7ea4b868fcbc4e435928e8cbd25da3c1e56&bank_ref_no=6816991112151281&bank_ref_num=6816991112151281&bankcode=VISA&surl=http%3A%2F%2Fexample.com&curl=http%3A%2F%2Fexample.com&furl=http%3A%2F%2Fexample.com&card_hash=f25e4f9ea802050c23423966d35adc54046f651f0d9a2b837b49c75f964d1fa7\"}" + read 1012 bytes + Conn close PRE_SCRUBBED end def post_scrubbed - <<-POST_SCRUBBED -opening connection to test.payu.in:443... -opened -starting SSL for test.payu.in:443... -SSL established -<- "POST /_payment HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept-Encoding: identity\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: test.payu.in\r\nContent-Length: 460\r\n\r\n" -<- "amount=1.00&txnid=19ceaa9a230d3057dba07b78ad5c7d46&productinfo=Store+Purchase&surl=http%3A%2F%2Fexample.com&furl=http%3A%2F%2Fexample.com&pg=CC&firstname=Longbob&bankcode=VISA&ccnum=[FILTERED]&ccvv=[FILTERED]&ccname=Longbob+Longsen&ccexpmon=5&ccexpyr=2017&email=unknown%40example.com&phone=11111111111&key=Gzv04m&txn_s2s_flow=1&hash=a255c1b5107556b7f00b7c5bbebf92392ec4d2c0675253ca20ef459d4259775efbeae039b59357ddd42374d278dedb432f2e9c238acc6358afe9b22cf908fbb3" --> "HTTP/1.1 200 OK\r\n" --> "Date: Fri, 08 May 2015 15:41:17 GMT\r\n" --> "Server: Apache\r\n" --> "P3P: CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\"\r\n" --> "Set-Cookie: PHPSESSID=ud24vi12os6m7f7g0lpmked4a0; path=/; secure; HttpOnly\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" --> "Vary: Accept-Encoding\r\n" --> "Content-Length: 691\r\n" --> "Connection: close\r\n" --> "Content-Type: text/html; charset=UTF-8\r\n" --> "\r\n" -reading 691 bytes... --> "" --> "{\"status\":\"success\",\"response\":{\"form_post_vars\":{\"transactionId\":\"b84436e889cf6864a9fa2ab267f3f76a766ad6437b017ccb5093e8217996b814\",\"pgId\":\"8\",\"eci\":\"7\",\"nonEnrolled\":1,\"nonDomestic\":0,\"bank\":\"VISA\",\"cccat\":\"creditcard\",\"ccnum\":\"[FILTERED]\",\"ccname\":\"53ab689fdb1b025c7e9c53c6b4a6e27f51e0d627579e7c12af2cb6cbc4944cc0\",\"ccvv\":\"[FILTERED]\",\"ccexpmon\":\"5ddf3702e74f473ec89762f6efece025737c2ab999e695cf10496e6fa3946079\",\"ccexpyr\":\"5da83563fcaa945063dc4c2094c48e800badf7c8246c9d13b43757fe99d63e6d\",\"is_seamless\":\"1\"},\"post_uri\":\"https:\\/\\/test.payu.in\\/hdfc_not_enrolled\",\"enrolled\":\"0\"}}" -read 691 bytes -Conn close -opening connection to test.payu.in:443... -opened -starting SSL for test.payu.in:443... -SSL established -<- "POST /hdfc_not_enrolled HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept-Encoding: identity\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: test.payu.in\r\nContent-Length: 520\r\n\r\n" -<- "transactionId=b84436e889cf6864a9fa2ab267f3f76a766ad6437b017ccb5093e8217996b814&pgId=8&eci=7&nonEnrolled=1&nonDomestic=0&bank=VISA&cccat=creditcard&ccnum=[FILTERED]&ccname=53ab689fdb1b025c7e9c53c6b4a6e27f51e0d627579e7c12af2cb6cbc4944cc0&ccvv=[FILTERED]&ccexpmon=5ddf3702e74f473ec89762f6efece025737c2ab999e695cf10496e6fa3946079&ccexpyr=5da83563fcaa945063dc4c2094c48e800badf7c8246c9d13b43757fe99d63e6d&is_seamless=1" --> "HTTP/1.1 200 OK\r\n" --> "Date: Fri, 08 May 2015 15:41:27 GMT\r\n" --> "Server: Apache\r\n" --> "P3P: CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\"\r\n" --> "Set-Cookie: PHPSESSID=n717g1mr5lvht96ukdobu6m344; path=/; secure; HttpOnly\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" --> "Vary: Accept-Encoding\r\n" --> "Content-Length: 1012\r\n" --> "Connection: close\r\n" --> "Content-Type: text/html; charset=UTF-8\r\n" --> "\r\n" -reading 1012 bytes... --> "" --> "{\"status\":\"success\",\"result\":\"mihpayid=403993715511983692&mode=CC&status=success&key=Gzv04m&txnid=19ceaa9a230d3057dba07b78ad5c7d46&amount=1.00&addedon=2015-05-08+21%3A11%3A17&productinfo=Store+Purchase&firstname=Longbob&lastname=&address1=&address2=&city=&state=&country=&zipcode=&email=unknown%40example.com&phone=11111111111&udf1=&udf2=&udf3=&udf4=&udf5=&udf6=&udf7=&udf8=&udf9=&udf10=&card_token=&card_no=512345XXXXXX2346&field0=&field1=512816420000&field2=999999&field3=6816991112151281&field4=-1&field5=&field6=&field7=&field8=&field9=SUCCESS&PG_TYPE=HDFCPG&error=E000&error_Message=No+Error&net_amount_debit=1&unmappedstatus=success&hash=c0d3e5346c37ddd32bb3b386ed1d0709a612d304180e7a25dcbf047cc1c3a4e9de9940af0179c6169c0038b2a826d7ea4b868fcbc4e435928e8cbd25da3c1e56&bank_ref_no=6816991112151281&bank_ref_num=6816991112151281&bankcode=VISA&surl=http%3A%2F%2Fexample.com&curl=http%3A%2F%2Fexample.com&furl=http%3A%2F%2Fexample.com&card_hash=[FILTERED]\"}" -read 1012 bytes -Conn close + <<~POST_SCRUBBED + opening connection to test.payu.in:443... + opened + starting SSL for test.payu.in:443... + SSL established + <- "POST /_payment HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept-Encoding: identity\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: test.payu.in\r\nContent-Length: 460\r\n\r\n" + <- "amount=1.00&txnid=19ceaa9a230d3057dba07b78ad5c7d46&productinfo=Store+Purchase&surl=http%3A%2F%2Fexample.com&furl=http%3A%2F%2Fexample.com&pg=CC&firstname=Longbob&bankcode=VISA&ccnum=[FILTERED]&ccvv=[FILTERED]&ccname=Longbob+Longsen&ccexpmon=5&ccexpyr=2017&email=unknown%40example.com&phone=11111111111&key=Gzv04m&txn_s2s_flow=1&hash=a255c1b5107556b7f00b7c5bbebf92392ec4d2c0675253ca20ef459d4259775efbeae039b59357ddd42374d278dedb432f2e9c238acc6358afe9b22cf908fbb3" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Fri, 08 May 2015 15:41:17 GMT\r\n" + -> "Server: Apache\r\n" + -> "P3P: CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\"\r\n" + -> "Set-Cookie: PHPSESSID=ud24vi12os6m7f7g0lpmked4a0; path=/; secure; HttpOnly\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" + -> "Vary: Accept-Encoding\r\n" + -> "Content-Length: 691\r\n" + -> "Connection: close\r\n" + -> "Content-Type: text/html; charset=UTF-8\r\n" + -> "\r\n" + reading 691 bytes... + -> "" + -> "{\"status\":\"success\",\"response\":{\"form_post_vars\":{\"transactionId\":\"b84436e889cf6864a9fa2ab267f3f76a766ad6437b017ccb5093e8217996b814\",\"pgId\":\"8\",\"eci\":\"7\",\"nonEnrolled\":1,\"nonDomestic\":0,\"bank\":\"VISA\",\"cccat\":\"creditcard\",\"ccnum\":\"[FILTERED]\",\"ccname\":\"53ab689fdb1b025c7e9c53c6b4a6e27f51e0d627579e7c12af2cb6cbc4944cc0\",\"ccvv\":\"[FILTERED]\",\"ccexpmon\":\"5ddf3702e74f473ec89762f6efece025737c2ab999e695cf10496e6fa3946079\",\"ccexpyr\":\"5da83563fcaa945063dc4c2094c48e800badf7c8246c9d13b43757fe99d63e6d\",\"is_seamless\":\"1\"},\"post_uri\":\"https:\\/\\/test.payu.in\\/hdfc_not_enrolled\",\"enrolled\":\"0\"}}" + read 691 bytes + Conn close + opening connection to test.payu.in:443... + opened + starting SSL for test.payu.in:443... + SSL established + <- "POST /hdfc_not_enrolled HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept-Encoding: identity\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: test.payu.in\r\nContent-Length: 520\r\n\r\n" + <- "transactionId=b84436e889cf6864a9fa2ab267f3f76a766ad6437b017ccb5093e8217996b814&pgId=8&eci=7&nonEnrolled=1&nonDomestic=0&bank=VISA&cccat=creditcard&ccnum=[FILTERED]&ccname=53ab689fdb1b025c7e9c53c6b4a6e27f51e0d627579e7c12af2cb6cbc4944cc0&ccvv=[FILTERED]&ccexpmon=5ddf3702e74f473ec89762f6efece025737c2ab999e695cf10496e6fa3946079&ccexpyr=5da83563fcaa945063dc4c2094c48e800badf7c8246c9d13b43757fe99d63e6d&is_seamless=1" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Fri, 08 May 2015 15:41:27 GMT\r\n" + -> "Server: Apache\r\n" + -> "P3P: CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\"\r\n" + -> "Set-Cookie: PHPSESSID=n717g1mr5lvht96ukdobu6m344; path=/; secure; HttpOnly\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" + -> "Vary: Accept-Encoding\r\n" + -> "Content-Length: 1012\r\n" + -> "Connection: close\r\n" + -> "Content-Type: text/html; charset=UTF-8\r\n" + -> "\r\n" + reading 1012 bytes... + -> "" + -> "{\"status\":\"success\",\"result\":\"mihpayid=403993715511983692&mode=CC&status=success&key=Gzv04m&txnid=19ceaa9a230d3057dba07b78ad5c7d46&amount=1.00&addedon=2015-05-08+21%3A11%3A17&productinfo=Store+Purchase&firstname=Longbob&lastname=&address1=&address2=&city=&state=&country=&zipcode=&email=unknown%40example.com&phone=11111111111&udf1=&udf2=&udf3=&udf4=&udf5=&udf6=&udf7=&udf8=&udf9=&udf10=&card_token=&card_no=512345XXXXXX2346&field0=&field1=512816420000&field2=999999&field3=6816991112151281&field4=-1&field5=&field6=&field7=&field8=&field9=SUCCESS&PG_TYPE=HDFCPG&error=E000&error_Message=No+Error&net_amount_debit=1&unmappedstatus=success&hash=c0d3e5346c37ddd32bb3b386ed1d0709a612d304180e7a25dcbf047cc1c3a4e9de9940af0179c6169c0038b2a826d7ea4b868fcbc4e435928e8cbd25da3c1e56&bank_ref_no=6816991112151281&bank_ref_num=6816991112151281&bankcode=VISA&surl=http%3A%2F%2Fexample.com&curl=http%3A%2F%2Fexample.com&furl=http%3A%2F%2Fexample.com&card_hash=[FILTERED]\"}" + read 1012 bytes + Conn close POST_SCRUBBED end diff --git a/test/unit/gateways/payu_latam_test.rb b/test/unit/gateways/payu_latam_test.rb index 897cf82be56..664db4e5490 100644 --- a/test/unit/gateways/payu_latam_test.rb +++ b/test/unit/gateways/payu_latam_test.rb @@ -12,10 +12,14 @@ def setup @pending_card = credit_card('4097440000000004', verification_value: '222', first_name: 'PENDING', last_name: '') @no_cvv_visa_card = credit_card('4097440000000004', verification_value: ' ') @no_cvv_amex_card = credit_card('4097440000000004', verification_value: ' ', brand: 'american_express') + @cabal_credit_card = credit_card('5896570000000004', verification_value: '123', first_name: 'APPROVED', last_name: '', brand: 'cabal') + @maestro_card = credit_card('6759000000000000005', verification_value: '123', first_name: 'APPROVED', brand: 'maestro') + @codensa_card = credit_card('5907120000000009', verification_value: '123', first_name: 'APPROVED', brand: 'maestro') @options = { dni_number: '5415668464654', dni_type: 'TI', + merchant_buyer_id: '1', currency: 'ARS', order_id: generate_unique_id, description: 'Active Merchant Transaction', @@ -51,11 +55,20 @@ def test_successful_purchase def test_successful_purchase_with_specified_language stub_comms do @gateway.purchase(@amount, @credit_card, @options.merge(language: 'es')) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/"language":"es"/, data) end.respond_with(successful_purchase_response) end + def test_successful_purchase_with_cabal_card + @gateway.expects(:ssl_post).returns(successful_purchase_with_cabal_response) + + response = @gateway.purchase(@amount, @cabal_credit_card, @options) + assert_success response + assert_equal 'APPROVED', response.message + assert response.test? + end + def test_failed_purchase @gateway.expects(:ssl_post).returns(failed_purchase_response) @@ -65,6 +78,24 @@ def test_failed_purchase assert_equal 'DECLINED', response.params['transactionResponse']['state'] end + def test_failed_purchase_correct_message_when_payment_network_response_error_present + @gateway.expects(:ssl_post).returns(failed_purchase_response_when_payment_network_response_error_expected) + + 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) + + 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 + def test_successful_authorize @gateway.expects(:ssl_post).returns(successful_authorize_response) @@ -77,11 +108,20 @@ def test_successful_authorize def test_successful_authorize_with_specified_language stub_comms do @gateway.authorize(@amount, @credit_card, @options.merge(language: 'es')) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/"language":"es"/, data) end.respond_with(successful_purchase_response) end + def test_successful_authorize_with_cabal_card + @gateway.expects(:ssl_post).returns(successful_authorize_with_cabal_response) + + response = @gateway.authorize(@amount, @cabal_credit_card, @options) + assert_success response + assert_equal 'APPROVED', response.message + assert_match %r(^\d+\|(\w|-)+$), response.authorization + end + def test_failed_authorize @gateway.expects(:ssl_post).returns(pending_authorize_response) @@ -102,8 +142,19 @@ def test_pending_refund def test_pending_refund_with_specified_language stub_comms do @gateway.refund(@amount, '7edbaf68-8f3a-4ae7-b9c7-d1e27e314999', @options.merge(language: 'es')) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/"language":"es"/, data) + assert_match(/"type":"REFUND"/, data) + end.respond_with(pending_refund_response) + end + + def test_partial_refund + stub_comms do + @gateway.refund(2000, '7edbaf68-8f3a-4ae7-b9c7-d1e27e314999', @options.merge(partial_refund: true)) + end.check_request do |_endpoint, data, _headers| + assert_match(/"type":"PARTIAL_REFUND"/, data) + assert_match(/"TX_VALUE"/, data) + assert_match(/"value":"20.00"/, data) end.respond_with(pending_refund_response) end @@ -126,7 +177,7 @@ def test_successful_void def test_successful_void_with_specified_language stub_comms do @gateway.void('7edbaf68-8f3a-4ae7-b9c7-d1e27e314999', @options.merge(language: 'es')) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/"language":"es"/, data) end.respond_with(successful_void_response) end @@ -142,11 +193,44 @@ def test_failed_void def test_successful_purchase_with_dni_number stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/"dniNumber":"5415668464654"/, data) end.respond_with(successful_purchase_response) end + def test_successful_purchase_with_merchant_buyer_id + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/"merchantBuyerId":"1"/, data) + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_phone_number + options = @options.merge(billing_address: {}, shipping_address: { phone_number: 5555555555 }) + stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_equal 5555555555, JSON.parse(data)['transaction']['order']['buyer']['contactPhone'] + end.respond_with(successful_purchase_response) + end + + def test_card_type_maestro_maps_to_mastercard + stub_comms do + @gateway.purchase(@amount, @maestro_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_equal 'MASTERCARD', JSON.parse(data)['transaction']['paymentMethod'] + end.respond_with(successful_purchase_response) + end + + def test_card_type_codensa + stub_comms do + @gateway.purchase(@amount, @codensa_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_equal 'CODENSA', JSON.parse(data)['transaction']['paymentMethod'] + end.respond_with(successful_purchase_response) + end + def test_verify_good_credentials @gateway.expects(:ssl_post).returns(credentials_are_legit_response) assert @gateway.verify_credentials @@ -158,8 +242,8 @@ def test_verify_bad_credentials end def test_request_using_visa_card_with_no_cvv - @gateway.expects(:ssl_post).with { |url, body, headers| - body.match '"securityCode":"000"' + @gateway.expects(:ssl_post).with { |_url, body, _headers| + body =~ /"securityCode":"000"/ body.match '"processWithoutCvv2":true' }.returns(successful_purchase_response) response = @gateway.purchase(@amount, @no_cvv_visa_card, @options) @@ -169,8 +253,8 @@ def test_request_using_visa_card_with_no_cvv end def test_request_using_amex_card_with_no_cvv - @gateway.expects(:ssl_post).with { |url, body, headers| - body.match '"securityCode":"0000"' + @gateway.expects(:ssl_post).with { |_url, body, _headers| + body =~ /"securityCode":"0000"/ body.match '"processWithoutCvv2":true' }.returns(successful_purchase_response) response = @gateway.purchase(@amount, @no_cvv_amex_card, @options) @@ -180,8 +264,8 @@ def test_request_using_amex_card_with_no_cvv end def test_request_passes_cvv_option - @gateway.expects(:ssl_post).with { |url, body, headers| - body.match '"securityCode":"777"' + @gateway.expects(:ssl_post).with { |_url, body, _headers| + body =~ /"securityCode":"777"/ !body.match '"processWithoutCvv2"' }.returns(successful_purchase_response) options = @options.merge(cvv: '777') @@ -202,11 +286,19 @@ def test_successful_capture def test_successful_capture_with_specified_language stub_comms do @gateway.capture(@amount, '4000|authorization', @options.merge(language: 'es')) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/"language":"es"/, data) end.respond_with(successful_purchase_response) end + def test_successful_partial_capture + stub_comms do + @gateway.capture(@amount - 1, '4000|authorization', @options) + end.check_request do |_endpoint, data, _headers| + assert_equal '39.99', JSON.parse(data)['transaction']['additionalValues']['TX_VALUE']['value'] + end.respond_with(successful_purchase_response) + end + def test_failed_capture @gateway.expects(:ssl_post).returns(failed_void_response) @@ -224,27 +316,56 @@ def test_partial_buyer_hash_info state: 'SP', country: 'BR', zip: '01019-030', - phone: '(11)756312345' + phone_number: '(11)756312345' ), buyer: { name: 'Jorge Borges', dni_number: '5415668464456', + merchant_buyer_id: '1', email: 'axaxaxas@mlo.org' } } stub_comms do @gateway.purchase(@amount, @credit_card, @options.update(options_buyer)) - end.check_request do |endpoint, data, headers| - assert_match(/\"buyer\":{\"fullName\":\"Jorge Borges\",\"dniNumber\":\"5415668464456\",\"dniType\":null,\"emailAddress\":\"axaxaxas@mlo.org\",\"contactPhone\":\"7563126\",\"shippingAddress\":{\"street1\":\"Calle 200\",\"street2\":\"N107\",\"city\":\"Sao Paulo\",\"state\":\"SP\",\"country\":\"BR\",\"postalCode\":\"01019-030\",\"phone\":\"\(11\)756312345\"}}/, data) + end.check_request do |_endpoint, data, _headers| + assert_match(/\"buyer\":{\"fullName\":\"Jorge Borges\",\"dniNumber\":\"5415668464456\",\"dniType\":null,\"merchantBuyerId\":\"1\",\"emailAddress\":\"axaxaxas@mlo.org\",\"contactPhone\":\"7563126\",\"shippingAddress\":{\"street1\":\"Calle 200\",\"street2\":\"N107\",\"city\":\"Sao Paulo\",\"state\":\"SP\",\"country\":\"BR\",\"postalCode\":\"01019-030\",\"phone\":\"\(11\)756312345\"}}/, data) end.respond_with(successful_purchase_response) end def test_buyer_fields_default_to_payer stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| - assert_match(/\"buyer\":{\"fullName\":\"APPROVED\",\"dniNumber\":\"5415668464654\",\"dniType\":\"TI\",\"emailAddress\":\"username@domain.com\",\"contactPhone\":\"7563126\"/, data) + end.check_request do |_endpoint, data, _headers| + assert_match(/\"buyer\":{\"fullName\":\"APPROVED\",\"dniNumber\":\"5415668464654\",\"dniType\":\"TI\",\"merchantBuyerId\":\"1\",\"emailAddress\":\"username@domain.com\",\"contactPhone\":\"7563126\"/, data) + end.respond_with(successful_purchase_response) + end + + def test_request_with_blank_billing_address_fields + options = { + dni_number: '5415668464654', + dni_type: 'TI', + merchant_buyer_id: '1', + currency: 'ARS', + order_id: generate_unique_id, + description: 'Active Merchant Transaction', + billing_address: address( + address1: 'Viamonte', + address2: nil, + city: 'Plata', + state: 'Buenos Aires', + country: '', + zip: '64000', + phone: '7563126' + ) + } + + stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/"merchantBuyerId":"1"/, data) + assert_match(/"street2":null/, data) + refute_match(/"country"/, data) end.respond_with(successful_purchase_response) end @@ -269,7 +390,7 @@ def test_brazil_required_fields state: 'SP', country: 'BR', zip: '01019-030', - phone: '(11)756312633' + phone_number: '(11)756312633' ), buyer: { cnpj: '32593371000110' @@ -278,7 +399,7 @@ def test_brazil_required_fields stub_comms(gateway) do gateway.purchase(@amount, @credit_card, @options.update(options_brazil)) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/\"cnpj\":\"32593371000110\"/, data) end.respond_with(successful_purchase_response) end @@ -304,7 +425,7 @@ def test_colombia_required_fields state: 'Bogota DC', country: 'CO', zip: '01019-030', - phone: '(11)756312633' + phone_number: '(11)756312633' ), tx_tax: '3193', tx_tax_return_base: '16806' @@ -312,7 +433,7 @@ def test_colombia_required_fields stub_comms(gateway) do gateway.purchase(@amount, @credit_card, @options.update(options_colombia)) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/\"additionalValues\":{\"TX_VALUE\":{\"value\":\"40.00\",\"currency\":\"COP\"},\"TX_TAX\":{\"value\":0,\"currency\":\"COP\"},\"TX_TAX_RETURN_BASE\":{\"value\":0,\"currency\":\"COP\"}}/, data) end.respond_with(successful_purchase_response) end @@ -338,18 +459,28 @@ def test_mexico_required_fields state: 'Jalisco', country: 'MX', zip: '01019-030', - phone: '(11)756312633' + phone_number: '(11)756312633' ), birth_date: '1985-05-25' } stub_comms(gateway) do gateway.purchase(@amount, @credit_card, @options.update(options_mexico)) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/\"birthdate\":\"1985-05-25\"/, data) end.respond_with(successful_purchase_response) end + def test_extra_parameters_fields + stub_comms(@gateway) do + @gateway.purchase(@amount, @credit_card, @options.merge({ extra_1: '123456', extra_2: 'abcdef', extra_3: 'testing' })) + end.check_request do |_endpoint, data, _headers| + assert_match(/\"EXTRA1\":\"123456\"/, data) + assert_match(/\"EXTRA2\":\"abcdef\"/, data) + assert_match(/\"EXTRA3\":\"testing\"/, data) + end.respond_with(successful_purchase_response) + end + def test_scrub assert @gateway.supports_scrubbing? assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed @@ -443,6 +574,37 @@ def successful_purchase_response RESPONSE end + def successful_purchase_with_cabal_response + <<-RESPONSE + { + "code":"SUCCESS", + "error":null, + "transactionResponse": { + "orderId":846449068, + "transactionId":"34fa1616-f16c-4474-98dc-6163cb05f6d1", + "state":"APPROVED", + "paymentNetworkResponseCode":null, + "paymentNetworkResponseErrorMessage":null, + "trazabilityCode":"00000000", + "authorizationCode":"00000000", + "pendingReason":null, + "responseCode":"APPROVED", + "errorCode":null, + "responseMessage":null, + "transactionDate":null, + "transactionTime":null, + "operationDate":1567524354749, + "referenceQuestionnaire":null, + "extraParameters": { + "PAYMENT_WAY_ID":"28", + "BANK_REFERENCED_CODE":"DEBIT" + }, + "additionalInfo":null + } + } + RESPONSE + end + def failed_purchase_response <<-RESPONSE { @@ -470,6 +632,60 @@ def failed_purchase_response RESPONSE end + def failed_purchase_response_when_payment_network_response_error_expected + <<-RESPONSE + { + "code": "SUCCESS", + "error": null, + "transactionResponse": { + "orderId": 7354347, + "transactionId": "15b6cec0-9eec-4564-b6b9-c846b868203e", + "state": "DECLINED", + "paymentNetworkResponseCode": "290", + "paymentNetworkResponseErrorMessage": "Contactar con entidad emisora", + "trazabilityCode": null, + "authorizationCode": null, + "pendingReason": null, + "responseCode": "CONTACT_THE_ENTITY", + "errorCode": null, + "responseMessage": null, + "transactionDate": null, + "transactionTime": null, + "operationDate": null, + "referenceQuestionnaire": null, + "extraParameters": null + } + } + RESPONSE + end + + def failed_purchase_response_when_payment_network_response_error_not_expected + <<-RESPONSE + { + "code": "SUCCESS", + "error": null, + "transactionResponse": { + "orderId": 7354347, + "transactionId": "15b6cec0-9eec-4564-b6b9-c846b868203e", + "state": "DECLINED", + "paymentNetworkResponseCode": "51", + "paymentNetworkResponseErrorMessage": null, + "trazabilityCode": null, + "authorizationCode": null, + "pendingReason": null, + "responseCode": "CONTACT_THE_ENTITY", + "errorCode": null, + "responseMessage": null, + "transactionDate": null, + "transactionTime": null, + "operationDate": null, + "referenceQuestionnaire": null, + "extraParameters": null + } + } + RESPONSE + end + def successful_authorize_response <<-RESPONSE { @@ -496,6 +712,38 @@ def successful_authorize_response RESPONSE end + def successful_authorize_with_cabal_response + <<-RESPONSE + { + "code":"SUCCESS", + "error":null, + "transactionResponse": { + "orderId":846449155, + "transactionId":"c15e6015-87c2-4db9-9100-894bf5564330", + "state":"APPROVED", + "paymentNetworkResponseCode":null, + "paymentNetworkResponseErrorMessage":null, + "trazabilityCode":"00000000", + "authorizationCode":"00000000", + "pendingReason":null, + "responseCode":"APPROVED", + "errorCode":null, + "responseMessage":null, + "transactionDate":null, + "transactionTime":null, + "operationDate":1567524806987, + "referenceQuestionnaire":null, + "extraParameters": { + "PAYMENT_WAY_ID":"28", + "BANK_REFERENCED_CODE":"DEBIT" + }, + "additionalInfo":null + } + } + + RESPONSE + end + def pending_authorize_response <<-RESPONSE { diff --git a/test/unit/gateways/payway_dot_com_test.rb b/test/unit/gateways/payway_dot_com_test.rb new file mode 100644 index 00000000000..ee46f650366 --- /dev/null +++ b/test/unit/gateways/payway_dot_com_test.rb @@ -0,0 +1,1478 @@ +require 'test_helper' + +class PaywayDotComTest < Test::Unit::TestCase + def setup + @gateway = PaywayDotComGateway.new( + login: 'sprerestwsdev', + password: 'sprerestwsdev1!', + company_id: '3', + source_id: '67' + ) + @credit_card = credit_card + @amount = 100 + + @options = { + order_id: '1', + billing_address: address, + description: 'Store Purchase' + } + end + + def test_successful_purchase + @gateway.expects(:ssl_request).returns(successful_purchase_response) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal '5000', response.message[0, 4] + assert_equal '0987654321', response.params['cardTransaction']['authorizationCode'] + assert_equal '', response.error_code + assert response.test? + assert_equal 'Z', response.avs_result['code'] + assert_equal 'M', response.cvv_result['code'] + 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 '5035', response.message[0, 4] + assert_equal Gateway::STANDARD_ERROR_CODE[:invalid_number], response.error_code + end + + def test_successful_authorize + @gateway.expects(:ssl_request).returns(successful_authorize_response) + + auth_only = @gateway.authorize(103, @credit_card, @options) + assert_success auth_only + assert_equal '5000', auth_only.message[0, 4] + end + + def test_successful_authorize_and_capture + @gateway.expects(:ssl_request).returns(successful_authorize_and_capture_response) + + auth = @gateway.authorize(104, @credit_card, @options) + assert_success auth + + @gateway.expects(:ssl_request).returns(successful_capture_response) + + assert capture = @gateway.capture(@amount, auth.authorization, @options) + assert_success capture + assert_equal '5000', capture.message[0, 4] + end + + def test_failed_authorize + @gateway.expects(:ssl_request).returns(failed_authorize_response) + + response = @gateway.authorize(105, @credit_card, @options) + assert_failure response + assert_equal '5035', response.message[0, 4] + end + + def test_failed_capture + @gateway.expects(:ssl_request).returns(failed_capture_response) + + response = @gateway.capture(106, '') + assert_failure response + assert_equal '5025', response.message[0, 4] + end + + def test_successful_credit + @gateway.expects(:ssl_request).returns(successful_credit_response) + + credit = @gateway.credit(107, @credit_card, @options) + assert_success credit + assert_equal '5000', credit.message[0, 4] + end + + def test_failed_credit + @gateway.expects(:ssl_request).returns(failed_credit_response) + + response = @gateway.credit(108, @credit_card, @options) + assert_failure response + assert_equal '5035', response.message[0, 4] + end + + def test_successful_void + @gateway.expects(:ssl_request).returns(successful_auth_for_void_response) + + auth = @gateway.authorize(109, @credit_card, @options) + assert_success auth + + @gateway.expects(:ssl_request).returns(successful_void_auth_response) + + assert void = @gateway.void(auth.authorization, @options) + assert_success void + assert_equal '5000', void.message[0, 4] + end + + def test_failed_void + @gateway.expects(:ssl_request).returns(failed_void_response) + + response = @gateway.void('') + assert_failure response + assert_equal '5025', response.message[0, 4] + end + + def test_successful_void_of_sale + @gateway.expects(:ssl_request).returns(successful_sale_for_void_response) + + sale = @gateway.purchase(110, @credit_card, @options) + assert_success sale + + @gateway.expects(:ssl_request).returns(successful_void_sale_response) + + assert void = @gateway.void(sale.authorization, @options) + assert_success void + assert_equal '5000', void.message[0, 4] + end + + def test_successful_void_of_credit + @gateway.expects(:ssl_request).returns(successful_credit_for_void_response) + + credit = @gateway.credit(111, @credit_card, @options) + assert_success credit + + @gateway.expects(:ssl_request).returns(successful_credit_void_response) + + assert void = @gateway.void(credit.authorization, @options) + assert_success void + assert_equal '5000', void.message[0, 4] + end + + def test_invalid_login + gateway = PaywayDotComGateway.new(login: '', password: '', company_id: '', source_id: '') + gateway.expects(:ssl_request).returns(failed_invalid_login_response) + + assert response = gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_match %r{5001}, response.message[0, 4] + end + + def test_missing_source_id + error = assert_raises(ArgumentError) { PaywayDotComGateway.new(login: '', password: '', company_id: '') } + assert_equal 'Missing required parameter: source_id', error.message + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal post_scrubbed, @gateway.scrub(pre_scrubbed) + end + + def test_scrub_failed_purchase + assert @gateway.supports_scrubbing? + assert_equal post_scrubbed_failed_purchase, @gateway.scrub(pre_scrubbed_failed_purchase) + end + + private + + def pre_scrubbed + %q( + opening connection to devedgilpayway.net:443... + opened + starting SSL for devedgilpayway.net:443... + SSL established, protocol: TLSv1.2, cipher: AES256-GCM-SHA384 + <- "POST /PaywayWS/Payment/CreditCard HTTP/1.1\r\nContent-Type: application/json\r\nAccept: application/json\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nHost: devedgilpayway.net\r\nContent-Length: 423\r\n\r\n" + <- "{\"userName\":\"sprerestwsdev\",\"password\":\"sprerestwsdev1!\",\"companyId\":\"3\",\"accountInputMode\":\"primaryAccountNumber\",\"cardAccount\":{\"accountNumber\":\"4000100011112224\",\"fsv\":\"737\",\"expirationDate\":\"092022\",\"firstName\":\"Jim\",\"lastName\":\"Smith\",\"address\":\"456 My Street Apt 1\",\"city\":\"Ottawa\",\"state\":\"ON\",\"zip\":\"K1C2N6\",\"phone\":\"(555)555-5555\"},\"cardTransaction\":{\"amount\":\"100\",\"eciType\":\"1\",\"sourceId\":\"67\"},\"request\":\"sale\"}" + -> "HTTP/1.1 200 \r\n" + -> "Access-Control-Allow-Origin: *\r\n" + -> "Access-Control-Expose-Headers: Access-Control-Allow-Origin,Access-Control-Allow-Credentials\r\n" + -> "Content-Encoding: application/json\r\n" + -> "Content-Type: application/json\r\n" + -> "Content-Length: 2051\r\n" + -> "Date: Mon, 22 Mar 2021 19:06:00 GMT\r\n" + -> "Connection: close\r\n" + -> "\r\n" + reading 2051 bytes... + -> "{\n \"cardAccount\": {\n \"accountNotes1\": \"\",\n \"accountNotes2\": \"\",\n \"accountNotes3\": \"\",\n \"accountNumber\": \"400010******2224\",\n \"account_number_masked\": \"400010******2224\",\n \"address\": \"456 My Street Apt 1\",\n \"auLastUpdate\": \"1999-01-01 00:00:00-05\",\n \"auUpdateType\": 0,\n \"cardType\": 1,\n \"city\": \"Ottawa\",\n \"commercialCardType\": 0,\n \"divisionId\": 7,\n \"email\": \"\",\n \"expirationDate\": \"0922\",\n \"firstFour\": \"4000\",\n \"firstName\": \"Jim\",\n \"fsv\": \"123\",\n \"inputMode\": 1,\n \"lastFour\": \"2224\",\n \"lastName\": \"Smith\",\n \"lastUsed\": \"1999-01-01 00:00:00-05\",\n \"middleName\": \"\",\n \"onlinePaymentCryptogram\": \"\",\n \"p2peInput\": \"\",\n \"paywayToken\": 10163736,\n \"phone\": \"5555555555\",\n \"state\": \"ON\",\n \"status\": 2,\n \"zip\": \"K1C2N6\"\n },\n \"cardTransaction\": {\n \"addressVerificationResults\": \"\",\n \"amount\": 100,\n \"authorizationCode\": \"0987654321\",\n \"authorizedTime\": \"2021-03-22 00:00:00-04\",\n \"capturedTime\": \"2021-03-22 15:06:00\",\n \"cbMode\": 2,\n \"eciType\": 1,\n \"fraudSecurityResults\": \"\",\n \"fsvIndicator\": \"\",\n \"name\": \"6720210322150600930144\",\n \"pfpstatus\": 3601,\n \"pfpstatusString\": \"PFP Not Enabled\",\n \"processorErrorMessage\": \"\",\n \"processorOrderId\": \"\",\n \"processorRecurringAdvice\": \"\",\n \"processorResponseDate\": \"\",\n \"processorResultCode\": \"\",\n \"processorSequenceNumber\": 0,\n \"processorSoftDescriptor\": \"\",\n \"referenceNumber\": \"123456\",\n \"resultCode\": 0,\n \"sessionToken_string\": \"0\",\n \"settledTime\": \"1999-01-01 00:00\",\n \"sourceId\": 67,\n \"status\": 4,\n \"tax\": 0,\n \"testResultAVS\": \"\",\n \"testResultFSV\": \"\",\n \"transactionNotes1\": \"\",\n \"transactionNotes2\": \"\",\n \"transactionNotes3\": \"\"\n },\n \"paywayCode\": \"5000\",\n \"paywayMessage\": \"\"\n}" + read 2051 bytes + Conn close + ) + end + + def post_scrubbed + %q( + opening connection to devedgilpayway.net:443... + opened + starting SSL for devedgilpayway.net:443... + SSL established, protocol: TLSv1.2, cipher: AES256-GCM-SHA384 + <- "POST /PaywayWS/Payment/CreditCard HTTP/1.1\r\nContent-Type: application/json\r\nAccept: application/json\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nHost: devedgilpayway.net\r\nContent-Length: 423\r\n\r\n" + <- "{\"userName\":\"sprerestwsdev\",\"password\":\"[FILTERED]\",\"companyId\":\"3\",\"accountInputMode\":\"primaryAccountNumber\",\"cardAccount\":{\"accountNumber\":\"[FILTERED]\",\"fsv\":\"[FILTERED]\",\"expirationDate\":\"092022\",\"firstName\":\"Jim\",\"lastName\":\"Smith\",\"address\":\"456 My Street Apt 1\",\"city\":\"Ottawa\",\"state\":\"ON\",\"zip\":\"K1C2N6\",\"phone\":\"(555)555-5555\"},\"cardTransaction\":{\"amount\":\"100\",\"eciType\":\"1\",\"sourceId\":\"67\"},\"request\":\"sale\"}" + -> "HTTP/1.1 200 \r\n" + -> "Access-Control-Allow-Origin: *\r\n" + -> "Access-Control-Expose-Headers: Access-Control-Allow-Origin,Access-Control-Allow-Credentials\r\n" + -> "Content-Encoding: application/json\r\n" + -> "Content-Type: application/json\r\n" + -> "Content-Length: 2051\r\n" + -> "Date: Mon, 22 Mar 2021 19:06:00 GMT\r\n" + -> "Connection: close\r\n" + -> "\r\n" + reading 2051 bytes... + -> "{\n \"cardAccount\": {\n \"accountNotes1\": \"\",\n \"accountNotes2\": \"\",\n \"accountNotes3\": \"\",\n \"accountNumber\": \"[FILTERED]\",\n \"account_number_masked\": \"400010******2224\",\n \"address\": \"456 My Street Apt 1\",\n \"auLastUpdate\": \"1999-01-01 00:00:00-05\",\n \"auUpdateType\": 0,\n \"cardType\": 1,\n \"city\": \"Ottawa\",\n \"commercialCardType\": 0,\n \"divisionId\": 7,\n \"email\": \"\",\n \"expirationDate\": \"0922\",\n \"firstFour\": \"4000\",\n \"firstName\": \"Jim\",\n \"fsv\": \"[FILTERED]\",\n \"inputMode\": 1,\n \"lastFour\": \"2224\",\n \"lastName\": \"Smith\",\n \"lastUsed\": \"1999-01-01 00:00:00-05\",\n \"middleName\": \"\",\n \"onlinePaymentCryptogram\": \"\",\n \"p2peInput\": \"\",\n \"paywayToken\": 10163736,\n \"phone\": \"5555555555\",\n \"state\": \"ON\",\n \"status\": 2,\n \"zip\": \"K1C2N6\"\n },\n \"cardTransaction\": {\n \"addressVerificationResults\": \"\",\n \"amount\": 100,\n \"authorizationCode\": \"0987654321\",\n \"authorizedTime\": \"2021-03-22 00:00:00-04\",\n \"capturedTime\": \"2021-03-22 15:06:00\",\n \"cbMode\": 2,\n \"eciType\": 1,\n \"fraudSecurityResults\": \"\",\n \"fsvIndicator\": \"\",\n \"name\": \"6720210322150600930144\",\n \"pfpstatus\": 3601,\n \"pfpstatusString\": \"PFP Not Enabled\",\n \"processorErrorMessage\": \"\",\n \"processorOrderId\": \"\",\n \"processorRecurringAdvice\": \"\",\n \"processorResponseDate\": \"\",\n \"processorResultCode\": \"\",\n \"processorSequenceNumber\": 0,\n \"processorSoftDescriptor\": \"\",\n \"referenceNumber\": \"123456\",\n \"resultCode\": 0,\n \"sessionToken_string\": \"0\",\n \"settledTime\": \"1999-01-01 00:00\",\n \"sourceId\": 67,\n \"status\": 4,\n \"tax\": 0,\n \"testResultAVS\": \"\",\n \"testResultFSV\": \"\",\n \"transactionNotes1\": \"\",\n \"transactionNotes2\": \"\",\n \"transactionNotes3\": \"\"\n },\n \"paywayCode\": \"5000\",\n \"paywayMessage\": \"\"\n}" + read 2051 bytes + Conn close + ) + end + + def pre_scrubbed_failed_purchase + %q( + opening connection to devedgilpayway.net:443... + opened + starting SSL for devedgilpayway.net:443... + SSL established, protocol: TLSv1.2, cipher: AES256-GCM-SHA384 + <- "POST /PaywayWS/Payment/CreditCard HTTP/1.1\r\nContent-Type: application/json\r\nAccept: application/json\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nHost: devedgilpayway.net\r\nContent-Length: 423\r\n\r\n" + <- "{\"userName\":\"sprerestwsdev\",\"password\":\"sprerestwsdev1!\",\"companyId\":\"3\",\"accountInputMode\":\"primaryAccountNumber\",\"cardAccount\":{\"accountNumber\":\"4000300011112221\",\"fsv\":\"123\",\"expirationDate\":\"092022\",\"firstName\":\"Jim\",\"lastName\":\"Smith\",\"address\":\"456 My Street Apt 1\",\"city\":\"Ottawa\",\"state\":\"ON\",\"zip\":\"K1C2N6\",\"phone\":\"(555)555-5555\"},\"cardTransaction\":{\"amount\":\"102\",\"eciType\":\"1\",\"sourceId\":\"67\"},\"request\":\"sale\"}" + -> "HTTP/1.1 200 \r\n" + -> "Access-Control-Allow-Origin: *\r\n" + -> "Access-Control-Expose-Headers: Access-Control-Allow-Origin,Access-Control-Allow-Credentials\r\n" + -> "Content-Encoding: application/json\r\n" + -> "Content-Type: application/json\r\n" + -> "Content-Length: 2013\r\n" + -> "Date: Tue, 23 Mar 2021 15:04:53 GMT\r\n" + -> "Connection: close\r\n" + -> "\r\n" + reading 2013 bytes... + -> "{\n \"cardAccount\": {\n \"accountNotes1\": \"\",\n \"accountNotes2\": \"\",\n \"accountNotes3\": \"\",\n \"accountNumber\": \"400030******2221\",\n \"account_number_masked\": \"400030******2221\",\n \"address\": \"456 My Street Apt 1\",\n \"auLastUpdate\": \"1999-01-01 00:00\",\n \"auUpdateType\": 0,\n \"cardType\": 1,\n \"city\": \"Ottawa\",\n \"commercialCardType\": 0,\n \"divisionId\": 7,\n \"email\": \"\",\n \"expirationDate\": \"0922\",\n \"firstFour\": \"4000\",\n \"firstName\": \"Jim\",\n \"fsv\": \"123\",\n \"inputMode\": 1,\n \"lastFour\": \"2221\",\n \"lastName\": \"Smith\",\n \"lastUsed\": \"1999-01-01 00:00\",\n \"middleName\": \"\",\n \"onlinePaymentCryptogram\": \"\",\n \"p2peInput\": \"\",\n \"paywayToken\": 0,\n \"phone\": \"5555555555\",\n \"state\": \"ON\",\n \"status\": 2,\n \"zip\": \"K1C2N6\"\n },\n \"cardTransaction\": {\n \"addressVerificationResults\": \"\",\n \"amount\": 0,\n \"authorizationCode\": \"\",\n \"authorizedTime\": \"1999-01-01\",\n \"capturedTime\": \"1999-01-01\",\n \"cbMode\": 0,\n \"eciType\": 0,\n \"fraudSecurityResults\": \"\",\n \"fsvIndicator\": \"\",\n \"name\": \"\",\n \"pfpstatus\": 3601,\n \"pfpstatusString\": \"PFP Not Enabled\",\n \"processorErrorMessage\": \"\",\n \"processorOrderId\": \"\",\n \"processorRecurringAdvice\": \"\",\n \"processorResponseDate\": \"\",\n \"processorResultCode\": \"\",\n \"processorSequenceNumber\": 0,\n \"processorSoftDescriptor\": \"\",\n \"referenceNumber\": \"\",\n \"resultCode\": 1,\n \"sessionToken_string\": \"0\",\n \"settledTime\": \"1999-01-01 00:00\",\n \"sourceId\": 0,\n \"status\": 0,\n \"tax\": 0,\n \"testResultAVS\": \"\",\n \"testResultFSV\": \"\",\n \"transactionNotes1\": \"\",\n \"transactionNotes2\": \"\",\n \"transactionNotes3\": \"\"\n },\n \"paywayCode\": \"5035\",\n \"paywayMessage\": \"Invalid account number: 4000300011112221\"\n}" + read 2013 bytes + Conn close + ) + end + + def post_scrubbed_failed_purchase + %q( + opening connection to devedgilpayway.net:443... + opened + starting SSL for devedgilpayway.net:443... + SSL established, protocol: TLSv1.2, cipher: AES256-GCM-SHA384 + <- "POST /PaywayWS/Payment/CreditCard HTTP/1.1\r\nContent-Type: application/json\r\nAccept: application/json\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nHost: devedgilpayway.net\r\nContent-Length: 423\r\n\r\n" + <- "{\"userName\":\"sprerestwsdev\",\"password\":\"[FILTERED]\",\"companyId\":\"3\",\"accountInputMode\":\"primaryAccountNumber\",\"cardAccount\":{\"accountNumber\":\"[FILTERED]\",\"fsv\":\"[FILTERED]\",\"expirationDate\":\"092022\",\"firstName\":\"Jim\",\"lastName\":\"Smith\",\"address\":\"456 My Street Apt 1\",\"city\":\"Ottawa\",\"state\":\"ON\",\"zip\":\"K1C2N6\",\"phone\":\"(555)555-5555\"},\"cardTransaction\":{\"amount\":\"102\",\"eciType\":\"1\",\"sourceId\":\"67\"},\"request\":\"sale\"}" + -> "HTTP/1.1 200 \r\n" + -> "Access-Control-Allow-Origin: *\r\n" + -> "Access-Control-Expose-Headers: Access-Control-Allow-Origin,Access-Control-Allow-Credentials\r\n" + -> "Content-Encoding: application/json\r\n" + -> "Content-Type: application/json\r\n" + -> "Content-Length: 2013\r\n" + -> "Date: Tue, 23 Mar 2021 15:04:53 GMT\r\n" + -> "Connection: close\r\n" + -> "\r\n" + reading 2013 bytes... + -> "{\n \"cardAccount\": {\n \"accountNotes1\": \"\",\n \"accountNotes2\": \"\",\n \"accountNotes3\": \"\",\n \"accountNumber\": \"[FILTERED]\",\n \"account_number_masked\": \"400030******2221\",\n \"address\": \"456 My Street Apt 1\",\n \"auLastUpdate\": \"1999-01-01 00:00\",\n \"auUpdateType\": 0,\n \"cardType\": 1,\n \"city\": \"Ottawa\",\n \"commercialCardType\": 0,\n \"divisionId\": 7,\n \"email\": \"\",\n \"expirationDate\": \"0922\",\n \"firstFour\": \"4000\",\n \"firstName\": \"Jim\",\n \"fsv\": \"[FILTERED]\",\n \"inputMode\": 1,\n \"lastFour\": \"2221\",\n \"lastName\": \"Smith\",\n \"lastUsed\": \"1999-01-01 00:00\",\n \"middleName\": \"\",\n \"onlinePaymentCryptogram\": \"\",\n \"p2peInput\": \"\",\n \"paywayToken\": 0,\n \"phone\": \"5555555555\",\n \"state\": \"ON\",\n \"status\": 2,\n \"zip\": \"K1C2N6\"\n },\n \"cardTransaction\": {\n \"addressVerificationResults\": \"\",\n \"amount\": 0,\n \"authorizationCode\": \"\",\n \"authorizedTime\": \"1999-01-01\",\n \"capturedTime\": \"1999-01-01\",\n \"cbMode\": 0,\n \"eciType\": 0,\n \"fraudSecurityResults\": \"\",\n \"fsvIndicator\": \"\",\n \"name\": \"\",\n \"pfpstatus\": 3601,\n \"pfpstatusString\": \"PFP Not Enabled\",\n \"processorErrorMessage\": \"\",\n \"processorOrderId\": \"\",\n \"processorRecurringAdvice\": \"\",\n \"processorResponseDate\": \"\",\n \"processorResultCode\": \"\",\n \"processorSequenceNumber\": 0,\n \"processorSoftDescriptor\": \"\",\n \"referenceNumber\": \"\",\n \"resultCode\": 1,\n \"sessionToken_string\": \"0\",\n \"settledTime\": \"1999-01-01 00:00\",\n \"sourceId\": 0,\n \"status\": 0,\n \"tax\": 0,\n \"testResultAVS\": \"\",\n \"testResultFSV\": \"\",\n \"transactionNotes1\": \"\",\n \"transactionNotes2\": \"\",\n \"transactionNotes3\": \"\"\n },\n \"paywayCode\": \"5035\",\n \"paywayMessage\": \"Invalid account number: [FILTERED]\"\n}" + read 2013 bytes + Conn close + ) + end + + def successful_purchase_response + '{ + "cardAccount": { + "accountNotes1": "", + "accountNotes2": "", + "accountNotes3": "", + "accountNumber": "400010******2224", + "account_number_masked": "400010******2224", + "address": "456 My Street Apt 1", + "auLastUpdate": "1999-01-01 00:00:00-05", + "auUpdateType": 0, + "cardType": 1, + "city": "Ottawa", + "commercialCardType": 0, + "divisionId": 7, + "email": "", + "expirationDate": "0922", + "firstFour": "4000", + "firstName": "Jim", + "fsv": "", + "inputMode": 1, + "lastFour": "2224", + "lastName": "Smith", + "lastUsed": "1999-01-01 00:00:00-05", + "middleName": "", + "onlinePaymentCryptogram": "", + "p2peInput": "", + "paywayToken": 10163736, + "phone": "5555555555", + "state": "ON", + "status": 2, + "zip": "K1C2N6" + }, + "cardTransaction": { + "addressVerificationResults": "I4", + "amount": 100, + "authorizationCode": "0987654321", + "authorizedTime": "2021-02-08 00:00:00-05", + "capturedTime": "2021-02-08 18:17:49", + "cbMode": 2, + "eciType": 1, + "fraudSecurityResults": "M", + "fsvIndicator": "", + "name": "6720210208181749349115", + "pfpstatus": 3601, + "pfpstatusString": "PFP Not Enabled", + "processorErrorMessage": "", + "processorOrderId": "", + "processorRecurringAdvice": "", + "processorResponseDate": "", + "processorResultCode": "", + "processorSequenceNumber": 0, + "processorSoftDescriptor": "", + "referenceNumber": "123456", + "resultCode": 0, + "sessionToken_string": "0", + "settledTime": "1999-01-01 00:00", + "sourceId": 67, + "status": 4, + "tax": 0, + "testResultAVS": "", + "testResultFSV": "", + "transactionNotes1": "", + "transactionNotes2": "", + "transactionNotes3": "" + }, + "paywayCode": "5000", + "paywayMessage": "" + }' + end + + def failed_purchase_response + '{ + "cardAccount": { + "accountNotes1": "", + "accountNotes2": "", + "accountNotes3": "", + "accountNumber": "400030******2221", + "account_number_masked": "400030******2221", + "address": "456 My Street Apt 1", + "auLastUpdate": "1999-01-01 00:00", + "auUpdateType": 0, + "cardType": 1, + "city": "Ottawa", + "commercialCardType": 0, + "divisionId": 7, + "email": "", + "expirationDate": "0922", + "firstFour": "4000", + "firstName": "Jim", + "fsv": "123", + "inputMode": 1, + "lastFour": "2221", + "lastName": "Smith", + "lastUsed": "1999-01-01 00:00", + "middleName": "", + "onlinePaymentCryptogram": "", + "p2peInput": "", + "paywayToken": 0, + "phone": "5555555555", + "state": "ON", + "status": 2, + "zip": "K1C2N6" + }, + "cardTransaction": { + "addressVerificationResults": "", + "amount": 0, + "authorizationCode": "", + "authorizedTime": "1999-01-01", + "capturedTime": "1999-01-01", + "cbMode": 0, + "eciType": 0, + "fraudSecurityResults": "", + "fsvIndicator": "", + "name": "", + "pfpstatus": 3601, + "pfpstatusString": "PFP Not Enabled", + "processorErrorMessage": "", + "processorOrderId": "", + "processorRecurringAdvice": "", + "processorResponseDate": "", + "processorResultCode": "", + "processorSequenceNumber": 0, + "processorSoftDescriptor": "", + "referenceNumber": "", + "resultCode": 1, + "sessionToken_string": "0", + "settledTime": "1999-01-01 00:00", + "sourceId": 0, + "status": 0, + "tax": 0, + "testResultAVS": "", + "testResultFSV": "", + "transactionNotes1": "", + "transactionNotes2": "", + "transactionNotes3": "" + }, + "paywayCode": "5035", + "paywayMessage": "Invalid account number: 4000300011112221" + }' + end + + def successful_authorize_response + '{ + "cardAccount": { + "accountNotes1": "", + "accountNotes2": "", + "accountNotes3": "", + "accountNumber": "400010******2224", + "account_number_masked": "400010******2224", + "address": "456 My Street Apt 1", + "auLastUpdate": "1999-01-01 00:00", + "auUpdateType": 0, + "cardType": 1, + "city": "Ottawa", + "commercialCardType": 0, + "divisionId": 7, + "email": "", + "expirationDate": "0922", + "firstFour": "4000", + "firstName": "Jim", + "fsv": "737", + "inputMode": 1, + "lastFour": "2224", + "lastName": "Smith", + "lastUsed": "1999-01-01 00:00", + "middleName": "", + "onlinePaymentCryptogram": "", + "p2peInput": "", + "paywayToken": 10163736, + "phone": "5555555555", + "state": "ON", + "status": 0, + "zip": "K1C2N6" + }, + "cardTransaction": { + "addressVerificationResults": "", + "amount": 103, + "authorizationCode": "0987654321", + "authorizedTime": "2021-02-09", + "capturedTime": "1999-01-01 00:00:00-05", + "cbMode": 2, + "eciType": 1, + "fraudSecurityResults": "", + "fsvIndicator": "", + "name": "6720210209084239789167", + "pfpstatus": 3601, + "pfpstatusString": "PFP Not Enabled", + "processorErrorMessage": "", + "processorOrderId": "", + "processorRecurringAdvice": "", + "processorResponseDate": "", + "processorResultCode": "", + "processorSequenceNumber": 0, + "processorSoftDescriptor": "", + "referenceNumber": "123456", + "resultCode": 0, + "sessionToken_string": "0", + "settledTime": "1999-01-01 00:00", + "sourceId": 67, + "status": 3, + "tax": 0, + "testResultAVS": "", + "testResultFSV": "", + "transactionNotes1": "", + "transactionNotes2": "", + "transactionNotes3": "" + }, + "paywayCode": "5000", + "paywayMessage": "" + }' + end + + def successful_authorize_and_capture_response + '{ + "cardAccount": { + "accountNotes1": "", + "accountNotes2": "", + "accountNotes3": "", + "accountNumber": "400010******2224", + "account_number_masked": "400010******2224", + "address": "456 My Street Apt 1", + "auLastUpdate": "1999-01-01 00:00", + "auUpdateType": 0, + "cardType": 1, + "city": "Ottawa", + "commercialCardType": 0, + "divisionId": 7, + "email": "", + "expirationDate": "0922", + "firstFour": "4000", + "firstName": "Jim", + "fsv": "737", + "inputMode": 1, + "lastFour": "2224", + "lastName": "Smith", + "lastUsed": "1999-01-01 00:00", + "middleName": "", + "onlinePaymentCryptogram": "", + "p2peInput": "", + "paywayToken": 10163736, + "phone": "5555555555", + "state": "ON", + "status": 0, + "zip": "K1C2N6" + }, + "cardTransaction": { + "addressVerificationResults": "", + "amount": 104, + "authorizationCode": "0987654321", + "authorizedTime": "2021-02-09", + "capturedTime": "1999-01-01 00:00:00-05", + "cbMode": 2, + "eciType": 1, + "fraudSecurityResults": "", + "fsvIndicator": "", + "name": "6720210209085526437200", + "pfpstatus": 3601, + "pfpstatusString": "PFP Not Enabled", + "processorErrorMessage": "", + "processorOrderId": "", + "processorRecurringAdvice": "", + "processorResponseDate": "", + "processorResultCode": "", + "processorSequenceNumber": 0, + "processorSoftDescriptor": "", + "referenceNumber": "123456", + "resultCode": 0, + "sessionToken_string": "0", + "settledTime": "1999-01-01 00:00", + "sourceId": 67, + "status": 3, + "tax": 0, + "testResultAVS": "", + "testResultFSV": "", + "transactionNotes1": "", + "transactionNotes2": "", + "transactionNotes3": "" + }, + "paywayCode": "5000", + "paywayMessage": "" + }' + end + + def successful_capture_response + '{ + "cardAccount": { + "accountNotes1": "", + "accountNotes2": "", + "accountNotes3": "", + "accountNumber": "400010******2224", + "account_number_masked": "400010******2224", + "address": "456 My Street Apt 1", + "auLastUpdate": "1999-01-01 00:00:00-05", + "auUpdateType": 0, + "cardType": 1, + "city": "Ottawa", + "commercialCardType": 0, + "divisionId": 7, + "email": "", + "expirationDate": "0922", + "firstFour": "4000", + "firstName": "Jim", + "fsv": "", + "inputMode": 1, + "lastFour": "2224", + "lastName": "Smith", + "lastUsed": "1999-01-01 00:00:00-05", + "middleName": "", + "onlinePaymentCryptogram": "", + "p2peInput": "", + "paywayToken": 10163736, + "phone": "5555555555", + "state": "ON", + "status": 2, + "zip": "K1C2N6" + }, + "cardTransaction": { + "addressVerificationResults": "", + "amount": 104, + "authorizationCode": "0987654321", + "authorizedTime": "2021-02-09 00:00:00-05", + "capturedTime": "2021-02-09 08:55:26", + "cbMode": 2, + "eciType": 1, + "fraudSecurityResults": "", + "fsvIndicator": "", + "name": "6720210209085526437200", + "pfpstatus": 3601, + "pfpstatusString": "PFP Not Enabled", + "processorErrorMessage": "", + "processorOrderId": "", + "processorRecurringAdvice": "", + "processorResponseDate": "", + "processorResultCode": "", + "processorSequenceNumber": 0, + "processorSoftDescriptor": "", + "referenceNumber": "123456", + "resultCode": 0, + "sessionToken_string": "0", + "settledTime": "1999-01-01 00:00", + "sourceId": 67, + "status": 4, + "tax": 0, + "testResultAVS": "", + "testResultFSV": "", + "transactionNotes1": "", + "transactionNotes2": "", + "transactionNotes3": "" + }, + "paywayCode": "5000", + "paywayMessage": "" + }' + end + + def failed_authorize_response + '{ + "cardAccount": { + "accountNotes1": "", + "accountNotes2": "", + "accountNotes3": "", + "accountNumber": "400030******2221", + "account_number_masked": "400030******2221", + "address": "456 My Street Apt 1", + "auLastUpdate": "1999-01-01 00:00", + "auUpdateType": 0, + "cardType": 1, + "city": "Ottawa", + "commercialCardType": 0, + "divisionId": 7, + "email": "", + "expirationDate": "0922", + "firstFour": "4000", + "firstName": "Jim", + "fsv": "123", + "inputMode": 1, + "lastFour": "2221", + "lastName": "Smith", + "lastUsed": "1999-01-01 00:00", + "middleName": "", + "onlinePaymentCryptogram": "", + "p2peInput": "", + "paywayToken": 0, + "phone": "5555555555", + "state": "ON", + "status": 2, + "zip": "K1C2N6" + }, + "cardTransaction": { + "addressVerificationResults": "", + "amount": 0, + "authorizationCode": "", + "authorizedTime": "1999-01-01", + "capturedTime": "1999-01-01", + "cbMode": 0, + "eciType": 0, + "fraudSecurityResults": "", + "fsvIndicator": "", + "name": "", + "pfpstatus": 3601, + "pfpstatusString": "PFP Not Enabled", + "processorErrorMessage": "", + "processorOrderId": "", + "processorRecurringAdvice": "", + "processorResponseDate": "", + "processorResultCode": "", + "processorSequenceNumber": 0, + "processorSoftDescriptor": "", + "referenceNumber": "", + "resultCode": 1, + "sessionToken_string": "0", + "settledTime": "1999-01-01 00:00", + "sourceId": 0, + "status": 0, + "tax": 0, + "testResultAVS": "", + "testResultFSV": "", + "transactionNotes1": "", + "transactionNotes2": "", + "transactionNotes3": "" + }, + "paywayCode": "5035", + "paywayMessage": "Invalid account number: 4000300011112221" + }' + end + + def failed_capture_response + '{ + "cardAccount": { + "accountNotes1": "", + "accountNotes2": "", + "accountNotes3": "", + "accountNumber": "", + "account_number_masked": "", + "address": "", + "auLastUpdate": "1999-01-01 00:00", + "auUpdateType": 0, + "cardType": 8, + "city": "", + "commercialCardType": 0, + "divisionId": 0, + "email": "", + "expirationDate": "", + "firstFour": "", + "firstName": "", + "fsv": "", + "inputMode": 1, + "lastFour": "", + "lastName": "", + "lastUsed": "1999-01-01 00:00", + "middleName": "", + "onlinePaymentCryptogram": "", + "p2peInput": "", + "paywayToken": 0, + "phone": "", + "state": "", + "status": 0, + "zip": "" + }, + "cardTransaction": { + "addressVerificationResults": "", + "amount": 0, + "authorizationCode": "", + "authorizedTime": "1999-01-01", + "capturedTime": "1999-01-01", + "cbMode": 0, + "eciType": 0, + "fraudSecurityResults": "", + "fsvIndicator": "", + "name": "", + "pfpstatus": 3601, + "pfpstatusString": "PFP Not Enabled", + "processorErrorMessage": "", + "processorOrderId": "", + "processorRecurringAdvice": "", + "processorResponseDate": "", + "processorResultCode": "", + "processorSequenceNumber": 0, + "processorSoftDescriptor": "", + "referenceNumber": "", + "resultCode": 1, + "sessionToken_string": "0", + "settledTime": "1999-01-01 00:00", + "sourceId": 0, + "status": 0, + "tax": 0, + "testResultAVS": "", + "testResultFSV": "", + "transactionNotes1": "", + "transactionNotes2": "", + "transactionNotes3": "" + }, + "paywayCode": "5025", + "paywayMessage": "failed to read transaction with source 0 and name " + }' + end + + def successful_credit_response + '{ + "cardAccount": { + "accountNotes1": "", + "accountNotes2": "", + "accountNotes3": "", + "accountNumber": "400010******2224", + "account_number_masked": "400010******2224", + "address": "456 My Street Apt 1", + "auLastUpdate": "1999-01-01 00:00", + "auUpdateType": 0, + "cardType": 1, + "city": "Ottawa", + "commercialCardType": 0, + "divisionId": 7, + "email": "", + "expirationDate": "0922", + "firstFour": "4000", + "firstName": "Jim", + "fsv": "737", + "inputMode": 1, + "lastFour": "2224", + "lastName": "Smith", + "lastUsed": "1999-01-01 00:00", + "middleName": "", + "onlinePaymentCryptogram": "", + "p2peInput": "", + "paywayToken": 10163736, + "phone": "5555555555", + "state": "ON", + "status": 0, + "zip": "K1C2N6" + }, + "cardTransaction": { + "addressVerificationResults": "", + "amount": 107, + "authorizationCode": "0987654321", + "authorizedTime": "2021-02-09 13:09:23", + "capturedTime": "2021-02-09 13:09:23", + "cbMode": 2, + "eciType": 1, + "fraudSecurityResults": "", + "fsvIndicator": "", + "name": "6720210209130923241131", + "pfpstatus": 3601, + "pfpstatusString": "PFP Not Enabled", + "processorErrorMessage": "", + "processorOrderId": "", + "processorRecurringAdvice": "", + "processorResponseDate": "", + "processorResultCode": "", + "processorSequenceNumber": 0, + "processorSoftDescriptor": "", + "referenceNumber": "123456", + "resultCode": 0, + "sessionToken_string": "0", + "settledTime": "1999-01-01 00:00", + "sourceId": 67, + "status": 4, + "tax": 0, + "testResultAVS": "", + "testResultFSV": "", + "transactionNotes1": "", + "transactionNotes2": "", + "transactionNotes3": "" + }, + "paywayCode": "5000", + "paywayMessage": "" + }' + end + + def failed_credit_response + '{ + "cardAccount": { + "accountNotes1": "", + "accountNotes2": "", + "accountNotes3": "", + "accountNumber": "400030******2221", + "account_number_masked": "400030******2221", + "address": "456 My Street Apt 1", + "auLastUpdate": "1999-01-01 00:00", + "auUpdateType": 0, + "cardType": 1, + "city": "Ottawa", + "commercialCardType": 0, + "divisionId": 7, + "email": "", + "expirationDate": "0922", + "firstFour": "4000", + "firstName": "Jim", + "fsv": "123", + "inputMode": 1, + "lastFour": "2221", + "lastName": "Smith", + "lastUsed": "1999-01-01 00:00", + "middleName": "", + "onlinePaymentCryptogram": "", + "p2peInput": "", + "paywayToken": 0, + "phone": "5555555555", + "state": "ON", + "status": 2, + "zip": "K1C2N6" + }, + "cardTransaction": { + "addressVerificationResults": "", + "amount": 0, + "authorizationCode": "", + "authorizedTime": "1999-01-01", + "capturedTime": "1999-01-01", + "cbMode": 0, + "eciType": 0, + "fraudSecurityResults": "", + "fsvIndicator": "", + "name": "", + "pfpstatus": 3601, + "pfpstatusString": "PFP Not Enabled", + "processorErrorMessage": "", + "processorOrderId": "", + "processorRecurringAdvice": "", + "processorResponseDate": "", + "processorResultCode": "", + "processorSequenceNumber": 0, + "processorSoftDescriptor": "", + "referenceNumber": "", + "resultCode": 1, + "sessionToken_string": "0", + "settledTime": "1999-01-01 00:00", + "sourceId": 0, + "status": 0, + "tax": 0, + "testResultAVS": "", + "testResultFSV": "", + "transactionNotes1": "", + "transactionNotes2": "", + "transactionNotes3": "" + }, + "paywayCode": "5035", + "paywayMessage": "Invalid account number: 4000300011112221" + }' + end + + def successful_auth_for_void_response + '{ + "cardAccount": { + "accountNotes1": "", + "accountNotes2": "", + "accountNotes3": "", + "accountNumber": "400010******2224", + "account_number_masked": "400010******2224", + "address": "456 My Street Apt 1", + "auLastUpdate": "1999-01-01 00:00", + "auUpdateType": 0, + "cardType": 1, + "city": "Ottawa", + "commercialCardType": 0, + "divisionId": 7, + "email": "", + "expirationDate": "0922", + "firstFour": "4000", + "firstName": "Jim", + "fsv": "737", + "inputMode": 1, + "lastFour": "2224", + "lastName": "Smith", + "lastUsed": "1999-01-01 00:00", + "middleName": "", + "onlinePaymentCryptogram": "", + "p2peInput": "", + "paywayToken": 10163736, + "phone": "5555555555", + "state": "ON", + "status": 0, + "zip": "K1C2N6" + }, + "cardTransaction": { + "addressVerificationResults": "", + "amount": 108, + "authorizationCode": "0987654321", + "authorizedTime": "2021-02-09", + "capturedTime": "1999-01-01 00:00:00-05", + "cbMode": 2, + "eciType": 1, + "fraudSecurityResults": "", + "fsvIndicator": "", + "name": "6720210209135306469560", + "pfpstatus": 3601, + "pfpstatusString": "PFP Not Enabled", + "processorErrorMessage": "", + "processorOrderId": "", + "processorRecurringAdvice": "", + "processorResponseDate": "", + "processorResultCode": "", + "processorSequenceNumber": 0, + "processorSoftDescriptor": "", + "referenceNumber": "123456", + "resultCode": 0, + "sessionToken_string": "0", + "settledTime": "1999-01-01 00:00", + "sourceId": 67, + "status": 3, + "tax": 0, + "testResultAVS": "", + "testResultFSV": "", + "transactionNotes1": "", + "transactionNotes2": "", + "transactionNotes3": "" + }, + "paywayCode": "5000", + "paywayMessage": "" + }' + end + + def successful_void_auth_response + '{ + "cardAccount": { + "accountNotes1": "", + "accountNotes2": "", + "accountNotes3": "", + "accountNumber": "400010******2224", + "account_number_masked": "400010******2224", + "address": "456 My Street Apt 1", + "auLastUpdate": "1999-01-01 00:00:00-05", + "auUpdateType": 0, + "cardType": 1, + "city": "Ottawa", + "commercialCardType": 0, + "divisionId": 7, + "email": "", + "expirationDate": "0922", + "firstFour": "4000", + "firstName": "Jim", + "fsv": "", + "inputMode": 1, + "lastFour": "2224", + "lastName": "Smith", + "lastUsed": "1999-01-01 00:00:00-05", + "middleName": "", + "onlinePaymentCryptogram": "", + "p2peInput": "", + "paywayToken": 10163736, + "phone": "5555555555", + "state": "ON", + "status": 2, + "zip": "K1C2N6" + }, + "cardTransaction": { + "addressVerificationResults": "", + "amount": 108, + "authorizationCode": "0987654321", + "authorizedTime": "2021-02-09 00:00:00-05", + "capturedTime": "1999-01-01 00:00:00-05", + "cbMode": 2, + "eciType": 1, + "fraudSecurityResults": "", + "fsvIndicator": "", + "name": "6720210209135306469560", + "pfpstatus": 3601, + "pfpstatusString": "PFP Not Enabled", + "processorErrorMessage": "", + "processorOrderId": "", + "processorRecurringAdvice": "", + "processorResponseDate": "", + "processorResultCode": "", + "processorSequenceNumber": 0, + "processorSoftDescriptor": "", + "referenceNumber": "123456", + "resultCode": 0, + "sessionToken_string": "0", + "settledTime": "1999-01-01 00:00", + "sourceId": 67, + "status": 6, + "tax": 0, + "testResultAVS": "", + "testResultFSV": "", + "transactionNotes1": "", + "transactionNotes2": "", + "transactionNotes3": "" + }, + "paywayCode": "5000", + "paywayMessage": "" + }' + end + + def successful_void_response + '{ + "cardAccount": { + "accountNotes1": "", + "accountNotes2": "", + "accountNotes3": "", + "accountNumber": "", + "account_number_masked": "", + "address": "", + "auLastUpdate": "1999-01-01 00:00", + "auUpdateType": 0, + "cardType": 8, + "city": "", + "commercialCardType": 0, + "divisionId": 0, + "email": "", + "expirationDate": "", + "firstFour": "", + "firstName": "", + "fsv": "", + "inputMode": 1, + "lastFour": "", + "lastName": "", + "lastUsed": "1999-01-01 00:00", + "middleName": "", + "onlinePaymentCryptogram": "", + "p2peInput": "", + "paywayToken": 0, + "phone": "", + "state": "", + "status": 0, + "zip": "" + }, + "cardTransaction": { + "addressVerificationResults": "", + "amount": 0, + "authorizationCode": "", + "authorizedTime": "1999-01-01", + "capturedTime": "1999-01-01", + "cbMode": 0, + "eciType": 0, + "fraudSecurityResults": "", + "fsvIndicator": "", + "name": "", + "pfpstatus": 3601, + "pfpstatusString": "PFP Not Enabled", + "processorErrorMessage": "", + "processorOrderId": "", + "processorRecurringAdvice": "", + "processorResponseDate": "", + "processorResultCode": "", + "processorSequenceNumber": 0, + "processorSoftDescriptor": "", + "referenceNumber": "", + "resultCode": 1, + "sessionToken_string": "0", + "settledTime": "1999-01-01 00:00", + "sourceId": 0, + "status": 0, + "tax": 0, + "testResultAVS": "", + "testResultFSV": "", + "transactionNotes1": "", + "transactionNotes2": "", + "transactionNotes3": "" + }, + "paywayCode": "5025", + "paywayMessage": "failed to read transaction with source 0 and name " + }' + end + + def failed_void_response + '{ + "cardAccount": { + "accountNotes1": "", + "accountNotes2": "", + "accountNotes3": "", + "accountNumber": "", + "account_number_masked": "", + "address": "", + "auLastUpdate": "1999-01-01 00:00", + "auUpdateType": 0, + "cardType": 8, + "city": "", + "commercialCardType": 0, + "divisionId": 0, + "email": "", + "expirationDate": "", + "firstFour": "", + "firstName": "", + "fsv": "", + "inputMode": 1, + "lastFour": "", + "lastName": "", + "lastUsed": "1999-01-01 00:00", + "middleName": "", + "onlinePaymentCryptogram": "", + "p2peInput": "", + "paywayToken": 0, + "phone": "", + "state": "", + "status": 0, + "zip": "" + }, + "cardTransaction": { + "addressVerificationResults": "", + "amount": 0, + "authorizationCode": "", + "authorizedTime": "1999-01-01", + "capturedTime": "1999-01-01", + "cbMode": 0, + "eciType": 0, + "fraudSecurityResults": "", + "fsvIndicator": "", + "name": "", + "pfpstatus": 3601, + "pfpstatusString": "PFP Not Enabled", + "processorErrorMessage": "", + "processorOrderId": "", + "processorRecurringAdvice": "", + "processorResponseDate": "", + "processorResultCode": "", + "processorSequenceNumber": 0, + "processorSoftDescriptor": "", + "referenceNumber": "", + "resultCode": 1, + "sessionToken_string": "0", + "settledTime": "1999-01-01 00:00", + "sourceId": 0, + "status": 0, + "tax": 0, + "testResultAVS": "", + "testResultFSV": "", + "transactionNotes1": "", + "transactionNotes2": "", + "transactionNotes3": "" + }, + "paywayCode": "5025", + "paywayMessage": "failed to read transaction with source 0 and name " + }' + end + + def successful_sale_for_void_response + '{ + "cardAccount": { + "accountNotes1": "", + "accountNotes2": "", + "accountNotes3": "", + "accountNumber": "400010******2224", + "account_number_masked": "400010******2224", + "address": "456 My Street Apt 1", + "auLastUpdate": "1999-01-01 00:00:00-05", + "auUpdateType": 0, + "cardType": 1, + "city": "Ottawa", + "commercialCardType": 0, + "divisionId": 7, + "email": "", + "expirationDate": "0922", + "firstFour": "4000", + "firstName": "Jim", + "fsv": "", + "inputMode": 1, + "lastFour": "2224", + "lastName": "Smith", + "lastUsed": "1999-01-01 00:00:00-05", + "middleName": "", + "onlinePaymentCryptogram": "", + "p2peInput": "", + "paywayToken": 10163736, + "phone": "5555555555", + "state": "ON", + "status": 2, + "zip": "K1C2N6" + }, + "cardTransaction": { + "addressVerificationResults": "", + "amount": 109, + "authorizationCode": "0987654321", + "authorizedTime": "2021-02-09 00:00:00-05", + "capturedTime": "2021-02-09 13:00:48", + "cbMode": 2, + "eciType": 1, + "fraudSecurityResults": "", + "fsvIndicator": "", + "name": "6720210209130047957988", + "pfpstatus": 3601, + "pfpstatusString": "PFP Not Enabled", + "processorErrorMessage": "", + "processorOrderId": "", + "processorRecurringAdvice": "", + "processorResponseDate": "", + "processorResultCode": "", + "processorSequenceNumber": 0, + "processorSoftDescriptor": "", + "referenceNumber": "123456", + "resultCode": 0, + "sessionToken_string": "0", + "settledTime": "1999-01-01 00:00", + "sourceId": 67, + "status": 4, + "tax": 0, + "testResultAVS": "", + "testResultFSV": "", + "transactionNotes1": "", + "transactionNotes2": "", + "transactionNotes3": "" + }, + "paywayCode": "5000", + "paywayMessage": "" + }' + end + + def successful_void_sale_response + '{ + "cardAccount": { + "accountNotes1": "", + "accountNotes2": "", + "accountNotes3": "", + "accountNumber": "400010******2224", + "account_number_masked": "400010******2224", + "address": "456 My Street Apt 1", + "auLastUpdate": "1999-01-01 00:00:00-05", + "auUpdateType": 0, + "cardType": 1, + "city": "Ottawa", + "commercialCardType": 0, + "divisionId": 7, + "email": "", + "expirationDate": "0922", + "firstFour": "4000", + "firstName": "Jim", + "fsv": "", + "inputMode": 1, + "lastFour": "2224", + "lastName": "Smith", + "lastUsed": "1999-01-01 00:00:00-05", + "middleName": "", + "onlinePaymentCryptogram": "", + "p2peInput": "", + "paywayToken": 10163736, + "phone": "5555555555", + "state": "ON", + "status": 2, + "zip": "K1C2N6" + }, + "cardTransaction": { + "addressVerificationResults": "", + "amount": 109, + "authorizationCode": "0987654321", + "authorizedTime": "2021-02-09 00:00:00-05", + "capturedTime": "2021-02-09 13:00:48-05", + "cbMode": 2, + "eciType": 1, + "fraudSecurityResults": "", + "fsvIndicator": "", + "name": "6720210209130047957988", + "pfpstatus": 3601, + "pfpstatusString": "PFP Not Enabled", + "processorErrorMessage": "", + "processorOrderId": "", + "processorRecurringAdvice": "", + "processorResponseDate": "", + "processorResultCode": "", + "processorSequenceNumber": 0, + "processorSoftDescriptor": "", + "referenceNumber": "123456", + "resultCode": 0, + "sessionToken_string": "0", + "settledTime": "1999-01-01 00:00", + "sourceId": 67, + "status": 6, + "tax": 0, + "testResultAVS": "", + "testResultFSV": "", + "transactionNotes1": "", + "transactionNotes2": "", + "transactionNotes3": "" + }, + "paywayCode": "5000", + "paywayMessage": "" + }' + end + + def successful_credit_for_void_response + '{ + "cardAccount": { + "accountNotes1": "", + "accountNotes2": "", + "accountNotes3": "", + "accountNumber": "400010******2224", + "account_number_masked": "400010******2224", + "address": "456 My Street Apt 1", + "auLastUpdate": "1999-01-01 00:00", + "auUpdateType": 0, + "cardType": 1, + "city": "Ottawa", + "commercialCardType": 0, + "divisionId": 7, + "email": "", + "expirationDate": "0922", + "firstFour": "4000", + "firstName": "Jim", + "fsv": "737", + "inputMode": 1, + "lastFour": "2224", + "lastName": "Smith", + "lastUsed": "1999-01-01 00:00", + "middleName": "", + "onlinePaymentCryptogram": "", + "p2peInput": "", + "paywayToken": 10163736, + "phone": "5555555555", + "state": "ON", + "status": 0, + "zip": "K1C2N6" + }, + "cardTransaction": { + "addressVerificationResults": "", + "amount": 110, + "authorizationCode": "0987654321", + "authorizedTime": "2021-02-09 13:06:33", + "capturedTime": "2021-02-09 13:06:33", + "cbMode": 2, + "eciType": 1, + "fraudSecurityResults": "", + "fsvIndicator": "", + "name": "6720210209130633236167", + "pfpstatus": 3601, + "pfpstatusString": "PFP Not Enabled", + "processorErrorMessage": "", + "processorOrderId": "", + "processorRecurringAdvice": "", + "processorResponseDate": "", + "processorResultCode": "", + "processorSequenceNumber": 0, + "processorSoftDescriptor": "", + "referenceNumber": "123456", + "resultCode": 0, + "sessionToken_string": "0", + "settledTime": "1999-01-01 00:00", + "sourceId": 67, + "status": 4, + "tax": 0, + "testResultAVS": "", + "testResultFSV": "", + "transactionNotes1": "", + "transactionNotes2": "", + "transactionNotes3": "" + }, + "paywayCode": "5000", + "paywayMessage": "" + }' + end + + def successful_credit_void_response + '{ + "cardAccount": { + "accountNotes1": "", + "accountNotes2": "", + "accountNotes3": "", + "accountNumber": "400010******2224", + "account_number_masked": "400010******2224", + "address": "456 My Street Apt 1", + "auLastUpdate": "1999-01-01 00:00:00-05", + "auUpdateType": 0, + "cardType": 1, + "city": "Ottawa", + "commercialCardType": 0, + "divisionId": 7, + "email": "", + "expirationDate": "0922", + "firstFour": "4000", + "firstName": "Jim", + "fsv": "", + "inputMode": 1, + "lastFour": "2224", + "lastName": "Smith", + "lastUsed": "1999-01-01 00:00:00-05", + "middleName": "", + "onlinePaymentCryptogram": "", + "p2peInput": "", + "paywayToken": 10163736, + "phone": "5555555555", + "state": "ON", + "status": 2, + "zip": "K1C2N6" + }, + "cardTransaction": { + "addressVerificationResults": "", + "amount": 110, + "authorizationCode": "0987654321", + "authorizedTime": "2021-02-09 14:02:51-05", + "capturedTime": "2021-02-09 14:02:51-05", + "cbMode": 2, + "eciType": 1, + "fraudSecurityResults": "", + "fsvIndicator": "", + "name": "672021020914025188146", + "pfpstatus": 3601, + "pfpstatusString": "PFP Not Enabled", + "processorErrorMessage": "", + "processorOrderId": "", + "processorRecurringAdvice": "", + "processorResponseDate": "", + "processorResultCode": "", + "processorSequenceNumber": 0, + "processorSoftDescriptor": "", + "referenceNumber": "123456", + "resultCode": 0, + "sessionToken_string": "0", + "settledTime": "1999-01-01 00:00", + "sourceId": 67, + "status": 6, + "tax": 0, + "testResultAVS": "", + "testResultFSV": "", + "transactionNotes1": "", + "transactionNotes2": "", + "transactionNotes3": "" + }, + "paywayCode": "5000", + "paywayMessage": "" + }' + end + + def failed_invalid_login_response + '{ + "paywayCode": "5001", + "paywayMessage": "Session timed out or other session error. Create new session" + }' + end +end diff --git a/test/unit/gateways/payway_test.rb b/test/unit/gateways/payway_test.rb index 86ae5139140..f5959f9b0fd 100644 --- a/test/unit/gateways/payway_test.rb +++ b/test/unit/gateways/payway_test.rb @@ -1,28 +1,27 @@ require 'test_helper' class PaywayTest < Test::Unit::TestCase - def setup @gateway = PaywayGateway.new( - :username => '12341234', - :password => 'abcdabcd', - :pem => certificate + username: '12341234', + password: 'abcdabcd', + pem: certificate ) @amount = 1000 @credit_card = ActiveMerchant::Billing::CreditCard.new( - :number => 4564710000000004, - :month => 2, - :year => 2019, - :first_name => 'Bob', - :last_name => 'Smith', - :verification_value => '847', - :brand => 'visa' + number: '4564710000000004', + month: 2, + year: 2019, + first_name: 'Bob', + last_name: 'Smith', + verification_value: '847', + brand: 'visa' ) @options = { - :order_id => 'abc' + order_id: 'abc' } end @@ -50,7 +49,6 @@ def test_succesful_purchase_visa_from_register_user assert_match '0', response.params['summary_code'] assert_match '08', response.params['response_code'] assert_match 'VISA', response.params['card_scheme_name'] - end def test_successful_purchase_master_card @@ -210,7 +208,7 @@ def test_bad_merchant def test_store @gateway.stubs(:ssl_post).returns(successful_response_store) - response = @gateway.store(@credit_card, :billing_id => 84517) + response = @gateway.store(@credit_card, billing_id: 84517) assert_instance_of Response, response assert_success response @@ -220,40 +218,40 @@ def test_store private - def successful_response_store - 'response.responseCode=00' - end + def successful_response_store + 'response.responseCode=00' + end - def successful_response_visa - 'response.summaryCode=0&response.responseCode=08&response.cardSchemeName=VISA' - end + def successful_response_visa + 'response.summaryCode=0&response.responseCode=08&response.cardSchemeName=VISA' + end - def successful_response_master_card - 'response.summaryCode=0&response.responseCode=08&response.cardSchemeName=MASTERCARD' - end + def successful_response_master_card + 'response.summaryCode=0&response.responseCode=08&response.cardSchemeName=MASTERCARD' + end - def purchase_with_invalid_credit_card_response - 'response.summaryCode=1&response.responseCode=14' - end + def purchase_with_invalid_credit_card_response + 'response.summaryCode=1&response.responseCode=14' + end - def purchase_with_expired_credit_card_response - 'response.summaryCode=1&response.responseCode=54' - end + def purchase_with_expired_credit_card_response + 'response.summaryCode=1&response.responseCode=54' + end - def purchase_with_invalid_month_response - 'response.summaryCode=3&response.responseCode=QA' - end + def purchase_with_invalid_month_response + 'response.summaryCode=3&response.responseCode=QA' + end - def bad_login_response - 'response.summaryCode=3&response.responseCode=QH' - end + def bad_login_response + 'response.summaryCode=3&response.responseCode=QH' + end - def bad_merchant_response - 'response.summaryCode=3&response.responseCode=QK' - end + def bad_merchant_response + 'response.summaryCode=3&response.responseCode=QK' + end - def certificate - '------BEGIN CERTIFICATE----- + def certificate + '------BEGIN CERTIFICATE----- -MIIDeDCCAmCgAwIBAgIBATANBgkqhkiG9w0BAQUFADBBMRMwEQYDVQQDDApjb2R5 -ZmF1c2VyMRUwEwYKCZImiZPyLGQBGRYFZ21haWwxEzARBgoJkiaJk/IsZAEZFgNj -b20wHhcNMTMxMTEzMTk1NjE2WhcNMTQxMTEzMTk1NjE2WjBBMRMwEQYDVQQDDApj @@ -274,5 +272,5 @@ def certificate -ZJB9YPQZG+vWBdDSca3sUMtvFxpLUFwdKF5APSPOVnhbFJ3vSXY1ulP/R6XW9vnw -6kkQi2fHhU20ugMzp881Eixr+TjC0RvUerLG7g== ------END CERTIFICATE-----' - end + end end diff --git a/test/unit/gateways/pin_test.rb b/test/unit/gateways/pin_test.rb index fc16c0d1f68..200ca321e98 100644 --- a/test/unit/gateways/pin_test.rb +++ b/test/unit/gateways/pin_test.rb @@ -2,16 +2,36 @@ class PinTest < Test::Unit::TestCase def setup - @gateway = PinGateway.new(:api_key => 'I_THISISNOTAREALAPIKEY') + @gateway = PinGateway.new(api_key: 'I_THISISNOTAREALAPIKEY') @credit_card = credit_card @amount = 100 @options = { - :email => 'roland@pin.net.au', - :billing_address => address, - :description => 'Store Purchase', - :ip => '127.0.0.1' + email: 'roland@pinpayments.com', + billing_address: address, + description: 'Store Purchase', + ip: '127.0.0.1' + } + + @three_d_secure = { + enabled: true, + fallback_ok: true, + callback_url: 'https://yoursite.com/authentication_complete' + } + + @three_d_secure_v1 = { + version: '1.0.2', + eci: '05', + cavv: '1234', + xid: '1234' + } + + @three_d_secure_v2 = { + version: '2.0.0', + eci: '06', + cavv: 'jEoEjMykRWFCBEAAAVOBSYAAAA=', + ds_transaction_id: 'f92a19e2-485f-4d21-81ea-69a7352f611e' } end @@ -30,19 +50,19 @@ def test_money_format end def test_url - assert_equal 'https://test-api.pin.net.au/1', PinGateway.test_url + assert_equal 'https://test-api.pinpayments.com/1', PinGateway.test_url end def test_live_url - assert_equal 'https://api.pin.net.au/1', PinGateway.live_url + assert_equal 'https://api.pinpayments.com/1', PinGateway.live_url end def test_supported_countries - assert_equal ['AU'], PinGateway.supported_countries + assert_equal %w(AU NZ), PinGateway.supported_countries end def test_supported_cardtypes - assert_equal [:visa, :master, :american_express], PinGateway.supported_cardtypes + assert_equal %i[visa master american_express diners_club discover jcb], PinGateway.supported_cardtypes end def test_display_name @@ -66,7 +86,7 @@ def test_successful_purchase headers = {} @gateway.stubs(:headers).returns(headers) @gateway.stubs(:post_data).returns(post_data) - @gateway.expects(:ssl_request).with(:post, 'https://test-api.pin.net.au/1/charges', post_data, headers).returns(successful_purchase_response) + @gateway.expects(:ssl_request).with(:post, 'https://test-api.pinpayments.com/1/charges', post_data, headers).returns(successful_purchase_response) assert response = @gateway.purchase(@amount, @credit_card, @options) assert_success response @@ -75,6 +95,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) @@ -101,16 +135,16 @@ def test_unparsable_body_of_failed_response end def test_successful_store - @gateway.expects(:ssl_request).returns(successful_store_response) + @gateway.expects(:ssl_request).returns(successful_customer_store_response) assert response = @gateway.store(@credit_card, @options) assert_success response - assert_equal 'card_sVOs8D9nANoNgDc38NvKow', response.authorization - assert_equal JSON.parse(successful_store_response), response.params + assert_equal 'card__o8I8GmoXDF0d35LEDZbNQ;cus_05p0n7UFPmcyCNjD8c6HdA', response.authorization + assert_equal JSON.parse(successful_customer_store_response), response.params assert response.test? end def test_unsuccessful_store - @gateway.expects(:ssl_request).returns(failed_store_response) + @gateway.expects(:ssl_request).returns(failed_customer_store_response) assert response = @gateway.store(@credit_card, @options) assert_failure response @@ -118,19 +152,39 @@ def test_unsuccessful_store assert response.test? end + def test_successful_unstore + token = 'cus_05p0n7UFPmcyCNjD8c6HdA' + @gateway.expects(:ssl_request).with(:delete, "https://test-api.pinpayments.com/1/customers/#{token}", instance_of(String), instance_of(Hash)).returns(nil) + + assert response = @gateway.unstore(token) + assert_success response + assert_nil response.message + assert response.test? + end + + def test_unsuccessful_unstore + token = 'cus_05p0n7UFPmcyCNjD8c6HdA' + @gateway.expects(:ssl_request).with(:delete, "https://test-api.pinpayments.com/1/customers/#{token}", instance_of(String), instance_of(Hash)).returns(failed_customer_unstore_response) + + assert response = @gateway.unstore(token) + assert_failure response + assert_equal 'The requested resource could not be found.', response.message + assert response.test? + end + def test_successful_update token = 'cus_05p0n7UFPmcyCNjD8c6HdA' - @gateway.expects(:ssl_request).with(:put, "https://test-api.pin.net.au/1/customers/#{token}", instance_of(String), instance_of(Hash)).returns(successful_customer_store_response) + @gateway.expects(:ssl_request).with(:put, "https://test-api.pinpayments.com/1/customers/#{token}", instance_of(String), instance_of(Hash)).returns(successful_customer_store_response) assert response = @gateway.update('cus_05p0n7UFPmcyCNjD8c6HdA', @credit_card, @options) assert_success response - assert_equal 'cus_05p0n7UFPmcyCNjD8c6HdA', response.authorization + assert_equal 'card__o8I8GmoXDF0d35LEDZbNQ;cus_05p0n7UFPmcyCNjD8c6HdA', response.authorization assert_equal JSON.parse(successful_customer_store_response), response.params assert response.test? end def test_successful_refund token = 'ch_encBuMDf17qTabmVjDsQlg' - @gateway.expects(:ssl_request).with(:post, "https://test-api.pin.net.au/1/charges/#{token}/refunds", {:amount => '100'}.to_json, instance_of(Hash)).returns(successful_refund_response) + @gateway.expects(:ssl_request).with(:post, "https://test-api.pinpayments.com/1/charges/#{token}/refunds", { amount: '100' }.to_json, instance_of(Hash)).returns(successful_refund_response) assert response = @gateway.refund(100, token) assert_equal 'rf_d2C7M6Mn4z2m3APqarNN6w', response.authorization @@ -140,7 +194,7 @@ def test_successful_refund def test_unsuccessful_refund token = 'ch_encBuMDf17qTabmVjDsQlg' - @gateway.expects(:ssl_request).with(:post, "https://test-api.pin.net.au/1/charges/#{token}/refunds", {:amount => '100'}.to_json, instance_of(Hash)).returns(failed_refund_response) + @gateway.expects(:ssl_request).with(:post, "https://test-api.pinpayments.com/1/charges/#{token}/refunds", { amount: '100' }.to_json, instance_of(Hash)).returns(failed_refund_response) assert response = @gateway.refund(100, token) assert_failure response @@ -153,7 +207,7 @@ def test_successful_authorize headers = {} @gateway.stubs(:headers).returns(headers) @gateway.stubs(:post_data).returns(post_data) - @gateway.expects(:ssl_request).with(:post, 'https://test-api.pin.net.au/1/charges', post_data, headers).returns(successful_purchase_response) + @gateway.expects(:ssl_request).with(:post, 'https://test-api.pinpayments.com/1/charges', post_data, headers).returns(successful_purchase_response) assert response = @gateway.authorize(@amount, @credit_card, @options) assert_success response @@ -168,7 +222,7 @@ def test_successful_capture token = 'ch_encBuMDf17qTabmVjDsQlg' @gateway.stubs(:headers).returns(headers) @gateway.stubs(:post_data).returns(post_data) - @gateway.expects(:ssl_request).with(:put, "https://test-api.pin.net.au/1/charges/#{token}/capture", post_data, headers).returns(successful_capture_response) + @gateway.expects(:ssl_request).with(:put, "https://test-api.pinpayments.com/1/charges/#{token}/capture", post_data, headers).returns(successful_capture_response) assert response = @gateway.capture(100, token) assert_success response @@ -176,6 +230,34 @@ def test_successful_capture assert response.test? end + def test_succesful_purchase_with_3ds + post_data = {} + headers = {} + @gateway.stubs(:headers).returns(headers) + @gateway.stubs(:post_data).returns(post_data) + @gateway.expects(:ssl_request).with(:post, 'https://test-api.pinpayments.com/1/charges', post_data, headers).returns(successful_purchase_response) + + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(three_d_secure: @three_d_secure_v1)) + assert_success response + assert_equal 'ch_Kw_JxmVqMeSOQU19_krRdw', response.authorization + assert_equal JSON.parse(successful_purchase_response), response.params + assert response.test? + end + + def test_succesful_authorize_with_3ds + post_data = {} + headers = {} + @gateway.stubs(:headers).returns(headers) + @gateway.stubs(:post_data).returns(post_data) + @gateway.expects(:ssl_request).with(:post, 'https://test-api.pinpayments.com/1/charges', post_data, headers).returns(successful_purchase_response) + + assert response = @gateway.authorize(@amount, @credit_card, @options.merge(three_d_secure: @three_d_secure_v1)) + assert_success response + assert_equal 'ch_Kw_JxmVqMeSOQU19_krRdw', response.authorization + assert_equal JSON.parse(successful_purchase_response), response.params + assert response.test? + end + def test_store_parameters @gateway.expects(:add_creditcard).with(instance_of(Hash), @credit_card) @gateway.expects(:add_address).with(instance_of(Hash), @credit_card, @options) @@ -225,7 +307,7 @@ def test_add_customer_data @gateway.send(:add_customer_data, post, @options) - assert_equal 'roland@pin.net.au', post[:email] + assert_equal 'roland@pinpayments.com', post[:email] assert_equal '127.0.0.1', post[:ip_address] end @@ -262,7 +344,7 @@ def test_add_capture @gateway.send(:add_capture, post, @options) assert_equal post[:capture], true - @gateway.send(:add_capture, post, :capture => false) + @gateway.send(:add_capture, post, capture: false) assert_equal post[:capture], false end @@ -291,6 +373,32 @@ def test_add_creditcard_with_customer_token assert_false post.has_key?(:card) end + def test_add_3ds + post = {} + @gateway.send(:add_3ds, post, @options.merge(three_d_secure: @three_d_secure)) + assert_equal true, post[:three_d_secure][:enabled] + assert_equal true, post[:three_d_secure][:fallback_ok] + assert_equal 'https://yoursite.com/authentication_complete', post[:three_d_secure][:callback_url] + end + + def test_add_3ds_v1 + post = {} + @gateway.send(:add_3ds, post, @options.merge(three_d_secure: @three_d_secure_v1)) + assert_equal '1.0.2', post[:three_d_secure][:version] + assert_equal '05', post[:three_d_secure][:eci] + assert_equal '1234', post[:three_d_secure][:cavv] + assert_equal '1234', post[:three_d_secure][:transaction_id] + end + + def test_add_3ds_v2 + post = {} + @gateway.send(:add_3ds, post, @options.merge(three_d_secure: @three_d_secure_v2)) + assert_equal '2.0.0', post[:three_d_secure][:version] + assert_equal '06', post[:three_d_secure][:eci] + assert_equal 'jEoEjMykRWFCBEAAAVOBSYAAAA=', post[:three_d_secure][:cavv] + assert_equal 'f92a19e2-485f-4d21-81ea-69a7352f611e', post[:three_d_secure][:transaction_id] + end + def test_post_data post = {} @gateway.send(:add_creditcard, post, @credit_card) @@ -304,13 +412,13 @@ def test_headers } @gateway.expects(:ssl_request).with(:post, anything, anything, expected_headers).returns(successful_purchase_response) - assert response = @gateway.purchase(@amount, @credit_card, {}) + assert @gateway.purchase(@amount, @credit_card, {}) expected_headers['X-Partner-Key'] = 'MyPartnerKey' expected_headers['X-Safe-Card'] = '1' @gateway.expects(:ssl_request).with(:post, anything, anything, expected_headers).returns(successful_purchase_response) - assert response = @gateway.purchase(@amount, @credit_card, :partner_key => 'MyPartnerKey', :safe_card => '1') + assert @gateway.purchase(@amount, @credit_card, partner_key: 'MyPartnerKey', safe_card: '1') end def test_transcript_scrubbing @@ -327,7 +435,7 @@ def successful_purchase_response "amount":400, "currency":"AUD", "description":"test charge", - "email":"roland@pin.net.au", + "email":"roland@pinpayments.com", "ip_address":"203.192.1.172", "created_at":"2013-01-14T03:00:41Z", "status_message":"Success!", @@ -407,7 +515,7 @@ def successful_customer_store_response '{ "response":{ "token":"cus_05p0n7UFPmcyCNjD8c6HdA", - "email":"roland@pin.net.au", + "email":"roland@pinpayments.com", "created_at":"2013-01-16T03:16:11Z", "card":{ "token":"card__o8I8GmoXDF0d35LEDZbNQ", @@ -438,6 +546,13 @@ def failed_customer_store_response }' end + def failed_customer_unstore_response + '{ + "error": "not_found", + "error_description": "The requested resource could not be found." + }' + end + def successful_refund_response '{ "response":{ @@ -473,7 +588,7 @@ def successful_capture_response "amount":400, "currency":"AUD", "description":"test charge", - "email":"roland@pin.net.au", + "email":"roland@pinpayments.com", "ip_address":"203.192.1.172", "created_at":"2013-01-14T03:00:41Z", "status_message":"Success!", @@ -504,7 +619,7 @@ def transcript '{ "amount":"100", "currency":"AUD", - "email":"roland@pin.net.au", + "email":"roland@pinpayments.com", "ip_address":"203.59.39.62", "description":"Store Purchase 1437598192", "card":{ @@ -526,7 +641,7 @@ def scrubbed_transcript '{ "amount":"100", "currency":"AUD", - "email":"roland@pin.net.au", + "email":"roland@pinpayments.com", "ip_address":"203.59.39.62", "description":"Store Purchase 1437598192", "card":{ @@ -543,5 +658,4 @@ def scrubbed_transcript } }' end - end diff --git a/test/unit/gateways/plexo_test.rb b/test/unit/gateways/plexo_test.rb new file mode 100644 index 00000000000..225864eae1e --- /dev/null +++ b/test/unit/gateways/plexo_test.rb @@ -0,0 +1,1077 @@ +require 'test_helper' + +class PlexoTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = PlexoGateway.new(client_id: 'abcd', api_key: 'efgh', merchant_id: 'test090') + + @amount = 100 + @credit_card = credit_card('5555555555554444', month: '12', year: '2024', verification_value: '111', first_name: 'Santiago', last_name: 'Navatta') + @declined_card = credit_card('5555555555554445') + @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', + items: [ + { + name: 'prueba', + description: 'prueba desc', + quantity: '1', + price: '100', + discount: '0' + } + ], + amount_details: { + tip_amount: '5' + }, + metadata: { + custom_one: 'test1', + test_a: 'abc' + }, + identification_type: '1', + identification_value: '123456', + billing_address: address + } + + @cancel_options = { + description: 'Test desc', + reason: 'requested by client' + } + end + + def test_successful_purchase + @gateway.expects(:ssl_post).returns(successful_purchase_response) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'You have been mocked', response.message + end + + def test_failed_purchase + @gateway.expects(:ssl_post).returns(failed_purchase_response) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_equal 400, response.error_code + end + + def test_successful_authorize + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal 'test090', request['MerchantId'] + assert_equal @credit_card.number, request['paymentMethod']['Card']['Number'] + assert_equal @credit_card.verification_value, request['paymentMethod']['Card']['Cvc'] + assert_equal @credit_card.first_name, request['paymentMethod']['Card']['Cardholder']['FirstName'] + assert_equal @options[:email], request['paymentMethod']['Card']['Cardholder']['Email'] + assert_equal @options[:identification_type], request['paymentMethod']['Card']['Cardholder']['Identification']['Type'] + assert_equal @options[:identification_value], request['paymentMethod']['Card']['Cardholder']['Identification']['Value'] + assert_equal @options[:billing_address][:city], request['paymentMethod']['Card']['Cardholder']['BillingAddress']['City'] + assert_equal @options[:billing_address][:country], request['paymentMethod']['Card']['Cardholder']['BillingAddress']['Country'] + assert_equal @options[:billing_address][:address1], request['paymentMethod']['Card']['Cardholder']['BillingAddress']['Line1'] + assert_equal @options[:billing_address][:address2], request['paymentMethod']['Card']['Cardholder']['BillingAddress']['Line2'] + assert_equal @options[:billing_address][:zip], request['paymentMethod']['Card']['Cardholder']['BillingAddress']['PostalCode'] + assert_equal @options[:billing_address][:state], request['paymentMethod']['Card']['Cardholder']['BillingAddress']['State'] + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_successful_authorize_with_items + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + request['Items'].each_with_index do |item, index| + assert_not_nil item['ReferenceId'] + assert_equal item['Name'], @options[:items][index][:name] if item['Name'] + assert_equal item['Description'], @options[:items][index][:description] if item['Description'] + assert_equal item['Quantity'], @options[:items][index][:quantity] if item['Quantity'] + assert_equal item['Price'], @options[:items][index][:price] if item['Price'] + assert_equal item['Discount'], @options[:items][index][:discount] if item['Discount'] + end + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_successful_authorize_with_meta_fields + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + @options[:metadata].keys.each do |meta_key| + camel_key = meta_key.to_s.camelize + assert_equal request['Metadata'][camel_key], @options[:metadata][meta_key] + end + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_successful_authorize_with_finger_print + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ finger_print: 'USABJHABSFASNJKN123532' })) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['BrowserDetails']['DeviceFingerprint'], 'USABJHABSFASNJKN123532' + end.respond_with(successful_authorize_response) + end + + def test_successful_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) + + original_response = JSON.parse(successful_authorize_response) + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + assert_equal response.params['amount'], original_response['amount']['total'] + assert_equal response.params['currency'], original_response['amount']['currency'] + assert_equal response.params['amount_details'], original_response['amount']['details'] + end + + def test_successful_authorize_with_extra_options + other_fields = { + installments: '1', + statement_descriptor: 'Plexo * Test', + customer_id: 'customer1', + cardholder_birthdate: '1999-08-18T19:49:37.023Z' + } + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(other_fields)) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['Installments'], other_fields[:installments] + assert_equal request['CustomerId'], other_fields[:customer_id] + assert_equal request['StatementDescriptor'], other_fields[:statement_descriptor] + assert_equal request['paymentMethod']['Card']['Cardholder']['Birthdate'], other_fields[:cardholder_birthdate] + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_successful_authorize_with_amount_fields + amount_fields = { + taxed_amount: '100', + tip_amount: '32', + discount_amount: '10', + taxable_amount: '302', + tax: { + type: '17934', + amount: '22', + rate: '0.22' + } + } + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ amount_details: amount_fields, currency: 'USD' })) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['Amount']['Currency'], 'USD' + assert_equal request['Amount']['Details']['TaxedAmount'], amount_fields[:taxed_amount] + assert_equal request['Amount']['Details']['TipAmount'], amount_fields[:tip_amount] + assert_equal request['Amount']['Details']['DiscountAmount'], amount_fields[:discount_amount] + assert_equal request['Amount']['Details']['TaxableAmount'], amount_fields[:taxable_amount] + assert_equal request['Amount']['Details']['Tax']['Type'], amount_fields[:tax][:type] + assert_equal request['Amount']['Details']['Tax']['Amount'], amount_fields[:tax][:amount] + assert_equal request['Amount']['Details']['Tax']['Rate'], amount_fields[:tax][:rate] + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_failed_authorize + @gateway.expects(:ssl_post).returns(failed_authorize_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert_equal 400, response.error_code + end + + def test_successful_capture + response = stub_comms do + @gateway.capture(@amount, '123456abcdef', { reference_id: 'reference123' }) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['ReferenceId'], 'reference123' + assert_includes endpoint, '123456abcdef' + end.respond_with(successful_capture_response) + + assert_success response + end + + def test_failed_capture + @gateway.expects(:ssl_post).returns(failed_capture_response) + + response = @gateway.capture(@amount, @credit_card, @options) + assert_failure response + assert_equal 400, response.error_code + end + + def test_successful_refund + refund_options = { + reference_id: 'reference123', + refund_type: 'partial-refund', + description: 'my description', + reason: 'reason abc' + } + response = stub_comms do + @gateway.refund(@amount, '123456abcdef', refund_options) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['ReferenceId'], refund_options[:reference_id] + assert_equal request['Type'], refund_options[:refund_type] + assert_equal request['Description'], refund_options[:description] + assert_equal request['Reason'], refund_options[:reason] + assert_includes endpoint, '123456abcdef' + end.respond_with(successful_refund_response) + + assert_success response + end + + def test_failed_refund + @gateway.expects(:ssl_post).returns(failed_refund_response) + + response = @gateway.refund(@amount, @credit_card, @options) + assert_failure response + assert_equal 400, response.error_code + end + + def test_successful_void + void_options = { + reference_id: 'reference123', + description: 'my description', + reason: 'reason abc' + } + response = stub_comms do + @gateway.void('123456abcdef', void_options) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['ReferenceId'], void_options[:reference_id] + assert_equal request['Description'], void_options[:description] + assert_equal request['Reason'], void_options[:reason] + assert_includes endpoint, '123456abcdef' + end.respond_with(successful_void_response) + + assert_success response + end + + def test_failed_void + @gateway.expects(:ssl_post).returns(failed_void_response) + + response = @gateway.void(@credit_card, @options) + assert_failure response + assert_equal 400, response.error_code + end + + def test_successful_verify + @gateway.expects(:ssl_post).returns(successful_verify_response) + + response = @gateway.verify(@credit_card, @options) + assert_success response + assert_equal 'You have been mocked.', response.message + end + + def test_successful_verify_with_custom_amount + stub_comms do + @gateway.verify(@credit_card, @options.merge({ verify_amount: '900' })) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['Amount']['Total'], '9.00' + end.respond_with(successful_verify_response) + end + + def test_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) + + response = @gateway.verify(@credit_card, @options) + assert_failure response + assert_equal 400, response.error_code + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + 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 request['paymentMethod']['source'], 'network-token' + assert_equal @network_token_credit_card.number, request['paymentMethod']['NetworkToken']['Number'] + assert_equal @network_token_credit_card.payment_cryptogram, request['paymentMethod']['NetworkToken']['Cryptogram'] + assert_equal @network_token_credit_card.first_name, request['paymentMethod']['NetworkToken']['Cardholder']['FirstName'] + assert_equal request['paymentMethod']['NetworkToken']['ExpMonth'], '12' + assert_equal request['paymentMethod']['NetworkToken']['ExpYear'], '20' + end.respond_with(successful_network_token_response) + + assert_success purchase + assert_equal 'You have been mocked.', purchase.message + end + + private + + def pre_scrubbed + <<~PRE_SCRUBBED + opening connection to api.testing.plexo.com.uy:443... + opened + starting SSL for api.testing.plexo.com.uy:443... + SSL established, protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384 + <- "POST /v1/payments/628b723aa450dab85ba2fa03/captures HTTP/1.1\r\nContent-Type: application/json\r\nAuthorization: Basic MjIxOjlkZWZhZWVlYmMzOTQ1NDFhZmY2MzMyOTE4MmRkODQyNDA1MTJhYTI0NWE0NDY2MDkxZWQ3MGY2OTAxYjQ5NDc=\r\nX-Mock-Tokenization: true\r\nX-Mock-Switcher: true\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: api.testing.plexo.com.uy\r\nContent-Length: 66\r\n\r\n" + <- "{\"ReferenceId\":\"e6742109bb60458b1c5a7c69ffcc3f54\",\"Amount\":\"1.00\"}" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Mon, 23 May 2022 11:38:35 GMT\r\n" + -> "Content-Type: application/json; charset=utf-8\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "X-MiniProfiler-Ids: [\"c6b2ce60-757c-4115-b802-e33a27c2e311\",\"e1533461-72dc-4693-97a6-deea47601ca4\",\"da8b919f-a1f8-4051-870d-3679f4c8ac6b\",\"4465311a-ab60-470d-8f69-e06eed35c271\",\"c4b23b7d-e824-4fd6-95b9-82fa4786f4a2\",\"c5fe47c7-6155-4eb7-b9f4-84cd7fae7acf\",\"80e2f132-1ac1-4b25-b030-5eaccd44a0db\",\"525c97a7-5df7-4dd5-b1da-4c6abe9a5995\",\"98694fd6-f3ff-497a-b6d4-477a50a093aa\",\"802b9242-97c6-4438-bd72-960dbdf2f752\",\"7aa9078c-12f1-41f4-bc57-c77fb8a9ecc8\",\"4890d7e1-22c9-4e9d-afe1-88149e743aa0\",\"cafed17f-08ce-49cc-91d0-d8d9865facc7\",\"98fea53d-ad00-44cb-8e82-0829e5c8aaee\",\"5730d4fa-1c70-4679-a097-d9c8b7156f2d\",\"ba7d9c5a-e2bc-461f-b87d-552ae9fabb65\",\"3b1dbbbe-8112-4293-9be3-c865741c5494\",\"3ab01bd5-a2b5-4d9c-84c7-9c743f1e9978\",\"d6e397a3-cf95-413c-b3c6-4729aa463d33\",\"fc9cb79e-ab22-42b0-b611-0b3f62a203bb\",\"b16fd902-f50a-43e2-8e82-cc0fe763b16b\",\"dc702114-866c-4b9a-bc07-291b0b0f8b73\"]\r\n" + -> "x-correlation-id: 24ebd1ee-a69a-4163-85cf-e5a1ab7fd26b\r\n" + -> "Strict-Transport-Security: max-age=15724800; includeSubDomains\r\n" + -> "\r\n" + -> "192\r\n" + reading 402 bytes... + -> "{\"id\":\"628b723ba450dab85ba2fa0a\",\"uniqueId\":\"978260656060936192\",\"parentId\":\"cf8ecc4a-b0ed-4a40-945e-0eaff39e66f9\",\"referenceId\":\"e6742109bb60458b1c5a7c69ffcc3f54\",\"type\":\"capture\",\"status\":\"approved\",\"createdAt\":\"2022-05-23T11:38:35.6091676Z\",\"processedAt\":\"2022-05-23T11:38:35.6091521Z\",\"resultCode\":\"0\",\"resultMessage\":\"You have been mocked.\",\"authorization\":\"12133\",\"ticket\":\"111111\",\"amount\":1.00}" + read 402 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + PRE_SCRUBBED + end + + def post_scrubbed + <<~POST_SCRUBBED + opening connection to api.testing.plexo.com.uy:443... + opened + starting SSL for api.testing.plexo.com.uy:443... + SSL established, protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384 + <- "POST /v1/payments/628b723aa450dab85ba2fa03/captures HTTP/1.1\r\nContent-Type: application/json\r\nAuthorization: Basic [FILTERED]=\r\nX-Mock-Tokenization: true\r\nX-Mock-Switcher: true\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: api.testing.plexo.com.uy\r\nContent-Length: 66\r\n\r\n" + <- "{\"ReferenceId\":\"e6742109bb60458b1c5a7c69ffcc3f54",\"Amount\":\"1.00\"}" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Mon, 23 May 2022 11:38:35 GMT\r\n" + -> "Content-Type: application/json; charset=utf-8\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "X-MiniProfiler-Ids: [\"c6b2ce60-757c-4115-b802-e33a27c2e311\",\"e1533461-72dc-4693-97a6-deea47601ca4\",\"da8b919f-a1f8-4051-870d-3679f4c8ac6b\",\"4465311a-ab60-470d-8f69-e06eed35c271\",\"c4b23b7d-e824-4fd6-95b9-82fa4786f4a2\",\"c5fe47c7-6155-4eb7-b9f4-84cd7fae7acf\",\"80e2f132-1ac1-4b25-b030-5eaccd44a0db\",\"525c97a7-5df7-4dd5-b1da-4c6abe9a5995\",\"98694fd6-f3ff-497a-b6d4-477a50a093aa\",\"802b9242-97c6-4438-bd72-960dbdf2f752\",\"7aa9078c-12f1-41f4-bc57-c77fb8a9ecc8\",\"4890d7e1-22c9-4e9d-afe1-88149e743aa0\",\"cafed17f-08ce-49cc-91d0-d8d9865facc7\",\"98fea53d-ad00-44cb-8e82-0829e5c8aaee\",\"5730d4fa-1c70-4679-a097-d9c8b7156f2d\",\"ba7d9c5a-e2bc-461f-b87d-552ae9fabb65\",\"3b1dbbbe-8112-4293-9be3-c865741c5494\",\"3ab01bd5-a2b5-4d9c-84c7-9c743f1e9978\",\"d6e397a3-cf95-413c-b3c6-4729aa463d33\",\"fc9cb79e-ab22-42b0-b611-0b3f62a203bb\",\"b16fd902-f50a-43e2-8e82-cc0fe763b16b\",\"dc702114-866c-4b9a-bc07-291b0b0f8b73\"]\r\n" + -> "x-correlation-id: 24ebd1ee-a69a-4163-85cf-e5a1ab7fd26b\r\n" + -> "Strict-Transport-Security: max-age=15724800; includeSubDomains\r\n" + -> "\r\n" + -> "192\r\n" + reading 402 bytes... + -> "{\"id\":\"628b723ba450dab85ba2fa0a\",\"uniqueId\":\"978260656060936192\",\"parentId\":\"cf8ecc4a-b0ed-4a40-945e-0eaff39e66f9\",\"referenceId\":\"e6742109bb60458b1c5a7c69ffcc3f54",\"type\":\"capture\",\"status\":\"approved\",\"createdAt\":\"2022-05-23T11:38:35.6091676Z\",\"processedAt\":\"2022-05-23T11:38:35.6091521Z\",\"resultCode\":\"0\",\"resultMessage\":\"You have been mocked.\",\"authorization\":\"12133\",\"ticket\":\"111111\",\"amount\":1.00}" + read 402 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + POST_SCRUBBED + end + + def failed_purchase_response + <<~RESPONSE + { + "code": "merchant-not-found", + "message": "The requested Merchant was not found.", + "type": "invalid-request-error", + "status": 400 + } + RESPONSE + end + + def successful_authorize_response + <<~RESPONSE + { + "id": "62878b1fa450dab85ba2f983", + "token": "7c23b951-599f-462e-8a47-6bbbb4dc5ad0", + "referenceId": "e7dbc06224f646ad8e63ec1c6e670a39", + "status": "approved", + "processingMethod": "api", + "browserDetails": { + "DeviceFingerprint": "12345", + "IpAddress": "127.0.0.1" + }, + "createdAt": "2022-05-20T12:35:43.1389809Z", + "merchant": { + "id": 3243, + "name": "spreedly", + "settings": { + "id": 41363, + "issuerId": 4, + "issuer": { + "id": 4, + "code": "mastercard", + "name": "MASTERCARD", + "type": "online" + }, + "metadata": { + "ProviderCommerceNumber": "153289", + "TerminalNumber": "1K153289", + "SoftDescriptor": "VTEX-Testing", + "PaymentProcessorId": "oca" + }, + "paymentProcessor": { + "acquirer": "oca", + "settings": { + "commerce": { + "fields": [ + { + "name": "ProviderCommerceNumber", + "type": 2049 + }, + { + "name": "TerminalNumber", + "type": 2051 + } + ] + }, + "fingerprint": { + "name": "cybersource-oca" + }, + "fields": [ + { + "name": "Email", + "type": 261 + }, + { + "name": "FirstName", + "type": 271 + }, + { + "name": "LastName", + "type": 272 + }, + { + "name": "CVC", + "type": 33154 + } + ] + } + } + }, + "clientId": 221 + }, + "client": { + "id": 221, + "name": "Spreedly", + "tier": 2, + "sessionTimeInSeconds": 36000 + }, + "paymentMethod": { + "type": "card", + "card": { + "name": "555555XXXXXX4444", + "bin": "555555", + "last4": "4444", + "expMonth": 12, + "expYear": 24, + "cardholder": { + "firstName": "Santiago", + "lastName": "Navatta", + "email": "snavatta@plexo.com.uy" + }, + "fingerprint": "2cccefc7e6e54644b5f5540aaab7744b" + }, + "issuer": { + "id": "mastercard", + "name": "MasterCard", + "pictureUrl": "https://static.plexo.com.uy/issuers/4.svg", + "type": "online" + }, + "processor": { + "acquirer": "oca" + } + }, + "installments": 1, + "amount": { + "currency": "UYU", + "total": 147, + "details": { + "taxedAmount": 0 + } + }, + "items": [ + { + "referenceId": "7c34953392e84949ab511667db0ebef2", + "name": "prueba", + "description": "prueba desc", + "quantity": 1, + "price": 100, + "discount": 0 + } + ], + "capture": { + "method": "manual" + }, + "transactions": [ + { + "id": "62878b1fa450dab85ba2f987", + "uniqueId": "977187868889886720", + "parentId": "7c23b951-599f-462e-8a47-6bbbb4dc5ad0", + "referenceId": "e7dbc06224f646ad8e63ec1c6e670a39", + "type": "authorization", + "status": "approved", + "createdAt": "2022-05-20T12:35:43.2161946Z", + "processedAt": "2022-05-20T12:35:43.2161798Z", + "resultCode": "0", + "resultMessage": "You have been mocked.", + "authorization": "12133", + "ticket": "111111", + "amount": 147 + } + ] + } + RESPONSE + end + + def successful_purchase_response + <<~RESPONSE + { + "id": "6305dd2d000d6ed5d1ecf79b", + "token": "82ae122c-d235-43bc-a454-fba16b2ae3a4", + "referenceId": "e7dbc06224f646ad8e63ec1c6e670a39", + "status": "approved", + "processingMethod": "api", + "createdAt": "2022-08-24T08:11:25.677Z", + "updatedAt": "2022-08-24T08:11:26.2893146Z", + "processedAt": "2022-08-24T08:11:26.2893146Z", + "merchant": { + "id": 3243, + "name": "spreedly", + "settings": { + "merchantIdentificationNumber": "98001456", + "paymentProcessor": { + "acquirer": "fiserv" + } + }, + "clientId": 221 + }, + "client": { + "id": 221, + "name": "Spreedly", + "tier": 2, + "sessionTimeInSeconds": 36000 + }, + "paymentMethod": { + "id": "mastercard", + "name": "MASTERCARD", + "type": "card", + "card": { + "name": "555555XXXXXX4444", + "bin": "555555", + "last4": "4444", + "expMonth": 12, + "expYear": 24, + "cardholder": { + "firstName": "Santiago", + "lastName": "Navatta", + "email": "snavatta@plexo.com.uy", + "identification": { + "type": 1, + "value": "123456" + }, + "billingAddress": { + "city": "Karachi", + "country": "Pakistan", + "line1": "street 4" + } + }, + "type": "credit", + "origin": "international", + "token": "03d43b25971546e0ab27e8b4698c9b7d" + }, + "issuer": { + "id": "mastercard", + "name": "MasterCard", + "pictureUrl": "https://static.plexo.com.uy/issuers/4.svg", + "type": "online" + }, + "processor": { + "id": 4, + "acquirer": "fiserv" + } + }, + "installments": 1, + "amount": { + "currency": "UYU", + "total": 147.0, + "details": { + "tax": { + "type": "17934", + "amount": 22.0 + }, + "taxedAmount": 100.0, + "tipAmount": 25.0, + "discountAmount": 0.0 + } + }, + "items": [ + { + "referenceId": "7c34953392e84949ab511667db0ebef2", + "name": "prueba", + "description": "prueba desc", + "quantity": 1, + "price": 100.0, + "discount": 0.0 + } + ], + "transactions": [ + { + "id": "6305dd2e000d6ed5d1ecf79f", + "uniqueId": "1011910592648278016", + "parentId": "82ae122c-d235-43bc-a454-fba16b2ae3a4", + "traceId": "cbf814cd-8b28-4145-ac0b-7381980015e8", + "referenceId": "e7dbc06224f646ad8e63ec1c6e670a39", + "type": "purchase", + "status": "approved", + "createdAt": "2022-08-24T08:11:26.2893133Z", + "processedAt": "2022-08-24T08:11:26.2893129Z", + "resultCode": "0", + "resultMessage": "You have been mocked", + "authorization": "1234567890", + "ticket": "1234567890", + "amount": 147.0 + } + ] + } + RESPONSE + end + + def failed_authorize_response + <<~RESPONSE + { + "code": "merchant-not-found", + "message": "The requested Merchant was not found.", + "type": "invalid-request-error", + "status": 400 + } + RESPONSE + end + + def successful_capture_response + <<~RESPONSE + { + "id": "62878b1fa450dab85ba2f987", + "uniqueId": "977187868889886720", + "parentId": "7c23b951-599f-462e-8a47-6bbbb4dc5ad0", + "referenceId": "e7dbc06224f646ad8e63ec1c6e670a39", + "type": "capture", + "status": "approved", + "createdAt": "2022-05-20T12:35:43.216Z", + "processedAt": "2022-05-20T12:35:43.216Z", + "resultCode": "0", + "resultMessage": "You have been mocked.", + "authorization": "12133", + "ticket": "111111", + "amount": 147 + } + RESPONSE + end + + def failed_capture_response + <<~RESPONSE + { + "code": "internal-error", + "message": "An internal error occurred. Contact support.", + "type": "api-error", + "status": 400 + } + RESPONSE + end + + def successful_refund_response + <<~RESPONSE + { + "id": "62878b1fa450dab85ba2f987", + "uniqueId": "977187868889886720", + "parentId": "7c23b951-599f-462e-8a47-6bbbb4dc5ad0", + "referenceId": "e7dbc06224f646ad8e63ec1c6e670a39", + "type": "refund", + "status": "approved", + "resultCode": "0", + "resultMessage": "You have been mocked.", + "authorization": "12133", + "ticket": "111111", + "amount": 147, + "reason": "ClientRequest" + } + RESPONSE + end + + def failed_refund_response + <<~RESPONSE + { + "code": "internal-error", + "message": "An internal error occurred. Contact support.", + "type": "api-error", + "status": 400 + } + RESPONSE + end + + def successful_void_response + <<~RESPONSE + { + "id": "62878c0fa450dab85ba2f994", + "uniqueId": "977188875178913792", + "parentId": "49fe7306-d706-43e4-97cd-8de94683c9ae", + "referenceId": "e7dbc06224f646ad8e63ec1c6e670a39", + "type": "cancellation", + "status": "approved", + "createdAt": "2022-05-20T12:39:43.134Z", + "processedAt": "2022-05-20T12:39:43.134Z", + "resultCode": "0", + "resultMessage": "You have been mocked.", + "authorization": "12133", + "ticket": "111111", + "amount": 147.0 + } + RESPONSE + end + + def failed_void_response + <<~RESPONSE + { + "code": "internal-error", + "message": "An internal error occurred. Contact support.", + "type": "api-error", + "status": 400 + } + RESPONSE + end + + def successful_verify_response + <<~RESPONSE + { + "id": "62ac2c5eaf353be57867f977", + "token": "7220c5cc-4b57-43e6-ae91-3fd3f3e8d49f", + "referenceId": "e7dbc06224f646ad8e63ec1c6e670a39", + "status": "approved", + "processingMethod": "api", + "browserDetails": { + "DeviceFingerprint": "12345", + "IpAddress": "127.0.0.1" + }, + "createdAt": "2022-06-17T07:25:18.1421498Z", + "merchant": { + "id": 3243, + "name": "spreedly", + "settings": { + "id": 41363, + "issuerId": 4, + "issuer": { + "id": 4, + "code": "mastercard", + "name": "MASTERCARD", + "type": "online" + }, + "metadata": { + "ProviderCommerceNumber": "153289", + "TerminalNumber": "1K153289", + "SoftDescriptor": "VTEX-Testing", + "PaymentProcessorId": "oca" + }, + "paymentProcessor": { + "acquirer": "oca", + "settings": { + "commerce": { + "fields": [ + { + "name": "ProviderCommerceNumber", + "type": 2049 + }, + { + "name": "TerminalNumber", + "type": 2051 + } + ] + }, + "fingerprint": { + "name": "cybersource-oca" + }, + "fields": [ + { + "name": "Email", + "type": 261 + }, + { + "name": "FirstName", + "type": 271 + }, + { + "name": "LastName", + "type": 272 + }, + { + "name": "CVC", + "type": 33154 + } + ] + } + } + }, + "clientId": 221 + }, + "client": { + "id": 221, + "name": "Spreedly", + "tier": 2, + "sessionTimeInSeconds": 36000 + }, + "paymentMethod": { + "type": "card", + "card": { + "name": "555555XXXXXX4444", + "bin": "555555", + "last4": "4444", + "expMonth": 12, + "expYear": 24, + "cardholder": { + "firstName": "Santiago", + "lastName": "Navatta", + "email": "snavatta@plexo.com.uy" + }, + "fingerprint": "36e2219cc4734a61af258905c1c59ba4" + }, + "issuer": { + "id": "mastercard", + "name": "MasterCard", + "pictureUrl": "https://static.plexo.com.uy/issuers/4.svg", + "type": "online" + }, + "processor": { + "acquirer": "oca" + } + }, + "installments": 1, + "amount": { + "currency": "UYU", + "total": 20 + }, + "items": [ + { + "referenceId": "997d4aafe29b4421ac52a3ddf5b28dfd", + "name": "card-verification", + "quantity": 1, + "price": 20 + } + ], + "capture": { + "method": "manual", + "delay": 0 + }, + "metadata": { + "One": "abc" + }, + "transactions": [ + { + "id": "62ac2c5eaf353be57867f97b", + "uniqueId": "987256610059481088", + "parentId": "7220c5cc-4b57-43e6-ae91-3fd3f3e8d49f", + "referenceId": "e7dbc06224f646ad8e63ec1c6e670a39", + "type": "authorization", + "status": "approved", + "createdAt": "2022-06-17T07:25:18.1796516Z", + "processedAt": "2022-06-17T07:25:18.1796366Z", + "resultCode": "0", + "resultMessage": "You have been mocked.", + "authorization": "12133", + "ticket": "111111", + "amount": 20 + } + ] + } + RESPONSE + end + + def failed_verify_response + <<~RESPONSE + { + "code": "invalid-transaction-state", + "message": "The selected payment state is not valid.", + "type": "api-error", + "status": 400 + } + RESPONSE + end + + 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/plugnpay_test.rb b/test/unit/gateways/plugnpay_test.rb index 5591e435414..da0170ceef0 100644 --- a/test/unit/gateways/plugnpay_test.rb +++ b/test/unit/gateways/plugnpay_test.rb @@ -1,19 +1,18 @@ require 'test_helper' class PlugnpayTest < Test::Unit::TestCase - def setup Base.mode = :test @gateway = PlugnpayGateway.new( - :login => 'X', - :password => 'Y' + login: 'X', + password: 'Y' ) @credit_card = credit_card @options = { - :billing_address => address, - :description => 'Store purchase' + billing_address: address, + description: 'Store purchase' } @amount = 100 end @@ -43,7 +42,7 @@ def test_capture_partial_amount def test_capture_full_amount @gateway.expects(:ssl_post).with(anything, all_of(regexp_matches(/mode=mark/), regexp_matches(/card_amount=1.00/)), anything).returns('') - @gateway.expects(:parse).returns({'auth_msg' => 'Blah blah blah Transaction may not be reauthorized'}, {}) + @gateway.expects(:parse).returns({ 'auth_msg' => 'Blah blah blah Transaction may not be reauthorized' }, {}) @gateway.capture(@amount, @credit_card, @options) end @@ -70,7 +69,7 @@ def test_refund def test_add_address_outsite_north_america result = PlugnpayGateway::PlugnpayPostData.new - @gateway.send(:add_addresses, result, :billing_address => {:address1 => '164 Waverley Street', :country => 'DE', :state => 'Dortmund'} ) + @gateway.send(:add_addresses, result, billing_address: { address1: '164 Waverley Street', country: 'DE', state: 'Dortmund' }) assert_equal result[:state], 'ZZ' assert_equal result[:province], 'Dortmund' @@ -80,13 +79,12 @@ def test_add_address_outsite_north_america assert_equal result[:card_address1], '164 Waverley Street' assert_equal result[:card_country], 'DE' - end def test_add_address result = PlugnpayGateway::PlugnpayPostData.new - @gateway.send(:add_addresses, result, :billing_address => {:address1 => '164 Waverley Street', :country => 'US', :state => 'CO'} ) + @gateway.send(:add_addresses, result, billing_address: { address1: '164 Waverley Street', country: 'US', state: 'CO' }) assert_equal result[:card_state], 'CO' assert_equal result[:card_address1], '164 Waverley Street' @@ -108,6 +106,7 @@ def test_cvv_result end private + def successful_purchase_response "FinalStatus=success&IPaddress=72%2e138%2e32%2e216&MStatus=success&User_Agent=&acct_code3=newcard&address1=1234%20My%20Street&address2=Apt%201&app_level=5&auth_code=TSTAUT&auth_date=20080125&auth_msg=%20&authtype=authpostauth&avs_code=X&card_address1=1234%20My%20Street&card_amount=1%2e00&card_city=Ottawa&card_country=CA&card_name=Longbob%20Longsen&card_state=ON&card_type=VISA&card_zip=K1C2N6&city=Ottawa&convert=underscores&country=CA&currency=usd&cvvresp=M&dontsndmail=yes&easycart=0&merchant=pnpdemo2&merchfraudlev=&mode=auth&orderID=2008012522252119738&phone=555%2d555%2d5555&publisher_email=trash%40plugnpay%2ecom&publisher_name=pnpdemo2&publisher_password=pnpdemo222&resp_code=00&shipinfo=0&shipname=Jim%20Smith&sresp=A&state=ON&success=yes&zip=K1C2N6&a=b\n" end diff --git a/test/unit/gateways/priority_test.rb b/test/unit/gateways/priority_test.rb new file mode 100644 index 00000000000..5ffee4c8b40 --- /dev/null +++ b/test/unit/gateways/priority_test.rb @@ -0,0 +1,1536 @@ +require 'test_helper' +class PriorityTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = PriorityGateway.new(api_key: 'sandbox_key', secret: 'secret', merchant_id: 'merchant_id') + @amount = 4 + @credit_card = credit_card + @invalid_credit_card = credit_card('4111') + @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 + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.respond_with(successful_purchase_response) + + assert_success response + assert_equal @approval_message, response.message + assert_equal 'Sale', response.params['type'] + assert response.test? + end + + def test_failed_purchase_invalid_credit_card + response = stub_comms do + @gateway.purchase(@amount, @invalid_credit_card, @options) + end.respond_with(failed_purchase_response) + + assert_failure response + assert_equal 'Declined', response.error_code + assert_equal 'Invalid card number', response.message + assert response.test? + end + + def test_successful_authorize + response = stub_comms do + @gateway.authorize(333, @credit_card, @options) + end.respond_with(successful_authorize_response) + + assert_success response + assert_equal @approval_message, response.message + assert_equal 'Authorization', response.params['type'] + assert response.test? + end + + def test_failed_authorize_invalid_credit_card + response = stub_comms do + @gateway.purchase(@amount, @invalid_credit_card, @options) + end.respond_with(failed_authorize_response) + + assert_failure response + assert_equal 'Declined', response.error_code + assert_equal 'Invalid card number', response.message + assert_equal 'Authorization', response.params['type'] + assert response.test? + end + + def test_successful_capture + response = stub_comms do + @gateway.capture(@amount, '10000001625060|PaQLIYLRdWtcFKl5VaKTdUVxMutXJ5Ru', @options) + end.respond_with(successful_capture_response) + + assert_success response + assert_equal 'Approved', response.message + 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) + end.respond_with(failed_capture_response) + + assert_failure response + assert_equal 'Declined', response.error_code + assert_equal 'Original Transaction Not Found', response.message + assert_equal nil, response.authorization + end + + def test_failed_void + @gateway.expects(:ssl_request).returns(failed_void_response) + + response = @gateway.void('bogus authorization') + assert_failure response + assert_equal 'Unauthorized', response.error_code + assert_equal 'Original Payment Not Found Or You Do Not Have Access.', response.message + end + + def test_successful_refund + authorization = '86044396|PTp2WxLTXEP9Ml4DfDzTAbDWRaEFLKEM' + response = stub_comms do + @gateway.refund(544, authorization, @options) + end.respond_with(successful_refund_response) + + assert_success response + assert_equal @approval_message, response.message + assert response.test? + end + + def test_failed_duplicate_refund + authorization = '86044396|PTp2WxLTXEP9Ml4DfDzTAbDWRaEFLKEM' + response = stub_comms do + @gateway.refund(544, authorization, @options) + end.respond_with(failed_duplicate_refund) + + assert_failure response + assert_equal 'Declined', response.error_code + assert_equal 'Payment already refunded', response.message + assert response.test? + end + + def test_failed_get_payment_status + @gateway.expects(:ssl_get).returns('Not Found') + + batch_check = @gateway.get_payment_status(123456) + + assert_failure batch_check + assert_includes batch_check.message, 'Invalid JSON response' + assert_includes batch_check.message, 'Not Found' + end + + def test_purchase_passes_shipping_data + options_with_shipping = @options.merge({ ship_to_country: 'USA', ship_to_zip: 27703, ship_amount: 0.01 }) + + stub_comms do + @gateway.purchase(@amount, @credit_card, options_with_shipping) + end.check_request do |_endpoint, data, _headers| + assert_match(/shipAmount\":0.01/, data) + assert_match(/shipToZip\":27703/, data) + assert_match(/shipToCountry\":\"USA/, data) + end.respond_with(successful_purchase_response) + end + + def test_purchase_passes_purchases_data + purchases_data = { + 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 + } + ] + } + options_with_purchases = @options.merge(purchases_data) + + stub_comms do + @gateway.purchase(@amount, @credit_card, options_with_purchases) + end.check_request do |_endpoint, data, _headers| + purchase_item = purchases_data[:purchases].first + purchase_object = JSON.parse(data)['purchases'].first + + 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']) + end.respond_with(successful_purchase_response) + end + + def test_purchase_passes_pos_data + custom_pos_data = { + 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' + } + } + options_with_custom_pos_data = @options.merge(custom_pos_data) + + stub_comms do + @gateway.purchase(@amount, @credit_card, options_with_custom_pos_data) + end.check_request do |_endpoint, data, _headers| + pos_data_object = JSON.parse(data)['posData'] + assert_equal(custom_pos_data[:pos_data][:cardholder_presence], pos_data_object['cardholderPresence']) + assert_equal(custom_pos_data[:pos_data][:device_attendance], pos_data_object['deviceAttendance']) + assert_equal(custom_pos_data[:pos_data][:device_input_capability], pos_data_object['deviceInputCapability']) + assert_equal(custom_pos_data[:pos_data][:device_location], pos_data_object['deviceLocation']) + assert_equal(custom_pos_data[:pos_data][:pan_capture_method], pos_data_object['panCaptureMethod']) + assert_equal(custom_pos_data[:pos_data][:partial_approval_support], pos_data_object['partialApprovalSupport']) + assert_equal(custom_pos_data[:pos_data][:pin_capture_capability], pos_data_object['pinCaptureCapability']) + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_duplicate_replay_id + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(replay_id: @replay_id)) + end.check_request do |_endpoint, data, _headers| + assert_equal @replay_id, JSON.parse(data)['replayId'] + end.respond_with(successful_purchase_response_with_replay_id) + assert_success response + + duplicate_response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(replay_id: response.params['replayId'])) + end.check_request do |_endpoint, data, _headers| + assert_equal response.params['replayId'], JSON.parse(data)['replayId'] + end.respond_with(successful_purchase_response_with_replay_id) + assert_success duplicate_response + + assert_equal response.params['id'], duplicate_response.params['id'] + end + + def test_failed_purchase_with_duplicate_replay_id + response = stub_comms do + @gateway.purchase(@amount, @invalid_credit_card, @options.merge(replay_id: @replay_id)) + end.respond_with(failed_purchase_response_with_replay_id) + assert_failure response + + duplicate_response = stub_comms do + @gateway.purchase(@amount, @invalid_credit_card, @options.merge(replay_id: response.params['replayId'])) + end.respond_with(failed_purchase_response_with_replay_id) + assert_failure duplicate_response + + assert_equal response.params['id'], duplicate_response.params['id'] + end + + def test_successful_settled_purchase_recalled_with_replay_id + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(replay_id: '10101010101010101')) + end.respond_with(successful_purchase_response_with_settled_transaction) + assert_success response + end + + def test_successful_voided_purchase_recalled_with_replay_id + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(replay_id: '333333')) + end.respond_with(successful_response_with_voided_transaction) + assert_success response + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + def test_successful_credit + response = stub_comms do + @gateway.credit(@amount, @credit_card, @options) + end.respond_with(successful_credit_response) + + assert_success response + assert_equal @approval_message, response.message + assert_equal 'Return', response.params['type'] + assert response.test? + end + + def test_failed_credit_invalid_credit_card_month + response = stub_comms do + @gateway.credit(@amount, @invalid_credit_card, @options) + end.respond_with(failed_credit_response) + + assert_failure response + assert_equal 'ValidationError', response.error_code + assert_equal 'Year, Month, and Day parameters describe an un-representable DateTime.', response.message + assert response.test? + end + + def test_successful_verify + @gateway.stubs(:create_jwt).returns(jwt_response) + + response = stub_comms(@gateway, :ssl_get) do + @gateway.verify(@credit_card) + end.respond_with(successful_bin_search_response) + + assert_success response + end + + def test_failed_verify_no_bank + @gateway.stubs(:create_jwt).returns(jwt_response) + + response = stub_comms(@gateway, :ssl_get) do + @gateway.verify(credit_card('4242424242424242')) + end.respond_with(no_bank_bin_search_response) + + assert_failure response + end + + def successful_refund_response + %( + { + "created": "2021-08-03T04:11:24.51Z", + "paymentToken": "PdSp5zrZBr0Jwx34gbEGoZHkPzWRxXBJ", + "originalId": 10000001625073, + "id": 10000001625074, + "creatorName": "tester-api", + "isDuplicate": false, + "merchantId": 12345678, + "batch": "0001", + "batchId": 10000000227764, + "tenderType": "Card", + "currency": "USD", + "amount": "-3.21", + "cardAccount": { + "cardType": "Visa", + "entryMode": "Keyed", + "last4": "1111", + "cardId": "y15QvOteHZGBm7LH3GNIlTWbA1If", + "token": "PdSp5zrZBr0Jwx34gbEGoZHkPzWRxXBJ", + "expiryMonth": "02", + "expiryYear": "29", + "hasContract": false, + "cardPresent": false, + "isDebit": false, + "isCorp": false + }, + "posData": { + "panCaptureMethod": "Manual" + }, + "authOnly": false, + "authCode": "PPSe8b", + "status": "Approved", + "risk": {}, + "requireSignature": false, + "settledAmount": "0", + "settledCurrency": "USD", + "cardPresent": false, + "authMessage": "Approved or completed successfully. ", + "availableAuthAmount": "0", + "reference": "121504000047", + "tax": "0.04", + "invoice": "V00KCLJT", + "customerCode": "PTHHV00KCLJT", + "shipToCountry": "USA", + "purchases": [ + { + "dateCreated": "0001-01-01T00:00:00", + "iId": 0, + "transactionIId": 0, + "transactionId": "0", + "name": "Miscellaneous", + "description": "Miscellaneous", + "code": "MISC", + "unitOfMeasure": "EA", + "unitPrice": "3.17", + "quantity": 1, + "taxRate": "0.0126182965299684542586750789", + "taxAmount": "0.04", + "discountRate": "0", + "discountAmount": "0", + "extendedAmount": "3.21", + "lineItemId": 0 + } + ], + "clientReference": "PTHHV00KCLJT", + "type": "Return", + "taxExempt": false, + "reviewIndicator": 0, + "source": "Tester", + "shouldGetCreditCardLevel": false + } + ) + end + + def failed_void_response + '{ + "errorCode": "Unauthorized", + "message": "Unauthorized", + "details": [ + "Original Payment Not Found Or You Do Not Have Access." + ], + "responseCode": "eENKmhrToV9UYxsXAh7iGAQ" + }' + end + + def transcript + '{ + "amount":"100", + "currency":"USD", + "email":"john@trexle.com", + "ip_address":"66.249.79.118", + "description":"Store Purchase 1437598192", + "card":{ + "number":"5555555555554444", + "expiry_month":9, + "expiry_year":2017, + "cvc":"123", + "name":"Longbob Longsen", + "address_line1":"456 My Street", + "address_city":"Ottawa", + "address_postcode":"K1C2N6", + "address_state":"ON", + "address_country":"CA" + } + }' + end + + def scrubbed_transcript + '{ + "amount":"100", + "currency":"USD", + "email":"john@trexle.com", + "ip_address":"66.249.79.118", + "description":"Store Purchase 1437598192", + "card":{ + "number":"[FILTERED]", + "expiry_month":9, + "expiry_year":2017, + "cvc":"[FILTERED]", + "name":"Longbob Longsen", + "address_line1":"456 My Street", + "address_city":"Ottawa", + "address_postcode":"K1C2N6", + "address_state":"ON", + "address_country":"CA" + } + }' + end + + def successful_purchase_response + %( + { + "created": "2021-07-25T12:54:37.327Z", + "paymentToken": "Px0NmeG5uPe4xb9wQHq5WWHasBtIYloZ", + "id": 10000001620265, + "creatorName": "Mike B", + "isDuplicate": false, + "shouldVaultCard": true, + "merchantId": 12345678, + "batch": "0009", + "batchId": 10000000227516, + "tenderType": "Card", + "currency": "USD", + "amount": "9.87", + "cardAccount": { + "cardType": "Visa", + "entryMode": "Keyed", + "last4": "1111", + "cardId": "y15QvOteHZGBm7LH3GNIlTWbA1If", + "token": "Px0NmeG5uPe4xb9wQHq5WWHasBtIYloZ", + "expiryMonth": "02", + "expiryYear": "29", + "hasContract": false, + "cardPresent": false, + "isDebit": false, + "isCorp": false + }, + "posData": { + "panCaptureMethod": "Manual" + }, + "authOnly": false, + "authCode": "PPSe7b", + "status": "Approved", + "risk": { + "cvvResponseCode": "M", + "cvvResponse": "Match", + "cvvMatch": true, + "avsResponse": "No Response from AVS", + "avsAddressMatch": false, + "avsZipMatch": false + }, + "requireSignature": false, + "settledAmount": "0", + "settledCurrency": "USD", + "cardPresent": false, + "authMessage": "Approved or completed successfully. ", + "availableAuthAmount": "0", + "reference": "120612000096", + "tax": "0.12", + "invoice": "V00554CJ", + "customerCode": "PTHGV00554CJ", + "shipToCountry": "USA", + "purchases": [ + { + "dateCreated": "0001-01-01T00:00:00", + "iId": 0, + "transactionIId": 0, + "transactionId": "0", + "name": "Miscellaneous", + "description": "Miscellaneous", + "code": "MISC", + "unitOfMeasure": "EA", + "unitPrice": "9.75", + "quantity": 1, + "taxRate": "0.0123076923076923076923076923", + "taxAmount": "0.12", + "discountRate": "0", + "discountAmount": "0", + "extendedAmount": "9.87", + "lineItemId": 0 + } + ], + "clientReference": "PTHGV00554CJ", + "type": "Sale", + "taxExempt": false, + "reviewIndicator": 1, + "source": "Tester1", + "shouldGetCreditCardLevel": false + } +) + end + + def successful_purchase_response_with_replay_id + %( + { + "created": "2022-03-07T16:04:45.103Z", + "paymentToken": "PuUfnYT8Tt8YlNmIce1wkQamcjmJymuB", + "id": 86560202, + "creatorName": "API Key", + "replayId": #{@replay_id}, + "isDuplicate": false, + "shouldVaultCard": false, + "merchantId": 1000003310, + "batch": "0032", + "batchId": 10000000271187, + "tenderType": "Card", + "currency": "USD", + "amount": "0.02", + "cardAccount": { + "cardType": "Visa", + "entryMode": "Keyed", + "last4": "1111", + "cardId": "y15QvOteHZGBm7LH3GNIlTWbA1If", + "token": "PuUfnYT8Tt8YlNmIce1wkQamcjmJymuB", + "expiryMonth": "01", + "expiryYear": "29", + "hasContract": false, + "cardPresent": false, + "isDebit": false, + "isCorp": false + }, + "posData": { + "panCaptureMethod": "Manual" + }, + "authOnly": false, + "authCode": "PPS9f4", + "status": "Approved", + "risk": { + "cvvResponseCode": "N", + "cvvResponse": "No Match", + "cvvMatch": false, + "avsResponseCode": "D", + "avsAddressMatch": true, + "avsZipMatch": true + }, + "requireSignature": false, + "settledAmount": "0", + "settledCurrency": "USD", + "cardPresent": false, + "authMessage": "Approved or completed successfully", + "availableAuthAmount": "0", + "reference": "206616004772", + "shipAmount": "0.01", + "shipToZip": "55667", + "shipToCountry": "USA", + "purchases": [ + { + "dateCreated": "0001-01-01T00:00:00", + "iId": 0, + "transactionIId": 0, + "transactionId": "0", + "name": "Anita", + "description": "Dump", + "unitPrice": "0", + "quantity": 1, + "taxRate": "0", + "taxAmount": "0", + "discountRate": "0", + "discountAmount": "0", + "extendedAmount": "0", + "lineItemId": 0 + }, + { + "dateCreated": "0001-01-01T00:00:00", + "iId": 0, + "transactionIId": 0, + "transactionId": "0", + "name": "Old Peculier", + "description": "Beer", + "unitPrice": "0", + "quantity": 1, + "taxRate": "0", + "taxAmount": "0", + "discountRate": "0", + "discountAmount": "0", + "extendedAmount": "0", + "lineItemId": 0 + } + ], + "type": "Sale", + "taxExempt": false, + "reviewIndicator": 1, + "source": "API", + "shouldGetCreditCardLevel": false + } + ) + end + + def successful_purchase_response_with_settled_transaction + %( + { + "created": "2022-04-12T17:25:16.48Z", + "paymentToken": "P9NTT6JORP0kQsEW1mQOpQG2sWUwrZJq", + "id": 86816543, + "creatorName": "API Key", + "replayId": 10101010101010101, + "isDuplicate": false, + "shouldVaultCard": false, + "merchantId": 1000003310, + "batch": "0022", + "batchId": 10000000272639, + "tenderType": "Card", + "currency": "USD", + "amount": "0.02", + "cardAccount": { + "cardType": "MasterCard", + "entryMode": "Keyed", + "last4": "0008", + "cardId": "59n0xFxuRB77138B0zc3wZwljA8f", + "token": "P9NTT6JORP0kQsEW1mQOpQG2sWUwrZJq", + "expiryMonth": "12", + "expiryYear": "22", + "hasContract": false, + "cardPresent": false, + "isDebit": false, + "isCorp": false + }, + "posData": { + "panCaptureMethod": "Manual" + }, + "authOnly": false, + "authCode": "PPS4a2", + "status": "Settled", + "risk": { + "cvvResponseCode": "M", + "cvvResponse": "Match", + "cvvMatch": false, + "avsResponseCode": "X", + "avsAddressMatch": true, + "avsZipMatch": true + }, + "requireSignature": false, + "settledAmount": "0", + "settledCurrency": "USD", + "settledDate": "2022-04-12T17:38:26.283", + "cardPresent": false, + "authMessage": "Approved or completed successfully", + "availableAuthAmount": "0", + "reference": "210217004823", + "shipAmount": "0.01", + "shipToZip": "27705", + "shipToCountry": "USA", + "purchases": [ + { + "dateCreated": "0001-01-01T00:00:00", + "iId": 0, + "transactionIId": 0, + "transactionId": "0", + "name": "Cat Poster", + "description": "A sleeping cat", + "unitPrice": "0", + "quantity": 1, + "taxRate": "0", + "taxAmount": "0", + "discountRate": "0", + "discountAmount": "0", + "extendedAmount": "0", + "lineItemId": 0 + } + ], + "type": "Sale", + "taxExempt": false, + "reviewIndicator": 0, + "source": "API", + "shouldGetCreditCardLevel": false + } +) + end + + def successful_response_with_voided_transaction + %( + { + "created": "2022-04-13T20:18:58.357Z", + "paymentToken": "PWagbvmFarc7V3vhN5llPE1pA11phsqB", + "id": 86823831, + "creatorName": "API Key", + "replayId": 333333, + "isDuplicate": false, + "merchantId": 1000003310, + "batch": "0029", + "batchId": 10000000272684, + "tenderType": "Card", + "currency": "USD", + "amount": "0.02", + "cardAccount": + { + "cardType": "Visa", + "entryMode": "Keyed", + "last4": "4242", + "cardId": "ESkW1RwQPcSW12HOH4wdBllGQMsf", + "token": "PWagbvmFarc7V3vhN5llPE1pA11phsqB", + "expiryMonth": "09", + "expiryYear": "23", + "hasContract": false, + "cardPresent": false + }, + "authOnly": false, + "authCode": "PPS42e", + "status": "Voided", + "risk": + { + "cvvResponseCode": "N", + "cvvResponse": "No Match", + "cvvMatch": false, + "avsResponseCode": "D", + "avsAddressMatch": true, + "avsZipMatch": true + }, + "requireSignature": false, + "settledAmount": "0", + "settledCurrency": "USD", + "cardPresent": false, + "authMessage": "Approved or completed successfully", + "originalAmount": "0.02", + "availableAuthAmount": "0", + "reference": "210320005533", + "shipToZip": "K1C2N6", + "shipToCountry": "CA", + "type": "Sale", + "taxExempt": false, + "reviewIndicator": 1, + "source": "API", + "shouldGetCreditCardLevel": false + } +) + end + + def failed_purchase_response + %( + { + "created": "2021-07-25T14:59:46.617Z", + "paymentToken": "P3AmSeSyXQDRM0ioGlP05Q6ykRXXVaGx", + "id": 10000001620267, + "creatorName": "tester-api", + "isDuplicate": false, + "shouldVaultCard": true, + "merchantId": 12345678, + "batch": "0009", + "batchId": 10000000227516, + "tenderType": "Card", + "currency": "USD", + "amount": "411", + "cardAccount": { + "entryMode": "Keyed", + "cardId": "B6R6ItScfvnUDwHWjP6ea1OUVX0f", + "token": "P3AmSeSyXQDRM0ioGlP05Q6ykRXXVaGx", + "expiryMonth": "01", + "expiryYear": "29", + "hasContract": false, + "cardPresent": false + }, + "posData": { + "panCaptureMethod": "Manual" + }, + "authOnly": false, + "status": "Declined", + "risk": { + "avsResponse": "No Response from AVS", + "avsAddressMatch": false, + "avsZipMatch": false + }, + "requireSignature": false, + "settledAmount": "0", + "settledCurrency": "USD", + "cardPresent": false, + "authMessage": "Invalid card number", + "availableAuthAmount": "0", + "reference": "120614000100", + "tax": "0.05", + "invoice": "V009M2JZ", + "customerCode": "PTHGV009M2JZ", + "purchases": [ + { + "dateCreated": "0001-01-01T00:00:00", + "iId": 0, + "transactionIId": 0, + "transactionId": "0", + "name": "Miscellaneous", + "description": "Miscellaneous", + "code": "MISC", + "unitOfMeasure": "EA", + "unitPrice": "4.06", + "quantity": 1, + "taxRate": "0.0123152709359605911330049261", + "taxAmount": "0.05", + "discountRate": "0", + "discountAmount": "0", + "extendedAmount": "411", + "lineItemId": 0 + } + ], + "clientReference": "PTHGV009M2JZ", + "type": "Sale", + "taxExempt": false, + "source": "Tester", + "shouldGetCreditCardLevel": false + } +) + end + + def failed_purchase_response_with_replay_id + %( + { + "created": "2022-03-07T17:41:29.1Z", + "paymentToken": "PKWMpiNtZ1mlUk4E4d95UWirHfQDNLwv", + "id": 86560811, + "creatorName": "API Key", + "replayId": #{@replay_id}, + "isDuplicate": false, + "shouldVaultCard": false, + "merchantId": 1000003310, + "batch": "0050", + "batchId": 10000000271205, + "tenderType": "Card", + "currency": "USD", + "amount": "0.02", + "cardAccount": { + "entryMode": "Keyed", + "cardId": "B6R6ItScfvnUDwHWjP6ea1OUVX0f", + "token": "PKWMpiNtZ1mlUk4E4d95UWirHfQDNLwv", + "expiryMonth": "01", + "expiryYear": "29", + "hasContract": false, + "cardPresent": false + }, + "posData": { + "panCaptureMethod": "Manual" + }, + "authOnly": false, + "status": "Declined", + "risk": { + "avsResponse": "No Response from AVS", + "avsAddressMatch": false, + "avsZipMatch": false + }, + "requireSignature": false, + "settledAmount": "0", + "settledCurrency": "USD", + "cardPresent": false, + "authMessage": "Invalid card number", + "availableAuthAmount": "0", + "reference": "206617005381", + "shipAmount": "0.01", + "shipToZip": "55667", + "shipToCountry": "USA", + "purchases": [ + { + "dateCreated": "0001-01-01T00:00:00", + "iId": 0, + "transactionIId": 0, + "transactionId": "0", + "name": "Anita", + "description": "Dump", + "unitPrice": "0", + "quantity": 1, + "taxRate": "0", + "taxAmount": "0", + "discountRate": "0", + "discountAmount": "0", + "extendedAmount": "0", + "lineItemId": 0 + }, + { + "dateCreated": "0001-01-01T00:00:00", + "iId": 0, + "transactionIId": 0, + "transactionId": "0", + "name": "Old Peculier", + "description": "Beer", + "unitPrice": "0", + "quantity": 1, + "taxRate": "0", + "taxAmount": "0", + "discountRate": "0", + "discountAmount": "0", + "extendedAmount": "0", + "lineItemId": 0 + } + ], + "type": "Sale", + "taxExempt": false, + "source": "API", + "shouldGetCreditCardLevel": false + } + ) + end + + def successful_authorize_response + %( + { + "created": "2021-07-25T17:58:07.263Z", + "paymentToken": "PkEcPvkJ9DloiT26r5u6GmXV8yIevwcp", + "id": 10000001620268, + "creatorName": "Mike B", + "isDuplicate": false, + "shouldVaultCard": true, + "merchantId": 12345678, + "tenderType": "Card", + "currency": "USD", + "amount": "3.32", + "cardAccount": { + "cardType": "Visa", + "entryMode": "Keyed", + "last4": "1111", + "cardId": "y15QvOteHZGBm7LH3GNIlTWbA1If", + "token": "PkEcPvkJ9DloiT26r5u6GmXV8yIevwcp", + "expiryMonth": "02", + "expiryYear": "29", + "hasContract": false, + "cardPresent": false, + "isDebit": false, + "isCorp": false + }, + "posData": { + "panCaptureMethod": "Manual" + }, + "authOnly": true, + "authCode": "PPS72f", + "status": "Approved", + "risk": { + "cvvResponseCode": "M", + "cvvResponse": "Match", + "cvvMatch": true, + "avsResponse": "No Response from AVS", + "avsAddressMatch": false, + "avsZipMatch": false + }, + "requireSignature": false, + "settledAmount": "0", + "settledCurrency": "USD", + "cardPresent": false, + "authMessage": "Approved or completed successfully. ", + "availableAuthAmount": "3.32", + "reference": "120617000104", + "invoice": "V00FZF87", + "customerCode": "PTHGV00FZF87", + "shipToCountry": "USA", + "purchases": [ + { + "dateCreated": "0001-01-01T00:00:00", + "iId": 0, + "transactionIId": 0, + "transactionId": "0", + "name": "Miscellaneous", + "description": "Miscellaneous", + "code": "MISC", + "unitOfMeasure": "EA", + "unitPrice": "3.32", + "quantity": 1, + "taxRate": "0", + "taxAmount": "0", + "discountRate": "0", + "discountAmount": "0", + "extendedAmount": "3.32", + "lineItemId": 0 + } + ], + "clientReference": "PTHGV00FZF87", + "type": "Authorization", + "taxExempt": false, + "reviewIndicator": 1, + "source": "Tester1", + "shouldGetCreditCardLevel": false + } + ) + end + + def failed_authorize_response + %( + { + "created": "2021-07-25T20:32:47.84Z", + "paymentToken": "PyzLzQBl8xAgjKYyrDfbA0Dbs39mopvN", + "id": 10000001620269, + "creatorName": "tester-api", + "isDuplicate": false, + "shouldVaultCard": true, + "merchantId": 12345678, + "tenderType": "Card", + "currency": "USD", + "amount": "411", + "cardAccount": { + "entryMode": "Keyed", + "cardId": "B6R6ItScfvnUDwHWjP6ea1OUVX0f", + "token": "PyzLzQBl8xAgjKYyrDfbA0Dbs39mopvN", + "expiryMonth": "01", + "expiryYear": "29", + "hasContract": false, + "cardPresent": false + }, + "posData": { + "panCaptureMethod": "Manual" + }, + "authOnly": true, + "status": "Declined", + "risk": { + "avsResponse": "No Response from AVS", + "avsAddressMatch": false, + "avsZipMatch": false + }, + "requireSignature": false, + "settledAmount": "0", + "settledCurrency": "USD", + "cardPresent": false, + "authMessage": "Invalid card number", + "availableAuthAmount": "411", + "reference": "120620000107", + "invoice": "V00LIC5Y", + "customerCode": "PTHGV00LIC5Y", + "purchases": [ + { + "dateCreated": "0001-01-01T00:00:00", + "iId": 0, + "transactionIId": 0, + "transactionId": "0", + "name": "Miscellaneous", + "description": "Miscellaneous", + "code": "MISC", + "unitOfMeasure": "EA", + "unitPrice": "411", + "quantity": 1, + "taxRate": "0", + "taxAmount": "0", + "discountRate": "0", + "discountAmount": "0", + "extendedAmount": "411", + "lineItemId": 0 + } + ], + "clientReference": "PTHGV00LIC5Y", + "type": "Authorization", + "taxExempt": false, + "source": "Tester", + "shouldGetCreditCardLevel": false + } + ) + end + + def successful_capture_response + %( + { + "created": "2021-08-03T03:10:38.543Z", + "paymentToken": "PaQLIYLRdWtcFKl5VaKTdUVxMutXJ5Ru", + "originalId": 10000001625060, + "id": 10000001625061, + "creatorName": "tester-api", + "isDuplicate": false, + "merchantId": 12345678, + "batch": "0016", + "batchId": 10000000227758, + "tenderType": "Card", + "currency": "USD", + "amount": "7.99", + "cardAccount": { + "cardType": "Visa", + "entryMode": "Keyed", + "last4": "1111", + "cardId": "y15QvOteHZGBm7LH3GNIlTWbA1If", + "token": "PaQLIYLRdWtcFKl5VaKTdUVxMutXJ5Ru", + "expiryMonth": "01", + "expiryYear": "29", + "hasContract": false, + "cardPresent": false, + "isDebit": false, + "isCorp": false + }, + "posData": { + "panCaptureMethod": "Manual" + }, + "authOnly": false, + "authCode": "PPSbf7", + "status": "Approved", + "risk": {}, + "requireSignature": false, + "settledAmount": "0", + "settledCurrency": "USD", + "cardPresent": false, + "authMessage": "Approved", + "availableAuthAmount": "0", + "reference": "121503000033", + "tax": "0.1", + "invoice": "V00ICCMR", + "customerCode": "PTHHV00ICLFZ", + "purchases": [ + { + "dateCreated": "0001-01-01T00:00:00", + "iId": 0, + "transactionIId": 0, + "transactionId": "0", + "name": "Miscellaneous", + "description": "Miscellaneous", + "code": "MISC", + "unitOfMeasure": "EA", + "unitPrice": "7.89", + "quantity": 1, + "taxRate": "0.0126742712294043092522179975", + "taxAmount": "0.1", + "discountRate": "0", + "discountAmount": "0", + "extendedAmount": "7.99", + "lineItemId": 0 + } + ], + "clientReference": "PTHHV00ICLFZ", + "type": "SaleCompletion", + "reviewIndicator": 0, + "source": "Tester", + "shouldGetCreditCardLevel": false + } + ) + end + + def failed_capture_response + %( + {"created":"2022-04-06T16:54:08.9Z","paymentToken":"PHubmbgcqEPVUI2HmOAr2sF7Vl33MnuJ","id":86777943,"creatorName":"API Key","isDuplicate":false,"merchantId":12345678,"batch":"0028","batchId":10000000272426,"tenderType":"Card","currency":"USD","amount":"0.02","cardAccount":{"token":"PHubmbgcqEPVUI2HmOAr2sF7Vl33MnuJ","hasContract":false,"cardPresent":false},"posData":{"panCaptureMethod":"Manual"},"authOnly":false,"status":"Declined","risk":{},"requireSignature":false,"settledAmount":"0","settledCurrency":"USD","cardPresent":false,"authMessage":"Original Transaction Not Found","availableAuthAmount":"0","reference":"209616004816","type":"Sale","source":"API","shouldGetCreditCardLevel":false} + ) + end + + def successful_void_response + %( + #<Net::HTTPNoContent 204 No Content readbody=true> + ) + end + + def successful_refund_purchase_response + %( + { + "created": "2021-07-27T02:14:55.477Z", + "paymentToken": "PU2QSwaBlKx5OEzBKavi1L0Dy9yIMSEx", + "originalId": 10000001620800, + "id": 10000001620801, + "creatorName": "Mike B", + "isDuplicate": false, + "merchantId": 12345678, + "batch": "0001", + "batchId": 10000000227556, + "tenderType": "Card", + "currency": "USD", + "amount": "-14.14", + "cardAccount": { + "cardType": "Visa", + "entryMode": "Keyed", + "last4": "1111", + "cardId": "y15QvOteHZGBm7LH3GNIlTWbA1If", + "token": "PU2QSwaBlKx5OEzBKavi1L0Dy9yIMSEx", + "expiryMonth": "02", + "expiryYear": "29", + "hasContract": false, + "cardPresent": false, + "isDebit": false, + "isCorp": false + }, + "posData": { + "panCaptureMethod": "Manual" + }, + "authOnly": false, + "authCode": "PPS39c", + "status": "Approved", + "risk": {}, + "requireSignature": false, + "settledAmount": "0", + "settledCurrency": "USD", + "cardPresent": false, + "authMessage": "Approved or completed successfully. ", + "availableAuthAmount": "0", + "reference": "120802000004", + "tax": "0.17", + "invoice": "Z00C02TD", + "customerCode": "PTHGZ00C02TD", + "shipToCountry": "USA", + "purchases": [ + { + "dateCreated": "0001-01-01T00:00:00", + "iId": 11042381, + "transactionIId": 0, + "transactionId": "10000001620800", + "name": "Miscellaneous", + "description": "Miscellaneous", + "code": "MISC", + "unitOfMeasure": "EA", + "unitPrice": "13.97", + "quantity": 1, + "taxRate": "0.01", + "taxAmount": "0.17", + "discountRate": "0", + "discountAmount": "0", + "extendedAmount": "14.14", + "lineItemId": 0 + } + ], + "clientReference": "PTHGZ00C02TD", + "type": "Return", + "taxExempt": false, + "reviewIndicator": 0, + "source": "Tester", + "shouldGetCreditCardLevel": false + } + ) + end + + def failed_duplicate_refund + %( + { + "created": "2021-07-27T04:35:58.397Z", + "paymentToken": "P9cjoRNccieQXBmDxEmXi2NjLKWtVF9A", + "originalId": 10000001620798, + "id": 10000001620802, + "creatorName": "tester-api", + "isDuplicate": false, + "merchantId": 12345678, + "batch": "0001", + "batchId": 10000000227556, + "tenderType": "Card", + "currency": "USD", + "amount": "-14.14", + "cardAccount": { + "cardType": "Visa", + "entryMode": "Keyed", + "last4": "1111", + "cardId": "y15QvOteHZGBm7LH3GNIlTWbA1If", + "token": "P9cjoRNccieQXBmDxEmXi2NjLKWtVF9A", + "expiryMonth": "02", + "expiryYear": "29", + "hasContract": false, + "cardPresent": false, + "isDebit": false, + "isCorp": false + }, + "posData": { + "panCaptureMethod": "Manual" + }, + "authOnly": false, + "authCode": "PPSdda", + "status": "Declined", + "risk": {}, + "requireSignature": false, + "settledAmount": "0", + "settledCurrency": "USD", + "cardPresent": false, + "authMessage": "Payment already refunded", + "availableAuthAmount": "0", + "reference": "120804000007", + "tax": "0.17", + "invoice": "Z001MFP5", + "customerCode": "PTHGZ001MFP5", + "shipToCountry": "USA", + "purchases": [ + { + "dateCreated": "0001-01-01T00:00:00", + "iId": 0, + "transactionIId": 0, + "transactionId": "0", + "name": "Miscellaneous", + "description": "Miscellaneous", + "code": "MISC", + "unitOfMeasure": "EA", + "unitPrice": "13.97", + "quantity": 1, + "taxRate": "0.0121689334287759484609878311", + "taxAmount": "0.17", + "discountRate": "0", + "discountAmount": "0", + "extendedAmount": "14.14", + "lineItemId": 0 + } + ], + "clientReference": "PTHGZ001MFP5", + "type": "Return", + "taxExempt": false, + "source": "Tester", + "shouldGetCreditCardLevel": false + } + ) + end + + def pre_scrubbed + %( + {\"achIndicator\":null,\"amount\":2.11,\"authCode\":null,\"authOnly\":false,\"bankAccount\":null,\"cardAccount\":{\"avsStreet\":\"1\",\"avsZip\":\"88888\",\"cvv\":\"123\",\"entryMode\":\"Keyed\",\"expiryDate\":\"01/29\",\"expiryMonth\":\"01\",\"expiryYear\":\"29\",\"last4\":null,\"magstripe\":null,\"number\":\"4111111111111111\"},\"cardPresent\":false,\"cardPresentType\":\"CardNotPresent\",\"isAuth\":true,\"isSettleFunds\":true,\"isTicket\":false,\"merchantId\":12345678,\"mxAdvantageEnabled\":false,\"mxAdvantageFeeLabel\":\"\",\"paymentType\":\"Sale\",\"purchases\":[{\"taxRate\":\"0.0000\",\"additionalTaxRate\":null,\"discountRate\":null}],\"shouldGetCreditCardLevel\":true,\"shouldVaultCard\":true,\"source\":\"Tester\",\"sourceZip\":\"K1C2N6\",\"taxExempt\":false,\"tenderType\":\"Card\",\"terminals\":[]} + ) + end + + def post_scrubbed + %( + {\"achIndicator\":null,\"amount\":2.11,\"authCode\":null,\"authOnly\":false,\"bankAccount\":null,\"cardAccount\":{\"avsStreet\":\"1\",\"avsZip\":\"88888\",\"cvv\":\"[FILTERED]\",\"entryMode\":\"Keyed\",\"expiryDate\":\"01/29\",\"expiryMonth\":\"01\",\"expiryYear\":\"29\",\"last4\":null,\"magstripe\":null,\"number\":\"[FILTERED]\"},\"cardPresent\":false,\"cardPresentType\":\"CardNotPresent\",\"isAuth\":true,\"isSettleFunds\":true,\"isTicket\":false,\"merchantId\":12345678,\"mxAdvantageEnabled\":false,\"mxAdvantageFeeLabel\":\"\",\"paymentType\":\"Sale\",\"purchases\":[{\"taxRate\":\"0.0000\",\"additionalTaxRate\":null,\"discountRate\":null}],\"shouldGetCreditCardLevel\":true,\"shouldVaultCard\":true,\"source\":\"Tester\",\"sourceZip\":\"K1C2N6\",\"taxExempt\":false,\"tenderType\":\"Card\",\"terminals\":[]} + ) + end + + def successful_credit_response + %( + { + "created": "2022-07-21T15:12:48.543Z", + "paymentToken": "PfUCohlRQpcR1cKarXkKFHV4pjcX2RJl", + "id": 87523059, + "creatorName": "spreedlyprapi", + "replayId": 16584146247154989, + "isDuplicate": false, + "shouldVaultCard": true, + "merchantId": 1000003310, + "batch": "0015", + "batchId": 10000000275553, + "tenderType": "Card", + "currency": "USD", + "amount": "-26.34", + "meta": "I like beer", + "cardAccount": { + "cardType": "Visa", + "entryMode": "Keyed", + "last4": "4242", + "cardId": "ESkW1RwQPcSW12HOH4wdBllGQMsf", + "token": "PfUCohlRQpcR1cKarXkKFHV4pjcX2RJl", + "expiryMonth": "09", + "expiryYear": "23", + "hasContract": false, + "cardPresent": false + }, + "posData": { + "panCaptureMethod": "Manual" + }, + "authOnly": false, + "authCode": "PPS6bf", + "status": "Approved", + "risk": { + "cvvResponseCode": "N", + "cvvResponse": "No Match", + "cvvMatch": false, + "avsResponseCode": "D", + "avsAddressMatch": true, + "avsZipMatch": true + }, + "requireSignature": false, + "settledAmount": "0", + "settledCurrency": "USD", + "cardPresent": false, + "authMessage": "Approved or completed successfully. ", + "availableAuthAmount": "0", + "reference": "220215004077", + "tax": "0", + "invoice": "12345", + "type": "Return", + "taxExempt": false, + "reviewIndicator": 1, + "source": "Spreedly", + "shouldGetCreditCardLevel": false + } + ) + end + + def failed_credit_response + %( + { + "errorCode": "ValidationError", + "message": "Validation error happened", + "details": [ + "Year, Month, and Day parameters describe an un-representable DateTime." + ], + "responseCode": "eSD7row8WL3JkUkOymB3FlQ" + } + ) + end + + def jwt_response + response = { + processorName: 'TSYS', + jwtToken: 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InZvcnRleDAwMDEifQ.eyJ1aWQiOiIxMjc2QzU5Qi03OUJFLTQ5QzEtQkE4Ri01RTM0MkFENzBGQjkiLCJncmFudHMiOlsiZ2xvYmFsIiwicHBzOm14LW1lcmNoYW50IiwibWVyY2hhbnQ6MTAwMDAwMzMxMCIsInhtaWQ6ODczOTczMTIwODY2MTM0NCJdLCJyb2xlIjoibXgtbWVyY2hhbnQtcmVwb3J0IiwiZmx5SW5BbGxvd2VkIjpmYWxzZSwidXNlck5hbWUiOiJzeXN0ZW11c2VyLm14bWVyY2hhbnQiLCJpYXQiOjE3MzQxMTk3ODIsImV4cCI6MTczNDIwNjE4MiwiYXVkIjoiaHR0cHM6Ly9zYW5kYm94Lm14bWVyY2hhbnQuY29tIiwiaXNzIjoiaHR0cHM6Ly9zYW5kYm94Lm14bWVyY2hhbnQuY29tIiwic3ViIjoiblNGQ0V5eHdkVG9vOHhJOWVNcldMUWRnIn0.Tvm7c_YXQupGXCn2Pf6MlN0VMnTVZxHFn4OQ2ojaSyMJUq4tTFdf8xPqAbQql-JEaekZfUJmwbGF_zaxgY4VKQ' + } + + ActiveMerchant::Billing::Response.new( + true, + response, + response + ) + end + + def successful_bin_search_response + %( + { + "id": "41111111", + "bin": "41111111", + "bank": { + "www": "www.jpmorganchase.com", + "name": "JPMORGAN CHASE BANK N.A.", + "phone": "416-981-9200" + }, + "type": "", + "brand": "VISA", + "level": "", + "source": "pci.bindb.com", + "country": { + "iso": "840", + "info": "Wilmington", + "name": "United States", + "abbreviation2": "US", + "abbreviation3": "USA" + }, + "created": "2024-02-15T15:58:38.306Z", + "creator": { + "id": "system", + "user": "system" + }, + "prepaid": null, + "business": null, + "modified": "2024-02-15T15:58:38.306Z", + "modifier": { + "id": "system", + "user": "system" + }, + "reloadable": null, + "isCreditCard": true + } + ) + end + + def no_bank_bin_search_response + %( + { + "id": "42424242", + "bin": "42424242", + "bank": { + "www": "", + "name": "", + "phone": "" + }, + "type": "CREDIT", + "brand": "VISA", + "level": "CLASSIC", + "source": "pci.bindb.com", + "country": { + "iso": "826", + "info": "", + "name": "United Kingdom", + "abbreviation2": "GB", + "abbreviation3": "GBR" + }, + "created": "2023-04-03T12:39:37.033Z", + "creator": { + "id": "system", + "user": "system" + }, + "prepaid": false, + "business": false, + "modified": "2024-01-16T06:29:37.125Z", + "modifier": { + "id": "system", + "user": "system" + }, + "reloadable": false, + "isCreditCard": true + } + ) + end +end diff --git a/test/unit/gateways/pro_pay_test.rb b/test/unit/gateways/pro_pay_test.rb index b6704553ff2..f96b6f0a474 100644 --- a/test/unit/gateways/pro_pay_test.rb +++ b/test/unit/gateways/pro_pay_test.rb @@ -1,5 +1,4 @@ require 'test_helper' - class ProPayTest < Test::Unit::TestCase include CommStub @@ -90,7 +89,7 @@ def test_failed_refund def test_successful_void response = stub_comms do @gateway.void('auth', @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(%r(<transType>07</transType>), data) end.respond_with(successful_void_response) @@ -100,7 +99,7 @@ def test_successful_void def test_failed_void response = stub_comms do @gateway.void('invalid-auth', @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(%r(<transType>07</transType>), data) end.respond_with(failed_void_response) @@ -153,67 +152,77 @@ def test_scrub assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed end + def test_does_not_send_dashed_zip_code + options = @options.merge(billing_address: address.update(zip: '12345-3456')) + + stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<zip>123453456</, data) + end.respond_with(successful_purchase_response) + end + private def pre_scrubbed - <<-RESPONSE -opening connection to xmltest.propay.com:443... -opened -starting SSL for xmltest.propay.com:443... -SSL established -<- "POST /API/PropayAPI.aspx 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: xmltest.propay.com\r\nContent-Length: 547\r\n\r\n" -<- "<?xml version=\"1.0\"?>\n<XMLRequest>\n <certStr>5ab9cddef2e4911b77e0c4ffb70f03</certStr>\n <class>partner</class>\n <XMLTrans>\n <amount>100</amount>\n <currencyCode>USD</currencyCode>\n <ccNum>4747474747474747</ccNum>\n <expDate>0918</expDate>\n <CVV2>999</CVV2>\n <cardholderName>Longbob Longsen</cardholderName>\n <addr>456 My Street</addr>\n <aptNum>Apt 1</aptNum>\n <city>Ottawa</city>\n <state>ON</state>\n <zip>K1C2N6</zip>\n <accountNum>32287391</accountNum>\n <transType>04</transType>\n </XMLTrans>\n</XMLRequest>\n" --> "HTTP/1.1 200 OK\r\n" --> "Cache-Control: max-age=0,no-cache,no-store,must-revalidate\r\n" --> "Pragma: no-cache\r\n" --> "Content-Type: text/xml; charset=utf-8\r\n" --> "Content-Encoding: gzip\r\n" --> "Expires: Thu, 01 Jan 1970 00:00:00 GMT\r\n" --> "Vary: Accept-Encoding\r\n" --> "Server: Microsoft-IIS/7.5\r\n" --> "Set-Cookie: ASP.NET_SessionId=hn1orxwu31yeoym5fkdhac4o; path=/; secure; HttpOnly\r\n" --> "Set-Cookie: sessionValidation=1a1d69b6-6e53-408b-b054-602593da00e7; path=/; secure; HttpOnly\r\n" --> "X-Powered-By: ASP.NET\r\n" --> "X-Frame-Options: SAMEORIGIN\r\n" --> "Date: Tue, 25 Apr 2017 19:44:03 GMT\r\n" --> "Connection: close\r\n" --> "Content-Length: 343\r\n" --> "\r\n" -reading 343 bytes... --> "" -read 343 bytes -Conn close + <<~RESPONSE + opening connection to xmltest.propay.com:443... + opened + starting SSL for xmltest.propay.com:443... + SSL established + <- "POST /API/PropayAPI.aspx 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: xmltest.propay.com\r\nContent-Length: 547\r\n\r\n" + <- "<?xml version=\"1.0\"?>\n<XMLRequest>\n <certStr>5ab9cddef2e4911b77e0c4ffb70f03</certStr>\n <class>partner</class>\n <XMLTrans>\n <amount>100</amount>\n <currencyCode>USD</currencyCode>\n <ccNum>4747474747474747</ccNum>\n <expDate>0918</expDate>\n <CVV2>999</CVV2>\n <cardholderName>Longbob Longsen</cardholderName>\n <addr>456 My Street</addr>\n <aptNum>Apt 1</aptNum>\n <city>Ottawa</city>\n <state>ON</state>\n <zip>K1C2N6</zip>\n <accountNum>32287391</accountNum>\n <transType>04</transType>\n </XMLTrans>\n</XMLRequest>\n" + -> "HTTP/1.1 200 OK\r\n" + -> "Cache-Control: max-age=0,no-cache,no-store,must-revalidate\r\n" + -> "Pragma: no-cache\r\n" + -> "Content-Type: text/xml; charset=utf-8\r\n" + -> "Content-Encoding: gzip\r\n" + -> "Expires: Thu, 01 Jan 1970 00:00:00 GMT\r\n" + -> "Vary: Accept-Encoding\r\n" + -> "Server: Microsoft-IIS/7.5\r\n" + -> "Set-Cookie: ASP.NET_SessionId=hn1orxwu31yeoym5fkdhac4o; path=/; secure; HttpOnly\r\n" + -> "Set-Cookie: sessionValidation=1a1d69b6-6e53-408b-b054-602593da00e7; path=/; secure; HttpOnly\r\n" + -> "X-Powered-By: ASP.NET\r\n" + -> "X-Frame-Options: SAMEORIGIN\r\n" + -> "Date: Tue, 25 Apr 2017 19:44:03 GMT\r\n" + -> "Connection: close\r\n" + -> "Content-Length: 343\r\n" + -> "\r\n" + reading 343 bytes... + -> "" + read 343 bytes + Conn close RESPONSE end def post_scrubbed - <<-POST_SCRUBBED -opening connection to xmltest.propay.com:443... -opened -starting SSL for xmltest.propay.com:443... -SSL established -<- "POST /API/PropayAPI.aspx 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: xmltest.propay.com\r\nContent-Length: 547\r\n\r\n" -<- "<?xml version=\"1.0\"?>\n<XMLRequest>\n <certStr>[FILTERED]</certStr>\n <class>partner</class>\n <XMLTrans>\n <amount>100</amount>\n <currencyCode>USD</currencyCode>\n <ccNum>[FILTERED]</ccNum>\n <expDate>0918</expDate>\n <CVV2>[FILTERED]</CVV2>\n <cardholderName>Longbob Longsen</cardholderName>\n <addr>456 My Street</addr>\n <aptNum>Apt 1</aptNum>\n <city>Ottawa</city>\n <state>ON</state>\n <zip>K1C2N6</zip>\n <accountNum>32287391</accountNum>\n <transType>04</transType>\n </XMLTrans>\n</XMLRequest>\n" --> "HTTP/1.1 200 OK\r\n" --> "Cache-Control: max-age=0,no-cache,no-store,must-revalidate\r\n" --> "Pragma: no-cache\r\n" --> "Content-Type: text/xml; charset=utf-8\r\n" --> "Content-Encoding: gzip\r\n" --> "Expires: Thu, 01 Jan 1970 00:00:00 GMT\r\n" --> "Vary: Accept-Encoding\r\n" --> "Server: Microsoft-IIS/7.5\r\n" --> "Set-Cookie: ASP.NET_SessionId=hn1orxwu31yeoym5fkdhac4o; path=/; secure; HttpOnly\r\n" --> "Set-Cookie: sessionValidation=1a1d69b6-6e53-408b-b054-602593da00e7; path=/; secure; HttpOnly\r\n" --> "X-Powered-By: ASP.NET\r\n" --> "X-Frame-Options: SAMEORIGIN\r\n" --> "Date: Tue, 25 Apr 2017 19:44:03 GMT\r\n" --> "Connection: close\r\n" --> "Content-Length: 343\r\n" --> "\r\n" -reading 343 bytes... --> "" -read 343 bytes -Conn close + <<~POST_SCRUBBED + opening connection to xmltest.propay.com:443... + opened + starting SSL for xmltest.propay.com:443... + SSL established + <- "POST /API/PropayAPI.aspx 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: xmltest.propay.com\r\nContent-Length: 547\r\n\r\n" + <- "<?xml version=\"1.0\"?>\n<XMLRequest>\n <certStr>[FILTERED]</certStr>\n <class>partner</class>\n <XMLTrans>\n <amount>100</amount>\n <currencyCode>USD</currencyCode>\n <ccNum>[FILTERED]</ccNum>\n <expDate>0918</expDate>\n <CVV2>[FILTERED]</CVV2>\n <cardholderName>Longbob Longsen</cardholderName>\n <addr>456 My Street</addr>\n <aptNum>Apt 1</aptNum>\n <city>Ottawa</city>\n <state>ON</state>\n <zip>K1C2N6</zip>\n <accountNum>32287391</accountNum>\n <transType>04</transType>\n </XMLTrans>\n</XMLRequest>\n" + -> "HTTP/1.1 200 OK\r\n" + -> "Cache-Control: max-age=0,no-cache,no-store,must-revalidate\r\n" + -> "Pragma: no-cache\r\n" + -> "Content-Type: text/xml; charset=utf-8\r\n" + -> "Content-Encoding: gzip\r\n" + -> "Expires: Thu, 01 Jan 1970 00:00:00 GMT\r\n" + -> "Vary: Accept-Encoding\r\n" + -> "Server: Microsoft-IIS/7.5\r\n" + -> "Set-Cookie: ASP.NET_SessionId=hn1orxwu31yeoym5fkdhac4o; path=/; secure; HttpOnly\r\n" + -> "Set-Cookie: sessionValidation=1a1d69b6-6e53-408b-b054-602593da00e7; path=/; secure; HttpOnly\r\n" + -> "X-Powered-By: ASP.NET\r\n" + -> "X-Frame-Options: SAMEORIGIN\r\n" + -> "Date: Tue, 25 Apr 2017 19:44:03 GMT\r\n" + -> "Connection: close\r\n" + -> "Content-Length: 343\r\n" + -> "\r\n" + reading 343 bytes... + -> "" + read 343 bytes + Conn close POST_SCRUBBED end diff --git a/test/unit/gateways/psigate_test.rb b/test/unit/gateways/psigate_test.rb index 1bcb49689fb..a8561b40256 100644 --- a/test/unit/gateways/psigate_test.rb +++ b/test/unit/gateways/psigate_test.rb @@ -3,13 +3,13 @@ class PsigateTest < Test::Unit::TestCase def setup @gateway = PsigateGateway.new( - :login => 'teststore', - :password => 'psigate1234' + login: 'teststore', + password: 'psigate1234' ) @amount = 100 @credit_card = credit_card('4111111111111111') - @options = { :order_id => '1', :billing_address => address } + @options = { order_id: '1', billing_address: address } end def test_successful_authorization @@ -70,7 +70,7 @@ def test_supported_countries end def test_supported_card_types - assert_equal [:visa, :master, :american_express], PsigateGateway.supported_cardtypes + assert_equal %i[visa master american_express], PsigateGateway.supported_cardtypes end def test_avs_result @@ -95,92 +95,92 @@ def test_scrub private def successful_authorization_response - <<-RESPONSE -<?xml version="1.0" encoding="UTF-8"?> -<Result> - <TransTime>Sun Jan 06 23:10:53 EST 2008</TransTime> - <OrderID>1000</OrderID> - <TransactionType>PREAUTH</TransactionType> - <Approved>APPROVED</Approved> - <ReturnCode>Y:123456:0abcdef:M:X:NNN</ReturnCode> - <ErrMsg/> - <TaxTotal>0.00</TaxTotal> - <ShipTotal>0.00</ShipTotal> - <SubTotal>24.00</SubTotal> - <FullTotal>24.00</FullTotal> - <PaymentType>CC</PaymentType> - <CardNumber>......4242</CardNumber> - <TransRefNumber>1bdde305d7658367</TransRefNumber> - <CardIDResult>M</CardIDResult> - <AVSResult>X</AVSResult> - <CardAuthNumber>123456</CardAuthNumber> - <CardRefNumber>0abcdef</CardRefNumber> - <CardType>VISA</CardType> - <IPResult>NNN</IPResult> - <IPCountry>UN</IPCountry> - <IPRegion>UNKNOWN</IPRegion> - <IPCity>UNKNOWN</IPCity> -</Result> + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <Result> + <TransTime>Sun Jan 06 23:10:53 EST 2008</TransTime> + <OrderID>1000</OrderID> + <TransactionType>PREAUTH</TransactionType> + <Approved>APPROVED</Approved> + <ReturnCode>Y:123456:0abcdef:M:X:NNN</ReturnCode> + <ErrMsg/> + <TaxTotal>0.00</TaxTotal> + <ShipTotal>0.00</ShipTotal> + <SubTotal>24.00</SubTotal> + <FullTotal>24.00</FullTotal> + <PaymentType>CC</PaymentType> + <CardNumber>......4242</CardNumber> + <TransRefNumber>1bdde305d7658367</TransRefNumber> + <CardIDResult>M</CardIDResult> + <AVSResult>X</AVSResult> + <CardAuthNumber>123456</CardAuthNumber> + <CardRefNumber>0abcdef</CardRefNumber> + <CardType>VISA</CardType> + <IPResult>NNN</IPResult> + <IPCountry>UN</IPCountry> + <IPRegion>UNKNOWN</IPRegion> + <IPCity>UNKNOWN</IPCity> + </Result> RESPONSE end def successful_purchase_response - <<-RESPONSE -<?xml version="1.0" encoding="UTF-8"?> -<Result> - <TransTime>Sun Jan 06 23:15:30 EST 2008</TransTime> - <OrderID>1000</OrderID> - <TransactionType>SALE</TransactionType> - <Approved>APPROVED</Approved> - <ReturnCode>Y:123456:0abcdef:M:X:NNN</ReturnCode> - <ErrMsg/> - <TaxTotal>0.00</TaxTotal> - <ShipTotal>0.00</ShipTotal> - <SubTotal>24.00</SubTotal> - <FullTotal>24.00</FullTotal> - <PaymentType>CC</PaymentType> - <CardNumber>......4242</CardNumber> - <TransRefNumber>1bdde305da3ee234</TransRefNumber> - <CardIDResult>M</CardIDResult> - <AVSResult>X</AVSResult> - <CardAuthNumber>123456</CardAuthNumber> - <CardRefNumber>0abcdef</CardRefNumber> - <CardType>VISA</CardType> - <IPResult>NNN</IPResult> - <IPCountry>UN</IPCountry> - <IPRegion>UNKNOWN</IPRegion> - <IPCity>UNKNOWN</IPCity> -</Result> + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <Result> + <TransTime>Sun Jan 06 23:15:30 EST 2008</TransTime> + <OrderID>1000</OrderID> + <TransactionType>SALE</TransactionType> + <Approved>APPROVED</Approved> + <ReturnCode>Y:123456:0abcdef:M:X:NNN</ReturnCode> + <ErrMsg/> + <TaxTotal>0.00</TaxTotal> + <ShipTotal>0.00</ShipTotal> + <SubTotal>24.00</SubTotal> + <FullTotal>24.00</FullTotal> + <PaymentType>CC</PaymentType> + <CardNumber>......4242</CardNumber> + <TransRefNumber>1bdde305da3ee234</TransRefNumber> + <CardIDResult>M</CardIDResult> + <AVSResult>X</AVSResult> + <CardAuthNumber>123456</CardAuthNumber> + <CardRefNumber>0abcdef</CardRefNumber> + <CardType>VISA</CardType> + <IPResult>NNN</IPResult> + <IPCountry>UN</IPCountry> + <IPRegion>UNKNOWN</IPRegion> + <IPCity>UNKNOWN</IPCity> + </Result> RESPONSE end def failed_purchase_response - <<-RESPONSE -<?xml version="1.0" encoding="UTF-8"?> -<Result> - <TransTime>Sun Jan 06 23:24:29 EST 2008</TransTime> - <OrderID>b3dca49e3ec77e42ab80a0f0f590fff0</OrderID> - <TransactionType>SALE</TransactionType> - <Approved>DECLINED</Approved> - <ReturnCode>N:TESTDECLINE</ReturnCode> - <ErrMsg/> - <TaxTotal>0.00</TaxTotal> - <ShipTotal>0.00</ShipTotal> - <SubTotal>24.00</SubTotal> - <FullTotal>24.00</FullTotal> - <PaymentType>CC</PaymentType> - <CardNumber>......4242</CardNumber> - <TransRefNumber>1bdde305df991f89</TransRefNumber> - <CardIDResult>M</CardIDResult> - <AVSResult>X</AVSResult> - <CardAuthNumber>TEST</CardAuthNumber> - <CardRefNumber>TESTTRANS</CardRefNumber> - <CardType>VISA</CardType> - <IPResult>NNN</IPResult> - <IPCountry>UN</IPCountry> - <IPRegion>UNKNOWN</IPRegion> - <IPCity>UNKNOWN</IPCity> -</Result> + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <Result> + <TransTime>Sun Jan 06 23:24:29 EST 2008</TransTime> + <OrderID>b3dca49e3ec77e42ab80a0f0f590fff0</OrderID> + <TransactionType>SALE</TransactionType> + <Approved>DECLINED</Approved> + <ReturnCode>N:TESTDECLINE</ReturnCode> + <ErrMsg/> + <TaxTotal>0.00</TaxTotal> + <ShipTotal>0.00</ShipTotal> + <SubTotal>24.00</SubTotal> + <FullTotal>24.00</FullTotal> + <PaymentType>CC</PaymentType> + <CardNumber>......4242</CardNumber> + <TransRefNumber>1bdde305df991f89</TransRefNumber> + <CardIDResult>M</CardIDResult> + <AVSResult>X</AVSResult> + <CardAuthNumber>TEST</CardAuthNumber> + <CardRefNumber>TESTTRANS</CardRefNumber> + <CardType>VISA</CardType> + <IPResult>NNN</IPResult> + <IPCountry>UN</IPCountry> + <IPRegion>UNKNOWN</IPRegion> + <IPCity>UNKNOWN</IPCity> + </Result> RESPONSE end @@ -193,14 +193,14 @@ def xml_capture_fixture end def pre_scrubbed - <<-PRE_SCRUBBED + <<~PRE_SCRUBBED <?xml version='1.0'?><Order><StoreID>teststore</StoreID><Passphrase>psigate1234</Passphrase><OrderID>1b7b4b36bf61e972a9e6a6be8fff15d8</OrderID><Email>jack@example.com</Email><PaymentType>CC</PaymentType><CardAction>0</CardAction><SubTotal>24.00</SubTotal><CardNumber>4242424242424242</CardNumber><CardExpMonth>09</CardExpMonth><CardExpYear>14</CardExpYear><CardIDCode>1</CardIDCode><CardIDNumber>123</CardIDNumber><Bname>Jim Smith</Bname><Baddress1>1234 My Street</Baddress1><Baddress2>Apt 1</Baddress2><Bcity>Ottawa</Bcity><Bprovince>ON</Bprovince><Bpostalcode>K1C2N6</Bpostalcode><Bcountry>CA</Bcountry><Bcompany>Widgets Inc</Bcompany></Order> <CardNumber>......4242</CardNumber> PRE_SCRUBBED end def post_scrubbed - <<-POST_SCRUBBED + <<~POST_SCRUBBED <?xml version='1.0'?><Order><StoreID>teststore</StoreID><Passphrase>[FILTERED]</Passphrase><OrderID>1b7b4b36bf61e972a9e6a6be8fff15d8</OrderID><Email>jack@example.com</Email><PaymentType>CC</PaymentType><CardAction>0</CardAction><SubTotal>24.00</SubTotal><CardNumber>[FILTERED]</CardNumber><CardExpMonth>09</CardExpMonth><CardExpYear>14</CardExpYear><CardIDCode>1</CardIDCode><CardIDNumber>[FILTERED]</CardIDNumber><Bname>Jim Smith</Bname><Baddress1>1234 My Street</Baddress1><Baddress2>Apt 1</Baddress2><Bcity>Ottawa</Bcity><Bprovince>ON</Bprovince><Bpostalcode>K1C2N6</Bpostalcode><Bcountry>CA</Bcountry><Bcompany>Widgets Inc</Bcompany></Order> <CardNumber>[FILTERED]</CardNumber> POST_SCRUBBED diff --git a/test/unit/gateways/psl_card_test.rb b/test/unit/gateways/psl_card_test.rb index 64d62400301..64de15284b2 100644 --- a/test/unit/gateways/psl_card_test.rb +++ b/test/unit/gateways/psl_card_test.rb @@ -1,21 +1,20 @@ require 'test_helper' class PslCardTest < Test::Unit::TestCase - def setup @gateway = PslCardGateway.new( - :login => 'LOGIN', - :password => 'PASSWORD' - ) + login: 'LOGIN', + password: 'PASSWORD' + ) - @credit_card = credit_card + @credit_card = credit_card @options = { - :billing_address => address, - :description => 'Store purchase' + billing_address: address, + description: 'Store purchase' } @amount = 100 end - + def test_successful_authorization @gateway.expects(:ssl_post).returns(successful_purchase_response) assert response = @gateway.authorize(@amount, @credit_card, @options) @@ -34,15 +33,15 @@ def test_unsuccessful_request def test_supported_countries assert_equal ['GB'], PslCardGateway.supported_countries end - + def test_supported_card_types - assert_equal [ :visa, :master, :american_express, :diners_club, :jcb, :switch, :solo, :maestro ], PslCardGateway.supported_cardtypes + assert_equal %i[visa master american_express diners_club jcb maestro], PslCardGateway.supported_cardtypes end - + def test_avs_result @gateway.expects(:ssl_post).returns(successful_purchase_response) - response = @gateway.purchase(@amount, @credit_card, @options) + response = @gateway.purchase(@amount, @credit_card, @options) assert_equal 'Y', response.avs_result['code'] end @@ -52,13 +51,14 @@ def test_cvv_result response = @gateway.purchase(@amount, @credit_card, @options) assert_equal 'M', response.cvv_result['code'] end - + private + def successful_purchase_response 'ResponseCode=00&Message=AUTHCODE:01256&CrossReference=08012522454901256086&First4=4543&Last4=9982&ExpMonth=12&ExpYear=2010&AVSCV2Check=ALL MATCH&Amount=1000&QAAddress=76 Roseby Avenue Manchester&QAPostcode=M63X 7TH&MerchantName=Merchant Name&QAName=John Smith' end - + def unsuccessful_purchase_response 'ResponseCode=05&Message=CARD DECLINED&QAAddress=The Parkway Larches Approach Hull North Humberside&QAPostcode=HU7 9OP&MerchantName=Merchant Name&QAName=' end -end \ No newline at end of file +end diff --git a/test/unit/gateways/qbms_test.rb b/test/unit/gateways/qbms_test.rb index 894a3e0571d..f526c67865c 100644 --- a/test/unit/gateways/qbms_test.rb +++ b/test/unit/gateways/qbms_test.rb @@ -5,9 +5,10 @@ def setup Base.mode = :test @gateway = QbmsGateway.new( - :login => 'test', - :ticket => 'abc123', - :pem => 'PEM') + login: 'test', + ticket: 'abc123', + pem: 'PEM' + ) @amount = 100 @card = credit_card('4111111111111111') @@ -45,7 +46,7 @@ def test_truncated_address_is_sent with(anything, regexp_matches(/12345 Ridiculously Lengthy Roa\<.*445566778\</), anything). returns(charge_response) - options = { :billing_address => address.update(:address1 => '12345 Ridiculously Lengthy Road Name', :zip => '4455667788') } + options = { billing_address: address.update(address1: '12345 Ridiculously Lengthy Road Name', zip: '4455667788') } assert response = @gateway.purchase(@amount, @card, options) assert_success response end @@ -53,7 +54,7 @@ def test_truncated_address_is_sent def test_partial_address_is_ok @gateway.expects(:ssl_post).returns(charge_response) - options = { :billing_address => address.update(:address1 => nil, :zip => nil) } + options = { billing_address: address.update(address1: nil, zip: nil) } assert response = @gateway.purchase(@amount, @card, options) assert_success response end @@ -90,17 +91,17 @@ def test_avs_result assert_equal 'Y', response.avs_result['street_match'] assert_equal 'Y', response.avs_result['postal_match'] - @gateway.expects(:ssl_post).returns(authorization_response(:avs_street => 'Fail')) + @gateway.expects(:ssl_post).returns(authorization_response(avs_street: 'Fail')) assert response = @gateway.authorize(@amount, @card) assert_equal 'N', response.avs_result['street_match'] assert_equal 'Y', response.avs_result['postal_match'] - @gateway.expects(:ssl_post).returns(authorization_response(:avs_zip => 'Fail')) + @gateway.expects(:ssl_post).returns(authorization_response(avs_zip: 'Fail')) assert response = @gateway.authorize(@amount, @card) assert_equal 'Y', response.avs_result['street_match'] assert_equal 'N', response.avs_result['postal_match'] - @gateway.expects(:ssl_post).returns(authorization_response(:avs_street => 'Fail', :avs_zip => 'Fail')) + @gateway.expects(:ssl_post).returns(authorization_response(avs_street: 'Fail', avs_zip: 'Fail')) assert response = @gateway.authorize(@amount, @card) assert_equal 'N', response.avs_result['street_match'] assert_equal 'N', response.avs_result['postal_match'] @@ -111,11 +112,11 @@ def test_cvv_result assert response = @gateway.authorize(@amount, @card) assert_equal 'M', response.cvv_result['code'] - @gateway.expects(:ssl_post).returns(authorization_response(:card_security_code_match => 'Fail')) + @gateway.expects(:ssl_post).returns(authorization_response(card_security_code_match: 'Fail')) assert response = @gateway.authorize(@amount, @card) assert_equal 'N', response.cvv_result['code'] - @gateway.expects(:ssl_post).returns(authorization_response(:card_security_code_match => 'NotAvailable')) + @gateway.expects(:ssl_post).returns(authorization_response(card_security_code_match: 'NotAvailable')) assert response = @gateway.authorize(@amount, @card) assert_equal 'P', response.cvv_result['code'] end @@ -129,7 +130,7 @@ def test_successful_query end def test_failed_signon - @gateway.expects(:ssl_post).returns(query_response(:signon_status_code => 2000)) + @gateway.expects(:ssl_post).returns(query_response(signon_status_code: 2000)) assert response = @gateway.query() assert_instance_of Response, response @@ -137,7 +138,7 @@ def test_failed_signon end def test_failed_authorization - @gateway.expects(:ssl_post).returns(authorization_response(:status_code => 10301)) + @gateway.expects(:ssl_post).returns(authorization_response(status_code: 10301)) assert response = @gateway.authorize(@amount, @card) assert_instance_of Response, response @@ -147,7 +148,7 @@ def test_failed_authorization def test_use_test_url_when_overwriting_with_test_option ActiveMerchant::Billing::Base.mode = :production - @gateway = QbmsGateway.new(:login => 'test', :ticket => 'abc123', :test => true) + @gateway = QbmsGateway.new(login: 'test', ticket: 'abc123', test: true) @gateway.stubs(:parse).returns({}) @gateway.expects(:ssl_post).with(QbmsGateway.test_url, anything, anything).returns(authorization_response) @gateway.authorize(@amount, @card) @@ -173,9 +174,9 @@ def query_response(opts = {}) def authorization_response(opts = {}) opts = { - :avs_street => 'Pass', - :avs_zip => 'Pass', - :card_security_code_match => 'Pass', + avs_street: 'Pass', + avs_zip: 'Pass', + card_security_code_match: 'Pass' }.merge(opts) wrap 'CustomerCreditCardAuth', opts, <<-"XML" @@ -200,9 +201,9 @@ def capture_response(opts = {}) def charge_response(opts = {}) opts = { - :avs_street => 'Pass', - :avs_zip => 'Pass', - :card_security_code_match => 'Pass', + avs_street: 'Pass', + avs_zip: 'Pass', + card_security_code_match: 'Pass' }.merge(opts) wrap 'CustomerCreditCardCharge', opts, <<-"XML" @@ -236,9 +237,9 @@ def credit_response(opts = {}) def wrap(type, opts, xml) opts = { - :signon_status_code => 0, - :request_id => 'x', - :status_code => 0, + signon_status_code: 0, + request_id: 'x', + status_code: 0 }.merge(opts) <<-"XML" @@ -257,7 +258,7 @@ def wrap(type, opts, xml) </#{type}Rs> </QBMSXMLMsgsRs> </QBMSXML> - XML + XML end def pre_scrubbed diff --git a/test/unit/gateways/quantum_test.rb b/test/unit/gateways/quantum_test.rb index a1d771cd8b0..3af7c0ae3f7 100644 --- a/test/unit/gateways/quantum_test.rb +++ b/test/unit/gateways/quantum_test.rb @@ -3,25 +3,25 @@ class QuantumTest < Test::Unit::TestCase def setup @gateway = QuantumGateway.new( - :login => '', - :password => '' - ) + login: '', + password: '' + ) @credit_card = credit_card @amount = 100 - - @options = { - :billing_address => address, - :description => 'Store Purchase' + + @options = { + billing_address: address, + description: 'Store Purchase' } end - + def test_successful_purchase @gateway.expects(:ssl_post).returns(successful_purchase_response) - + assert response = @gateway.purchase(@amount, @credit_card, @options) assert_success response - + # Replace with authorization number from the successful response assert_equal '2983691;2224', response.authorization assert response.test? @@ -29,14 +29,14 @@ def test_successful_purchase def test_unsuccessful_request @gateway.expects(:ssl_post).returns(failed_purchase_response) - + assert response = @gateway.purchase(@amount, @credit_card, @options) assert_failure response assert response.test? end - + private - + # Place raw successful response from gateway here def successful_purchase_response %(<QGWRequest> @@ -74,7 +74,7 @@ def successful_purchase_response </Result> </QGWRequest>) end - + # Place raw failed response from gateway here def failed_purchase_response %(<QGWRequest> diff --git a/test/unit/gateways/quickbooks_test.rb b/test/unit/gateways/quickbooks_test.rb index bf6bc9d2017..0f753f3d2ce 100644 --- a/test/unit/gateways/quickbooks_test.rb +++ b/test/unit/gateways/quickbooks_test.rb @@ -4,12 +4,19 @@ class QuickBooksTest < Test::Unit::TestCase include CommStub def setup - @gateway = QuickbooksGateway.new( + @oauth_1_gateway = QuickbooksGateway.new( consumer_key: 'consumer_key', consumer_secret: 'consumer_secret', access_token: 'access_token', token_secret: 'token_secret', - realm: 'realm_ID', + realm: 'realm_ID' + ) + + @oauth_2_gateway = QuickbooksGateway.new( + client_id: 'client_id', + client_secret: 'client_secret', + access_token: 'access_token', + refresh_token: 'refresh_token' ) @credit_card = credit_card @@ -21,105 +28,232 @@ def setup description: 'Store Purchase' } - @authorization = 'ECZ7U0SO423E' + @authorization = 'ECZ7U0SO423E|d40f8a8007ba1af90a656d7f6371f641' + @authorization_no_request_id = 'ECZ7U0SO423E' 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 'EF1IQ9GGXS2D', response.authorization - assert response.test? + [@oauth_1_gateway, @oauth_2_gateway].each do |gateway| + gateway.expects(:ssl_post).returns(successful_purchase_response) + response = gateway.purchase(@amount, @credit_card, @options) + assert_success response + + assert_match(/EF1IQ9GGXS2D|/, response.authorization) + assert response.test? + end end def test_failed_purchase - @gateway.expects(:ssl_post).returns(failed_purchase_response) + [@oauth_1_gateway, @oauth_2_gateway].each do |gateway| + gateway.expects(:ssl_post).returns(failed_purchase_response) - response = @gateway.purchase(@amount, @credit_card, @options) - assert_failure response - assert_equal Gateway::STANDARD_ERROR_CODE[:processing_error], response.error_code + response = gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_equal Gateway::STANDARD_ERROR_CODE[:processing_error], response.error_code + end 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 @authorization, response.authorization - assert response.test? + [@oauth_1_gateway, @oauth_2_gateway].each do |gateway| + gateway.expects(:ssl_post).returns(successful_authorize_response) + response = gateway.authorize(@amount, @credit_card, @options) + assert_success response + + assert_match(/ECZ7U0SO423E|/, response.authorization) + assert response.test? + end end def test_failed_authorize - @gateway.expects(:ssl_post).returns(failed_authorize_response) + [@oauth_1_gateway, @oauth_2_gateway].each do |gateway| + gateway.expects(:ssl_post).returns(failed_authorize_response) - response = @gateway.authorize(@amount, @credit_card, @options) - assert_failure response - assert_equal Gateway::STANDARD_ERROR_CODE[:card_declined], response.error_code + response = gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert_equal Gateway::STANDARD_ERROR_CODE[:card_declined], response.error_code + end end def test_successful_capture - @gateway.expects(:ssl_post).returns(successful_capture_response) + [@oauth_1_gateway, @oauth_2_gateway].each do |gateway| + gateway.expects(:ssl_post).returns(successful_capture_response) - response = @gateway.capture(@amount, @authorization) - assert_success response + response = gateway.capture(@amount, @authorization) + assert_success response + end + end + + def test_successful_capture_when_authorization_does_not_include_request_id + [@oauth_1_gateway, @oauth_2_gateway].each do |gateway| + gateway.expects(:ssl_post).returns(successful_capture_response) + + response = gateway.capture(@amount, @authorization_no_request_id) + assert_success response + end end def test_failed_capture - @gateway.expects(:ssl_post).returns(failed_capture_response) + [@oauth_1_gateway, @oauth_2_gateway].each do |gateway| + gateway.expects(:ssl_post).returns(failed_capture_response) - response = @gateway.capture(@amount, @authorization) - assert_failure response + response = gateway.capture(@amount, @authorization) + assert_failure response + end end def test_successful_refund - @gateway.expects(:ssl_post).returns(successful_refund_response) + [@oauth_1_gateway, @oauth_2_gateway].each do |gateway| + gateway.expects(:ssl_post).returns(successful_refund_response) - response = @gateway.refund(@amount, @authorization) - assert_success response + response = gateway.refund(@amount, @authorization) + assert_success response + end + end + + def test_successful_refund_when_authorization_does_not_include_request_id + [@oauth_1_gateway, @oauth_2_gateway].each do |gateway| + gateway.expects(:ssl_post).returns(successful_refund_response) + + response = gateway.refund(@amount, @authorization_no_request_id) + assert_success response + end end def test_failed_refund - @gateway.expects(:ssl_post).returns(failed_refund_response) + [@oauth_1_gateway, @oauth_2_gateway].each do |gateway| + gateway.expects(:ssl_post).returns(failed_refund_response) - response = @gateway.refund(@amount, @authorization) - assert_failure response + response = gateway.refund(@amount, @authorization) + assert_failure response + end end def test_successful_verify - response = stub_comms do - @gateway.verify(@credit_card) - end.respond_with(successful_authorize_response) + [@oauth_1_gateway, @oauth_2_gateway].each do |gateway| + response = stub_comms(gateway) do + gateway.verify(@credit_card) + end.respond_with(successful_authorize_response) - assert_success response + assert_success response + end end def test_failed_verify - response = stub_comms do - @gateway.verify(@credit_card, @options) - end.respond_with(failed_authorize_response) + [@oauth_1_gateway, @oauth_2_gateway].each do |gateway| + response = stub_comms(gateway) do + gateway.verify(@credit_card, @options) + end.respond_with(failed_authorize_response) + + assert_failure response + assert_not_nil response.message + end + end - assert_failure response - assert_not_nil response.message + def test_successful_void + [@oauth_1_gateway, @oauth_2_gateway].each do |gateway| + response = stub_comms(gateway) do + gateway.void(@authorization) + end.respond_with(successful_void_response) + + assert_success response + end + end + + def test_failed_void + [@oauth_1_gateway, @oauth_2_gateway].each do |gateway| + response = stub_comms(gateway) do + gateway.void(@authorization) + end.respond_with(failed_void_response) + + assert_failure response + end end - def test_scrub - assert @gateway.supports_scrubbing? - assert_equal @gateway.send(:scrub, pre_scrubbed), post_scrubbed + def test_scrub_oauth_1 + assert @oauth_1_gateway.supports_scrubbing? + assert_equal @oauth_1_gateway.send(:scrub, pre_scrubbed), post_scrubbed + end + + def test_scrub_oauth_2 + assert @oauth_2_gateway.supports_scrubbing? + assert_equal @oauth_2_gateway.send(:scrub, pre_scrubbed_oauth_2), post_scrubbed_oauth_2 end def test_scrub_with_small_json - assert_equal @gateway.scrub(pre_scrubbed_small_json), post_scrubbed_small_json + assert_equal @oauth_1_gateway.scrub(pre_scrubbed_small_json), post_scrubbed_small_json end def test_default_context - stub_comms do - @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |_endpoint, data, _headers| - json = JSON.parse(data) - refute json.fetch('context').fetch('mobile') - assert json.fetch('context').fetch('isEcommerce') - end.respond_with(successful_purchase_response) + [@oauth_1_gateway, @oauth_2_gateway].each do |gateway| + stub_comms(gateway) do + gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + json = JSON.parse(data) + refute json.fetch('context').fetch('mobile') + assert json.fetch('context').fetch('isEcommerce') + end.respond_with(successful_purchase_response) + end + end + + def test_refresh_does_not_occur_for_oauth_1 + @oauth_1_gateway.expects(:ssl_post).with( + anything, + Not(regexp_matches(%r{grant_type=refresh_token})), + anything + ).returns(successful_purchase_response) + + response = @oauth_1_gateway.purchase(@amount, @credit_card, @options.merge(allow_refresh: true)) + + assert_success response + + assert_match(/EF1IQ9GGXS2D|/, response.authorization) + assert response.test? + end + + def test_refresh_does_not_occur_when_token_valid_for_oauth_2 + @oauth_2_gateway.expects(:ssl_post).with( + anything, + Not(regexp_matches(%r{grant_type=refresh_token})), + has_entries('Authorization' => 'Bearer access_token') + ).returns(successful_purchase_response) + + response = @oauth_2_gateway.purchase(@amount, @credit_card, @options.merge(allow_refresh: true)) + assert_success response + end + + def test_refresh_does_occur_when_token_invalid_for_oauth_2 + @oauth_2_gateway.expects(:ssl_post).with( + anything, + anything, + has_entries('Authorization' => 'Bearer access_token') + ).returns(authentication_failed_oauth_2_response) + + @oauth_2_gateway.expects(:ssl_post).with( + anything, + all_of(regexp_matches(%r{grant_type=refresh_token})), + anything + ).returns(successful_refresh_token_response) + + @oauth_2_gateway.expects(:ssl_post).with( + anything, + anything, + has_entries('Authorization' => 'Bearer new_access_token') + ).returns(successful_purchase_response) + + response = @oauth_2_gateway.purchase(@amount, @credit_card, @options.merge(allow_refresh: true)) + assert_success response + + assert_match(/EF1IQ9GGXS2D|/, response.authorization) + assert response.test? + end + + def test_authorization_failed_code_results_in_failure + @oauth_2_gateway.expects(:ssl_post).returns(authorization_failed_oauth_2_response) + + response = @oauth_2_gateway.authorize(@amount, @credit_card, @options) + + assert_failure response + assert_equal 'AuthorizationFailed', response.error_code end private @@ -333,6 +467,42 @@ def failed_void_response RESPONSE end + def authentication_failed_oauth_2_response + <<-RESPONSE + { + "code": "AuthenticationFailed", + "type": "INPUT", + "message": null, + "detail": null, + "moreInfo": null + } + RESPONSE + end + + def authorization_failed_oauth_2_response + <<-RESPONSE + { + "code": "AuthorizationFailed", + "type": "INPUT", + "message": null, + "detail": null, + "moreInfo": null + } + RESPONSE + end + + def successful_refresh_token_response + <<-RESPONSE + { + "x_refresh_token_expires_in": 8719040, + "refresh_token": "refresh_token", + "access_token": "new_access_token", + "token_type": "bearer", + "expires_in": 3600 + } + RESPONSE + end + def pre_scrubbed <<-PRE_SCRUBBED opening connection to sandbox.api.intuit.com:443... @@ -340,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" @@ -371,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" @@ -394,4 +564,150 @@ def post_scrubbed Conn close POST_SCRUBBED end + + def pre_scrubbed_oauth_2 + %q( + opening connection to sandbox.api.intuit.com:443... + opened + 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: 3b098cc41f53562ec0f36a0fc7071ff8\r\nAccept: application/json\r\nAuthorization: Bearer eyabcd9ewjie0w9fj9jkewjaiojfiew0..rEVIND9few90zsg.CyFO4k9gR-t5yJsc0lxGrPPLGeO-JRa_5MZ_vG_H5AMlObrPpfhBRK51jUukhh0QOUjgkGm2jJb8c_haieKnkb3nY_W7giZyIG6d5g5XPqRZLhDnMCVVFHZyLIbBT_TDvZWROeOGY10xrDnUY5O05LYnOZc8gq7k_VTHHDrrmyeon3EmerAGjDUhnpp1DJRvR7SLUWgZQOuR997OuaP31_ZesKACzdVSw5QBJAhBeRqGl8LaNjjveQMo1c20CjWr_-c0EWbp0frMAA_UYaxtuzgRRs_opnMr4_PD7axQQevAzftSR1cQfUDAu_uybV5lyiUTfX80B3NBlLihWLiqCD9yWiYmup4TpNbapTL4x9CQz_WobicwWbhIJ7P1IrnxeJh2pW3ijjrBhbgLCCZ-6tcNUsD697ywn3YknT-iTSH-BIpGE_43bEOHyUtwZcIZIeb-6KtZIjQ_fjHfkRz66IrpP0V-XZ7_N5hJ7UIuQ34gOiuxdFJtbiMSUW1GnanJ9aRH8Fbzk_UzrWyuSs.XnsOxzQ\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nHost: sandbox.api.intuit.com\r\nContent-Length: 310\r\n\r\n" + <- "{\"amount\":\"1.00\",\"currency\":\"USD\",\"card\":{\"number\":\"4000100011112224\",\"expMonth\":\"09\",\"expYear\":2020,\"cvc\":\"123\",\"name\":\"Longbob Longsen\",\"address\":{\"streetAddress\":\"456 My Street\",\"city\":\"Ottawa\",\"region\":\"CA\",\"country\":\"US\",\"postalCode\":90210}},\"context\":{\"mobile\":false,\"isEcommerce\":true},\"capture\":\"true\"}" + -> "HTTP/1.1 401 Unauthorized\r\n" + -> "Date: Thu, 17 Oct 2019 15:40:43 GMT\r\n" + -> "Content-Type: application/json\r\n" + -> "Content-Length: 91\r\n" + -> "Connection: close\r\n" + -> "Server: nginx\r\n" + -> "intuit_tid: adca0516-0af2-48cd-a704-095529fe615c\r\n" + -> "WWW-Authenticate: Bearer realm=\"Intuit\", error=\"invalid_token\"\r\n" + -> "\r\n" + reading 91 bytes... + -> "{\"code\":\"AuthenticationFailed\",\"type\":\"INPUT\",\"message\":null,\"detail\":null,\"moreInfo\":null}" + read 91 bytes + Conn close + opening connection to oauth.platform.intuit.com:443... + opened + starting SSL for oauth.platform.intuit.com:443... + SSL established + <- "POST /oauth2/v1/tokens/bearer HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept: application/json\r\nAuthorization: Basic QUI3QWFkWGZYRWZyRE1WN0k2a3RFYXoyT2hCeHdhVkdtZUU5N3pmeGdjSllPUU40Qmo6ZEVJcms2bHozclVvQ05wRXFSbFV6bFd6STBYRUtyeDBYcDdoYVd3RQ==\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nHost: oauth.platform.intuit.com\r\nContent-Length: 89\r\n\r\n" + <- "grant_type=refresh_token&refresh_token=DE123456780s7AvBrjWjfiowji9IIKDU4zE237CmbGO" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Thu, 17 Oct 2019 15:40:43 GMT\r\n" + -> "Content-Type: application/json;charset=utf-8\r\n" + -> "Content-Length: 1007\r\n" + -> "Connection: close\r\n" + -> "Server: nginx\r\n" + -> "Strict-Transport-Security: max-age=15552000\r\n" + -> "intuit_tid: c9aff174-410e-4683-8a35-2591c659550f\r\n" + -> "Cache-Control: no-cache, no-store\r\n" + -> "Pragma: no-cache\r\n" + -> "X-Frame-Options: SAMEORIGIN\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "X-XSS-Protection: 1; mode=block\r\n" + -> "\r\n" + reading 1007 bytes... + -> "{\"x_refresh_token_expires_in\":8719040,\"refresh_token\":\"DE987654s7AvBrjWOCJYWsifjewaifjeiowja4zE237CmbGO\",\"access_token\":\"abcifejwiajJJJIDJFIEJQ0JDLUhTMjU2IiwiYWxnIjoiZGlyIn0..DIoIqgR5MP51jw.SK_1VawNWV1SC9ZSRu278imQXb-Fsn4K6gJK_IuEcG2p5xf9bj5fO6p8M8cN2HOw8D9TNuqR3u4POypw-QR4xfjiewjaifeDzc_L1D9cR_Zypcss0CWlk3Wl5Sm-Yel6Ej6DZPdMRYDVzFQIy-ugvlcbBMs_TBhPWuidiL7Gdy61iMW-CUG80iy0VN8TrOTTxI7oRlrsKeVF_htYbwfafeYxUnMIMnjz8BxsWHCj2Dj3Osx1d1RScHPlrzQhO8t9s0MpGbpO0Ygiu5H3-E5KC5ihnDtgTFeyyHFx8hPiG_ScbdnYgXQPqJiJIJ47Us9Jv0kXA1YxQr35-vL2IGHa6haofByqLJjXIKlYi-suu1Xl6wlCCZufXvELBcfhdkG4iCKGO3KXOozUkZOav9IqPM7qjGskTzbmR4zMzCmf0ypQbmk-4NXQT3N1Z_mxTX4ebCfjF7h0LjX3sgFcwYtNKS_iLsygU8mPZScCthBH67bO2ce35ZjHr2kHYKKxAYS-wXmiMpFM7NvEkVjoWJarrMF-Q4DB7eLKezmEKuRMDr6Q6_gDEbeyHqqCauEczBriq61LnWlDuqJtySL-amSrADFU7SU8fmD4DhgxU.f0o4123vdcxH_zvzfaewa7Q\",\"token_type\":\"bearer\",\"expires_in\":3600}" + read 1007 bytes + Conn close + opening connection to sandbox.api.intuit.com:443... + opened + 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: da14d01c3608a0a036c4e7298cb5d56a\r\nAccept: application/json\r\nAuthorization: Bearer abcifejwiajJJJIDJFIEJQ0JDLUhTMjU2IiwiYWxnIjoiZGlyIn0..DIoIqgR5MP51jw.SK_1VawNWV1SC9ZSRu278imQXb-Fsn4K6gJK_IuEcG2p5xf9bj5fO6p8M8cN2HOw8D9TNuqR3u4POypw-QR4xfjiewjaifeDzc_L1D9cR_Zypcss0CWlk3Wl5Sm-Yel6Ej6DZPdMRYDVzFQIy-ugvlcbBMs_TBhPWuidiL7Gdy61iMW-CUG80iy0VN8TrOTTxI7oRlrsKeVF_htYbwfafeYxUnMIMnjz8BxsWHCj2Dj3Osx1d1RScHPlrzQhO8t9s0MpGbpO0Ygiu5H3-E5KC5ihnDtgTFeyyHFx8hPiG_ScbdnYgXQPqJiJIJ47Us9Jv0kXA1YxQr35-vL2IGHa6haofByqLJjXIKlYi-suu1Xl6wlCCZufXvELBcfhdkG4iCKGO3KXOozUkZOav9IqPM7qjGskTzbmR4zMzCmf0ypQbmk-4NXQT3N1Z_mxTX4ebCfjF7h0LjX3sgFcwYtNKS_iLsygU8mPZScCthBH67bO2ce35ZjHr2kHYKKxAYS-wXmiMpFM7NvEkVjoWJarrMF-Q4DB7eLKezmEKuRMDr6Q6_gDEbeyHqqCauEczBriq61LnWlDuqJtySL-amSrADFU7SU8fmD4DhgxU.f0o4123vdcxH_zvzfaewa7Q\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nHost: sandbox.api.intuit.com\r\nContent-Length: 310\r\n\r\n" + <- "{\"amount\":\"1.00\",\"currency\":\"USD\",\"card\":{\"number\":\"4000100011112224\",\"expMonth\":\"09\",\"expYear\":2020,\"cvc\":\"123\",\"name\":\"Longbob Longsen\",\"address\":{\"streetAddress\":\"456 My Street\",\"city\":\"Ottawa\",\"region\":\"CA\",\"country\":\"US\",\"postalCode\":90210}},\"context\":{\"mobile\":false,\"isEcommerce\":true},\"capture\":\"true\"}" + -> "HTTP/1.1 201 Created\r\n" + -> "Date: Thu, 17 Oct 2019 15:40:44 GMT\r\n" + -> "Content-Type: application/json;charset=utf-8\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "Server: nginx\r\n" + -> "Strict-Transport-Security: max-age=15552000\r\n" + -> "intuit_tid: 09ce7b7f-e19a-4567-8d7f-cf6ce81a9c75\r\n" + -> "\r\n" + -> "213\r\n" + reading 531 bytes... + -> "{\"created\":\"2019-10-17T15:40:44Z\",\"status\":\"CAPTURED\",\"amount\":\"1.00\",\"currency\":\"USD\",\"card\":{\"number\":\"xxxxxxxxxxxx2224\",\"name\":\"Longbob Longsen\",\"address\":{\"city\":\"Ottawa\",\"region\":\"CA\",\"country\":\"US\",\"streetAddress\":\"456 My Street\",\"postalCode\":\"90210\"},\"cardType\":\"Visa\",\"expMonth\":\"09\",\"expYear\":\"2020\",\"cvc\":\"xxx\"},\"capture\":true,\"avsStreet\":\"Pass\",\"avsZip\":\"Pass\",\"cardSecurityCodeMatch\":\"NotAvailable\",\"id\":\"ES2Q849Y8KQ9\",\"context\":{\"mobile\":false,\"deviceInfo\":{},\"recurring\":false,\"isEcommerce\":true},\"authCode\":\"574943\"}" + read 531 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + ) + end + + def post_scrubbed_oauth_2 + %q( + opening connection to sandbox.api.intuit.com:443... + opened + 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: 3b098cc41f53562ec0f36a0fc7071ff8\r\nAccept: 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\nUser-Agent: Ruby\r\nHost: sandbox.api.intuit.com\r\nContent-Length: 310\r\n\r\n" + <- "{\"amount\":\"1.00\",\"currency\":\"USD\",\"card\":{\"number\":\"[FILTERED]\",\"expMonth\":\"09\",\"expYear\":2020,\"cvc\":\"[FILTERED]\",\"name\":\"Longbob Longsen\",\"address\":{\"streetAddress\":\"456 My Street\",\"city\":\"Ottawa\",\"region\":\"CA\",\"country\":\"US\",\"postalCode\":90210}},\"context\":{\"mobile\":false,\"isEcommerce\":true},\"capture\":\"true\"}" + -> "HTTP/1.1 401 Unauthorized\r\n" + -> "Date: Thu, 17 Oct 2019 15:40:43 GMT\r\n" + -> "Content-Type: application/json\r\n" + -> "Content-Length: 91\r\n" + -> "Connection: close\r\n" + -> "Server: nginx\r\n" + -> "intuit_tid: adca0516-0af2-48cd-a704-095529fe615c\r\n" + -> "WWW-Authenticate: Bearer realm=\"Intuit\", error=\"invalid_token\"\r\n" + -> "\r\n" + reading 91 bytes... + -> "{\"code\":\"AuthenticationFailed\",\"type\":\"INPUT\",\"message\":null,\"detail\":null,\"moreInfo\":null}" + read 91 bytes + Conn close + opening connection to oauth.platform.intuit.com:443... + opened + starting SSL for oauth.platform.intuit.com:443... + SSL established + <- "POST /oauth2/v1/tokens/bearer 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: oauth.platform.intuit.com\r\nContent-Length: 89\r\n\r\n" + <- "grant_type=refresh_token&refresh_token=[FILTERED]" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Thu, 17 Oct 2019 15:40:43 GMT\r\n" + -> "Content-Type: application/json;charset=utf-8\r\n" + -> "Content-Length: 1007\r\n" + -> "Connection: close\r\n" + -> "Server: nginx\r\n" + -> "Strict-Transport-Security: max-age=15552000\r\n" + -> "intuit_tid: c9aff174-410e-4683-8a35-2591c659550f\r\n" + -> "Cache-Control: no-cache, no-store\r\n" + -> "Pragma: no-cache\r\n" + -> "X-Frame-Options: SAMEORIGIN\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "X-XSS-Protection: 1; mode=block\r\n" + -> "\r\n" + reading 1007 bytes... + -> "{\"x_refresh_token_expires_in\":8719040,\"refresh_token\":\"[FILTERED]\",\"access_token\":\"[FILTERED]\",\"token_type\":\"bearer\",\"expires_in\":3600}" + read 1007 bytes + Conn close + opening connection to sandbox.api.intuit.com:443... + opened + 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: da14d01c3608a0a036c4e7298cb5d56a\r\nAccept: 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\nUser-Agent: Ruby\r\nHost: sandbox.api.intuit.com\r\nContent-Length: 310\r\n\r\n" + <- "{\"amount\":\"1.00\",\"currency\":\"USD\",\"card\":{\"number\":\"[FILTERED]\",\"expMonth\":\"09\",\"expYear\":2020,\"cvc\":\"[FILTERED]\",\"name\":\"Longbob Longsen\",\"address\":{\"streetAddress\":\"456 My Street\",\"city\":\"Ottawa\",\"region\":\"CA\",\"country\":\"US\",\"postalCode\":90210}},\"context\":{\"mobile\":false,\"isEcommerce\":true},\"capture\":\"true\"}" + -> "HTTP/1.1 201 Created\r\n" + -> "Date: Thu, 17 Oct 2019 15:40:44 GMT\r\n" + -> "Content-Type: application/json;charset=utf-8\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "Server: nginx\r\n" + -> "Strict-Transport-Security: max-age=15552000\r\n" + -> "intuit_tid: 09ce7b7f-e19a-4567-8d7f-cf6ce81a9c75\r\n" + -> "\r\n" + -> "213\r\n" + reading 531 bytes... + -> "{\"created\":\"2019-10-17T15:40:44Z\",\"status\":\"CAPTURED\",\"amount\":\"1.00\",\"currency\":\"USD\",\"card\":{\"number\":\"xxxxxxxxxxxx2224\",\"name\":\"Longbob Longsen\",\"address\":{\"city\":\"Ottawa\",\"region\":\"CA\",\"country\":\"US\",\"streetAddress\":\"456 My Street\",\"postalCode\":\"90210\"},\"cardType\":\"Visa\",\"expMonth\":\"09\",\"expYear\":\"2020\",\"cvc\":\"xxx\"},\"capture\":true,\"avsStreet\":\"Pass\",\"avsZip\":\"Pass\",\"cardSecurityCodeMatch\":\"NotAvailable\",\"id\":\"ES2Q849Y8KQ9\",\"context\":{\"mobile\":false,\"deviceInfo\":{},\"recurring\":false,\"isEcommerce\":true},\"authCode\":\"574943\"}" + read 531 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + ) + end end diff --git a/test/unit/gateways/quickpay_test.rb b/test/unit/gateways/quickpay_test.rb index 4eb99dcf7ed..8209f95c965 100644 --- a/test/unit/gateways/quickpay_test.rb +++ b/test/unit/gateways/quickpay_test.rb @@ -1,23 +1,19 @@ - require 'test_helper' class QuickpayTest < Test::Unit::TestCase - def test_error_without_login_option assert_raise ArgumentError do QuickpayGateway.new end end - + def test_v4to7 - gateway = QuickpayGateway.new(:login => 50000000, :password => 'secret') + gateway = QuickpayGateway.new(login: 50000000, password: 'secret') assert_instance_of QuickpayV4to7Gateway, gateway end - + def test_v10 - gateway = QuickpayGateway.new(:login => 100, :api_key => 'APIKEY') + gateway = QuickpayGateway.new(login: 100, api_key: 'APIKEY') assert_instance_of QuickpayV10Gateway, gateway end - end - diff --git a/test/unit/gateways/quickpay_v10_test.rb b/test/unit/gateways/quickpay_v10_test.rb index aac4bdb1e10..fccafc60268 100644 --- a/test/unit/gateways/quickpay_v10_test.rb +++ b/test/unit/gateways/quickpay_v10_test.rb @@ -4,13 +4,13 @@ class QuickpayV10Test < Test::Unit::TestCase include CommStub def setup - @gateway = QuickpayV10Gateway.new(:api_key => 'APIKEY') + @gateway = QuickpayV10Gateway.new(api_key: 'APIKEY') @credit_card = credit_card('4242424242424242') @amount = 100 - @options = { :order_id => '1', :billing_address => address, :customer_ip => '1.1.1.1' } + @options = { order_id: '1', billing_address: address, customer_ip: '1.1.1.1' } end - def parse body + def parse(body) JSON.parse(body) end @@ -28,7 +28,7 @@ def test_successful_purchase assert_success response assert_equal '1145', response.authorization assert response.test? - end.check_request do |endpoint, data, headers| + end.check_request do |endpoint, data, _headers| parsed = parse(data) if parsed['order_id'] assert_match %r{/payments}, endpoint @@ -47,7 +47,7 @@ def test_successful_authorization assert_success response assert_equal '1145', response.authorization assert response.test? - end.check_request do |endpoint, data, headers| + end.check_request do |endpoint, data, _headers| parsed_data = parse(data) if parsed_data['order_id'] assert_match %r{/payments}, endpoint @@ -58,14 +58,38 @@ def test_successful_authorization end.respond_with(successful_payment_response, successful_authorization_response) end + def test_successful_authorization_with_3ds + options = @options.merge( + three_d_secure: { + cavv: '1234', + eci: '1234', + xid: '1234' + } + ) + stub_comms do + assert response = @gateway.authorize(@amount, @credit_card, options) + assert_success response + assert_equal '1145', response.authorization + assert response.test? + end.check_request do |endpoint, data, _headers| + parsed_data = parse(data) + if parsed_data['order_id'] + assert_match %r{/payments}, endpoint + assert_match '1.1.1.1', options[:customer_ip] + else + assert_match %r{/payments/\d+/authorize}, endpoint + end + end.respond_with(successful_payment_response, successful_authorization_response) + end + def test_successful_void stub_comms do assert response = @gateway.void(1145) assert_success response assert response.test? - end.check_request do |endpoint, data, headers| + end.check_request do |endpoint, _data, _headers| assert_match %r{/payments/1145/cancel}, endpoint - end.respond_with({'id' => 1145}.to_json) + end.respond_with({ 'id' => 1145 }.to_json) end def test_failed_authorization @@ -91,7 +115,7 @@ def test_successful_store assert response = @gateway.store(@credit_card, @options) assert_success response assert response.test? - end.check_request do |endpoint, data, headers| + end.check_request do |endpoint, _data, _headers| assert_match %r{/card}, endpoint end.respond_with(successful_store_response, successful_sauthorize_response) end @@ -101,9 +125,9 @@ def test_successful_unstore assert response = @gateway.unstore('123') assert_success response assert response.test? - end.check_request do |endpoint, data, headers| + end.check_request do |endpoint, _data, _headers| assert_match %r{/cards/\d+/cancel}, endpoint - end.respond_with({'id' => '123'}.to_json) + end.respond_with({ 'id' => '123' }.to_json) end def test_successful_verify @@ -117,19 +141,19 @@ def test_successful_verify def test_failed_verify response = stub_comms do @gateway.verify(@credit_card, @options) - end.respond_with(failed_authorization_response, {'id' => 1145}.to_json) + end.respond_with(failed_authorization_response, { 'id' => 1145 }.to_json) assert_failure response assert_equal 'Validation error', response.message end def test_supported_countries klass = @gateway.class - assert_equal ['DE', 'DK', 'ES', 'FI', 'FR', 'FO', 'GB', 'IS', 'NO', 'SE'], klass.supported_countries + assert_equal %w[DE DK ES FI FR FO GB IS NO SE], klass.supported_countries end def test_supported_card_types klass = @gateway.class - assert_equal [:dankort, :forbrugsforeningen, :visa, :master, :american_express, :diners_club, :jcb, :maestro ], klass.supported_cardtypes + assert_equal %i[dankort forbrugsforeningen visa master american_express diners_club jcb maestro], klass.supported_cardtypes end def test_successful_capture @@ -146,42 +170,42 @@ def test_transcript_scrubbing def successful_payment_response { - 'id' =>1145, - 'order_id' =>'310f59c57a', - 'accepted' =>false, - 'test_mode' =>false, - 'branding_id' =>nil, - 'variables' =>{}, - 'acquirer' =>nil, - 'operations' =>[], - 'metadata' =>{}, - 'created_at' =>'2015-03-30T16:56:17Z', - 'balance' =>0, - 'currency' =>'DKK' + 'id' => 1145, + 'order_id' => '310f59c57a', + 'accepted' => false, + 'test_mode' => false, + 'branding_id' => nil, + 'variables' => {}, + 'acquirer' => nil, + 'operations' => [], + 'metadata' => {}, + 'created_at' => '2015-03-30T16:56:17Z', + 'balance' => 0, + 'currency' => 'DKK' }.to_json end def successful_authorization_response { - 'id' => 1145, - 'order_id' => '310f59c57a', - 'accepted' => false, - 'test_mode' => true, - 'branding_id' => nil, - 'variables' => {}, - 'acquirer' => 'clearhaus', - 'operations' => [], - 'metadata' => { - 'type' =>'card', - 'brand' =>'quickpay-test-card', - 'last4' =>'0008', - 'exp_month' =>9, - 'exp_year' =>2016, - 'country' =>'DK', - 'is_3d_secure' =>false, - 'customer_ip' =>nil, - 'customer_country' =>nil - }, + 'id' => 1145, + 'order_id' => '310f59c57a', + 'accepted' => false, + 'test_mode' => true, + 'branding_id' => nil, + 'variables' => {}, + 'acquirer' => 'clearhaus', + 'operations' => [], + 'metadata' => { + 'type' => 'card', + 'brand' => 'quickpay-test-card', + 'last4' => '0008', + 'exp_month' => 9, + 'exp_year' => 2016, + 'country' => 'DK', + 'is_3d_secure' => false, + 'customer_ip' => nil, + 'customer_country' => nil + }, 'created_at' => '2015-03-30T16:56:17Z', 'balance' => 0, 'currency' => 'DKK' @@ -190,46 +214,46 @@ def successful_authorization_response def successful_capture_response { - 'id' =>1145, - 'order_id' =>'310f59c57a', - 'accepted' =>true, - 'test_mode' =>true, - 'branding_id' =>nil, - 'variables' =>{}, - 'acquirer' =>'clearhaus', - 'operations' =>[], - 'metadata' =>{'type'=>'card', 'brand'=>'quickpay-test-card', 'last4'=>'0008', 'exp_month'=>9, 'exp_year'=>2016, 'country'=>'DK', 'is_3d_secure'=>false, 'customer_ip'=>nil, 'customer_country'=>nil}, - 'created_at' =>'2015-03-30T16:56:17Z', - 'balance' =>0, - 'currency' =>'DKK' + 'id' => 1145, + 'order_id' => '310f59c57a', + 'accepted' => true, + 'test_mode' => true, + 'branding_id' => nil, + 'variables' => {}, + 'acquirer' => 'clearhaus', + 'operations' => [], + 'metadata' => { 'type' => 'card', 'brand' => 'quickpay-test-card', 'last4' => '0008', 'exp_month' => 9, 'exp_year' => 2016, 'country' => 'DK', 'is_3d_secure' => false, 'customer_ip' => nil, 'customer_country' => nil }, + 'created_at' => '2015-03-30T16:56:17Z', + 'balance' => 0, + 'currency' => 'DKK' }.to_json end def succesful_refund_response { - 'id' =>1145, - 'order_id' =>'310f59c57a', - 'accepted' =>true, - 'test_mode' =>true, - 'branding_id' =>nil, - 'variables' =>{}, - 'acquirer' =>'clearhaus', - 'operations' =>[], - 'metadata'=>{ - 'type' =>'card', - 'brand' =>'quickpay-test-card', - 'last4' =>'0008', - 'exp_month' =>9, - 'exp_year' =>2016, - 'country' =>'DK', - 'is_3d_secure' =>false, - 'customer_ip' =>nil, - 'customer_country' =>nil - }, - 'created_at' =>'2015-03-30T16:56:17Z', - 'balance' =>100, - 'currency' =>'DKK' - }.to_json + 'id' => 1145, + 'order_id' => '310f59c57a', + 'accepted' => true, + 'test_mode' => true, + 'branding_id' => nil, + 'variables' => {}, + 'acquirer' => 'clearhaus', + 'operations' => [], + 'metadata' => { + 'type' => 'card', + 'brand' => 'quickpay-test-card', + 'last4' => '0008', + 'exp_month' => 9, + 'exp_year' => 2016, + 'country' => 'DK', + 'is_3d_secure' => false, + 'customer_ip' => nil, + 'customer_country' => nil + }, + 'created_at' => '2015-03-30T16:56:17Z', + 'balance' => 100, + 'currency' => 'DKK' + }.to_json end def failed_authorization_response @@ -284,5 +308,4 @@ def scrubbed_transcript D, [2015-08-17T11:44:26.710099 #75027] DEBUG -- : {"amount":"100","card":{"number":"[FILTERED]","cvd":"[FILTERED]","expiration":"1609","issued_to":"Longbob Longsen"},"auto_capture":false} ) end - end diff --git a/test/unit/gateways/quickpay_v4to7_test.rb b/test/unit/gateways/quickpay_v4to7_test.rb index e666efbadb3..e4beffd6571 100644 --- a/test/unit/gateways/quickpay_v4to7_test.rb +++ b/test/unit/gateways/quickpay_v4to7_test.rb @@ -2,21 +2,21 @@ class QuickpayV4to7Test < Test::Unit::TestCase include CommStub - + def merchant_id - '80000000000' + '80000000000' end - + def setup @gateway = QuickpayGateway.new( - :login => merchant_id, - :password => 'PASSWORD', - :version => 7 + login: merchant_id, + password: 'PASSWORD', + version: 7 ) @credit_card = credit_card('4242424242424242') @amount = 100 - @options = { :order_id => '1', :billing_address => address } + @options = { order_id: '1', billing_address: address } end def test_successful_purchase @@ -39,15 +39,15 @@ def test_successful_authorization def test_successful_store_for_v6 @gateway = QuickpayGateway.new( - :login => merchant_id, - :password => 'PASSWORD', - :version => 6 + login: merchant_id, + password: 'PASSWORD', + version: 6 ) @gateway.expects(:generate_check_hash).returns(mock_md5_hash) response = stub_comms do - @gateway.store(@credit_card, {:order_id => 'fa73664073e23597bbdd', :description => 'Storing Card'}) - end.check_request do |endpoint, data, headers| + @gateway.store(@credit_card, { order_id: 'fa73664073e23597bbdd', description: 'Storing Card' }) + end.check_request do |_endpoint, data, _headers| assert_equal(expected_store_parameters_v6, CGI::parse(data)) end.respond_with(successful_store_response_v6) @@ -61,8 +61,8 @@ def test_successful_store_for_v7 @gateway.expects(:generate_check_hash).returns(mock_md5_hash) response = stub_comms do - @gateway.store(@credit_card, {:order_id => 'ed7546cb4ceb8f017ea4', :description => 'Storing Card'}) - end.check_request do |endpoint, data, headers| + @gateway.store(@credit_card, { order_id: 'ed7546cb4ceb8f017ea4', description: 'Storing Card' }) + end.check_request do |_endpoint, data, _headers| assert_equal(expected_store_parameters_v7, CGI::parse(data)) end.respond_with(successful_store_response_v7) @@ -124,16 +124,16 @@ def test_parsing_successful_response def test_supported_countries klass = @gateway.class - assert_equal ['DE', 'DK', 'ES', 'FI', 'FR', 'FO', 'GB', 'IS', 'NO', 'SE'], klass.supported_countries + assert_equal %w[DE DK ES FI FR FO GB IS NO SE], klass.supported_countries end def test_supported_card_types klass = @gateway.class - assert_equal [ :dankort, :forbrugsforeningen, :visa, :master, :american_express, :diners_club, :jcb, :maestro ], klass.supported_cardtypes + assert_equal %i[dankort forbrugsforeningen visa master american_express diners_club jcb maestro], klass.supported_cardtypes end def test_add_testmode_does_not_add_testmode_if_transaction_id_present - post_hash = {:transaction => '12345'} + post_hash = { transaction: '12345' } @gateway.send(:add_testmode, post_hash) assert_equal nil, post_hash[:testmode] end @@ -147,7 +147,7 @@ def test_add_testmode_adds_a_testmode_param_if_transaction_id_not_present def test_finalize_is_disabled_by_default stub_comms(@gateway, :ssl_request) do @gateway.capture(@amount, '12345') - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert data =~ /finalize=0/ end.respond_with(successful_capture_response) end @@ -155,7 +155,7 @@ def test_finalize_is_disabled_by_default def test_finalize_is_enabled stub_comms(@gateway, :ssl_request) do @gateway.capture(@amount, '12345', finalize: true) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert data =~ /finalize=1/ end.respond_with(successful_capture_response) end @@ -192,33 +192,33 @@ def failed_authorization_response def expected_store_parameters_v6 { - 'cardnumber'=>['4242424242424242'], - 'cvd'=>['123'], - 'expirationdate'=>[expected_expiration_date], - 'ordernumber'=>['fa73664073e23597bbdd'], - 'description'=>['Storing Card'], - 'testmode'=>['1'], - 'protocol'=>['6'], - 'msgtype'=>['subscribe'], - 'merchant'=>[merchant_id], - 'md5check'=>[mock_md5_hash] + 'cardnumber' => ['4242424242424242'], + 'cvd' => ['123'], + 'expirationdate' => [expected_expiration_date], + 'ordernumber' => ['fa73664073e23597bbdd'], + 'description' => ['Storing Card'], + 'testmode' => ['1'], + 'protocol' => ['6'], + 'msgtype' => ['subscribe'], + 'merchant' => [merchant_id], + 'md5check' => [mock_md5_hash] } end def expected_store_parameters_v7 { - 'amount'=>['0'], - 'currency'=>['DKK'], - 'cardnumber'=>['4242424242424242'], - 'cvd'=>['123'], - 'expirationdate'=>[expected_expiration_date], - 'ordernumber'=>['ed7546cb4ceb8f017ea4'], - 'description'=>['Storing Card'], - 'testmode'=>['1'], - 'protocol'=>['7'], - 'msgtype'=>['subscribe'], - 'merchant'=>[merchant_id], - 'md5check'=>[mock_md5_hash] + 'amount' => ['0'], + 'currency' => ['DKK'], + 'cardnumber' => ['4242424242424242'], + 'cvd' => ['123'], + 'expirationdate' => [expected_expiration_date], + 'ordernumber' => ['ed7546cb4ceb8f017ea4'], + 'description' => ['Storing Card'], + 'testmode' => ['1'], + 'protocol' => ['7'], + 'msgtype' => ['subscribe'], + 'merchant' => [merchant_id], + 'md5check' => [mock_md5_hash] } end diff --git a/test/unit/gateways/qvalent_test.rb b/test/unit/gateways/qvalent_test.rb index 12a53e48d1d..ebf25c7fc28 100644 --- a/test/unit/gateways/qvalent_test.rb +++ b/test/unit/gateways/qvalent_test.rb @@ -16,6 +16,12 @@ def setup @amount = 100 end + def test_successful_gateway_creation_without_pem_password + gateway = QvalentGateway.new(username: 'username', password: 'password', merchant: 'merchant', pem: 'pem') + + assert_instance_of QvalentGateway, gateway + end + def test_successful_purchase response = stub_comms do @gateway.purchase(@amount, @credit_card) @@ -24,6 +30,7 @@ def test_successful_purchase assert_success response assert_equal '5d53a33d960c46d00f5dc061947d998c', response.authorization + assert_equal 'M', response.cvv_result['code'] assert response.test? end @@ -55,7 +62,7 @@ def test_failed_authorize end.respond_with(failed_authorize_response) assert_failure response - assert_equal 'Expired card',response.message + assert_equal 'Expired card', response.message assert response.test? end @@ -90,7 +97,7 @@ def test_successful_refund refund = stub_comms do @gateway.refund(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match %r{5d53a33d960c46d00f5dc061947d998c}, data end.respond_with(successful_refund_response) @@ -176,8 +183,8 @@ def test_empty_response_fails def test_3d_secure_fields response = stub_comms(@gateway, :ssl_request) do - @gateway.purchase(@amount, @credit_card, {xid: '123', cavv: '456', eci: '5'}) - end.check_request do |method, endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, { xid: '123', cavv: '456', eci: '5' }) + end.check_request do |_method, _endpoint, data, _headers| assert_match(/xid=123/, data) assert_match(/cavv=456/, data) assert_match(/ECI=5/, data) @@ -186,15 +193,121 @@ def test_3d_secure_fields assert_success response end + def test_stored_credential_fields_initial + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, { stored_credential: { initial_transaction: true, reason_type: 'unscheduled', initiator: 'merchant' } }) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/posEntryMode=MANUAL/, data) + assert_match(/storedCredentialUsage=UNSCHEDULED_MIT/, data) + assert_match(/ECI=SSL/, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_stored_credential_fields_recurring + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, { stored_credential: { reason_type: 'recurring', initiator: 'merchant', network_transaction_id: '7890' } }) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/posEntryMode=STORED_CREDENTIAL/, data) + assert_match(/storedCredentialUsage=RECURRING/, data) + assert_match(/ECI=REC/, data) + assert_match(/authTraceId=7890/, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_stored_credential_fields_unscheduled + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, { stored_credential: { reason_type: 'unscheduled', initiator: 'merchant', network_transaction_id: '7890' } }) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/posEntryMode=STORED_CREDENTIAL/, data) + assert_match(/storedCredentialUsage=UNSCHEDULED/, data) + assert_match(/ECI=MTO/, data) + assert_match(/authTraceId=7890/, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_stored_credential_fields_cardholder_initiated + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, { stored_credential: { reason_type: 'unscheduled', initiator: 'cardholder', network_transaction_id: '7890' } }) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/posEntryMode=STORED_CREDENTIAL/, data) + refute_match(/storedCredentialUsage/, data) + assert_match(/ECI=MTO/, data) + assert_match(/authTraceId=7890/, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_stored_credential_fields_mastercard + @credit_card.brand = 'master' + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, { stored_credential: { reason_type: 'recurring', initiator: 'merchant', network_transaction_id: '7890' } }) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/posEntryMode=STORED_CREDENTIAL/, data) + refute_match(/storedCredentialUsage/, data) + assert_match(/ECI=REC/, data) + assert_match(/authTraceId=7890/, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_cvv_result + response = stub_comms do + @gateway.purchase(@amount, @credit_card) + end.respond_with(mapped_cvv_response) + + assert_success response + assert_equal 'D', response.cvv_result['code'] + end + def test_transcript_scrubbing assert_equal scrubbed_transcript, @gateway.scrub(transcript) end + def test_default_add_card_reference_number + post = {} + options = {} + options[:order_id] = 1234534 + @gateway.send(:add_card_reference, post, options) + assert_equal post['customer.customerReferenceNumber'], 1234534 + end + + def test_add_card_reference_number + post = {} + options = {} + options[:order_id] = 1234 + options[:customer_reference_number] = 4321 + @gateway.send(:add_card_reference, post, options) + assert_equal post['customer.customerReferenceNumber'], 4321 + end + + def test_default_add_customer_reference_number + post = {} + @gateway.send(:add_customer_reference, post, {}) + assert_nil post['customer.customerReferenceNumber'] + end + + def test_add_customer_reference_number + post = {} + options = {} + options[:customer_reference_number] = 4321 + @gateway.send(:add_customer_reference, post, options) + assert_equal post['customer.customerReferenceNumber'], 4321 + end + private def successful_purchase_response %( - response.summaryCode=0\r\nresponse.responseCode=08\r\nresponse.text=Honour with identification\r\nresponse.referenceNo=723907124\r\nresponse.orderNumber=5d53a33d960c46d00f5dc061947d998c\r\nresponse.RRN=723907124 \r\nresponse.settlementDate=20150228\r\nresponse.transactionDate=28-FEB-2015 09:34:15\r\nresponse.cardSchemeName=VISA\r\nresponse.creditGroup=VI/BC/MC\r\nresponse.previousTxn=0\r\nresponse.end\r\n + response.summaryCode=0\r\nresponse.responseCode=08\r\nresponse.text=Honour with identification\r\nresponse.referenceNo=723907124\r\nresponse.orderNumber=5d53a33d960c46d00f5dc061947d998c\r\nresponse.RRN=723907124 \r\nresponse.settlementDate=20150228\r\nresponse.transactionDate=28-FEB-2015 09:34:15\r\nresponse.cardSchemeName=VISA\r\nresponse.creditGroup=VI/BC/MC\r\nresponse.previousTxn=0\r\nresponse.cvnResponse=M +\r\nresponse.end\r\n ) end @@ -281,6 +394,13 @@ def empty_purchase_response ) end + def mapped_cvv_response + %( + response.summaryCode=0\r\nresponse.responseCode=08\r\nresponse.text=Honour with identification\r\nresponse.referenceNo=723907124\r\nresponse.orderNumber=5d53a33d960c46d00f5dc061947d998c\r\nresponse.RRN=723907124 \r\nresponse.settlementDate=20150228\r\nresponse.transactionDate=28-FEB-2015 09:34:15\r\nresponse.cardSchemeName=VISA\r\nresponse.creditGroup=VI/BC/MC\r\nresponse.previousTxn=0\r\nresponse.cvnResponse=S +\r\nresponse.end\r\n + ) + end + def transcript %( opening connection to ccapi.client.support.qvalent.com:443... diff --git a/test/unit/gateways/rapyd_test.rb b/test/unit/gateways/rapyd_test.rb new file mode 100644 index 00000000000..9fef4dac2d2 --- /dev/null +++ b/test/unit/gateways/rapyd_test.rb @@ -0,0 +1,814 @@ +require 'test_helper' + +class RapydTest < Test::Unit::TestCase + include CommStub + + 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 + @authorization = 'cus_9e1b5a357b2b7f25f8dd98827fbc4f22|card_cf105df9e77462deb34ffef33c3e3d05' + + @options = { + pm_type: 'in_amex_card', + currency: 'USD', + complete_payment_url: 'www.google.com', + error_payment_url: 'www.google.com', + description: 'Describe this transaction', + statement_descriptor: 'Statement Descriptor', + email: 'test@example.com', + billing_address: address(name: 'Jim Reynolds'), + order_id: '987654321', + idempotency_key: '123' + } + + @metadata = { + array_of_objects: [ + { name: 'John Doe' }, + { type: 'customer' } + ], + array_of_strings: %w[ + color + size + ], + number: 1234567890, + object: { + string: 'person' + }, + 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 + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge(billing_address: address(name: 'Joe John-ston'))) + end.check_request do |_method, _endpoint, data, _headers| + assert_equal JSON.parse(data)['address']['name'], 'Joe John-ston' + end.respond_with(successful_purchase_response) + + assert_success response + 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'))) + end.check_request do |_method, _endpoint, data, _headers| + assert_nil JSON.parse(data)['capture'] + end.respond_with(successful_ach_purchase_response) + + assert_success response + assert_equal 'ACT', response.params['data']['status'] + end + + def test_successful_purchase_with_token + @options[:customer_id] = 'cus_9e1b5a357b2b7f25f8dd98827fbc4f22' + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @authorization, @options) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['payment_method'], @authorization.split('|')[1] + assert_equal request['customer'], @options[:customer_id] + end.respond_with(successful_purchase_with_options_response) + + assert_success response + assert_equal @metadata, response.params['data']['metadata'].deep_transform_keys(&:to_sym) + end + + def test_successful_purchase_with_payment_options + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"complete_payment_url":"www.google.com"/, data) + assert_match(/"error_payment_url":"www.google.com"/, data) + assert_match(/"description":"Describe this transaction"/, data) + assert_match(/"statement_descriptor":"Statement Descriptor"/, data) + assert_match(/"merchant_reference_id":"987654321"/, data) + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_successful_purchase_with_explicit_merchant_reference_id + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge({ merchant_reference_id: '99988877776' })) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"complete_payment_url":"www.google.com"/, data) + assert_match(/"error_payment_url":"www.google.com"/, data) + assert_match(/"description":"Describe this transaction"/, data) + assert_match(/"statement_descriptor":"Statement Descriptor"/, data) + assert_match(/"merchant_reference_id":"99988877776"/, data) + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_successful_purchase_with_stored_credential + @options[:stored_credential] = { + reason_type: 'recurring', + network_transaction_id: '12345' + } + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['payment_method']['fields']['network_reference_id'], @options[:stored_credential][:network_transaction_id] + assert_equal request['initiation_type'], @options[:stored_credential][:reason_type] + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_network_transaction_id_and_initiation_type_fields + @options[:network_transaction_id] = '54321' + @options[:initiation_type] = 'customer_present' + + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['payment_method']['fields']['network_reference_id'], @options[:network_transaction_id] + assert_equal request['initiation_type'], @options[:initiation_type] + end.respond_with(successful_purchase_response) + end + + def test_success_purchase_with_recurrence_type + @options[:recurrence_type] = 'recurring' + + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['payment_method']['fields']['recurrence_type'], @options[:recurrence_type] + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_save_payment_method + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge({ save_payment_method: true })) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"save_payment_method":true/, data) + end.respond_with(successful_authorize_response) + + assert_success 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) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_equal 'ERROR_PROCESSING_CARD - [05]', response.error_code + end + + def test_successful_authorize + response = stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_method, _endpoint, _data, headers| + assert_equal '123', headers['idempotency'] + end.respond_with(successful_authorize_response) + + assert_success response + assert_equal 'SUCCESS', response.message + 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 'Do Not Honor', response.message + end + + def test_successful_capture + transaction_id = 'payment_e0979a1c6843e5d7bf0c18335794cccb' + response = stub_comms(@gateway, :ssl_request) do + @gateway.capture(@amount, transaction_id, @options) + end.check_request do |_method, _endpoint, _data, headers| + assert_equal '123', headers['idempotency'] + end.respond_with(successful_capture_response) + + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_failed_capture + @gateway.expects(:ssl_request).returns(failed_capture_response) + + response = @gateway.capture(@amount, '', @options) + assert_failure response + assert_equal 'The request tried to retrieve a payment, but the payment was not found. The request was rejected. Corrective action: Use a valid payment ID.', response.message + end + + def test_successful_refund + @gateway.expects(:ssl_request).returns(successful_refund_response) + transaction_id = 'refund_2a575991bee3b010f44e438f7f6a6d5f' + + response = @gateway.refund(@amount, transaction_id, @options) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_failed_refund + @gateway.expects(:ssl_request).returns(failed_refund_response) + + response = @gateway.refund(@amount, '', @options) + assert_failure response + assert_equal 'The request tried to retrieve a payment, but the payment was not found. The request was rejected. Corrective action: Use a valid payment ID.', response.message + end + + def test_successful_void + @gateway.expects(:ssl_request).returns(successful_void_response) + authorization = 'payment_a29a73f09d6f55defddc779dbb2d1089' + + response = @gateway.void(authorization) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_failed_void + @gateway.expects(:ssl_request).returns(failed_void_response) + + response = @gateway.void('') + assert_failure response + assert_equal 'UNAUTHORIZED_API_CALL', response.message + end + + def test_successful_verify + response = stub_comms(@gateway, :ssl_request) do + @gateway.verify(@credit_card, @options) + end.check_request do |_method, _endpoint, _data, headers| + assert_equal '123', headers['idempotency'] + end.respond_with(successful_verify_response) + + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_failed_verify + @gateway.expects(:ssl_request).returns(failed_authorize_response) + + response = @gateway.verify(@credit_card, @options) + assert_failure response + assert_equal 'Do Not Honor', response.message + end + + def test_successful_store_and_unstore + store = stub_comms(@gateway, :ssl_request) do + @gateway.store(@credit_card, @options) + end.check_request do |_method, _endpoint, _data, headers| + assert_match '123', headers['idempotency'] + end.respond_with(successful_store_response) + + assert_success store + assert customer_id = store.params.dig('data', 'id') + + unstore = stub_comms(@gateway, :ssl_request) do + @gateway.unstore(store.authorization) + end.respond_with(successful_unstore_response) + + assert_success unstore + assert_equal true, unstore.params.dig('data', 'deleted') + assert_equal customer_id, unstore.params.dig('data', 'id') + end + + def test_unstore + stub_comms(@gateway, :ssl_request) do + @gateway.unstore('123456') + end.check_request do |_method, _endpoint, _data, headers| + assert_not_match '123', headers['idempotency'] + end.respond_with(successful_unstore_response) + 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:)) + 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) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"name":"Jim Reynolds"/, data) + assert_match(/"email":"test@example.com"/, data) + assert_match(/"phone_number":"5555555555"/, data) + end.respond_with(successful_store_response) + + assert_success response + end + + def test_payment_urls_correctly_nested_by_operation + response = stub_comms(@gateway, :ssl_request) do + @gateway.store(@credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + request_body = JSON.parse(data) + assert_equal @options[:complete_payment_url], request_body['payment_method']['complete_payment_url'] + assert_equal @options[:error_payment_url], request_body['payment_method']['error_payment_url'] + end.respond_with(successful_store_response) + + assert_success response + + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + request_body = JSON.parse(data) + assert_equal @options[:complete_payment_url], request_body['complete_payment_url'] + assert_equal @options[:error_payment_url], request_body['error_payment_url'] + end.respond_with(successful_store_response) + + assert_success response + end + + def test_purchase_with_customer_and_card_id + store = stub_comms(@gateway, :ssl_request) do + @gateway.store(@credit_card, @options) + end.respond_with(successful_store_response) + + assert customer_id = store.params.dig('data', 'id') + assert card_id = store.params.dig('data', 'default_payment_method') + + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, store.authorization, @options) + end.check_request do |_method, _endpoint, data, _headers| + request_body = JSON.parse(data) + assert_equal request_body['customer'], customer_id + assert_equal request_body['payment_method'], card_id + end.respond_with(successful_purchase_response) + end + + def test_three_d_secure + options = { + three_d_secure: { + cavv: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + eci: '5', + xid: 'TTBCSkVTa1ZpbDI1bjRxbGk5ODE=' + } + } + + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"cavv":"EHuWW9PiBkWvqE5juRwDzAUFBAk="/, data) + assert_match(/"eci":"5"/, data) + assert_match(/"xid":"TTBCSkVTa1ZpbDI1bjRxbGk5ODE="/, data) + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_scrub + assert @gateway.supports_scrubbing? + 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... + opened + starting SSL for sandboxapi.rapyd.net:443... + SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES128-GCM-SHA256 + <- "POST /v1/payments HTTP/1.1\r\nContent-Type: application/json\r\nAccess_key: A6E93651174B48E0EF1E\r\nSalt: +3mM6dOjHsOwF/VQ\r\nTimestamp: 1647870006\r\nSignature: YjY4NTA1NDY3ZTUxMWUyNzk0NjFkOTJhZjIwYWUzZTA5YzYyMzUzZDE1ZjY2NWFmM2NhZTlmZDY2ZDZjNjEwYQ==\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: sandboxapi.rapyd.net\r\nContent-Length: 212\r\n\r\n" + <- "{\"amount\":\"1.0\",\"currency\":\"USD\",\"payment_method\":{\"type\":\"in_amex_card\",\"fields\":{\"number\":\"4111111111111111\",\"expiration_month\":\"12\",\"expiration_year\":\"2035\",\"cvv\":\"345\",\"name\":\"Ryan Reynolds\"}},\"capture\":true}" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Mon, 21 Mar 2022 13:40:08 GMT\r\n" + -> "Content-Type: application/json; charset=utf-8\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "Vary: X-HTTP-Method-Override, Accept-Encoding\r\n" + -> "Strict-Transport-Security: max-age=8640000; includeSubDomains\r\n" + -> "ETag: W/\"7d1-tsdr4eAZn2y+2my4kMxz2w\"\r\n" + -> "Content-Encoding: gzip\r\n" + -> "\r\n" + -> "a\r\n" + -> "0\r\n" + -> "\r\n" + Conn close + ' + end + + def post_scrubbed + ' + opening connection to sandboxapi.rapyd.net:443... + opened + starting SSL for sandboxapi.rapyd.net:443... + SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES128-GCM-SHA256 + <- "POST /v1/payments HTTP/1.1\r\nContent-Type: application/json\r\nAccess_key: [FILTERED]\r\nSalt: +3mM6dOjHsOwF/VQ\r\nTimestamp: 1647870006\r\nSignature: YjY4NTA1NDY3ZTUxMWUyNzk0NjFkOTJhZjIwYWUzZTA5YzYyMzUzZDE1ZjY2NWFmM2NhZTlmZDY2ZDZjNjEwYQ==\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: sandboxapi.rapyd.net\r\nContent-Length: 212\r\n\r\n" + <- "{\"amount\":\"1.0\",\"currency\":\"USD\",\"payment_method\":{\"type\":\"in_amex_card\",\"fields\":{\"number\":\"[FILTERED]\",\"expiration_month\":\"12\",\"expiration_year\":\"2035\",\"cvv\":\"[FILTERED]\",\"name\":\"Ryan Reynolds\"}},\"capture\":true}" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Mon, 21 Mar 2022 13:40:08 GMT\r\n" + -> "Content-Type: application/json; charset=utf-8\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "Vary: X-HTTP-Method-Override, Accept-Encoding\r\n" + -> "Strict-Transport-Security: max-age=8640000; includeSubDomains\r\n" + -> "ETag: W/\"7d1-tsdr4eAZn2y+2my4kMxz2w\"\r\n" + -> "Content-Encoding: gzip\r\n" + -> "\r\n" + -> "a\r\n" + -> "0\r\n" + -> "\r\n" + Conn close + ' + end + + def successful_purchase_response + %( + {"status":{"error_code":"","status":"SUCCESS","message":"","response_code":"","operation_id":"99571e34-f236-4f86-9040-5e0f256d6f64"},"data":{"id":"payment_716ce0efc63aa8d91579e873d29d9d5e","amount":1,"original_amount":1,"is_partial":false,"currency_code":"USD","country_code":"in","status":"CLO","description":"","merchant_reference_id":"","customer_token":"cus_f991c9a9f0cc7abdad64f9f7aea13f31","payment_method":"card_652d9fef3ec0089689fcaf0154340c64","payment_method_data":{"id":"card_652d9fef3ec0089689fcaf0154340c64","type":"in_amex_card","category":"card","metadata":null,"image":"","webhook_url":"","supporting_documentation":"","next_action":"not_applicable","name":"Ryan Reynolds","last4":"1111","acs_check":"unchecked","cvv_check":"unchecked","bin_details":{"type":null,"brand":null,"country":null,"bin_number":"411111"},"expiration_year":"35","expiration_month":"12","fingerprint_token":"ocfp_eb9edd24a3f3f59651aee0bd3d16201e"},"expiration":1648237659,"captured":true,"refunded":false,"refunded_amount":0,"receipt_email":"","redirect_url":"","complete_payment_url":"","error_payment_url":"","receipt_number":"","flow_type":"","address":null,"statement_descriptor":"N/A","transaction_id":"","created_at":1647632859,"metadata":{},"failure_code":"","failure_message":"","paid":true,"paid_at":1647632859,"dispute":null,"refunds":null,"order":null,"outcome":null,"visual_codes":{},"textual_codes":{},"instructions":{},"ewallet_id":"ewallet_1936682fdca7a188c49eb9f9817ade77","ewallets":[{"ewallet_id":"ewallet_1936682fdca7a188c49eb9f9817ade77","amount":1,"percent":100,"refunded_amount":0}],"payment_method_options":{},"payment_method_type":"in_amex_card","payment_method_type_category":"card","fx_rate":1,"merchant_requested_currency":null,"merchant_requested_amount":null,"fixed_side":"","payment_fees":null,"invoice":"","escrow":null,"group_payment":"","cancel_reason":null,"initiation_type":"customer_present","mid":"","next_action":"not_applicable","error_code":"","remitter_information":{}}} + ) + end + + def successful_purchase_with_options_response + %( + {"status":{"error_code":"", "status":"SUCCESS", "message":"", "response_code":"", "operation_id":"2852540b-ffa4-4547-9260-26f101f649ad"}, "data":{"id":"payment_6b00756cfefb0fdf6fb295fa507594d3", "amount":1000, "original_amount":1000, "is_partial":false, "currency_code":"USD", "country_code":"US", "status":"CLO", "description":"", "merchant_reference_id":"", "customer_token":"cus_9cb7908aec8a75a95846f1b3759ad1ef", "payment_method":"card_a838c23ef7be1ece86aa27a330167737", "payment_method_data":{"id":"card_a838c23ef7be1ece86aa27a330167737", "type":"us_visa_card", "category":"card", "metadata":null, "image":"", "webhook_url":"", "supporting_documentation":"", "next_action":"not_applicable", "name":"Ryan Reynolds", "last4":"1111", "acs_check":"unchecked", "cvv_check":"unchecked", "bin_details":{"type":null, "brand":null, "country":null, "bin_number":"411111"}, "expiration_year":"35", "expiration_month":"12", "fingerprint_token":"ocfp_eb9edd24a3f3f59651aee0bd3d16201e"}, "expiration":1649955834, "captured":true, "refunded":false, "refunded_amount":0, "receipt_email":"", "redirect_url":"", "complete_payment_url":"", "error_payment_url":"", "receipt_number":"", "flow_type":"", "address":null, "statement_descriptor":"N/A", "transaction_id":"", "created_at":1649351034, "metadata":{"number":1234567890, "object":{"string":"person"}, "string":"preferred", "Boolean":true, "array_of_objects":[{"name":"John Doe"}, {"type":"customer"}], "array_of_strings":["color", "size"]}, "failure_code":"", "failure_message":"", "paid":true, "paid_at":1649351034, "dispute":null, "refunds":null, "order":null, "outcome":null, "visual_codes":{}, "textual_codes":{}, "instructions":[], "ewallet_id":"ewallet_1936682fdca7a188c49eb9f9817ade77", "ewallets":[{"ewallet_id":"ewallet_1936682fdca7a188c49eb9f9817ade77", "amount":1000, "percent":100, "refunded_amount":0}], "payment_method_options":{}, "payment_method_type":"us_visa_card", "payment_method_type_category":"card", "fx_rate":1, "merchant_requested_currency":null, "merchant_requested_amount":null, "fixed_side":"", "payment_fees":null, "invoice":"", "escrow":null, "group_payment":"", "cancel_reason":null, "initiation_type":"customer_present", "mid":"", "next_action":"not_applicable", "error_code":"", "remitter_information":{}}} + ) + end + + def successful_ach_purchase_response + %( + {"status":{"error_code":"","status":"SUCCESS","message":"","response_code":"","operation_id":"7362425c-06ef-4a31-b50c-234e84352bb9"},"data":{"id":"payment_59daaa8786d9120a8487dc0b86d32a9e","amount":0,"original_amount":2100,"is_partial":false,"currency_code":"USD","country_code":"US","status":"ACT","description":"","merchant_reference_id":"","customer_token":"cus_99ed3308f30dd5b14c2f2cde40fac98e","payment_method":"other_73b2e0fcd0ddb3200c1fcc5a4aeaeebf","payment_method_data":{"id":"other_73b2e0fcd0ddb3200c1fcc5a4aeaeebf","type":"us_ach_bank","category":"bank_transfer","metadata":{},"image":"","webhook_url":"","supporting_documentation":"","next_action":"not_applicable","last_name":"Smith","first_name":"Jim","account_number":"15378535","routing_number":"244183602","payment_purpose":"Testing Purpose","proof_of_authorization":true},"expiration":1649093215,"captured":true,"refunded":false,"refunded_amount":0,"receipt_email":"","redirect_url":"","complete_payment_url":"","error_payment_url":"","receipt_number":"","flow_type":"","address":null,"statement_descriptor":"N/A","transaction_id":"","created_at":1647883616,"metadata":{},"failure_code":"","failure_message":"","paid":false,"paid_at":0,"dispute":null,"refunds":null,"order":null,"outcome":null,"visual_codes":{},"textual_codes":{},"instructions":{"name":"instructions","steps":[{"step1":"Provide your routing and account number to process the transaction"},{"step2":"Once completed, the transaction will take approximately 2-3 days to process"}]},"ewallet_id":"ewallet_1936682fdca7a188c49eb9f9817ade77","ewallets":[{"ewallet_id":"ewallet_1936682fdca7a188c49eb9f9817ade77","amount":2100,"percent":100,"refunded_amount":0}],"payment_method_options":{},"payment_method_type":"us_ach_bank","payment_method_type_category":"bank_transfer","fx_rate":1,"merchant_requested_currency":null,"merchant_requested_amount":null,"fixed_side":"","payment_fees":null,"invoice":"","escrow":null,"group_payment":"","cancel_reason":null,"initiation_type":"customer_present","mid":"","next_action":"pending_confirmation","error_code":"","remitter_information":{}}} + ) + end + + def failed_purchase_response + %( + {"status":{"error_code":"ERROR_PROCESSING_CARD - [05]","status":"ERROR","message":"Do Not Honor","response_code":"ERROR_PROCESSING_CARD - [05]","operation_id":"5486c9f2-2c11-47eb-adec-993fc3a8c302"}} + ) + end + + def successful_authorize_response + %( + {"status":{"error_code":"","status":"SUCCESS","message":"","response_code":"","operation_id":"4ac3438d-8afe-4fdd-bc93-38f78a2a52ba"},"data":{"id":"payment_e0979a1c6843e5d7bf0c18335794cccb","amount":0,"original_amount":1,"is_partial":false,"currency_code":"USD","country_code":"in","status":"ACT","description":"","merchant_reference_id":"","customer_token":"cus_bcf45118ae3e8bf45abf01aaae8bfd5b","payment_method":"card_23db2eb985533e23cf56de4a46cee312","payment_method_data":{"id":"card_23db2eb985533e23cf56de4a46cee312","type":"in_amex_card","category":"card","metadata":null,"image":"","webhook_url":"","supporting_documentation":"","next_action":"not_applicable","name":"Ryan Reynolds","last4":"1111","acs_check":"unchecked","cvv_check":"unchecked","bin_details":{"type":null,"brand":null,"country":null,"bin_number":"411111"},"expiration_year":"35","expiration_month":"12","fingerprint_token":"ocfp_eb9edd24a3f3f59651aee0bd3d16201e"},"expiration":1648242162,"captured":false,"refunded":false,"refunded_amount":0,"receipt_email":"","redirect_url":"","complete_payment_url":"","error_payment_url":"","receipt_number":"","flow_type":"","address":null,"statement_descriptor":"N/A","transaction_id":"","created_at":1647637362,"metadata":{},"failure_code":"","failure_message":"","paid":false,"paid_at":0,"dispute":null,"refunds":null,"order":null,"outcome":null,"visual_codes":{},"textual_codes":{},"instructions":{},"ewallet_id":"ewallet_1936682fdca7a188c49eb9f9817ade77","ewallets":[{"ewallet_id":"ewallet_1936682fdca7a188c49eb9f9817ade77","amount":1,"percent":100,"refunded_amount":0}],"payment_method_options":{},"payment_method_type":"in_amex_card","payment_method_type_category":"card","fx_rate":1,"merchant_requested_currency":null,"merchant_requested_amount":null,"fixed_side":"","payment_fees":null,"invoice":"","escrow":null,"group_payment":"","cancel_reason":null,"initiation_type":"customer_present","mid":"","next_action":"pending_capture","error_code":"","remitter_information":{}}} + ) + end + + def failed_authorize_response + %( + {"status":{"error_code":"ERROR_PROCESSING_CARD - [05]","status":"ERROR","message":"Do Not Honor","response_code":"ERROR_PROCESSING_CARD - [05]","operation_id":"410488ba-523f-480a-a497-053ca2327866"}} + ) + end + + def successful_capture_response + %( + {"status":{"error_code":"","status":"SUCCESS","message":"","response_code":"","operation_id":"015c41d0-f11a-4a91-9518-dc4117d8017b"},"data":{"id":"payment_e0979a1c6843e5d7bf0c18335794cccb","amount":1,"original_amount":1,"is_partial":false,"currency_code":"USD","country_code":"in","status":"CLO","description":"","merchant_reference_id":"","customer_token":"cus_bcf45118ae3e8bf45abf01aaae8bfd5b","payment_method":"card_23db2eb985533e23cf56de4a46cee312","payment_method_data":{"id":"card_23db2eb985533e23cf56de4a46cee312","type":"in_amex_card","category":"card","metadata":null,"image":"","webhook_url":"","supporting_documentation":"","next_action":"not_applicable","name":"Ryan Reynolds","last4":"1111","acs_check":"unchecked","cvv_check":"unchecked","bin_details":{"type":null,"brand":null,"country":null,"bin_number":"411111"},"expiration_year":"35","expiration_month":"12","fingerprint_token":"ocfp_eb9edd24a3f3f59651aee0bd3d16201e"},"expiration":1648242162,"captured":true,"refunded":false,"refunded_amount":0,"receipt_email":"","redirect_url":"","complete_payment_url":"","error_payment_url":"","receipt_number":"","flow_type":"","address":null,"statement_descriptor":"N/A","transaction_id":"","created_at":1647637362,"metadata":{},"failure_code":"","failure_message":"","paid":true,"paid_at":1647637363,"dispute":null,"refunds":null,"order":null,"outcome":null,"visual_codes":{},"textual_codes":{},"instructions":{},"ewallet_id":"ewallet_1936682fdca7a188c49eb9f9817ade77","ewallets":[{"ewallet_id":"ewallet_1936682fdca7a188c49eb9f9817ade77","amount":1,"percent":100,"refunded_amount":0}],"payment_method_options":{},"payment_method_type":"in_amex_card","payment_method_type_category":"card","fx_rate":1,"merchant_requested_currency":null,"merchant_requested_amount":null,"fixed_side":"","payment_fees":null,"invoice":"","escrow":null,"group_payment":"","cancel_reason":null,"initiation_type":"customer_present","mid":"","next_action":"not_applicable","error_code":"","remitter_information":{}}} + ) + end + + def failed_capture_response + %( + {"status":{"error_code":"ERROR_GET_PAYMENT","status":"ERROR","message":"The request tried to retrieve a payment, but the payment was not found. The request was rejected. Corrective action: Use a valid payment ID.","response_code":"ERROR_GET_PAYMENT","operation_id":"a836ca9b-def8-4e4e-a4e8-a249b0c0e0ff"}} + ) + end + + def successful_refund_response + %( + {"status":{"error_code":"","status":"SUCCESS","message":"","response_code":"","operation_id":"5bfa30c5-c698-4e43-861c-e6fe5e82b324"},"data":{"id":"refund_2a575991bee3b010f44e438f7f6a6d5f","amount":1,"payment":"payment_c861474086bd50305f51ee7855d65eb5","currency":"USD","failure_reason":"","metadata":{},"reason":"","status":"Completed","receipt_number":0,"created_at":1647637499,"updated_at":1647637499,"merchant_reference_id":"","payment_created_at":1647637498,"payment_method_type":"in_amex_card","ewallets":[{"ewallet":"ewallet_1936682fdca7a188c49eb9f9817ade77","amount":1}],"proportional_refund":true,"merchant_debited_amount":null,"merchant_debited_currency":null,"fx_rate":null,"fixed_side":null}} + ) + end + + def failed_refund_response + %( + {"status":{"error_code":"ERROR_GET_PAYMENT","status":"ERROR","message":"The request tried to retrieve a payment, but the payment was not found. The request was rejected. Corrective action: Use a valid payment ID.","response_code":"ERROR_GET_PAYMENT","operation_id":"29a59e7c-8e82-4abe-ad9e-bf47eb72f6c1"}} + ) + end + + def successful_void_response + %( + {"status":{"error_code":"","status":"SUCCESS","message":"","response_code":"","operation_id":"af46ead1-8b34-48c5-903d-07eefef6cbbd"},"data":{"id":"payment_a29a73f09d6f55defddc779dbb2d1089","amount":0,"original_amount":1,"is_partial":false,"currency_code":"USD","country_code":"in","status":"CAN","description":"","merchant_reference_id":"","customer_token":"cus_256d8f8a97f252f32210a27a97b855a5","payment_method":"card_e42d1d0bdca84661f0b62640330c4c65","payment_method_data":{},"expiration":1648242349,"captured":false,"refunded":false,"refunded_amount":0,"receipt_email":"","redirect_url":"","complete_payment_url":"","error_payment_url":"","receipt_number":"","flow_type":"","address":null,"statement_descriptor":"N/A","transaction_id":"","created_at":1647637549,"metadata":{},"failure_code":"","failure_message":"","paid":false,"paid_at":0,"dispute":null,"refunds":null,"order":null,"outcome":null,"visual_codes":{},"textual_codes":{},"instructions":{},"ewallet_id":"ewallet_1936682fdca7a188c49eb9f9817ade77","ewallets":[{"ewallet_id":"ewallet_1936682fdca7a188c49eb9f9817ade77","amount":1,"percent":100,"refunded_amount":0}],"payment_method_options":{},"payment_method_type":"in_amex_card","payment_method_type_category":"card","fx_rate":1,"merchant_requested_currency":null,"merchant_requested_amount":null,"fixed_side":"","payment_fees":null,"invoice":"","escrow":null,"group_payment":"","cancel_reason":null,"initiation_type":"customer_present","mid":"","next_action":"not_applicable","error_code":"","remitter_information":{}}} + ) + end + + def failed_void_response + %( + {"status":{"error_code":"UNAUTHORIZED_API_CALL","status":"ERROR","message":"","response_code":"UNAUTHORIZED_API_CALL","operation_id":"12e59804-b742-44eb-aa49-4b722629faa8"}} + ) + end + + def successful_verify_response + %( + {"status":{"error_code":"","status":"SUCCESS","message":"","response_code":"","operation_id":"27385814-fc69-46fc-bbcc-2a5e0aac442d"},"data":{"id":"payment_2736748fec92a96c7c1280f7e46e2876","amount":0,"original_amount":0,"is_partial":false,"currency_code":"USD","country_code":"US","status":"ACT","description":"","merchant_reference_id":"","customer_token":"cus_c99aab5dae41102b0bb4276ab32e7777","payment_method":"card_5a07af7ff5c038eef4802ffb200fffa6","payment_method_data":{"id":"card_5a07af7ff5c038eef4802ffb200fffa6","type":"us_visa_card","category":"card","metadata":null,"image":"","webhook_url":"","supporting_documentation":"","next_action":"3d_verification","name":"Ryan Reynolds","last4":"1111","acs_check":"unchecked","cvv_check":"unchecked","bin_details":{"type":null,"brand":null,"level":null,"country":null,"bin_number":"411111"},"expiration_year":"35","expiration_month":"12","fingerprint_token":"ocfp_eb9edd24a3f3f59651aee0bd3d16201e"},"expiration":1653942478,"captured":false,"refunded":false,"refunded_amount":0,"receipt_email":"","redirect_url":"https://sandboxcheckout.rapyd.net/3ds-payment?token=payment_2736748fec92a96c7c1280f7e46e2876","complete_payment_url":"","error_payment_url":"","receipt_number":"","flow_type":"","address":null,"statement_descriptor":"N/A","transaction_id":"","created_at":1653337678,"metadata":{},"failure_code":"","failure_message":"","paid":false,"paid_at":0,"dispute":null,"refunds":null,"order":null,"outcome":null,"visual_codes":{},"textual_codes":{},"instructions":[],"ewallet_id":null,"ewallets":[],"payment_method_options":{},"payment_method_type":"us_visa_card","payment_method_type_category":"card","fx_rate":1,"merchant_requested_currency":null,"merchant_requested_amount":null,"fixed_side":"","payment_fees":null,"invoice":"","escrow":null,"group_payment":"","cancel_reason":null,"initiation_type":"customer_present","mid":"","next_action":"3d_verification","error_code":"","remitter_information":{}}} + ) + end + + def successful_store_response + %( + {"status":{"error_code":"","status":"SUCCESS","message":"","response_code":"","operation_id":"47e8bbbc-baa5-43c6-9395-df8a01645e91"},"data":{"id":"cus_4d8509d0997c7ce8aa1f63c19c1b6870","delinquent":false,"discount":null,"name":"Ryan Reynolds","default_payment_method":"card_94a3a70510109163a4eb438f06d82f78","description":"","email":"","phone_number":"","invoice_prefix":"","addresses":[],"payment_methods":{"data":[{"id":"card_94a3a70510109163a4eb438f06d82f78","type":"us_visa_card","category":"card","metadata":null,"image":"https://iconslib.rapyd.net/checkout/us_visa_card.png","webhook_url":"","supporting_documentation":"","next_action":"3d_verification","name":"Ryan Reynolds","last4":"1111","acs_check":"unchecked","cvv_check":"unchecked","bin_details":{"type":null,"brand":null,"level":null,"country":null,"bin_number":"411111"},"expiration_year":"35","expiration_month":"12","fingerprint_token":"ocfp_eb9edd24a3f3f59651aee0bd3d16201e","redirect_url":"https://sandboxcheckout.rapyd.net/3ds-payment?token=payment_f4ab1b25a09cbd769df05b30a29f71a4"}],"has_more":false,"total_count":1,"url":"/v1/customers/cus_4d8509d0997c7ce8aa1f63c19c1b6870/payment_methods"},"subscriptions":null,"created_at":1653487824,"metadata":{},"business_vat_id":"","ewallet":""}} + ) + end + + def successful_unstore_response + %( + {"status":{"error_code":"","status":"SUCCESS","message":"","response_code":"","operation_id":"6f7857f4-e063-4edb-ab93-da60c8563c52"},"data":{"deleted":true,"id":"cus_4d8509d0997c7ce8aa1f63c19c1b6870"}} + ) + end +end diff --git a/test/unit/gateways/reach_test.rb b/test/unit/gateways/reach_test.rb new file mode 100644 index 00000000000..edae4bbb133 --- /dev/null +++ b/test/unit/gateways/reach_test.rb @@ -0,0 +1,258 @@ +require 'test_helper' + +class ReachTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = ReachGateway.new(fixtures(:reach)) + @credit_card = credit_card + @amount = 100 + + @options = { + email: 'johndoe@reach.com', + order_id: '123', + currency: 'USD', + billing_address: { + address1: '1670', + address2: '1670 NW 82ND AVE', + city: 'Miami', + state: 'FL', + zip: '32191', + country: 'US' + } + } + end + + def test_required_merchant_id_and_secret + error = assert_raises(ArgumentError) { ReachGateway.new } + assert_equal 'Missing required parameter: merchant_id', error.message + end + + def test_supported_card_types + assert_equal ReachGateway.supported_cardtypes, %i[visa diners_club american_express jcb master discover maestro] + end + + def test_should_be_able_format_a_request + post = { + request: { someId: 'abc123' }, + card: { number: '12132323', name: 'John doe' } + } + + formatted = @gateway.send :format_and_sign, post + + refute_empty formatted[:signature] + assert_kind_of String, formatted[:request] + assert_kind_of String, formatted[:card] + + assert_equal 'abc123', JSON.parse(formatted[:request])['someId'] + assert_equal '12132323', JSON.parse(formatted[:card])['number'] + assert formatted[:signature].present? + end + + def test_properly_format_on_zero_decilmal + @options[:currency] = 'BYR' + stub_comms do + @gateway.authorize(1000, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(URI.decode_www_form(data)[0][1]) + assert_equal '10', request['Items'].first['ConsumerPrice'] + end.respond_with(successful_purchase_response) + end + + def test_successfully_build_a_purchase + stub_comms do + @gateway.authorize(1250, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(URI.decode_www_form(data)[0][1]) + card = JSON.parse(URI.decode_www_form(data)[1][1]) + + # request + assert_equal request['ReferenceId'], @options[:order_id] + assert_equal request['Consumer']['Email'], @options[:email] + assert_equal request['ConsumerCurrency'], @options[:currency] + assert_equal request['Capture'], false + assert_equal '12.50', request['Items'].first['ConsumerPrice'] + + # card + assert_equal card['Number'], @credit_card.number + assert_equal card['Name'], @credit_card.name + assert_equal card['VerificationCode'], @credit_card.verification_value + end.respond_with(successful_purchase_response) + end + + def test_successfully_build_a_purchase_with_fingerprint + stub_comms do + @options[:device_fingerprint] = '54fd66c2-b5b5-4dbd-ab89-12a8b6177347' + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(URI.decode_www_form(data)[0][1]) + assert_equal request['DeviceFingerprint'], @options[:device_fingerprint] + end.respond_with(successful_purchase_response) + end + + def test_properly_set_capture_flag_on_purchase + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(URI.decode_www_form(data)[0][1]) + assert_equal true, request['Capture'] + end.respond_with(successful_purchase_response) + end + + def test_sending_item_sku_and_item_price + @options[:item_sku] = '1212121212' + @options[:item_quantity] = 250 + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(URI.decode_www_form(data)[0][1]) + + # request + assert_equal request['Items'].first['Sku'], @options[:item_sku] + assert_equal request['Items'].first['Quantity'], @options[:item_quantity] + end.respond_with(successful_purchase_response) + end + + def test_successfull_retrieve_error_message + response = { 'response' => { 'Error' => { 'ReasonCode' => 'is an error' } } } + + message = @gateway.send(:message_from, response) + assert_equal 'is an error', message + end + + def test_safe_retrieve_error_message + response = { 'response' => { 'Error' => { 'Code' => 'is an error' } } } + + message = @gateway.send(:message_from, response) + assert_nil message + end + + def test_sucess_from_on_sucess_result + response = { 'response' => { OrderId: '' } } + + assert @gateway.send(:success_from, response) + end + + def test_sucess_from_on_failure + response = { 'response' => { 'Error' => 'is an error' } } + + refute @gateway.send(:success_from, response) + end + + def test_stored_credential + cases = + [ + { { initial_transaction: true, initiator: 'cardholder', reason_type: 'installment' } => 'CIT-Setup-Scheduled' }, + { { initial_transaction: true, initiator: 'cardholder', reason_type: 'unscheduled' } => 'CIT-Setup-Unscheduled-MIT' }, + { { initial_transaction: true, initiator: 'cardholder', reason_type: 'recurring' } => 'CIT-Setup-Unscheduled' }, + { { initial_transaction: false, initiator: 'cardholder', reason_type: 'unscheduled' } => 'CIT-Subsequent-Unscheduled' }, + { { initial_transaction: false, initiator: 'merchant', reason_type: 'recurring' } => 'MIT-Subsequent-Scheduled' }, + { { initial_transaction: false, initiator: 'merchant', reason_type: 'unscheduled' } => 'MIT-Subsequent-Unscheduled' } + ] + + cases.each do |stored_credential_case| + stored_credential_options = stored_credential_case.keys[0] + expected = stored_credential_case[stored_credential_options] + @options[:stored_credential] = stored_credential_options + stub_comms do + @gateway.expects(:ssl_request).returns(succesful_query_response) + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(URI.decode_www_form(data)[0][1]) + assert_equal expected, request['PaymentModel'] + end.respond_with(successful_purchase_response) + end + end + + def test_stored_credential_with_no_store_credential_parameters + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(URI.decode_www_form(data)[0][1]) + assert_equal 'CIT-One-Time', request['PaymentModel'] + end.respond_with(successful_purchase_response) + end + + def test_stored_credential_with_wrong_combination_stored_credential_paramaters + @options[:stored_credential] = { initiator: 'merchant', initial_transaction: true, reason_type: 'unscheduled' } + @gateway.expects(:get_network_payment_reference).returns(stub(message: 'abc123', success?: true)) + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(URI.decode_www_form(data)[0][1]) + assert_empty request['PaymentModel'] + end.respond_with(successful_purchase_response) + end + + def test_stored_credential_with_at_lest_one_stored_credential_paramaters_nil + @options[:stored_credential] = { initiator: 'merchant', initial_transaction: true, reason_type: nil } + @gateway.expects(:get_network_payment_reference).returns(stub(message: 'abc123', success?: true)) + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(URI.decode_www_form(data)[0][1]) + assert_empty request['PaymentModel'] + end.respond_with(successful_purchase_response) + end + + def test_scrub + assert @gateway.supports_scrubbing? + + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + private + + def successful_purchase_response + 'response=%7B%22OrderId%22%3A%22e8f8c529-15c7-46c1-b28b-9d43bb5efe92%22%2C%22UnderReview%22%3Afalse%2C%22Expiry%22%3A%222022-11-03T12%3A47%3A21Z%22%2C%22Authorized%22%3Atrue%2C%22Completed%22%3Afalse%2C%22Captured%22%3Afalse%7D&signature=JqLa7Y68OYRgRcA5ALHOZwXXzdZFeNzqHma2RT2JWAg%3D' + end + + def succesful_query_response + 'response=%7B%22Meta%22%3A%20null%2C%20%22Rate%22%3A%201.000000000000%2C%20%22Items%22%3A%20%5B%7B%22Sku%22%3A%20%22RLaP7OsSZjbR2pJK%22%2C%20%22Quantity%22%3A%201%2C%20%22ConsumerPrice%22%3A%20100.00%2C%20%22MerchantPrice%22%3A%20100.00%7D%5D%2C%20%22Store%22%3A%20null%2C%20%22Times%22%3A%20%7B%22Created%22%3A%20%222022-12-05T17%3A48%3A18.830991Z%22%2C%20%22Processed%22%3A%20null%2C%20%22Authorized%22%3A%20%222022-12-05T17%3A48%3A19.855608Z%22%7D%2C%20%22Action%22%3A%20null%2C%20%22Expiry%22%3A%20%222022-12-12T17%3A48%3A19.855608Z%22%2C%20%22Reason%22%3A%20null%2C%20%22Charges%22%3A%20null%2C%20%22OrderId%22%3A%20%226ec68268-a4a5-44dd-8997-e76df4aa9c97%22%2C%20%22Payment%22%3A%20%7B%22Class%22%3A%20%22Card%22%2C%20%22Expiry%22%3A%20%222030-03%22%2C%20%22Method%22%3A%20%22VISA%22%2C%20%22AccountIdentifier%22%3A%20%22444433******1111%22%2C%20%22NetworkPaymentReference%22%3A%20%22546646904394415%22%7D%2C%20%22Refunds%22%3A%20%5B%5D%2C%20%22Consumer%22%3A%20%7B%22City%22%3A%20%22Miami%22%2C%20%22Name%22%3A%20%22Longbob%20Longsen%22%2C%20%22Email%22%3A%20%22johndoe%40reach.com%22%2C%20%22Address%22%3A%20%221670%22%2C%20%22Country%22%3A%20%22US%22%2C%20%22EffectiveIpAddress%22%3A%20%22181.78.14.203%22%7D%2C%20%22Shipping%22%3A%20null%2C%20%22Consignee%22%3A%20null%2C%20%22Discounts%22%3A%20null%2C%20%22Financing%22%3A%20null%2C%20%22Chargeback%22%3A%20false%2C%20%22ContractId%22%3A%20null%2C%20%22MerchantId%22%3A%20%22testMerchantId%22%2C%20%22OrderState%22%3A%20%22PaymentAuthorized%22%2C%20%22RateOfferId%22%3A%20%22c754012f-e0fc-4630-9cb5-11c3450f462e%22%2C%20%22ReferenceId%22%3A%20%22123%22%2C%20%22UnderReview%22%3A%20false%2C%20%22ConsumerTotal%22%3A%20100.00%2C%20%22MerchantTotal%22%3A%20100.00%2C%20%22TransactionId%22%3A%20%22e08f6501-2607-4be1-9dba-97d6780dfe9a%22%2C%20%22ConsumerCurrency%22%3A%20%22USD%22%7D&signature=no%2BEojgxrO5JK4wt4EWtbuY9M7h1eVQ9SLezu10X%2Bn4%3D' + end + + def pre_scrubbed + <<-PRE_SCRUBBED + <- "POST /v2.21/checkout HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: checkout.rch.how\r\nContent-Length: 756\r\n\r\n" + <- "request=%7B%22MerchantId%22%3A%22Some-30value-4for3-9test35-f93086cd7crednet1%22%2C%22ReferenceId%22%3A%22123%22%2C%22ConsumerCurrency%22%3A%22USD%22%2C%22Capture%22%3Atrue%2C%22PaymentMethod%22%3A%22VISA%22%2C%22Items%22%3A%5B%7B%22Sku%22%3A%22d99oJA8rkwgQANFJ%22%2C%22ConsumerPrice%22%3A100%2C%22Quantity%22%3A1%7D%5D%2C%22ViaAgent%22%3Atrue%2C%22Consumer%22%3A%7B%22Name%22%3A%22Longbob+Longsen%22%2C%22Email%22%3A%22johndoe%40reach.com%22%2C%22Address%22%3A%221670%22%2C%22City%22%3A%22Miami%22%2C%22Country%22%3A%22US%22%7D%7D&card=%7B%22Name%22%3A%22Longbob+Longsen%22%2C%22Number%22%3A%224444333322221111%22%2C%22Expiry%22%3A%7B%22Month%22%3A3%2C%22Year%22%3A2030%7D%2C%22VerificationCode%22%3A737%7D&signature=5nimSignatUre%3D" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Thu, 03 Nov 2022 23:04:01 GMT\r\n" + -> "Content-Type: application/x-www-form-urlencoded; charset=utf-8\r\n" + -> "Content-Length: 235\r\n" + -> "Connection: close\r\n" + -> "Server: ipCheckoutApi/unreleased ibiHttpServer\r\n" + -> "Strict-Transport-Security: max-age=60000\r\n" + -> "Cache-Control: no-cache\r\n" + -> "Access-Control-Allow-Origin: *\r\n" + -> "\r\n" + reading 235 bytes... + -> "response=%7B%22OrderId%22%3A%22621a0c76-69fb-4c05-854a-e7e731759ad3%22%2C%22UnderReview%22%3Afalse%2C%22Authorized%22%3Atrue%2C%22Completed%22%3Afalse%2C%22Captured%22%3Afalse%7D&signature=23475signature23123%3D" + read 235 bytes + Conn close + PRE_SCRUBBED + end + + def post_scrubbed + <<-POST_SRCUBBED + <- "POST /v2.21/checkout HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: checkout.rch.how\r\nContent-Length: 756\r\n\r\n" + <- "request=%7B%22MerchantId%22%3A%22[FILTERED]%22%2C%22ReferenceId%22%3A%22123%22%2C%22ConsumerCurrency%22%3A%22USD%22%2C%22Capture%22%3Atrue%2C%22PaymentMethod%22%3A%22VISA%22%2C%22Items%22%3A%5B%7B%22Sku%22%3A%22d99oJA8rkwgQANFJ%22%2C%22ConsumerPrice%22%3A100%2C%22Quantity%22%3A1%7D%5D%2C%22ViaAgent%22%3Atrue%2C%22Consumer%22%3A%7B%22Name%22%3A%22Longbob+Longsen%22%2C%22Email%22%3A%22johndoe%40reach.com%22%2C%22Address%22%3A%221670%22%2C%22City%22%3A%22Miami%22%2C%22Country%22%3A%22US%22%7D%7D&card=%7B%22Name%22%3A%22Longbob+Longsen%22%2C%22Number%22%3A%22[FILTERED]%22%2C%22Expiry%22%3A%7B%22Month%22%3A3%2C%22Year%22%3A2030%7D%2C%22VerificationCode%22%3A[FILTERED]%7D&signature=[FILTERED]" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Thu, 03 Nov 2022 23:04:01 GMT\r\n" + -> "Content-Type: application/x-www-form-urlencoded; charset=utf-8\r\n" + -> "Content-Length: 235\r\n" + -> "Connection: close\r\n" + -> "Server: ipCheckoutApi/unreleased ibiHttpServer\r\n" + -> "Strict-Transport-Security: max-age=60000\r\n" + -> "Cache-Control: no-cache\r\n" + -> "Access-Control-Allow-Origin: *\r\n" + -> "\r\n" + reading 235 bytes... + -> "response=%7B%22OrderId%22%3A%22621a0c76-69fb-4c05-854a-e7e731759ad3%22%2C%22UnderReview%22%3Afalse%2C%22Authorized%22%3Atrue%2C%22Completed%22%3Afalse%2C%22Captured%22%3Afalse%7D&signature=[FILTERED]" + read 235 bytes + Conn close + POST_SRCUBBED + end +end diff --git a/test/unit/gateways/realex_test.rb b/test/unit/gateways/realex_test.rb index 4830137b271..c0bc5586583 100644 --- a/test/unit/gateways/realex_test.rb +++ b/test/unit/gateways/realex_test.rb @@ -1,9 +1,16 @@ require 'test_helper' class RealexTest < Test::Unit::TestCase + include CommStub + 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 + public :build_purchase_or_authorization_request + public :build_refund_request + public :build_void_request + public :build_capture_request + public :build_verify_request + public :build_credit_request end def setup @@ -11,52 +18,90 @@ def setup @password = 'your_secret' @account = 'your_account' @rebate_secret = 'your_rebate_secret' + @refund_secret = 'your_refund_secret' @gateway = RealexGateway.new( - :login => @login, - :password => @password, - :account => @account + login: @login, + password: @password, + account: @account ) @gateway_with_account = RealexGateway.new( - :login => @login, - :password => @password, - :account => 'bill_web_cengal' + login: @login, + password: @password, + account: 'bill_web_cengal' ) @credit_card = CreditCard.new( - :number => '4263971921001307', - :month => 8, - :year => 2008, - :first_name => 'Longbob', - :last_name => 'Longsen', - :brand => 'visa' + number: '4263971921001307', + month: 8, + year: 2008, + first_name: 'Longbob', + last_name: 'Longsen', + brand: 'visa' ) @options = { - :order_id => '1' + order_id: '1' } @address = { - :name => 'Longbob Longsen', - :address1 => '123 Fake Street', - :city => 'Belfast', - :state => 'Antrim', - :country => 'Northern Ireland', - :zip => 'BT2 8XX' + name: 'Longbob Longsen', + address1: '123 Fake Street', + city: 'Belfast', + state: 'Antrim', + country: 'Northern Ireland', + zip: 'BT2 8XX' } @amount = 100 end + def test_initialize_sets_refund_and_credit_hashes + refund_secret = 'refund' + rebate_secret = 'rebate' + + gateway = RealexGateway.new( + login: @login, + password: @password, + rebate_secret:, + refund_secret: + ) + + assert gateway.options[:refund_hash] == Digest::SHA1.hexdigest(rebate_secret) + assert gateway.options[:credit_hash] == Digest::SHA1.hexdigest(refund_secret) + end + + def test_initialize_with_nil_refund_and_rebate_secrets + gateway = RealexGateway.new( + login: @login, + password: @password, + rebate_secret: nil, + refund_secret: nil + ) + + assert_false gateway.options.key?(:refund_hash) + assert_false gateway.options.key?(:credit_hash) + end + + def test_initialize_without_refund_and_rebate_secrets + gateway = RealexGateway.new( + login: @login, + password: @password + ) + + assert_false gateway.options.key?(:refund_hash) + assert_false gateway.options.key?(:credit_hash) + end + def test_hash gateway = RealexGateway.new( - :login => 'thestore', - :password => 'mysecret' + login: 'thestore', + password: 'mysecret' ) Time.stubs(:now).returns(Time.new(2001, 4, 3, 12, 32, 45)) gateway.expects(:ssl_post).with(anything, regexp_matches(/9af7064afd307c9f988e8dfc271f9257f1fc02f6/)).returns(successful_purchase_response) - gateway.purchase(29900, credit_card('5105105105105100'), :order_id => 'ORD453-11') + gateway.purchase(29900, credit_card('5105105105105100'), order_id: 'ORD453-11') end def test_successful_purchase @@ -77,6 +122,28 @@ def test_unsuccessful_purchase assert response.test? end + def test_purchase_passes_stored_credential + options = @options.merge({ + stored_credential: { + initial_transaction: true, + reason_type: 'unscheduled', + initiator: 'cardholder', + network_transaction_id: nil + } + }) + + stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + stored_credential_params = Nokogiri::XML.parse(data).xpath('//storedcredential') + + assert_equal stored_credential_params.xpath('type').text, 'oneoff' + assert_equal stored_credential_params.xpath('initiator').text, 'cardholder' + assert_equal stored_credential_params.xpath('sequence').text, 'first' + assert_equal stored_credential_params.xpath('srd').text, '' + end.respond_with(successful_purchase_response) + end + def test_successful_refund @gateway.expects(:ssl_post).returns(successful_refund_response) assert_success @gateway.refund(@amount, '1234;1234;1234') @@ -87,19 +154,22 @@ def test_unsuccessful_refund assert_failure @gateway.refund(@amount, '1234;1234;1234') end - def test_deprecated_credit - @gateway.expects(:ssl_post).returns(successful_refund_response) - assert_deprecation_warning(Gateway::CREDIT_DEPRECATION_MESSAGE) do - assert_success @gateway.credit(@amount, '1234;1234;1234') - end + def test_successful_credit + @gateway.expects(:ssl_post).returns(successful_credit_response) + assert_success @gateway.credit(@amount, @credit_card, @options) + end + + def test_unsuccessful_credit + @gateway.expects(:ssl_post).returns(unsuccessful_credit_response) + assert_failure @gateway.credit(@amount, @credit_card, @options) end def test_supported_countries - assert_equal ['IE', 'GB', 'FR', 'BE', 'NL', 'LU', 'IT', 'US', 'CA', 'ES'], RealexGateway.supported_countries + assert_equal %w[IE GB FR BE NL LU IT US CA ES], RealexGateway.supported_countries end def test_supported_card_types - assert_equal [ :visa, :master, :american_express, :diners_club, :switch, :solo, :laser ], RealexGateway.supported_cardtypes + assert_equal %i[visa master american_express diners_club], RealexGateway.supported_cardtypes end def test_avs_result_not_supported @@ -129,49 +199,86 @@ def test_malformed_xml def test_capture_xml @gateway.expects(:new_timestamp).returns('20090824160201') - valid_capture_xml = <<-SRC -<request timestamp="20090824160201" type="settle"> - <merchantid>your_merchant_id</merchantid> - <account>your_account</account> - <amount>100</amount> - <orderid>1</orderid> - <pasref>4321</pasref> - <authcode>1234</authcode> - <sha1hash>ef0a6c485452f3f94aff336fa90c6c62993056ca</sha1hash> -</request> -SRC + valid_capture_xml = <<~SRC + <request timestamp="20090824160201" type="settle"> + <merchantid>your_merchant_id</merchantid> + <account>your_account</account> + <amount>100</amount> + <orderid>1</orderid> + <pasref>4321</pasref> + <authcode>1234</authcode> + <sha1hash>ef0a6c485452f3f94aff336fa90c6c62993056ca</sha1hash> + </request> + SRC assert_xml_equal valid_capture_xml, @gateway.build_capture_request(@amount, '1;4321;1234', {}) end def test_purchase_xml options = { - :order_id => '1' + order_id: '1', + ip: '123.456.789.0' } @gateway.expects(:new_timestamp).returns('20090824160201') - valid_purchase_request_xml = <<-SRC -<request timestamp="20090824160201" type="auth"> - <merchantid>your_merchant_id</merchantid> - <account>your_account</account> - <orderid>1</orderid> - <amount currency="EUR">100</amount> - <card> - <number>4263971921001307</number> - <expdate>0808</expdate> - <chname>Longbob Longsen</chname> - <type>VISA</type> - <issueno></issueno> - <cvn> - <number></number> - <presind></presind> - </cvn> - </card> - <autosettle flag="1"/> - <sha1hash>3499d7bc8dbacdcfba2286bd74916d026bae630f</sha1hash> -</request> -SRC + valid_purchase_request_xml = <<~SRC + <request timestamp="20090824160201" type="auth"> + <merchantid>your_merchant_id</merchantid> + <account>your_account</account> + <orderid>1</orderid> + <amount currency="EUR">100</amount> + <card> + <number>4263971921001307</number> + <expdate>0808</expdate> + <chname>Longbob Longsen</chname> + <type>VISA</type> + <issueno></issueno> + <cvn> + <number></number> + <presind></presind> + </cvn> + </card> + <autosettle flag="1"/> + <sha1hash>3499d7bc8dbacdcfba2286bd74916d026bae630f</sha1hash> + <tssinfo> + <custipaddress>123.456.789.0</custipaddress> + </tssinfo> + </request> + SRC + + assert_xml_equal valid_purchase_request_xml, @gateway.build_purchase_or_authorization_request(:purchase, @amount, @credit_card, options) + end + + def test_purchase_xml_with_ipv6 + options = { + order_id: '1', + ip: '2a02:c7d:da18:ac00:6d10:4f13:1795:4890' + } + + @gateway.expects(:new_timestamp).returns('20090824160201') + + valid_purchase_request_xml = <<~SRC + <request timestamp="20090824160201" type="auth"> + <merchantid>your_merchant_id</merchantid> + <account>your_account</account> + <orderid>1</orderid> + <amount currency="EUR">100</amount> + <card> + <number>4263971921001307</number> + <expdate>0808</expdate> + <chname>Longbob Longsen</chname> + <type>VISA</type> + <issueno></issueno> + <cvn> + <number></number> + <presind></presind> + </cvn> + </card> + <autosettle flag="1"/> + <sha1hash>3499d7bc8dbacdcfba2286bd74916d026bae630f</sha1hash> + </request> + SRC assert_xml_equal valid_purchase_request_xml, @gateway.build_purchase_or_authorization_request(:purchase, @amount, @credit_card, options) end @@ -179,48 +286,77 @@ def test_purchase_xml def test_void_xml @gateway.expects(:new_timestamp).returns('20090824160201') - valid_void_request_xml = <<-SRC -<request timestamp="20090824160201" type="void"> - <merchantid>your_merchant_id</merchantid> - <account>your_account</account> - <orderid>1</orderid> - <pasref>4321</pasref> - <authcode>1234</authcode> - <sha1hash>4132600f1dc70333b943fc292bd0ca7d8e722f6e</sha1hash> -</request> -SRC + valid_void_request_xml = <<~SRC + <request timestamp="20090824160201" type="void"> + <merchantid>your_merchant_id</merchantid> + <account>your_account</account> + <orderid>1</orderid> + <pasref>4321</pasref> + <authcode>1234</authcode> + <sha1hash>4132600f1dc70333b943fc292bd0ca7d8e722f6e</sha1hash> + </request> + SRC assert_xml_equal valid_void_request_xml, @gateway.build_void_request('1;4321;1234', {}) end + def test_verify_xml + options = { + order_id: '1' + } + @gateway.expects(:new_timestamp).returns('20181026114304') + + valid_verify_request_xml = <<~SRC + <request timestamp="20181026114304" type="otb"> + <merchantid>your_merchant_id</merchantid> + <account>your_account</account> + <orderid>1</orderid> + <card> + <number>4263971921001307</number> + <expdate>0808</expdate> + <chname>Longbob Longsen</chname> + <type>VISA</type> + <issueno></issueno> + <cvn> + <number></number> + <presind></presind> + </cvn> + </card> + <sha1hash>d53aebf1eaee4c3ff4c30f83f27b80ce99ba5644</sha1hash> + </request> + SRC + + assert_xml_equal valid_verify_request_xml, @gateway.build_verify_request(@credit_card, options) + end + def test_auth_xml options = { - :order_id => '1' + order_id: '1' } @gateway.expects(:new_timestamp).returns('20090824160201') - valid_auth_request_xml = <<-SRC -<request timestamp="20090824160201" type="auth"> - <merchantid>your_merchant_id</merchantid> - <account>your_account</account> - <orderid>1</orderid> - <amount currency=\"EUR\">100</amount> - <card> - <number>4263971921001307</number> - <expdate>0808</expdate> - <chname>Longbob Longsen</chname> - <type>VISA</type> - <issueno></issueno> - <cvn> - <number></number> - <presind></presind> - </cvn> - </card> - <autosettle flag="0"/> - <sha1hash>3499d7bc8dbacdcfba2286bd74916d026bae630f</sha1hash> -</request> -SRC + valid_auth_request_xml = <<~SRC + <request timestamp="20090824160201" type="auth"> + <merchantid>your_merchant_id</merchantid> + <account>your_account</account> + <orderid>1</orderid> + <amount currency=\"EUR\">100</amount> + <card> + <number>4263971921001307</number> + <expdate>0808</expdate> + <chname>Longbob Longsen</chname> + <type>VISA</type> + <issueno></issueno> + <cvn> + <number></number> + <presind></presind> + </cvn> + </card> + <autosettle flag="0"/> + <sha1hash>3499d7bc8dbacdcfba2286bd74916d026bae630f</sha1hash> + </request> + SRC assert_xml_equal valid_auth_request_xml, @gateway.build_purchase_or_authorization_request(:authorization, @amount, @credit_card, options) end @@ -228,53 +364,114 @@ def test_auth_xml def test_refund_xml @gateway.expects(:new_timestamp).returns('20090824160201') - valid_refund_request_xml = <<-SRC -<request timestamp="20090824160201" type="rebate"> - <merchantid>your_merchant_id</merchantid> - <account>your_account</account> - <orderid>1</orderid> - <pasref>4321</pasref> - <authcode>1234</authcode> - <amount currency="EUR">100</amount> - <autosettle flag="1"/> - <sha1hash>ef0a6c485452f3f94aff336fa90c6c62993056ca</sha1hash> -</request> -SRC + valid_refund_request_xml = <<~SRC + <request timestamp="20090824160201" type="rebate"> + <merchantid>your_merchant_id</merchantid> + <account>your_account</account> + <orderid>1</orderid> + <pasref>4321</pasref> + <authcode>1234</authcode> + <amount currency="EUR">100</amount> + <autosettle flag="1"/> + <sha1hash>ef0a6c485452f3f94aff336fa90c6c62993056ca</sha1hash> + </request> + SRC assert_xml_equal valid_refund_request_xml, @gateway.build_refund_request(@amount, '1;4321;1234', {}) - end def test_refund_with_rebate_secret_xml - gateway = RealexGateway.new(:login => @login, :password => @password, :account => @account, :rebate_secret => @rebate_secret) + gateway = RealexGateway.new(login: @login, password: @password, account: @account, rebate_secret: @rebate_secret) gateway.expects(:new_timestamp).returns('20090824160201') - valid_refund_request_xml = <<-SRC -<request timestamp="20090824160201" type="rebate"> - <merchantid>your_merchant_id</merchantid> - <account>your_account</account> - <orderid>1</orderid> - <pasref>4321</pasref> - <authcode>1234</authcode> - <amount currency="EUR">100</amount> - <refundhash>f94ff2a7c125a8ad87e5683114ba1e384889240e</refundhash> - <autosettle flag="1"/> - <sha1hash>ef0a6c485452f3f94aff336fa90c6c62993056ca</sha1hash> -</request> -SRC + valid_refund_request_xml = <<~SRC + <request timestamp="20090824160201" type="rebate"> + <merchantid>your_merchant_id</merchantid> + <account>your_account</account> + <orderid>1</orderid> + <pasref>4321</pasref> + <authcode>1234</authcode> + <amount currency="EUR">100</amount> + <refundhash>f94ff2a7c125a8ad87e5683114ba1e384889240e</refundhash> + <autosettle flag="1"/> + <sha1hash>ef0a6c485452f3f94aff336fa90c6c62993056ca</sha1hash> + </request> + SRC assert_xml_equal valid_refund_request_xml, gateway.build_refund_request(@amount, '1;4321;1234', {}) + end + def test_credit_xml + options = { + order_id: '1' + } + + @gateway.expects(:new_timestamp).returns('20190717161006') + + valid_credit_request_xml = <<~SRC + <request timestamp="20190717161006" type="credit"> + <merchantid>your_merchant_id</merchantid> + <account>your_account</account> + <orderid>1</orderid> + <amount currency="EUR">100</amount> + <card> + <number>4263971921001307</number> + <expdate>0808</expdate> + <chname>Longbob Longsen</chname> + <type>VISA</type> + <issueno></issueno> + <cvn> + <number></number> + <presind></presind> + </cvn> + </card> + <autosettle flag="1"/> + <sha1hash>73ff566dcfc3a73bebf1a2d387316162111f030e</sha1hash> + </request> + SRC + + assert_xml_equal valid_credit_request_xml, @gateway.build_credit_request(@amount, @credit_card, options) + end + + def test_credit_with_refund_secret_xml + gateway = RealexGateway.new(login: @login, password: @password, account: @account, refund_secret: @refund_secret) + + gateway.expects(:new_timestamp).returns('20190717161006') + + valid_credit_request_xml = <<~SRC + <request timestamp="20190717161006" type="credit"> + <merchantid>your_merchant_id</merchantid> + <account>your_account</account> + <orderid>1</orderid> + <amount currency="EUR">100</amount> + <card> + <number>4263971921001307</number> + <expdate>0808</expdate> + <chname>Longbob Longsen</chname> + <type>VISA</type> + <issueno></issueno> + <cvn> + <number></number> + <presind></presind> + </cvn> + </card> + <refundhash>bbc192c6eac0132a039c23eae8550a22907c6796</refundhash> + <autosettle flag="1"/> + <sha1hash>73ff566dcfc3a73bebf1a2d387316162111f030e</sha1hash> + </request> + SRC + + assert_xml_equal valid_credit_request_xml, gateway.build_credit_request(@amount, @credit_card, @options) end def test_auth_with_address @gateway.expects(:ssl_post).returns(successful_purchase_response) options = { - :order_id => '1', - :billing_address => @address, - :shipping_address => @address + order_id: '1', + billing_address: @address, + shipping_address: @address } @gateway.expects(:new_timestamp).returns('20090824160201') @@ -283,15 +480,14 @@ def test_auth_with_address assert_instance_of Response, response assert_success response assert response.test? - end def test_zip_in_shipping_address @gateway.expects(:ssl_post).with(anything, regexp_matches(/<code>28\|123<\/code>/)).returns(successful_purchase_response) options = { - :order_id => '1', - :shipping_address => @address + order_id: '1', + shipping_address: @address } @gateway.authorize(@amount, @credit_card, options) @@ -301,8 +497,8 @@ def test_zip_in_billing_address @gateway.expects(:ssl_post).with(anything, regexp_matches(/<code>28\|123<\/code>/)).returns(successful_purchase_response) options = { - :order_id => '1', - :billing_address => @address + order_id: '1', + billing_address: @address } @gateway.authorize(@amount, @credit_card, options) @@ -312,128 +508,270 @@ def test_transcript_scrubbing assert_equal scrubbed_transcript, @gateway.scrub(transcript) end + def test_three_d_secure_1 + @gateway.expects(:ssl_post).returns(successful_purchase_response) + + options = { + order_id: '1', + three_d_secure: { + cavv: '1234', + eci: '1234', + xid: '1234', + version: '1.0.2' + } + } + + response = @gateway.authorize(@amount, @credit_card, options) + assert_equal 'M', response.cvv_result['code'] + end + + def test_auth_xml_with_three_d_secure_1 + options = { + order_id: '1', + three_d_secure: { + cavv: '1234', + eci: '1234', + xid: '1234', + version: '1.0.2' + } + } + + @gateway.expects(:new_timestamp).returns('20090824160201') + + valid_auth_request_xml = <<~SRC + <request timestamp="20090824160201" type="auth"> + <merchantid>your_merchant_id</merchantid> + <account>your_account</account> + <orderid>1</orderid> + <amount currency=\"EUR\">100</amount> + <card> + <number>4263971921001307</number> + <expdate>0808</expdate> + <chname>Longbob Longsen</chname> + <type>VISA</type> + <issueno></issueno> + <cvn> + <number></number> + <presind></presind> + </cvn> + </card> + <autosettle flag="0"/> + <sha1hash>3499d7bc8dbacdcfba2286bd74916d026bae630f</sha1hash> + <mpi> + <cavv>1234</cavv> + <xid>1234</xid> + <eci>1234</eci> + <message_version>1</message_version> + </mpi> + </request> + SRC + + assert_xml_equal valid_auth_request_xml, @gateway.build_purchase_or_authorization_request(:authorization, @amount, @credit_card, options) + end + + def test_auth_xml_with_three_d_secure_2 + options = { + order_id: '1', + three_d_secure: { + cavv: '1234', + eci: '1234', + ds_transaction_id: '1234', + version: '2.1.0' + } + } + + @gateway.expects(:new_timestamp).returns('20090824160201') + + valid_auth_request_xml = <<~SRC + <request timestamp="20090824160201" type="auth"> + <merchantid>your_merchant_id</merchantid> + <account>your_account</account> + <orderid>1</orderid> + <amount currency=\"EUR\">100</amount> + <card> + <number>4263971921001307</number> + <expdate>0808</expdate> + <chname>Longbob Longsen</chname> + <type>VISA</type> + <issueno></issueno> + <cvn> + <number></number> + <presind></presind> + </cvn> + </card> + <autosettle flag="0"/> + <sha1hash>3499d7bc8dbacdcfba2286bd74916d026bae630f</sha1hash> + <mpi> + <authentication_value>1234</authentication_value> + <ds_trans_id>1234</ds_trans_id> + <eci>1234</eci> + <message_version>2.1.0</message_version> + </mpi> + </request> + SRC + + assert_xml_equal valid_auth_request_xml, @gateway.build_purchase_or_authorization_request(:authorization, @amount, @credit_card, options) + end + private def successful_purchase_response - <<-RESPONSE -<response timestamp='20010427043422'> - <merchantid>your merchant id</merchantid> - <account>account to use</account> - <orderid>order id from request</orderid> - <authcode>authcode received</authcode> - <result>00</result> - <message>[ test system ] message returned from system</message> - <pasref> realex payments reference</pasref> - <cvnresult>M</cvnresult> - <batchid>batch id for this transaction (if any)</batchid> - <cardissuer> - <bank>Issuing Bank Name</bank> - <country>Issuing Bank Country</country> - <countrycode>Issuing Bank Country Code</countrycode> - <region>Issuing Bank Region</region> - </cardissuer> - <tss> - <result>89</result> - <check id="1000">9</check> - <check id="1001">9</check> - </tss> - <sha1hash>7384ae67....ac7d7d</sha1hash> - <md5hash>34e7....a77d</md5hash> -</response>" + <<~RESPONSE + <response timestamp='20010427043422'> + <merchantid>your merchant id</merchantid> + <account>account to use</account> + <orderid>order id from request</orderid> + <authcode>authcode received</authcode> + <result>00</result> + <message>[ test system ] message returned from system</message> + <pasref> realex payments reference</pasref> + <cvnresult>M</cvnresult> + <batchid>batch id for this transaction (if any)</batchid> + <cardissuer> + <bank>Issuing Bank Name</bank> + <country>Issuing Bank Country</country> + <countrycode>Issuing Bank Country Code</countrycode> + <region>Issuing Bank Region</region> + </cardissuer> + <tss> + <result>89</result> + <check id="1000">9</check> + <check id="1001">9</check> + </tss> + <sha1hash>7384ae67....ac7d7d</sha1hash> + <md5hash>34e7....a77d</md5hash> + </response>" RESPONSE end def unsuccessful_purchase_response - <<-RESPONSE -<response timestamp='20010427043422'> - <merchantid>your merchant id</merchantid> - <account>account to use</account> - <orderid>order id from request</orderid> - <authcode>authcode received</authcode> - <result>01</result> - <message>[ test system ] message returned from system</message> - <pasref> realex payments reference</pasref> - <cvnresult>M</cvnresult> - <batchid>batch id for this transaction (if any)</batchid> - <cardissuer> - <bank>Issuing Bank Name</bank> - <country>Issuing Bank Country</country> - <countrycode>Issuing Bank Country Code</countrycode> - <region>Issuing Bank Region</region> - </cardissuer> - <tss> - <result>89</result> - <check id="1000">9</check> - <check id="1001">9</check> - </tss> - <sha1hash>7384ae67....ac7d7d</sha1hash> - <md5hash>34e7....a77d</md5hash> -</response>" + <<~RESPONSE + <response timestamp='20010427043422'> + <merchantid>your merchant id</merchantid> + <account>account to use</account> + <orderid>order id from request</orderid> + <authcode>authcode received</authcode> + <result>01</result> + <message>[ test system ] message returned from system</message> + <pasref> realex payments reference</pasref> + <cvnresult>M</cvnresult> + <batchid>batch id for this transaction (if any)</batchid> + <cardissuer> + <bank>Issuing Bank Name</bank> + <country>Issuing Bank Country</country> + <countrycode>Issuing Bank Country Code</countrycode> + <region>Issuing Bank Region</region> + </cardissuer> + <tss> + <result>89</result> + <check id="1000">9</check> + <check id="1001">9</check> + </tss> + <sha1hash>7384ae67....ac7d7d</sha1hash> + <md5hash>34e7....a77d</md5hash> + </response>" RESPONSE end def malformed_unsuccessful_purchase_response - <<-RESPONSE -<response timestamp='20010427043422'> - <merchantid>your merchant id</merchantid> - <account>account to use</account> - <orderid>order id from request</orderid> - <authcode>authcode received</authcode> - <result>01</result> - <message>[ test system ] This is & not awesome</message> - <pasref> realex payments reference</pasref> - <cvnresult>M</cvnresult> - <batchid>batch id for this transaction (if any)</batchid> - <cardissuer> - <bank>Issuing Bank Name</bank> - <country>Issuing Bank Country</country> - <countrycode>Issuing Bank Country Code</countrycode> - <region>Issuing Bank Region</region> - </cardissuer> - <tss> - <result>89</result> - <check id="1000">9</check> - <check id="1001">9</check> - </tss> - <sha1hash>7384ae67....ac7d7d</sha1hash> - <md5hash>34e7....a77d</md5hash> -</response>" + <<~RESPONSE + <response timestamp='20010427043422'> + <merchantid>your merchant id</merchantid> + <account>account to use</account> + <orderid>order id from request</orderid> + <authcode>authcode received</authcode> + <result>01</result> + <message>[ test system ] This is & not awesome</message> + <pasref> realex payments reference</pasref> + <cvnresult>M</cvnresult> + <batchid>batch id for this transaction (if any)</batchid> + <cardissuer> + <bank>Issuing Bank Name</bank> + <country>Issuing Bank Country</country> + <countrycode>Issuing Bank Country Code</countrycode> + <region>Issuing Bank Region</region> + </cardissuer> + <tss> + <result>89</result> + <check id="1000">9</check> + <check id="1001">9</check> + </tss> + <sha1hash>7384ae67....ac7d7d</sha1hash> + <md5hash>34e7....a77d</md5hash> + </response>" RESPONSE end def successful_refund_response - <<-RESPONSE -<response timestamp='20010427043422'> - <merchantid>your merchant id</merchantid> - <account>account to use</account> - <orderid>order id from request</orderid> - <authcode>authcode received</authcode> - <result>00</result> - <message>[ test system ] message returned from system</message> - <pasref> realex payments reference</pasref> - <cvnresult>M</cvnresult> - <batchid>batch id for this transaction (if any)</batchid> - <sha1hash>7384ae67....ac7d7d</sha1hash> - <md5hash>34e7....a77d</md5hash> -</response>" + <<~RESPONSE + <response timestamp='20010427043422'> + <merchantid>your merchant id</merchantid> + <account>account to use</account> + <orderid>order id from request</orderid> + <authcode>authcode received</authcode> + <result>00</result> + <message>[ test system ] message returned from system</message> + <pasref> realex payments reference</pasref> + <cvnresult>M</cvnresult> + <batchid>batch id for this transaction (if any)</batchid> + <sha1hash>7384ae67....ac7d7d</sha1hash> + <md5hash>34e7....a77d</md5hash> + </response>" RESPONSE end def unsuccessful_refund_response + <<~RESPONSE + <response timestamp='20010427043422'> + <merchantid>your merchant id</merchantid> + <account>account to use</account> + <orderid>order id from request</orderid> + <authcode>authcode received</authcode> + <result>508</result> + <message>[ test system ] You may only rebate up to 115% of the original amount.</message> + <pasref> realex payments reference</pasref> + <cvnresult>M</cvnresult> + <batchid>batch id for this transaction (if any)</batchid> + <sha1hash>7384ae67....ac7d7d</sha1hash> + <md5hash>34e7....a77d</md5hash> + </response>" + RESPONSE + end + + def successful_credit_response <<-RESPONSE -<response timestamp='20010427043422'> - <merchantid>your merchant id</merchantid> - <account>account to use</account> - <orderid>order id from request</orderid> - <authcode>authcode received</authcode> - <result>508</result> - <message>[ test system ] You may only rebate up to 115% of the original amount.</message> - <pasref> realex payments reference</pasref> - <cvnresult>M</cvnresult> - <batchid>batch id for this transaction (if any)</batchid> - <sha1hash>7384ae67....ac7d7d</sha1hash> - <md5hash>34e7....a77d</md5hash> -</response>" + <response timestamp="20190717205030"> + <merchantid>spreedly</merchantid> + <account>internet</account> + <orderid>57a861e97273371e6f1b1737a9bc5710</orderid> + <authcode>005030</authcode> + <result>00</result> + <cvnresult>U</cvnresult> + <avspostcoderesponse>U</avspostcoderesponse> + <avsaddressresponse>U</avsaddressresponse> + <batchid>674655</batchid> + <message>AUTH CODE: 005030</message> + <pasref>15633930303644971</pasref> + <timetaken>0</timetaken> + <authtimetaken>0</authtimetaken> + <cardissuer> + <bank>AIB BANK</bank> + <country>IRELAND</country> + <countrycode>IE</countrycode> + <region>EUR</region> + </cardissuer> + <sha1hash>6d2fc...67814</sha1hash> + </response>" + RESPONSE + end + + def unsuccessful_credit_response + <<-RESPONSE + <response timestamp="20190717210119"> + <result>502</result> + <message>Refund Hash not present.</message> + <orderid>_refund_fd4ea2d10b339011bdba89f580c5b207</orderid> + </response>" RESPONSE end @@ -498,10 +836,9 @@ def scrubbed_transcript </address> </tssinfo> </request> - REQUEST + REQUEST end - require 'nokogiri' def assert_xml_equal(expected, actual) assert_xml_equal_recursive(Nokogiri::XML(expected).root, Nokogiri::XML(actual).root) end @@ -513,6 +850,6 @@ def assert_xml_equal_recursive(a, b) assert_equal a1.name, b1.name assert_equal a1.value, b1.value end - a.children.zip(b.children).all?{|a1, b1| assert_xml_equal_recursive(a1, b1)} + a.children.zip(b.children).all? { |a1, b1| assert_xml_equal_recursive(a1, b1) } end end diff --git a/test/unit/gateways/redsys_rest_test.rb b/test/unit/gateways/redsys_rest_test.rb new file mode 100644 index 00000000000..247051799c4 --- /dev/null +++ b/test/unit/gateways/redsys_rest_test.rb @@ -0,0 +1,388 @@ +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 + + @nt_credit_card = network_tokenization_credit_card( + '4895370015293175', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + eci: '07', + source: :network_token, + verification_value: '737', + brand: 'visa' + ) + + @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: + } + } + + 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_successful_purchase_with_network_token + stub_comms(@gateway, :commit) do + @gateway.purchase(100, @nt_credit_card, @options) + end.check_request do |post, _options| + assert_equal post[:DS_MERCHANT_TRANSACTIONTYPE], '0' + assert_equal post[:DS_MERCHANT_AMOUNT], @amount.to_s + assert_equal post[:DS_MERCHANT_CURRENCY], '978' + assert_equal post[:DS_MERCHANT_ORDER], @options[:order_id] + assert_equal post[:Ds_Merchant_TokenData][:token], @nt_credit_card.number + assert_equal post[:Ds_Merchant_TokenData][:tokenCryptogram], @nt_credit_card.payment_cryptogram + assert_equal post[:Ds_Merchant_TokenData][:expirationDate], "#{@nt_credit_card.year.to_s[-2..]}09" + assert_equal post[:DS_MERCHANT_PRODUCTDESCRIPTION], 'Store+Purchase' + assert_equal post[:DS_MERCHANT_DIRECTPAYMENT], true + end.respond_with(successful_purchase_response_with_network_token) + 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_use_of_add_stored_credentials_cit + stored_credentials_post = {} + options = { + stored_credential: { + network_transaction_id: nil, + initial_transaction: true, + reason_type: 'recurring', + initiator: 'cardholder' + } + } + @gateway.send(:add_stored_credentials, stored_credentials_post, options) + assert_equal stored_credentials_post[:DS_MERCHANT_IDENTIFIER], 'REQUIRED' + assert_equal stored_credentials_post[:DS_MERCHANT_COF_TYPE], 'R' + assert_equal stored_credentials_post[:DS_MERCHANT_COF_INI], 'S' + end + + def test_use_of_add_stored_credentials_mit + stored_credentials_post = {} + options = { + stored_credential: { + network_transaction_id: '9999999999', + initial_transaction: false, + reason_type: 'recurring', + initiator: 'merchant' + } + } + @gateway.send(:add_stored_credentials, stored_credentials_post, options) + assert_equal stored_credentials_post[:DS_MERCHANT_COF_TYPE], 'R' + assert_equal stored_credentials_post[:DS_MERCHANT_COF_INI], 'N' + assert_equal stored_credentials_post[:DS_MERCHANT_COF_TXNID], options[:stored_credential][:network_transaction_id] + end + + def test_use_of_three_ds_exemption + post = {} + options = { three_ds_exemption_type: 'low_value' } + @gateway.send(:add_threeds_exemption_data, post, options) + assert_equal post[:DS_MERCHANT_EXCEP_SCA], 'LWV' + end + + def test_use_of_three_ds_exemption_moto_option + post = {} + options = { three_ds_exemption_type: 'moto' } + @gateway.send(:add_threeds_exemption_data, post, options) + assert_equal post[:DS_MERCHANT_DIRECTPAYMENT], 'MOTO' + 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 patagonia_365 tarjeta_sol], 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 + + def successful_purchase_response_with_network_token + %[{\"Ds_SignatureVersion\":\"HMAC_SHA256_V1\",\"Ds_MerchantParameters\":\"eyJEc19BbW91bnQiOiIxMDAiLCJEc19DdXJyZW5jeSI6Ijk3OCIsIkRzX09yZGVyIjoiMTc3ODY4ODM3LjMzIiwiRHNfTWVyY2hhbnRDb2RlIjoiOTk5MDA4ODgxIiwiRHNfVGVybWluYWwiOiIxIiwiRHNfUmVzcG9uc2UiOiIwMTk1IiwiRHNfQXV0aG9yaXNhdGlvbkNvZGUiOiIiLCJEc19UcmFuc2FjdGlvblR5cGUiOiIwIiwiRHNfU2VjdXJlUGF5bWVudCI6IjAiLCJEc19MYW5ndWFnZSI6IjEiLCJEc19DYXJkTnVtYmVyIjoiNDU0ODgxKioqKioqMDAwNCIsIkRzX01lcmNoYW50RGF0YSI6IiIsIkRzX0NhcmRfQ291bnRyeSI6IjcyNCIsIkRzX1Byb2Nlc3NlZFBheU1ldGhvZCI6IjMiLCJEc19Db250cm9sXzE3MzE1MTEzMjQ1MzYiOiIxNzMxNTExMzI0NTM2IiwiRHNfRUNJIjoiMDciLCJEc19SZXNwb25zZV9EZXNjcmlwdGlvbiI6IkVNSVNPUiBFWElHRSBBVVRFTlRJQ0FDScOTTiJ9\",\"Ds_Signature\":\"rn7nE_-I6V3cbxGN_0EK7SM8CcaMud7bssHzP97OOs8=\"}] + end +end diff --git a/test/unit/gateways/redsys_sha256_test.rb b/test/unit/gateways/redsys_sha256_test.rb index f1fbac3c70f..ab92d91b55b 100644 --- a/test/unit/gateways/redsys_sha256_test.rb +++ b/test/unit/gateways/redsys_sha256_test.rb @@ -6,12 +6,13 @@ class RedsysSHA256Test < Test::Unit::TestCase def setup Base.mode = :test @credentials = { - :login => '091952713', - :secret_key => 'QIK77hYl6UFcoCYFKcj+ZjJg8Q6I93Dx', - :signature_algorithm => 'sha256' + login: '091952713', + secret_key: 'QIK77hYl6UFcoCYFKcj+ZjJg8Q6I93Dx', + signature_algorithm: 'sha256' } @gateway = RedsysGateway.new(@credentials) @credit_card = credit_card('4548812049400004') + @threeds2_credit_card = credit_card('4918019199883839') @headers = { 'Content-Type' => 'application/x-www-form-urlencoded' } @@ -22,17 +23,17 @@ def test_purchase_payload @credit_card.month = 9 @credit_card.year = 2017 @gateway.expects(:ssl_post).with(RedsysGateway.test_url, purchase_request, @headers).returns(successful_purchase_response) - @gateway.purchase(100, @credit_card, :order_id => '144742736014') + @gateway.purchase(100, @credit_card, order_id: '144742736014') end def test_purchase_payload_with_credit_card_token @gateway.expects(:ssl_post).with(RedsysGateway.test_url, purchase_request_with_credit_card_token, @headers).returns(successful_purchase_response) - @gateway.purchase(100, '3126bb8b80a79e66eb1ecc39e305288b60075f86', :order_id => '144742884282') + @gateway.purchase(100, '3126bb8b80a79e66eb1ecc39e305288b60075f86', order_id: '144742884282') end def test_successful_purchase @gateway.expects(:ssl_post).returns(successful_purchase_response) - res = @gateway.purchase(100, credit_card, :order_id => '144742736014') + res = @gateway.purchase(100, credit_card, order_id: '144742736014') assert_success res assert_equal 'Transaction Approved', res.message assert_equal '144742736014|100|978', res.authorization @@ -42,7 +43,7 @@ def test_successful_purchase # This one is being werid... def test_successful_purchase_requesting_credit_card_token @gateway.expects(:ssl_post).returns(successful_purchase_response_with_credit_card_token) - res = @gateway.purchase(100, 'e55e1d0ef338e281baf1d0b5b68be433260ddea0', :order_id => '144742955848') + res = @gateway.purchase(100, 'e55e1d0ef338e281baf1d0b5b68be433260ddea0', order_id: '144742955848') assert_success res assert_equal 'Transaction Approved', res.message assert_equal '144742955848|100|978', res.authorization @@ -52,7 +53,7 @@ def test_successful_purchase_requesting_credit_card_token def test_failed_purchase @gateway.expects(:ssl_post).returns(failed_purchase_response) - res = @gateway.purchase(100, credit_card, :order_id => '144743314659') + res = @gateway.purchase(100, credit_card, order_id: '144743314659') assert_failure res assert_equal 'SIS0093 ERROR', res.message end @@ -65,7 +66,7 @@ def test_purchase_without_order_id def test_error_purchase @gateway.expects(:ssl_post).returns(error_purchase_response) - res = @gateway.purchase(100, credit_card, :order_id => '123') + res = @gateway.purchase(100, credit_card, order_id: '123') assert_failure res assert_equal 'SIS0051 ERROR', res.message end @@ -104,7 +105,7 @@ def test_authorize ), anything ).returns(successful_authorize_response) - response = @gateway.authorize(100, credit_card, :order_id => '144743367273') + response = @gateway.authorize(100, credit_card, order_id: '144743367273') assert_success response end @@ -114,10 +115,203 @@ def test_authorize_without_order_id end end + def test_successful_authorize_with_3ds + @gateway.expects(:ssl_post).returns(successful_authorize_with_3ds_response) + response = @gateway.authorize(100, credit_card, { execute_threed: true, order_id: '156270437866' }) + assert response.test? + assert response.params['ds_emv3ds'] + assert_equal response.message, 'CardConfiguration' + assert_equal response.authorization, '156270437866||' + end + + def test_successful_purchase_with_3ds + @gateway.expects(:ssl_post).returns(successful_authorize_with_3ds_response) + response = @gateway.purchase(100, credit_card, { execute_threed: true, order_id: '156270437866' }) + assert response.test? + assert response.params['ds_emv3ds'] + assert_equal response.message, 'CardConfiguration' + assert_equal response.authorization, '156270437866||' + end + + def test_successful_purchase_with_3ds2 + @gateway.expects(:ssl_post).returns(successful_authorize_with_3ds_response) + response = @gateway.purchase(100, @threeds2_credit_card, { execute_threed: true, order_id: '156270437866' }) + assert response.test? + assert response.params['ds_emv3ds'] + assert_equal response.message, 'CardConfiguration' + assert_equal response.authorization, '156270437866||' + end + + def test_successful_purchase_with_3ds2_and_mit_exemption + @gateway.expects(:ssl_post).returns(successful_purchase_with_3ds2_and_mit_exemption_response) + response = @gateway.purchase(100, @threeds2_credit_card, { execute_threed: true, order_id: '161608782525', sca_exemption: 'MIT', sca_exemption_direct_payment_enabled: true }) + assert response.test? + assert response.params['ds_emv3ds'] + + assert_equal response.message, 'CardConfiguration' + assert_equal response.authorization, '161608782525||' + + assert response.params['ds_card_psd2'] + assert_equal '2.1.0', JSON.parse(response.params['ds_emv3ds'])['protocolVersion'] + assert_equal 'Y', response.params['ds_card_psd2'] + assert_equal 'CardConfiguration', response.message + end + + def test_3ds_data_passed + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(100, credit_card, { execute_threed: true, order_id: '156270437866', terminal: 12, sca_exemption: 'LWV' }) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/iniciaPeticion/, data) + assert_match(/<DS_MERCHANT_TERMINAL>12<\/DS_MERCHANT_TERMINAL>/, data) + assert_match(/\"threeDSInfo\":\"CardData\"/, data) + + # as per docs on Inicia Peticion Y must be passed + assert_match(/<DS_MERCHANT_EXCEP_SCA>Y<\/DS_MERCHANT_EXCEP_SCA>/, data) + end.respond_with(successful_authorize_with_3ds_response) + end + + def test_3ds_data_with_special_characters_properly_escaped + @credit_card.first_name = 'Julián' + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(100, @credit_card, { execute_threed: true, order_id: '156270437866', terminal: 12, sca_exemption: 'LWV', description: 'esta es la descripción' }) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/iniciaPeticion/, data) + assert_match(/<DS_MERCHANT_TERMINAL>12<\/DS_MERCHANT_TERMINAL>/, data) + assert_match(/\"threeDSInfo\":\"CardData\"/, data) + + # as per docs on Inicia Peticion Y must be passed + assert_match(/<DS_MERCHANT_EXCEP_SCA>Y<\/DS_MERCHANT_EXCEP_SCA>/, data) + assert_match(/Juli%C3%A1n/, data) + assert_match(/descripci%C3%B3n/, data) + end.respond_with(successful_authorize_with_3ds_response) + end + + def test_3ds1_data_passed_as_mpi + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(100, credit_card, { order_id: '156270437866', description: 'esta es la descripción', three_d_secure: { version: '1.0.2', xid: 'xid', ds_transaction_id: 'ds_transaction_id', cavv: 'cavv', eci: '02' } }) + end.check_request do |_method, _endpoint, encdata, _headers| + data = CGI.unescape(encdata) + assert_match(/<DS_MERCHANT_MPIEXTERNAL>/, data) + assert_match(%r("TXID":"xid"), data) + assert_match(%r("CAVV":"cavv"), data) + assert_match(%r("ECI":"02"), data) + + assert_not_match(%r("authenticacionMethod"), data) + assert_not_match(%r("authenticacionType"), data) + assert_not_match(%r("authenticacionFlow"), data) + + assert_not_match(%r("protocolVersion":"2.1.0"), data) + assert_match(/descripción/, data) + end.respond_with(successful_authorize_with_3ds_response) + end + + def test_3ds2_data_passed_as_mpi + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(100, credit_card, { order_id: '156270437866', description: 'esta es la descripción', three_d_secure: { version: '2.1.0', three_ds_server_trans_id: 'three_ds_server_trans_id', ds_transaction_id: 'ds_transaction_id', cavv: 'cavv', eci: '02' } }) + end.check_request do |_method, _endpoint, encdata, _headers| + data = CGI.unescape(encdata) + assert_match(/<DS_MERCHANT_MPIEXTERNAL>/, data) + assert_match(%r("threeDSServerTransID":"three_ds_server_trans_id"), data) + assert_match(%r("dsTransID":"ds_transaction_id"), data) + assert_match(%r("authenticacionValue":"cavv"), data) + assert_match(%r("Eci":"02"), data) + + assert_not_match(%r("authenticacionMethod"), data) + assert_not_match(%r("authenticacionType"), data) + assert_not_match(%r("authenticacionFlow"), data) + + assert_match(%r("protocolVersion":"2.1.0"), data) + assert_match(/descripción/, data) + end.respond_with(successful_authorize_with_3ds_response) + end + + def test_3ds2_data_passed_as_mpi_with_optional_values + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(100, credit_card, { order_id: '156270437866', description: 'esta es la descripción', three_d_secure: { version: '2.1.0', three_ds_server_trans_id: 'three_ds_server_trans_id', ds_transaction_id: 'ds_transaction_id', cavv: 'cavv', eci: '02' }, + authentication_method: '01', + authentication_type: 'anything', + authentication_flow: 'F' }) + end.check_request do |_method, _endpoint, encdata, _headers| + data = CGI.unescape(encdata) + assert_match(/<DS_MERCHANT_MPIEXTERNAL>/, data) + assert_match(%r("threeDSServerTransID":"three_ds_server_trans_id"), data) + assert_match(%r("dsTransID":"ds_transaction_id"), data) + assert_match(%r("authenticacionValue":"cavv"), data) + assert_match(%r("Eci":"02"), data) + + assert_match(%r("authenticacionMethod":"01"), data) + assert_match(%r("authenticacionType":"anything"), data) + assert_match(%r("authenticacionFlow":"F"), data) + + assert_match(%r("protocolVersion":"2.1.0"), data) + assert_match(/descripción/, data) + end.respond_with(successful_authorize_with_3ds_response) + end + + def test_3ds2_data_as_mpi_with_special_characters_properly_escaped + @credit_card.first_name = 'Julián' + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(100, @credit_card, { order_id: '156270437866', terminal: 12, description: 'esta es la descripción', three_d_secure: { version: '2.1.0', xid: 'xid', ds_transaction_id: 'ds_transaction_id', cavv: 'cavv' } }) + end.check_request do |_method, _endpoint, encdata, _headers| + assert_match(/Juli%C3%A1n/, encdata) + assert_match(%r(descripci%C3%B3n), encdata) + end.respond_with(successful_authorize_with_3ds_response) + end + + def test_mit_exemption_sets_direct_payment + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(100, @threeds2_credit_card, { order_id: '161608782525', terminal: 12, sca_exemption: 'MIT' }) + end.check_request do |_method, _endpoint, encdata, _headers| + assert_match(/<DS_MERCHANT_DIRECTPAYMENT>true/, encdata) + end.respond_with(successful_non_3ds_purchase_with_mit_exemption_response) + end + + def test_mit_exemption_hits_webservice_endpoint + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(100, @threeds2_credit_card, { order_id: '161608782525', terminal: 12, sca_exemption: 'MIT' }) + end.check_request do |_method, endpoint, _encdata, _headers| + assert_match(/\/sis\/services\/SerClsWSEntradaV2/, endpoint) + end.respond_with(successful_non_3ds_purchase_with_mit_exemption_response) + end + + def test_webservice_endpoint_override_hits_webservice_endpoint + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(100, @credit_card, { order_id: '156270437866', use_webservice_endpoint: true }) + end.check_request do |_method, endpoint, _encdata, _headers| + assert_match(/\/sis\/services\/SerClsWSEntradaV2/, endpoint) + end.respond_with(successful_non_3ds_purchase_with_mit_exemption_response) + end + + def test_webservice_endpoint_requests_escapes_special_characters_in_card_name_and_description + @credit_card.first_name = 'Julián' + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(100, @credit_card, { order_id: '156270437866', description: 'esta es la descripción', use_webservice_endpoint: true }) + end.check_request do |_method, _endpoint, encdata, _headers| + assert_match(/Juli%C3%A1n/, encdata) + assert_match(%r(descripci%C3%B3n), encdata) + end.respond_with(successful_non_3ds_purchase_with_mit_exemption_response) + end + + def test_moto_flag_passed + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(100, credit_card, { order_id: '156270437866', moto: true, metadata: { manual_entry: true } }) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/DS_MERCHANT_DIRECTPAYMENT%3Emoto%3C%2FDS_MERCHANT_DIRECTPAYMENT/, data) + end.respond_with(successful_authorize_with_3ds_response) + end + + def test_moto_flag_not_passed_if_not_explicitly_requested + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(100, credit_card, { order_id: '156270437866', metadata: { manual_entry: true } }) + end.check_request do |_method, _endpoint, data, _headers| + refute_match(/DS_MERCHANT_DIRECTPAYMENT%3Emoto%3C%2FDS_MERCHANT_DIRECTPAYMENT/, data) + end.respond_with(successful_authorize_with_3ds_response) + end + def test_bad_order_id_format stub_comms(@gateway, :ssl_request) do @gateway.authorize(100, credit_card, order_id: 'Una#cce-ptable44Format') - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/MERCHANT_ORDER%3E\d\d\d\dUnaccept%3C/, data) end.respond_with(successful_authorize_response) end @@ -125,7 +319,7 @@ def test_bad_order_id_format def test_order_id_numeric_start_but_too_long stub_comms(@gateway, :ssl_request) do @gateway.authorize(100, credit_card, order_id: '1234ThisIs]FineButTooLong') - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/MERCHANT_ORDER%3E1234ThisIsFi%3C/, data) end.respond_with(successful_authorize_response) end @@ -163,25 +357,18 @@ def test_override_currency includes(CGI.escape('<DS_MERCHANT_CURRENCY>840</DS_MERCHANT_CURRENCY>')), anything ).returns(successful_purchase_response) - @gateway.authorize(100, credit_card, :order_id => '1001', :currency => 'USD') + @gateway.authorize(100, credit_card, order_id: '1001', currency: 'USD') 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) - response = @gateway.verify(credit_card, :order_id => '144743367273') + @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) - response = @gateway.verify(credit_card, :order_id => '141278225678') + @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 end @@ -197,11 +384,11 @@ 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 - assert_equal [:visa, :master, :american_express, :jcb, :diners_club], RedsysGateway.supported_cardtypes + assert_equal %i[visa master american_express jcb diners_club unionpay patagonia_365 tarjeta_sol], RedsysGateway.supported_cardtypes end def test_using_test_mode @@ -212,10 +399,10 @@ def test_using_test_mode def test_overriding_options Base.mode = :production gw = RedsysGateway.new( - :terminal => 1, - :login => '1234', - :secret_key => '12345', - :test => true + terminal: 1, + login: '1234', + secret_key: '12345', + test: true ) assert gw.test? assert_equal RedsysGateway.test_url, gw.send(:url) @@ -224,9 +411,9 @@ def test_overriding_options def test_production_mode Base.mode = :production gw = RedsysGateway.new( - :terminal => 1, - :login => '1234', - :secret_key => '12345' + terminal: 1, + login: '1234', + secret_key: '12345' ) assert !gw.test? assert_equal RedsysGateway.live_url, gw.send(:url) @@ -240,6 +427,10 @@ def test_failed_transaction_transcript_scrubbing assert_equal failed_transaction_post_scrubbed, @gateway.scrub(failed_transaction_pre_scrubbed) end + def test_failed_3ds_transaction_transcript_scrubbing + assert_equal failed_3ds_transaction_post_scrubbed, @gateway.scrub(failed_3ds_transaction_pre_scrubbed) + end + def test_nil_cvv_transcript_scrubbing assert_equal nil_cvv_post_scrubbed, @gateway.scrub(nil_cvv_pre_scrubbed) end @@ -262,11 +453,11 @@ def generate_order_id # one with card and another without. def purchase_request - 'entrada=%3C%3Fxml+version%3D%221.0%22+encoding%3D%22UTF-8%22%3F%3E%3CREQUEST%3E%3CDATOSENTRADA%3E%3CDS_Version%3E0.1%3C%2FDS_Version%3E%3CDS_MERCHANT_CURRENCY%3E978%3C%2FDS_MERCHANT_CURRENCY%3E%3CDS_MERCHANT_AMOUNT%3E100%3C%2FDS_MERCHANT_AMOUNT%3E%3CDS_MERCHANT_ORDER%3E144742736014%3C%2FDS_MERCHANT_ORDER%3E%3CDS_MERCHANT_TRANSACTIONTYPE%3EA%3C%2FDS_MERCHANT_TRANSACTIONTYPE%3E%3CDS_MERCHANT_PRODUCTDESCRIPTION%2F%3E%3CDS_MERCHANT_TERMINAL%3E1%3C%2FDS_MERCHANT_TERMINAL%3E%3CDS_MERCHANT_MERCHANTCODE%3E091952713%3C%2FDS_MERCHANT_MERCHANTCODE%3E%3CDS_MERCHANT_TITULAR%3ELongbob+Longsen%3C%2FDS_MERCHANT_TITULAR%3E%3CDS_MERCHANT_PAN%3E4548812049400004%3C%2FDS_MERCHANT_PAN%3E%3CDS_MERCHANT_EXPIRYDATE%3E1709%3C%2FDS_MERCHANT_EXPIRYDATE%3E%3CDS_MERCHANT_CVV2%3E123%3C%2FDS_MERCHANT_CVV2%3E%3C%2FDATOSENTRADA%3E%3CDS_SIGNATUREVERSION%3EHMAC_SHA256_V1%3C%2FDS_SIGNATUREVERSION%3E%3CDS_SIGNATURE%3Eq9QH2P%2B4qm8w%2FS85KRPVaepWOrOT2RXlEmyPUce5XRM%3D%3C%2FDS_SIGNATURE%3E%3C%2FREQUEST%3E' + 'entrada=%3C%3Fxml+version%3D%221.0%22+encoding%3D%22UTF-8%22%3F%3E%3CREQUEST%3E%3CDATOSENTRADA%3E%3CDS_Version%3E0.1%3C%2FDS_Version%3E%3CDS_MERCHANT_CURRENCY%3E978%3C%2FDS_MERCHANT_CURRENCY%3E%3CDS_MERCHANT_AMOUNT%3E100%3C%2FDS_MERCHANT_AMOUNT%3E%3CDS_MERCHANT_ORDER%3E144742736014%3C%2FDS_MERCHANT_ORDER%3E%3CDS_MERCHANT_TRANSACTIONTYPE%3E0%3C%2FDS_MERCHANT_TRANSACTIONTYPE%3E%3CDS_MERCHANT_PRODUCTDESCRIPTION%2F%3E%3CDS_MERCHANT_TERMINAL%3E1%3C%2FDS_MERCHANT_TERMINAL%3E%3CDS_MERCHANT_MERCHANTCODE%3E091952713%3C%2FDS_MERCHANT_MERCHANTCODE%3E%3CDS_MERCHANT_TITULAR%3ELongbob+Longsen%3C%2FDS_MERCHANT_TITULAR%3E%3CDS_MERCHANT_PAN%3E4548812049400004%3C%2FDS_MERCHANT_PAN%3E%3CDS_MERCHANT_EXPIRYDATE%3E1709%3C%2FDS_MERCHANT_EXPIRYDATE%3E%3CDS_MERCHANT_CVV2%3E123%3C%2FDS_MERCHANT_CVV2%3E%3C%2FDATOSENTRADA%3E%3CDS_SIGNATUREVERSION%3EHMAC_SHA256_V1%3C%2FDS_SIGNATUREVERSION%3E%3CDS_SIGNATURE%3Ef46TQxKLJJ6SjcETDp%2Bul92Qsb5kVve2QzGnZMj8JkI%3D%3C%2FDS_SIGNATURE%3E%3C%2FREQUEST%3E' end def purchase_request_with_credit_card_token - 'entrada=%3C%3Fxml+version%3D%221.0%22+encoding%3D%22UTF-8%22%3F%3E%3CREQUEST%3E%3CDATOSENTRADA%3E%3CDS_Version%3E0.1%3C%2FDS_Version%3E%3CDS_MERCHANT_CURRENCY%3E978%3C%2FDS_MERCHANT_CURRENCY%3E%3CDS_MERCHANT_AMOUNT%3E100%3C%2FDS_MERCHANT_AMOUNT%3E%3CDS_MERCHANT_ORDER%3E144742884282%3C%2FDS_MERCHANT_ORDER%3E%3CDS_MERCHANT_TRANSACTIONTYPE%3EA%3C%2FDS_MERCHANT_TRANSACTIONTYPE%3E%3CDS_MERCHANT_PRODUCTDESCRIPTION%2F%3E%3CDS_MERCHANT_TERMINAL%3E1%3C%2FDS_MERCHANT_TERMINAL%3E%3CDS_MERCHANT_MERCHANTCODE%3E091952713%3C%2FDS_MERCHANT_MERCHANTCODE%3E%3CDS_MERCHANT_IDENTIFIER%3E3126bb8b80a79e66eb1ecc39e305288b60075f86%3C%2FDS_MERCHANT_IDENTIFIER%3E%3CDS_MERCHANT_DIRECTPAYMENT%3Etrue%3C%2FDS_MERCHANT_DIRECTPAYMENT%3E%3C%2FDATOSENTRADA%3E%3CDS_SIGNATUREVERSION%3EHMAC_SHA256_V1%3C%2FDS_SIGNATUREVERSION%3E%3CDS_SIGNATURE%3EN0tYMrHGf1PmmJ7WIiRONdqbIGmyhaV%2BhP4acTyfJYE%3D%3C%2FDS_SIGNATURE%3E%3C%2FREQUEST%3E' + 'entrada=%3C%3Fxml+version%3D%221.0%22+encoding%3D%22UTF-8%22%3F%3E%3CREQUEST%3E%3CDATOSENTRADA%3E%3CDS_Version%3E0.1%3C%2FDS_Version%3E%3CDS_MERCHANT_CURRENCY%3E978%3C%2FDS_MERCHANT_CURRENCY%3E%3CDS_MERCHANT_AMOUNT%3E100%3C%2FDS_MERCHANT_AMOUNT%3E%3CDS_MERCHANT_ORDER%3E144742884282%3C%2FDS_MERCHANT_ORDER%3E%3CDS_MERCHANT_TRANSACTIONTYPE%3E0%3C%2FDS_MERCHANT_TRANSACTIONTYPE%3E%3CDS_MERCHANT_PRODUCTDESCRIPTION%2F%3E%3CDS_MERCHANT_TERMINAL%3E1%3C%2FDS_MERCHANT_TERMINAL%3E%3CDS_MERCHANT_MERCHANTCODE%3E091952713%3C%2FDS_MERCHANT_MERCHANTCODE%3E%3CDS_MERCHANT_IDENTIFIER%3E3126bb8b80a79e66eb1ecc39e305288b60075f86%3C%2FDS_MERCHANT_IDENTIFIER%3E%3CDS_MERCHANT_DIRECTPAYMENT%3Etrue%3C%2FDS_MERCHANT_DIRECTPAYMENT%3E%3C%2FDATOSENTRADA%3E%3CDS_SIGNATUREVERSION%3EHMAC_SHA256_V1%3C%2FDS_SIGNATUREVERSION%3E%3CDS_SIGNATURE%3Eeozf9m%2FmDx7JKtcJSPvUa%2FdCZQmzzEAU2nrOVD84fp4%3D%3C%2FDS_SIGNATURE%3E%3C%2FREQUEST%3E' end def successful_purchase_response @@ -289,6 +480,18 @@ def successful_authorize_response "<?xml version='1.0' encoding=\"UTF-8\" ?><RETORNOXML><CODIGO>0</CODIGO><Ds_Version>0.1</Ds_Version><OPERACION><Ds_Amount>100</Ds_Amount><Ds_Currency>978</Ds_Currency><Ds_Order>144743367273</Ds_Order><Ds_Signature>29qv8K/6k3P1zyk5F+ZYmMel0uuOzC58kXCgp5rcnhI=</Ds_Signature><Ds_MerchantCode>091952713</Ds_MerchantCode><Ds_Terminal>1</Ds_Terminal><Ds_Response>0000</Ds_Response><Ds_AuthorisationCode>399957</Ds_AuthorisationCode><Ds_TransactionType>1</Ds_TransactionType><Ds_SecurePayment>0</Ds_SecurePayment><Ds_Language>1</Ds_Language><Ds_MerchantData></Ds_MerchantData><Ds_Card_Country>724</Ds_Card_Country></OPERACION></RETORNOXML>\n" end + def successful_authorize_with_3ds_response + '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><soapenv:Header/><soapenv:Body><p231:iniciaPeticionResponse xmlns:p231="http://webservice.sis.sermepa.es"><p231:iniciaPeticionReturn>&lt;RETORNOXML&gt;&lt;CODIGO&gt;0&lt;/CODIGO&gt;&lt;INFOTARJETA&gt;&lt;Ds_Order&gt;156270437866&lt;/Ds_Order&gt;&lt;Ds_MerchantCode&gt;091952713&lt;/Ds_MerchantCode&gt;&lt;Ds_Terminal&gt;1&lt;/Ds_Terminal&gt;&lt;Ds_TransactionType&gt;0&lt;/Ds_TransactionType&gt;&lt;Ds_EMV3DS&gt;{&quot;protocolVersion&quot;:&quot;NO_3DS_v2&quot;,&quot;threeDSInfo&quot;:&quot;CardConfiguration&quot;}&lt;/Ds_EMV3DS&gt;&lt;Ds_Signature&gt;LIWUaQh+lwsE0DBNpv2EOYALCY6ZxHDQ6gLvOcWiSB4=&lt;/Ds_Signature&gt;&lt;/INFOTARJETA&gt;&lt;/RETORNOXML&gt;</p231:iniciaPeticionReturn></p231:iniciaPeticionResponse></soapenv:Body></soapenv:Envelope>' + end + + def successful_purchase_with_3ds2_and_mit_exemption_response + '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><soapenv:Header/><soapenv:Body><p231:iniciaPeticionResponse xmlns:p231="http://webservice.sis.sermepa.es"><p231:iniciaPeticionReturn>&lt;RETORNOXML&gt;&lt;CODIGO&gt;0&lt;/CODIGO&gt;&lt;INFOTARJETA&gt;&lt;Ds_Order&gt;161608782525&lt;/Ds_Order&gt;&lt;Ds_MerchantCode&gt;091952713&lt;/Ds_MerchantCode&gt;&lt;Ds_Terminal&gt;12&lt;/Ds_Terminal&gt;&lt;Ds_TransactionType&gt;0&lt;/Ds_TransactionType&gt;&lt;Ds_EMV3DS&gt;{&quot;protocolVersion&quot;:&quot;2.1.0&quot;,&quot;threeDSServerTransID&quot;:&quot;65120b61-28a3-476a-9aac-7b78c63a907a&quot;,&quot;threeDSInfo&quot;:&quot;CardConfiguration&quot;,&quot;threeDSMethodURL&quot;:&quot;https://sis-d.redsys.es/sis-simulador-web/threeDsMethod.jsp&quot;}&lt;/Ds_EMV3DS&gt;&lt;Ds_Card_PSD2&gt;Y&lt;/Ds_Card_PSD2&gt;&lt;Ds_Signature&gt;q4ija0q0x48NBb3O6EFLwEavCUMbtUWR/U38Iv0qSn0=&lt;/Ds_Signature&gt;&lt;/INFOTARJETA&gt;&lt;/RETORNOXML&gt;</p231:iniciaPeticionReturn></p231:iniciaPeticionResponse></soapenv:Body></soapenv:Envelope>' + end + + def successful_non_3ds_purchase_with_mit_exemption_response + '<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:soapenc=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"><soapenv:Header/><soapenv:Body><p231:trataPeticionResponse xmlns:p231=\"http://webservice.sis.sermepa.es\"><p231:trataPeticionReturn>&lt;RETORNOXML&gt;&lt;CODIGO&gt;0&lt;/CODIGO&gt;&lt;Ds_Version&gt;0.1&lt;/Ds_Version&gt;&lt;OPERACION&gt;&lt;Ds_Amount&gt;100&lt;/Ds_Amount&gt;&lt;Ds_Currency&gt;978&lt;/Ds_Currency&gt;&lt;Ds_Order&gt;162068064777&lt;/Ds_Order&gt;&lt;Ds_Signature&gt;axkiHGkNxpGp/JZFGC5/S0+u2n5r/S7jj6FY+F9eZ6k=&lt;/Ds_Signature&gt;&lt;Ds_MerchantCode&gt;091952713&lt;/Ds_MerchantCode&gt;&lt;Ds_Terminal&gt;1&lt;/Ds_Terminal&gt;&lt;Ds_Response&gt;0000&lt;/Ds_Response&gt;&lt;Ds_AuthorisationCode&gt;363732&lt;/Ds_AuthorisationCode&gt;&lt;Ds_TransactionType&gt;A&lt;/Ds_TransactionType&gt;&lt;Ds_SecurePayment&gt;0&lt;/Ds_SecurePayment&gt;&lt;Ds_Language&gt;1&lt;/Ds_Language&gt;&lt;Ds_MerchantData&gt;&lt;/Ds_MerchantData&gt;&lt;Ds_Card_Country&gt;724&lt;/Ds_Card_Country&gt;&lt;Ds_Card_Brand&gt;1&lt;/Ds_Card_Brand&gt;&lt;Ds_ProcessedPayMethod&gt;3&lt;/Ds_ProcessedPayMethod&gt;&lt;/OPERACION&gt;&lt;/RETORNOXML&gt;</p231:trataPeticionReturn></p231:trataPeticionResponse></soapenv:Body></soapenv:Envelope>' + end + def failed_authorize_response "<?xml version='1.0' encoding=\"ISO-8859-1\" ?><RETORNOXML><CODIGO>SIS0093</CODIGO><RECIBIDO><DATOSENTRADA>\n <DS_Version>0.1</DS_Version>\n <DS_MERCHANT_CURRENCY>978</DS_MERCHANT_CURRENCY>\n <DS_MERCHANT_AMOUNT>100</DS_MERCHANT_AMOUNT>\n <DS_MERCHANT_ORDER>141278225678</DS_MERCHANT_ORDER>\n <DS_MERCHANT_TRANSACTIONTYPE>1</DS_MERCHANT_TRANSACTIONTYPE>\n <DS_MERCHANT_TERMINAL>1</DS_MERCHANT_TERMINAL>\n <DS_MERCHANT_MERCHANTCODE>91952713</DS_MERCHANT_MERCHANTCODE>\n <DS_MERCHANT_MERCHANTSIGNATURE>1c34699589507802f800b929ea314dc143b0b8a5</DS_MERCHANT_MERCHANTSIGNATURE>\n <DS_MERCHANT_TITULAR>Longbob Longsen</DS_MERCHANT_TITULAR>\n <DS_MERCHANT_PAN>4242424242424242</DS_MERCHANT_PAN>\n <DS_MERCHANT_EXPIRYDATE>1509</DS_MERCHANT_EXPIRYDATE>\n <DS_MERCHANT_CVV2>123</DS_MERCHANT_CVV2>\n</DATOSENTRADA>\n</RECIBIDO></RETORNOXML>" end @@ -345,6 +548,18 @@ def failed_transaction_post_scrubbed ) end + def failed_3ds_transaction_pre_scrubbed + %q( +<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><soapenv:Header/><soapenv:Body><p231:trataPeticionResponse xmlns:p231="http://webservice.sis.sermepa.es"><p231:trataPeticionReturn>&lt;RETORNOXML&gt;&lt;CODIGO&gt;SIS0571&lt;/CODIGO&gt;&lt;RECIBIDO&gt;\n &lt;REQUEST&gt;&lt;DATOSENTRADA&gt;&lt;DS_Version&gt;0.1&lt;/DS_Version&gt;&lt;DS_MERCHANT_CURRENCY&gt;978&lt;/DS_MERCHANT_CURRENCY&gt;&lt;DS_MERCHANT_AMOUNT&gt;100&lt;/DS_MERCHANT_AMOUNT&gt;&lt;DS_MERCHANT_ORDER&gt;82973d604ba1&lt;/DS_MERCHANT_ORDER&gt;&lt;DS_MERCHANT_TRANSACTIONTYPE&gt;1&lt;/DS_MERCHANT_TRANSACTIONTYPE&gt;&lt;DS_MERCHANT_PRODUCTDESCRIPTION/&gt;&lt;DS_MERCHANT_TERMINAL&gt;12&lt;/DS_MERCHANT_TERMINAL&gt;&lt;DS_MERCHANT_MERCHANTCODE&gt;091952713&lt;/DS_MERCHANT_MERCHANTCODE&gt;&lt;DS_MERCHANT_TITULAR&gt;Jane Doe&lt;/DS_MERCHANT_TITULAR&gt;&lt;DS_MERCHANT_PAN&gt;4548812049400004&lt;/DS_MERCHANT_PAN&gt;&lt;DS_MERCHANT_EXPIRYDATE&gt;2012&lt;/DS_MERCHANT_EXPIRYDATE&gt;&lt;DS_MERCHANT_CVV2&gt;123&lt;/DS_MERCHANT_CVV2&gt;&lt;DS_MERCHANT_EMV3DS&gt;{&quot;threeDSInfo&quot;:&quot;AuthenticationData&quot;,&quot;browserAcceptHeader&quot;:&quot;text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3&quot;,&quot;browserUserAgent&quot;:&quot;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36&quot;}&lt;/DS_MERCHANT_EMV3DS&gt;&lt;/DATOSENTRADA&gt;&lt;DS_SIGNATUREVERSION&gt;HMAC_SHA256_V1&lt;/DS_SIGNATUREVERSION&gt;&lt;DS_SIGNATURE&gt;ips3TqR6upMAEbC0D6vmzV9tldU5224MSR63dpWPBT0=&lt;/DS_SIGNATURE&gt;&lt;/REQUEST&gt;\n &lt;/RECIBIDO&gt;&lt;/RETORNOXML&gt;</p231:trataPeticionReturn></p231:trataPeticionResponse></soapenv:Body></soapenv:Envelope> + ) + end + + def failed_3ds_transaction_post_scrubbed + %q( +<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><soapenv:Header/><soapenv:Body><p231:trataPeticionResponse xmlns:p231="http://webservice.sis.sermepa.es"><p231:trataPeticionReturn>&lt;RETORNOXML&gt;&lt;CODIGO&gt;SIS0571&lt;/CODIGO&gt;&lt;RECIBIDO&gt;\n &lt;REQUEST&gt;&lt;DATOSENTRADA&gt;&lt;DS_Version&gt;0.1&lt;/DS_Version&gt;&lt;DS_MERCHANT_CURRENCY&gt;978&lt;/DS_MERCHANT_CURRENCY&gt;&lt;DS_MERCHANT_AMOUNT&gt;100&lt;/DS_MERCHANT_AMOUNT&gt;&lt;DS_MERCHANT_ORDER&gt;82973d604ba1&lt;/DS_MERCHANT_ORDER&gt;&lt;DS_MERCHANT_TRANSACTIONTYPE&gt;1&lt;/DS_MERCHANT_TRANSACTIONTYPE&gt;&lt;DS_MERCHANT_PRODUCTDESCRIPTION/&gt;&lt;DS_MERCHANT_TERMINAL&gt;12&lt;/DS_MERCHANT_TERMINAL&gt;&lt;DS_MERCHANT_MERCHANTCODE&gt;091952713&lt;/DS_MERCHANT_MERCHANTCODE&gt;&lt;DS_MERCHANT_TITULAR&gt;Jane Doe&lt;/DS_MERCHANT_TITULAR&gt;&lt;DS_MERCHANT_PAN&gt;[FILTERED]&lt;/DS_MERCHANT_PAN&gt;&lt;DS_MERCHANT_EXPIRYDATE&gt;2012&lt;/DS_MERCHANT_EXPIRYDATE&gt;&lt;DS_MERCHANT_CVV2&gt;[FILTERED]&lt;/DS_MERCHANT_CVV2&gt;&lt;DS_MERCHANT_EMV3DS&gt;{&quot;threeDSInfo&quot;:&quot;AuthenticationData&quot;,&quot;browserAcceptHeader&quot;:&quot;text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3&quot;,&quot;browserUserAgent&quot;:&quot;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36&quot;}&lt;/DS_MERCHANT_EMV3DS&gt;&lt;/DATOSENTRADA&gt;&lt;DS_SIGNATUREVERSION&gt;HMAC_SHA256_V1&lt;/DS_SIGNATUREVERSION&gt;&lt;DS_SIGNATURE&gt;ips3TqR6upMAEbC0D6vmzV9tldU5224MSR63dpWPBT0=&lt;/DS_SIGNATURE&gt;&lt;/REQUEST&gt;\n &lt;/RECIBIDO&gt;&lt;/RETORNOXML&gt;</p231:trataPeticionReturn></p231:trataPeticionResponse></soapenv:Body></soapenv:Envelope> + ) + end + def nil_cvv_pre_scrubbed <<-PRE_SCRUBBED entrada=%3CDATOSENTRADA%3E%0A++%3CDS_Version%3E0.1%3C%2FDS_Version%3E%0A++%3CDS_MERCHANT_CURRENCY%3E978%3C%2FDS_MERCHANT_CURRENCY%3E%0A++%3CDS_MERCHANT_AMOUNT%3E100%3C%2FDS_MERCHANT_AMOUNT%3E%0A++%3CDS_MERCHANT_ORDER%3E135214014098%3C%2FDS_MERCHANT_ORDER%3E%0A++%3CDS_MERCHANT_TRANSACTIONTYPE%3EA%3C%2FDS_MERCHANT_TRANSACTIONTYPE%3E%0A++%3CDS_MERCHANT_TERMINAL%3E1%3C%2FDS_MERCHANT_TERMINAL%3E%0A++%3CDS_MERCHANT_MERCHANTCODE%3E91952713%3C%2FDS_MERCHANT_MERCHANTCODE%3E%0A++%3CDS_MERCHANT_MERCHANTSIGNATURE%3E39589b03cdd3c525885cdb3b3761e2fb7a8be9ee%3C%2FDS_MERCHANT_MERCHANTSIGNATURE%3E%0A++%3CDS_MERCHANT_TITULAR%3ELongbob+Longsen%3C%2FDS_MERCHANT_TITULAR%3E%0A++%3CDS_MERCHANT_PAN%3E4548812049400004%3C%2FDS_MERCHANT_PAN%3E%0A++%3CDS_MERCHANT_EXPIRYDATE%3E1309%3C%2FDS_MERCHANT_EXPIRYDATE%3E%0A++%3CDS_MERCHANT_CVV2%2F%3E%0A%3C%2FDATOSENTRADA%3E%0A @@ -377,13 +592,13 @@ def whitespace_string_cvv_pre_scrubbed <<-PRE_SCRUBBED entrada=%3CDATOSENTRADA%3E%0A++%3CDS_Version%3E0.1%3C%2FDS_Version%3E%0A++%3CDS_MERCHANT_CURRENCY%3E978%3C%2FDS_MERCHANT_CURRENCY%3E%0A++%3CDS_MERCHANT_AMOUNT%3E100%3C%2FDS_MERCHANT_AMOUNT%3E%0A++%3CDS_MERCHANT_ORDER%3E135214014098%3C%2FDS_MERCHANT_ORDER%3E%0A++%3CDS_MERCHANT_TRANSACTIONTYPE%3EA%3C%2FDS_MERCHANT_TRANSACTIONTYPE%3E%0A++%3CDS_MERCHANT_TERMINAL%3E1%3C%2FDS_MERCHANT_TERMINAL%3E%0A++%3CDS_MERCHANT_MERCHANTCODE%3E91952713%3C%2FDS_MERCHANT_MERCHANTCODE%3E%0A++%3CDS_MERCHANT_MERCHANTSIGNATURE%3E39589b03cdd3c525885cdb3b3761e2fb7a8be9ee%3C%2FDS_MERCHANT_MERCHANTSIGNATURE%3E%0A++%3CDS_MERCHANT_TITULAR%3ELongbob+Longsen%3C%2FDS_MERCHANT_TITULAR%3E%0A++%3CDS_MERCHANT_PAN%3E4548812049400004%3C%2FDS_MERCHANT_PAN%3E%0A++%3CDS_MERCHANT_EXPIRYDATE%3E1309%3C%2FDS_MERCHANT_EXPIRYDATE%3E%0A++%3CDS_MERCHANT_CVV2%3E+++%3C%2FDS_MERCHANT_CVV2%3E%0A%3C%2FDATOSENTRADA%3E%0A <?xml version='1.0' encoding="ISO-8859-1" ?><RETORNOXML><CODIGO>0</CODIGO><Ds_Version>0.1</Ds_Version><OPERACION><Ds_Amount>100</Ds_Amount><Ds_Currency>978</Ds_Currency><Ds_Order>135214014098</Ds_Order><Ds_Signature>97FBF7E648015AC8AFCA107CD67A1F600FBE9611</Ds_Signature><Ds_MerchantCode>91952713</Ds_MerchantCode><Ds_Terminal>1</Ds_Terminal><Ds_Response>0000</Ds_Response><Ds_AuthorisationCode>701841</Ds_AuthorisationCode><Ds_TransactionType>A</Ds_TransactionType><Ds_SecurePayment>0</Ds_SecurePayment><Ds_Language>1</Ds_Language><Ds_MerchantData></Ds_MerchantData><Ds_Card_Country>724</Ds_Card_Country></OPERACION></RETORNOXML> - PRE_SCRUBBED + PRE_SCRUBBED end def whitespace_string_cvv_post_scrubbed <<-PRE_SCRUBBED entrada=%3CDATOSENTRADA%3E%0A++%3CDS_Version%3E0.1%3C%2FDS_Version%3E%0A++%3CDS_MERCHANT_CURRENCY%3E978%3C%2FDS_MERCHANT_CURRENCY%3E%0A++%3CDS_MERCHANT_AMOUNT%3E100%3C%2FDS_MERCHANT_AMOUNT%3E%0A++%3CDS_MERCHANT_ORDER%3E135214014098%3C%2FDS_MERCHANT_ORDER%3E%0A++%3CDS_MERCHANT_TRANSACTIONTYPE%3EA%3C%2FDS_MERCHANT_TRANSACTIONTYPE%3E%0A++%3CDS_MERCHANT_TERMINAL%3E1%3C%2FDS_MERCHANT_TERMINAL%3E%0A++%3CDS_MERCHANT_MERCHANTCODE%3E91952713%3C%2FDS_MERCHANT_MERCHANTCODE%3E%0A++%3CDS_MERCHANT_MERCHANTSIGNATURE%3E39589b03cdd3c525885cdb3b3761e2fb7a8be9ee%3C%2FDS_MERCHANT_MERCHANTSIGNATURE%3E%0A++%3CDS_MERCHANT_TITULAR%3ELongbob+Longsen%3C%2FDS_MERCHANT_TITULAR%3E%0A++%3CDS_MERCHANT_PAN%3E[FILTERED]%3C%2FDS_MERCHANT_PAN%3E%0A++%3CDS_MERCHANT_EXPIRYDATE%3E1309%3C%2FDS_MERCHANT_EXPIRYDATE%3E%0A++%3CDS_MERCHANT_CVV2%3E[BLANK]%3C%2FDS_MERCHANT_CVV2%3E%0A%3C%2FDATOSENTRADA%3E%0A <?xml version='1.0' encoding="ISO-8859-1" ?><RETORNOXML><CODIGO>0</CODIGO><Ds_Version>0.1</Ds_Version><OPERACION><Ds_Amount>100</Ds_Amount><Ds_Currency>978</Ds_Currency><Ds_Order>135214014098</Ds_Order><Ds_Signature>97FBF7E648015AC8AFCA107CD67A1F600FBE9611</Ds_Signature><Ds_MerchantCode>91952713</Ds_MerchantCode><Ds_Terminal>1</Ds_Terminal><Ds_Response>0000</Ds_Response><Ds_AuthorisationCode>701841</Ds_AuthorisationCode><Ds_TransactionType>A</Ds_TransactionType><Ds_SecurePayment>0</Ds_SecurePayment><Ds_Language>1</Ds_Language><Ds_MerchantData></Ds_MerchantData><Ds_Card_Country>724</Ds_Card_Country></OPERACION></RETORNOXML> - PRE_SCRUBBED + PRE_SCRUBBED end end diff --git a/test/unit/gateways/redsys_test.rb b/test/unit/gateways/redsys_test.rb index 4a5e9ce3816..fca80ad9df3 100644 --- a/test/unit/gateways/redsys_test.rb +++ b/test/unit/gateways/redsys_test.rb @@ -6,9 +6,9 @@ class RedsysTest < Test::Unit::TestCase def setup Base.mode = :test @credentials = { - :login => '091952713', - :secret_key => 'qwertyasdf0123456789', - :terminal => '1', + login: '091952713', + secret_key: 'qwertyasdf0123456789', + terminal: '1' } @gateway = RedsysGateway.new(@credentials) @headers = { @@ -20,12 +20,12 @@ def setup def test_purchase_payload @gateway.expects(:ssl_post).with(RedsysGateway.test_url, purchase_request, @headers).returns(successful_purchase_response) - @gateway.purchase(123, credit_card, :order_id => '1001') + @gateway.purchase(123, credit_card, order_id: '1001') end def test_purchase_payload_with_credit_card_token @gateway.expects(:ssl_post).with(RedsysGateway.test_url, purchase_request_with_credit_card_token, @headers).returns(successful_purchase_response) - @gateway.purchase(123, '77bff3a969d6f97b2ec815448cdcff453971f573', :order_id => '1001') + @gateway.purchase(123, '77bff3a969d6f97b2ec815448cdcff453971f573', order_id: '1001') end def test_successful_purchase @@ -47,6 +47,157 @@ def test_successful_purchase_requesting_credit_card_token assert_equal '77bff3a969d6f97b2ec815448cdcff453971f573', res.params['ds_merchant_identifier'] 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 '2012102122020', 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: + } + } + 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 + @gateway.expects(:ssl_post).with( + anything, + all_of( + includes(CGI.escape('<DS_MERCHANT_TRANSACTIONTYPE>0</DS_MERCHANT_TRANSACTIONTYPE>')), + includes(CGI.escape('<DS_MERCHANT_ORDER>1001</DS_MERCHANT_ORDER>')), + includes(CGI.escape('<DS_MERCHANT_AMOUNT>123</DS_MERCHANT_AMOUNT>')), + includes(CGI.escape('<DS_MERCHANT_COF_INI>S</DS_MERCHANT_COF_INI>')), + includes(CGI.escape('<DS_MERCHANT_COF_TYPE>R</DS_MERCHANT_COF_TYPE>')), + includes(CGI.escape('<DS_MERCHANT_PAN>4242424242424242</DS_MERCHANT_PAN>')), + includes(CGI.escape("<DS_MERCHANT_EXPIRYDATE>#{1.year.from_now.strftime('%y')}09</DS_MERCHANT_EXPIRYDATE>")), + includes(CGI.escape('<DS_MERCHANT_CVV2>123</DS_MERCHANT_CVV2>')), + includes(CGI.escape('<DS_MERCHANT_DIRECTPAYMENT>false</DS_MERCHANT_DIRECTPAYMENT>')), + Not(includes(CGI.escape('<DS_MERCHANT_EXCEP_SCA>'))), + Not(includes(CGI.escape('<DS_MERCHANT_COF_TXNID>'))) + ), + 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('<DS_MERCHANT_TRANSACTIONTYPE>0</DS_MERCHANT_TRANSACTIONTYPE>'), + includes('<DS_MERCHANT_ORDER>1002</DS_MERCHANT_ORDER>'), + includes('<DS_MERCHANT_AMOUNT>123</DS_MERCHANT_AMOUNT>'), + includes('<DS_MERCHANT_COF_INI>N</DS_MERCHANT_COF_INI>'), + includes('<DS_MERCHANT_COF_TYPE>R</DS_MERCHANT_COF_TYPE>'), + includes('<DS_MERCHANT_PAN>4242424242424242</DS_MERCHANT_PAN>'), + includes("<DS_MERCHANT_EXPIRYDATE>#{1.year.from_now.strftime('%y')}09</DS_MERCHANT_EXPIRYDATE>"), + includes('<DS_MERCHANT_CVV2>123</DS_MERCHANT_CVV2>'), + includes('<DS_MERCHANT_DIRECTPAYMENT>true</DS_MERCHANT_DIRECTPAYMENT>'), + includes('<DS_MERCHANT_EXCEP_SCA>MIT</DS_MERCHANT_EXCEP_SCA>'), + includes("<DS_MERCHANT_COF_TXNID>#{network_transaction_id}</DS_MERCHANT_COF_TXNID>") + ), + 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: + } + } + 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('<DS_MERCHANT_TRANSACTIONTYPE>0</DS_MERCHANT_TRANSACTIONTYPE>')), + includes(CGI.escape('<DS_MERCHANT_ORDER>1001</DS_MERCHANT_ORDER>')), + includes(CGI.escape('<DS_MERCHANT_AMOUNT>123</DS_MERCHANT_AMOUNT>')), + includes(CGI.escape('<DS_MERCHANT_COF_INI>S</DS_MERCHANT_COF_INI>')), + includes(CGI.escape('<DS_MERCHANT_COF_TYPE>R</DS_MERCHANT_COF_TYPE>')), + includes(CGI.escape('<DS_MERCHANT_IDENTIFIER>77bff3a969d6f97b2ec815448cdcff453971f573</DS_MERCHANT_IDENTIFIER>')), + includes(CGI.escape('<DS_MERCHANT_DIRECTPAYMENT>false</DS_MERCHANT_DIRECTPAYMENT>')), + Not(includes(CGI.escape('<DS_MERCHANT_EXCEP_SCA>'))), + Not(includes(CGI.escape('<DS_MERCHANT_COF_TXNID>'))) + ), + 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('<DS_MERCHANT_TRANSACTIONTYPE>0</DS_MERCHANT_TRANSACTIONTYPE>'), + includes('<DS_MERCHANT_ORDER>1002</DS_MERCHANT_ORDER>'), + includes('<DS_MERCHANT_AMOUNT>123</DS_MERCHANT_AMOUNT>'), + includes('<DS_MERCHANT_COF_INI>N</DS_MERCHANT_COF_INI>'), + includes('<DS_MERCHANT_COF_TYPE>R</DS_MERCHANT_COF_TYPE>'), + includes('<DS_MERCHANT_IDENTIFIER>77bff3a969d6f97b2ec815448cdcff453971f573</DS_MERCHANT_IDENTIFIER>'), + includes('<DS_MERCHANT_DIRECTPAYMENT>true</DS_MERCHANT_DIRECTPAYMENT>'), + includes('<DS_MERCHANT_EXCEP_SCA>MIT</DS_MERCHANT_EXCEP_SCA>'), + includes("<DS_MERCHANT_COF_TXNID>#{network_transaction_id}</DS_MERCHANT_COF_TXNID>") + ), + 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: + } + } + 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) @@ -115,7 +266,7 @@ def test_authorize_without_order_id def test_bad_order_id_format stub_comms(@gateway, :ssl_request) do @gateway.authorize(123, credit_card, order_id: 'Una#cce-ptable44Format') - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/MERCHANT_ORDER%3E\d\d\d\dUnaccept%3C/, data) end.respond_with(successful_authorize_response) end @@ -123,7 +274,7 @@ def test_bad_order_id_format def test_order_id_numeric_start_but_too_long stub_comms(@gateway, :ssl_request) do @gateway.authorize(123, credit_card, order_id: '1234ThisIs]FineButTooLong') - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/MERCHANT_ORDER%3E1234ThisIsFi%3C/, data) end.respond_with(successful_authorize_response) end @@ -161,27 +312,37 @@ def test_override_currency includes(CGI.escape('<DS_MERCHANT_CURRENCY>840</DS_MERCHANT_CURRENCY>')), anything ).returns(successful_purchase_response) - @gateway.authorize(123, credit_card, :order_id => '1001', :currency => 'USD') + @gateway.authorize(123, credit_card, order_id: '1001', currency: 'USD') 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('<DS_MERCHANT_TRANSACTIONTYPE>0</DS_MERCHANT_TRANSACTIONTYPE>')), + includes(CGI.escape('<DS_MERCHANT_AMOUNT>0</DS_MERCHANT_AMOUNT>')) + ), + 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('<DS_MERCHANT_TRANSACTIONTYPE>0</DS_MERCHANT_TRANSACTIONTYPE>')), + includes(CGI.escape('<DS_MERCHANT_AMOUNT>0</DS_MERCHANT_AMOUNT>')) + ), + 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 @@ -195,11 +356,11 @@ 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 - assert_equal [:visa, :master, :american_express, :jcb, :diners_club], RedsysGateway.supported_cardtypes + assert_equal %i[visa master american_express jcb diners_club unionpay patagonia_365 tarjeta_sol], RedsysGateway.supported_cardtypes end def test_using_test_mode @@ -210,10 +371,10 @@ def test_using_test_mode def test_overriding_options Base.mode = :production gw = RedsysGateway.new( - :terminal => 1, - :login => '1234', - :secret_key => '12345', - :test => true + terminal: 1, + login: '1234', + secret_key: '12345', + test: true ) assert gw.test? assert_equal RedsysGateway.test_url, gw.send(:url) @@ -222,9 +383,9 @@ def test_overriding_options def test_production_mode Base.mode = :production gw = RedsysGateway.new( - :terminal => 1, - :login => '1234', - :secret_key => '12345' + terminal: 1, + login: '1234', + secret_key: '12345' ) assert !gw.test? assert_equal RedsysGateway.live_url, gw.send(:url) @@ -256,11 +417,11 @@ def test_whitespace_string_cvv_transcript_scrubbing # one with card and another without. def purchase_request - "entrada=%3CDATOSENTRADA%3E%0A++%3CDS_Version%3E0.1%3C%2FDS_Version%3E%0A++%3CDS_MERCHANT_CURRENCY%3E978%3C%2FDS_MERCHANT_CURRENCY%3E%0A++%3CDS_MERCHANT_AMOUNT%3E123%3C%2FDS_MERCHANT_AMOUNT%3E%0A++%3CDS_MERCHANT_ORDER%3E1001%3C%2FDS_MERCHANT_ORDER%3E%0A++%3CDS_MERCHANT_TRANSACTIONTYPE%3EA%3C%2FDS_MERCHANT_TRANSACTIONTYPE%3E%0A++%3CDS_MERCHANT_PRODUCTDESCRIPTION%2F%3E%0A++%3CDS_MERCHANT_TERMINAL%3E1%3C%2FDS_MERCHANT_TERMINAL%3E%0A++%3CDS_MERCHANT_MERCHANTCODE%3E091952713%3C%2FDS_MERCHANT_MERCHANTCODE%3E%0A++%3CDS_MERCHANT_MERCHANTSIGNATURE%3Eb98b606a6a588d8c45c239f244160efbbe30b4a8%3C%2FDS_MERCHANT_MERCHANTSIGNATURE%3E%0A++%3CDS_MERCHANT_TITULAR%3ELongbob+Longsen%3C%2FDS_MERCHANT_TITULAR%3E%0A++%3CDS_MERCHANT_PAN%3E4242424242424242%3C%2FDS_MERCHANT_PAN%3E%0A++%3CDS_MERCHANT_EXPIRYDATE%3E#{(Time.now.year + 1).to_s.slice(2,2)}09%3C%2FDS_MERCHANT_EXPIRYDATE%3E%0A++%3CDS_MERCHANT_CVV2%3E123%3C%2FDS_MERCHANT_CVV2%3E%0A%3C%2FDATOSENTRADA%3E%0A" + "entrada=%3CDATOSENTRADA%3E%0A++%3CDS_Version%3E0.1%3C%2FDS_Version%3E%0A++%3CDS_MERCHANT_CURRENCY%3E978%3C%2FDS_MERCHANT_CURRENCY%3E%0A++%3CDS_MERCHANT_AMOUNT%3E123%3C%2FDS_MERCHANT_AMOUNT%3E%0A++%3CDS_MERCHANT_ORDER%3E1001%3C%2FDS_MERCHANT_ORDER%3E%0A++%3CDS_MERCHANT_TRANSACTIONTYPE%3E0%3C%2FDS_MERCHANT_TRANSACTIONTYPE%3E%0A++%3CDS_MERCHANT_PRODUCTDESCRIPTION%2F%3E%0A++%3CDS_MERCHANT_TERMINAL%3E1%3C%2FDS_MERCHANT_TERMINAL%3E%0A++%3CDS_MERCHANT_MERCHANTCODE%3E091952713%3C%2FDS_MERCHANT_MERCHANTCODE%3E%0A++%3CDS_MERCHANT_MERCHANTSIGNATURE%3E0b930082f7905d7dba3d83be4d4331b8acd57624%3C%2FDS_MERCHANT_MERCHANTSIGNATURE%3E%0A++%3CDS_MERCHANT_TITULAR%3ELongbob+Longsen%3C%2FDS_MERCHANT_TITULAR%3E%0A++%3CDS_MERCHANT_PAN%3E4242424242424242%3C%2FDS_MERCHANT_PAN%3E%0A++%3CDS_MERCHANT_EXPIRYDATE%3E#{(Time.now.year + 1).to_s.slice(2, 2)}09%3C%2FDS_MERCHANT_EXPIRYDATE%3E%0A++%3CDS_MERCHANT_CVV2%3E123%3C%2FDS_MERCHANT_CVV2%3E%0A%3C%2FDATOSENTRADA%3E%0A" end def purchase_request_with_credit_card_token - 'entrada=%3CDATOSENTRADA%3E%0A++%3CDS_Version%3E0.1%3C%2FDS_Version%3E%0A++%3CDS_MERCHANT_CURRENCY%3E978%3C%2FDS_MERCHANT_CURRENCY%3E%0A++%3CDS_MERCHANT_AMOUNT%3E123%3C%2FDS_MERCHANT_AMOUNT%3E%0A++%3CDS_MERCHANT_ORDER%3E1001%3C%2FDS_MERCHANT_ORDER%3E%0A++%3CDS_MERCHANT_TRANSACTIONTYPE%3EA%3C%2FDS_MERCHANT_TRANSACTIONTYPE%3E%0A++%3CDS_MERCHANT_PRODUCTDESCRIPTION%2F%3E%0A++%3CDS_MERCHANT_TERMINAL%3E1%3C%2FDS_MERCHANT_TERMINAL%3E%0A++%3CDS_MERCHANT_MERCHANTCODE%3E091952713%3C%2FDS_MERCHANT_MERCHANTCODE%3E%0A++%3CDS_MERCHANT_MERCHANTSIGNATURE%3Ecbcc0dee5724cd3fff08bbd4371946a0599c7fb9%3C%2FDS_MERCHANT_MERCHANTSIGNATURE%3E%0A++%3CDS_MERCHANT_IDENTIFIER%3E77bff3a969d6f97b2ec815448cdcff453971f573%3C%2FDS_MERCHANT_IDENTIFIER%3E%0A++%3CDS_MERCHANT_DIRECTPAYMENT%3Etrue%3C%2FDS_MERCHANT_DIRECTPAYMENT%3E%0A%3C%2FDATOSENTRADA%3E%0A' + 'entrada=%3CDATOSENTRADA%3E%0A++%3CDS_Version%3E0.1%3C%2FDS_Version%3E%0A++%3CDS_MERCHANT_CURRENCY%3E978%3C%2FDS_MERCHANT_CURRENCY%3E%0A++%3CDS_MERCHANT_AMOUNT%3E123%3C%2FDS_MERCHANT_AMOUNT%3E%0A++%3CDS_MERCHANT_ORDER%3E1001%3C%2FDS_MERCHANT_ORDER%3E%0A++%3CDS_MERCHANT_TRANSACTIONTYPE%3E0%3C%2FDS_MERCHANT_TRANSACTIONTYPE%3E%0A++%3CDS_MERCHANT_PRODUCTDESCRIPTION%2F%3E%0A++%3CDS_MERCHANT_TERMINAL%3E1%3C%2FDS_MERCHANT_TERMINAL%3E%0A++%3CDS_MERCHANT_MERCHANTCODE%3E091952713%3C%2FDS_MERCHANT_MERCHANTCODE%3E%0A++%3CDS_MERCHANT_MERCHANTSIGNATURE%3E0c56bb3edd0cae65ef16c96c61c5ecd306973d2f%3C%2FDS_MERCHANT_MERCHANTSIGNATURE%3E%0A++%3CDS_MERCHANT_IDENTIFIER%3E77bff3a969d6f97b2ec815448cdcff453971f573%3C%2FDS_MERCHANT_IDENTIFIER%3E%0A++%3CDS_MERCHANT_DIRECTPAYMENT%3Etrue%3C%2FDS_MERCHANT_DIRECTPAYMENT%3E%0A%3C%2FDATOSENTRADA%3E%0A' end def successful_purchase_response @@ -271,6 +432,14 @@ def successful_purchase_response_with_credit_card_token "<?xml version='1.0' encoding=\"ISO-8859-1\" ?><RETORNOXML><CODIGO>0</CODIGO><Ds_Version>0.1</Ds_Version><OPERACION><Ds_Amount>100</Ds_Amount><Ds_Currency>978</Ds_Currency><Ds_Order>141661632759</Ds_Order><Ds_Signature>C65E11D80534B432042ABAA47DCA54F5AFEC23ED</Ds_Signature><Ds_MerchantCode>327234688</Ds_MerchantCode><Ds_Terminal>2</Ds_Terminal><Ds_Response>0000</Ds_Response><Ds_AuthorisationCode>341129</Ds_AuthorisationCode><Ds_TransactionType>A</Ds_TransactionType><Ds_SecurePayment>0</Ds_SecurePayment><Ds_Language>1</Ds_Language><Ds_Merchant_Identifier>77bff3a969d6f97b2ec815448cdcff453971f573</Ds_Merchant_Identifier><Ds_MerchantData></Ds_MerchantData><Ds_Card_Country>724</Ds_Card_Country></OPERACION></RETORNOXML>" end + def successful_purchase_initial_stored_credential_response + "<?xml version='1.0' encoding=\"ISO-8859-1\" ?><RETORNOXML><CODIGO>0</CODIGO><Ds_Version>0.1</Ds_Version><OPERACION><Ds_Amount>123</Ds_Amount><Ds_Currency>978</Ds_Currency><Ds_Order>1001</Ds_Order><Ds_Signature>989D357BCC9EF0962A456C51422C4FAF4BF4399F</Ds_Signature><Ds_MerchantCode>91952713</Ds_MerchantCode><Ds_Terminal>1</Ds_Terminal><Ds_Response>0000</Ds_Response><Ds_AuthorisationCode>561350</Ds_AuthorisationCode><Ds_TransactionType>A</Ds_TransactionType><Ds_SecurePayment>0</Ds_SecurePayment><Ds_Language>1</Ds_Language><Ds_Merchant_Identifier>77bff3a969d6f97b2ec815448cdcff453971f573</Ds_Merchant_Identifier><Ds_MerchantData></Ds_MerchantData><Ds_Card_Country>724</Ds_Card_Country><Ds_Merchant_Cof_Txnid>2012102122020</Ds_Merchant_Cof_Txnid><Ds_Card_Brand>1</Ds_Card_Brand><Ds_ProcessedPayMethod>3</Ds_ProcessedPayMethod></OPERACION></RETORNOXML>" + end + + def successful_purchase_used_stored_credential_response + "<?xml version='1.0' encoding=\"ISO-8859-1\" ?><RETORNOXML><CODIGO>0</CODIGO><Ds_Version>0.1</Ds_Version><OPERACION><Ds_Amount>123</Ds_Amount><Ds_Currency>978</Ds_Currency><Ds_Order>1001</Ds_Order><Ds_Signature>989D357BCC9EF0962A456C51422C4FAF4BF4399F</Ds_Signature><Ds_MerchantCode>91952713</Ds_MerchantCode><Ds_Terminal>1</Ds_Terminal><Ds_Response>0000</Ds_Response><Ds_AuthorisationCode>561350</Ds_AuthorisationCode><Ds_TransactionType>A</Ds_TransactionType><Ds_SecurePayment>0</Ds_SecurePayment><Ds_Language>1</Ds_Language><Ds_MerchantData></Ds_MerchantData><Ds_Card_Country>724</Ds_Card_Country><Ds_Card_Brand>1</Ds_Card_Brand><Ds_ProcessedPayMethod>3</Ds_ProcessedPayMethod></OPERACION></RETORNOXML>" + end + def failed_purchase_response "<?xml version='1.0' encoding=\"ISO-8859-1\" ?><RETORNOXML><CODIGO>0</CODIGO><Ds_Version>0.1</Ds_Version><OPERACION><Ds_Amount>123</Ds_Amount><Ds_Currency>978</Ds_Currency><Ds_Order>1002</Ds_Order><Ds_Signature>80D5D1BE64777946519C4E633EE5498C6187747B</Ds_Signature><Ds_MerchantCode>91952713</Ds_MerchantCode><Ds_Terminal>1</Ds_Terminal><Ds_Response>190</Ds_Response><Ds_AuthorisationCode>561350</Ds_AuthorisationCode><Ds_TransactionType>A</Ds_TransactionType><Ds_SecurePayment>0</Ds_SecurePayment><Ds_Language>1</Ds_Language><Ds_MerchantData></Ds_MerchantData><Ds_Card_Country>724</Ds_Card_Country></OPERACION></RETORNOXML>" end @@ -371,13 +540,13 @@ def whitespace_string_cvv_pre_scrubbed <<-PRE_SCRUBBED entrada=%3CDATOSENTRADA%3E%0A++%3CDS_Version%3E0.1%3C%2FDS_Version%3E%0A++%3CDS_MERCHANT_CURRENCY%3E978%3C%2FDS_MERCHANT_CURRENCY%3E%0A++%3CDS_MERCHANT_AMOUNT%3E100%3C%2FDS_MERCHANT_AMOUNT%3E%0A++%3CDS_MERCHANT_ORDER%3E135214014098%3C%2FDS_MERCHANT_ORDER%3E%0A++%3CDS_MERCHANT_TRANSACTIONTYPE%3EA%3C%2FDS_MERCHANT_TRANSACTIONTYPE%3E%0A++%3CDS_MERCHANT_TERMINAL%3E1%3C%2FDS_MERCHANT_TERMINAL%3E%0A++%3CDS_MERCHANT_MERCHANTCODE%3E91952713%3C%2FDS_MERCHANT_MERCHANTCODE%3E%0A++%3CDS_MERCHANT_MERCHANTSIGNATURE%3E39589b03cdd3c525885cdb3b3761e2fb7a8be9ee%3C%2FDS_MERCHANT_MERCHANTSIGNATURE%3E%0A++%3CDS_MERCHANT_TITULAR%3ELongbob+Longsen%3C%2FDS_MERCHANT_TITULAR%3E%0A++%3CDS_MERCHANT_PAN%3E4548812049400004%3C%2FDS_MERCHANT_PAN%3E%0A++%3CDS_MERCHANT_EXPIRYDATE%3E1309%3C%2FDS_MERCHANT_EXPIRYDATE%3E%0A++%3CDS_MERCHANT_CVV2%3E+++%3C%2FDS_MERCHANT_CVV2%3E%0A%3C%2FDATOSENTRADA%3E%0A <?xml version='1.0' encoding="ISO-8859-1" ?><RETORNOXML><CODIGO>0</CODIGO><Ds_Version>0.1</Ds_Version><OPERACION><Ds_Amount>100</Ds_Amount><Ds_Currency>978</Ds_Currency><Ds_Order>135214014098</Ds_Order><Ds_Signature>97FBF7E648015AC8AFCA107CD67A1F600FBE9611</Ds_Signature><Ds_MerchantCode>91952713</Ds_MerchantCode><Ds_Terminal>1</Ds_Terminal><Ds_Response>0000</Ds_Response><Ds_AuthorisationCode>701841</Ds_AuthorisationCode><Ds_TransactionType>A</Ds_TransactionType><Ds_SecurePayment>0</Ds_SecurePayment><Ds_Language>1</Ds_Language><Ds_MerchantData></Ds_MerchantData><Ds_Card_Country>724</Ds_Card_Country></OPERACION></RETORNOXML> - PRE_SCRUBBED + PRE_SCRUBBED end def whitespace_string_cvv_post_scrubbed <<-PRE_SCRUBBED entrada=%3CDATOSENTRADA%3E%0A++%3CDS_Version%3E0.1%3C%2FDS_Version%3E%0A++%3CDS_MERCHANT_CURRENCY%3E978%3C%2FDS_MERCHANT_CURRENCY%3E%0A++%3CDS_MERCHANT_AMOUNT%3E100%3C%2FDS_MERCHANT_AMOUNT%3E%0A++%3CDS_MERCHANT_ORDER%3E135214014098%3C%2FDS_MERCHANT_ORDER%3E%0A++%3CDS_MERCHANT_TRANSACTIONTYPE%3EA%3C%2FDS_MERCHANT_TRANSACTIONTYPE%3E%0A++%3CDS_MERCHANT_TERMINAL%3E1%3C%2FDS_MERCHANT_TERMINAL%3E%0A++%3CDS_MERCHANT_MERCHANTCODE%3E91952713%3C%2FDS_MERCHANT_MERCHANTCODE%3E%0A++%3CDS_MERCHANT_MERCHANTSIGNATURE%3E39589b03cdd3c525885cdb3b3761e2fb7a8be9ee%3C%2FDS_MERCHANT_MERCHANTSIGNATURE%3E%0A++%3CDS_MERCHANT_TITULAR%3ELongbob+Longsen%3C%2FDS_MERCHANT_TITULAR%3E%0A++%3CDS_MERCHANT_PAN%3E[FILTERED]%3C%2FDS_MERCHANT_PAN%3E%0A++%3CDS_MERCHANT_EXPIRYDATE%3E1309%3C%2FDS_MERCHANT_EXPIRYDATE%3E%0A++%3CDS_MERCHANT_CVV2%3E[BLANK]%3C%2FDS_MERCHANT_CVV2%3E%0A%3C%2FDATOSENTRADA%3E%0A <?xml version='1.0' encoding="ISO-8859-1" ?><RETORNOXML><CODIGO>0</CODIGO><Ds_Version>0.1</Ds_Version><OPERACION><Ds_Amount>100</Ds_Amount><Ds_Currency>978</Ds_Currency><Ds_Order>135214014098</Ds_Order><Ds_Signature>97FBF7E648015AC8AFCA107CD67A1F600FBE9611</Ds_Signature><Ds_MerchantCode>91952713</Ds_MerchantCode><Ds_Terminal>1</Ds_Terminal><Ds_Response>0000</Ds_Response><Ds_AuthorisationCode>701841</Ds_AuthorisationCode><Ds_TransactionType>A</Ds_TransactionType><Ds_SecurePayment>0</Ds_SecurePayment><Ds_Language>1</Ds_Language><Ds_MerchantData></Ds_MerchantData><Ds_Card_Country>724</Ds_Card_Country></OPERACION></RETORNOXML> - PRE_SCRUBBED + PRE_SCRUBBED end end diff --git a/test/unit/gateways/s5_test.rb b/test/unit/gateways/s5_test.rb index 5a858c1edf9..aebbd977634 100644 --- a/test/unit/gateways/s5_test.rb +++ b/test/unit/gateways/s5_test.rb @@ -34,7 +34,7 @@ def test_successful_purchase def test_successful_purchase_with_recurring_flag response = stub_comms do @gateway.purchase(@amount, @credit_card, @options.merge(recurring: true)) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/Recurrence.*REPEATED/, data) end.respond_with(successful_purchase_response) diff --git a/test/unit/gateways/safe_charge_test.rb b/test/unit/gateways/safe_charge_test.rb index 94d310907c6..60cf0795645 100644 --- a/test/unit/gateways/safe_charge_test.rb +++ b/test/unit/gateways/safe_charge_test.rb @@ -8,18 +8,48 @@ def setup @credit_card = credit_card @three_ds_enrolled_card = credit_card('4012 0010 3749 0014') @amount = 100 + @network_token_credit_card = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ + brand: 'Visa', + payment_cryptogram: 'AgAAAAAAAIR8CQrXcIhbQAAAAAA', + number: '4012001037490014', + source: :network_token, + month: '12', + year: 2020 + }) @options = { order_id: '1', billing_address: address, description: 'Store Purchase' } + @merchant_options = @options.merge( merchant_descriptor: 'Test Descriptor', merchant_phone_number: '(555)555-5555', - merchant_name: 'Test Merchant' + merchant_name: 'Test Merchant', + product_id: 'Test Product' ) + @three_ds_options = @options.merge(three_d_secure: true) + + @mpi_options_3ds1 = @options.merge({ + three_d_secure: { + eci: '05', + cavv: 'Vk83Y2t0cHRzRFZzRlZlR0JIQXo=', + xid: '00000000000000000501' + } + }) + + @mpi_options_3ds2 = @options.merge({ + three_d_secure: { + version: '2.1.0', + eci: '05', + cavv: 'Vk83Y2t0cHRzRFZzRlZlR0JIQXo=', + xid: '00000000000000000501', + ds_transaction_id: 'c5b808e7-1de1-4069-a17b-f70d3b3b1645', + challenge_preference: 'NoPreference' + } + }) end def test_successful_purchase @@ -37,7 +67,7 @@ def test_successful_purchase def test_successful_purchase_with_merchant_options purchase = stub_comms do @gateway.purchase(@amount, @credit_card, @merchant_options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/sg_Descriptor/, data) assert_match(/sg_MerchantPhoneNumber/, data) assert_match(/sg_MerchantName/, data) @@ -53,7 +83,7 @@ def test_successful_purchase_with_merchant_options def test_successful_purchase_with_truthy_stored_credential_mode purchase = stub_comms do @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential_mode: true)) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/sg_StoredCredentialMode=1/, data) end.respond_with(successful_purchase_response) @@ -64,10 +94,25 @@ def test_successful_purchase_with_truthy_stored_credential_mode assert purchase.test? end + def test_successful_purchase_with_card_holder_verification + purchase = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(middle_name: 'middle', card_holder_verification: 1)) + end.check_request do |_endpoint, data, _headers| + assert_match(/sg_middleName=middle/, data) + assert_match(/sg_doCardHolderNameVerification=1/, data) + end.respond_with(successful_purchase_response) + + assert_success purchase + assert_equal '111951|101508189567|ZQBpAFAASABGAHAAVgBPAFUAMABiADMAewBtAGsAd' \ + 'AAvAFIAQQBrAGoAYwBxACoAXABHAEEAOgA3ACsAMgA4AD0AOABDAG4AbQAzAF' \ + 'UAbQBYAFIAMwA=|%02d|%d|1.00|USD' % [@credit_card.month, @credit_card.year.to_s[-2..-1]], purchase.authorization + assert purchase.test? + end + def test_successful_purchase_with_falsey_stored_credential_mode purchase = stub_comms do @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential_mode: false)) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/sg_StoredCredentialMode=0/, data) end.respond_with(successful_purchase_response) @@ -78,6 +123,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) @@ -98,6 +162,35 @@ def test_successful_authorize assert response.test? end + def test_successful_authorize_with_not_use_cvv + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ not_use_cvv: true })) + end.check_request do |_endpoint, data, _headers| + assert_match(/sg_NotUseCVV=1/, data) + end.respond_with(successful_authorize_response) + + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ not_use_cvv: 'true' })) + end.check_request do |_endpoint, data, _headers| + assert_match(/sg_NotUseCVV=1/, data) + end.respond_with(successful_authorize_response) + + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ not_use_cvv: false })) + end.check_request do |_endpoint, data, _headers| + assert_match(/sg_NotUseCVV=0/, data) + end.respond_with(successful_authorize_response) + + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ not_use_cvv: 'false' })) + end.check_request do |_endpoint, data, _headers| + assert_match(/sg_NotUseCVV=0/, data) + end.respond_with(successful_authorize_response) + + assert_success response + assert response.test? + end + def test_failed_authorize @gateway.expects(:ssl_post).returns(failed_authorize_response) @@ -118,6 +211,16 @@ def test_successful_capture assert response.test? end + def test_successful_capture_with_options + capture = stub_comms do + @gateway.capture(@amount, 'auth|transaction_id|token|month|year|amount|currency', @options.merge(email: 'slowturtle86@aol.com')) + end.check_request do |_endpoint, data, _headers| + assert_match(/sg_Email/, data) + end.respond_with(successful_capture_response) + + assert_success capture + end + def test_failed_capture @gateway.expects(:ssl_post).returns(failed_capture_response) @@ -134,6 +237,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) @@ -150,6 +293,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) @@ -179,31 +332,13 @@ def test_failed_void assert response.test? end - def test_successful_verify - @gateway.expects(:ssl_post).times(2).returns(successful_authorize_response, successful_void_response) - - response = @gateway.verify(@credit_card, @options) - assert_success response - - assert_equal '111534|101508189855|MQBVAG4ASABkAEgAagB3AEsAbgAtACoAWgAzAFwAW' \ - 'wBNAF8ATQBUAD0AegBQAGwAQAAtAD0AXAB5AFkALwBtAFAALABaAHoAOgBFAE' \ - 'wAUAA1AFUAMwA=|%02d|%d|1.00|USD' % [@credit_card.month, @credit_card.year.to_s[-2..-1]], response.authorization - assert response.test? - end - - def test_successful_verify_with_failed_void - @gateway.expects(:ssl_post).times(2).returns(successful_authorize_response, failed_void_response) - - response = @gateway.verify(@credit_card, @options) - assert_success response - end - - def test_failed_verify - @gateway.expects(:ssl_post).returns(failed_authorize_response) - - response = @gateway.verify(@credit_card, @options) - assert_failure response - assert_equal '0', response.error_code + def test_verify_sends_zero_amount + stub_comms do + @gateway.verify(@credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/sg_TransType=Auth/, data) + assert_match(/sg_Amount=0.00/, data) + end.respond_with(successful_authorize_response) end def test_scrub @@ -214,7 +349,7 @@ def test_scrub def test_3ds_response purchase = stub_comms do @gateway.purchase(@amount, @three_ds_enrolled_card, @three_ds_options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/Sale3D/, data) assert_match(/sg_APIType/, data) end.respond_with(successful_3ds_purchase_response) @@ -225,6 +360,66 @@ def test_3ds_response assert_equal 'https://pit.3dsecure.net/VbVTestSuiteService/pit1/acsService/paReq?summary=MjRlZGYwY2EtZTk5Zi00NDJjLTljOTAtNWUxZmRhMjEwODg3', purchase.params['acsurl'] end + def test_mpi_response_fail + purchase = stub_comms do + @gateway.purchase(@amount, @three_ds_enrolled_card, @mpi_options_3ds1) + end.check_request do |_, data, _| + assert_match(/sg_ECI/, data) + assert_match(/sg_CAVV/, data) + assert_match(/sg_IsExternalMPI/, data) + end.respond_with(failed_mpi_response) + + assert_failure purchase + assert_equal 'DECLINED', purchase.params['status'] + end + + def test_mpi_response_success_3ds1 + purchase = stub_comms do + @gateway.purchase(@amount, @three_ds_enrolled_card, @mpi_options_3ds1) + end.check_request do |_, data, _| + assert_match(/sg_ECI/, data) + assert_match(/sg_CAVV/, data) + assert_match(/sg_IsExternalMPI/, data) + assert_match(/sg_threeDSProtocolVersion=1/, data) + assert_match(/sg_Xid/, data) + end.respond_with(successful_mpi_response) + + assert_success purchase + assert_equal 'APPROVED', purchase.params['status'] + end + + def test_mpi_response_success_3ds2 + purchase = stub_comms do + @gateway.purchase(@amount, @three_ds_enrolled_card, @mpi_options_3ds2) + end.check_request do |_, data, _| + assert_match(/sg_ECI/, data) + assert_match(/sg_CAVV/, data) + assert_match(/sg_IsExternalMPI/, data) + assert_match(/sg_dsTransID/, data) + assert_match(/sg_threeDSProtocolVersion=2/, data) + assert_match(/sg_challengePreference/, data) + refute_match(/sg_xid/, data) + end.respond_with(successful_mpi_response) + + assert_success purchase + assert_equal 'APPROVED', purchase.params['status'] + end + + def test_network_tokenization_success + purchase = stub_comms do + @gateway.purchase(@amount, @network_token_credit_card, @mpi_options_3ds2) + end.check_request do |_, data, _| + assert_match(/sg_CAVV/, data) + assert_match(/sg_ECI/, data) + assert_match(/sg_IsExternalMPI/, data) + assert_match(/sg_CardNumber/, data) + assert_match(/sg_challengePreference/, data) + end.respond_with(successful_network_token_response) + + assert_success purchase + assert_equal 'APPROVED', purchase.params['status'] + end + private def pre_scrubbed @@ -360,4 +555,76 @@ def successful_3ds_purchase_response <Response><Version>4.1.0</Version><ClientLoginID>SpreedlyManTestTRX</ClientLoginID><ClientUniqueID>98bd80c8c9534088311153ad6a67d108</ClientUniqueID><TransactionID>101510108310</TransactionID><Status>APPROVED</Status><AuthCode></AuthCode><AVSCode></AVSCode><CVV2Reply></CVV2Reply><ReasonCodes><Reason code="0"></Reason></ReasonCodes><ErrCode>0</ErrCode><ExErrCode>0</ExErrCode><Token>ZQBpAFAAMwBTAEcAMQBZAHcASQA4ADoAPQBlACQAZAB3ACMAWwAyAFoAWQBLAFUAPwBTAHYAKQAnAHQAUAA2AHYAYwAoAG0ARgBNAEEAcAAlAGEAMwA=</Token><CustomData></CustomData><ThreeDResponse><Auth3DResponse><Result>Y</Result><PaReq>eJxVUdtuwjAM/ZWK95GYgijIjVTWaUNTGdqQ4DUKFq2gF9J0A75+SVcuixTF59g+sY5xlWqi+ItUo0lgQnUtd+Rl27BXyScYAQce+MB7ApfRJx0FfpOus7IQ0Of9AbIrtK1apbIwAqU6zuYLMQSY8ABZBzEnPY8FfzhjGCH7o7GQOYlIq9J4K6qNd5VD1mZQlU1h9FkEQ47sCrDRB5EaU00ZO5RKHtKyth2ORXYfaNm4qLYqp2wrkjj6ud8XSFbRKYl3F/uGyFwFbqUhMeAwBvC5B6Opz6c+IGt5lLn73hlgR+kAVu6PqMu4xCOB1l1NhTqLydg6ckNIp6osyFZYJ28xsvvAz2/OT2WsRa+bdf2+X6cXtd9oHxZNPks+ojB0DrcFTi2zrkDAJ62cA8icBOuWx7oF2+jf4n8B</PaReq><MerchantID>000000000000715</MerchantID><ACSurl>https://pit.3dsecure.net/VbVTestSuiteService/pit1/acsService/paReq?summary=MjRlZGYwY2EtZTk5Zi00NDJjLTljOTAtNWUxZmRhMjEwODg3</ACSurl><XID>MDAwMDAwMDAwMDE1MTAxMDgzMTA=</XID><ThreeDReason></ThreeDReason></Auth3DResponse></ThreeDResponse><AcquirerID>19</AcquirerID><IssuerBankName>Visa Production Support Client Bid 1</IssuerBankName><IssuerBankCountry>us</IssuerBankCountry><Reference></Reference><AGVCode></AGVCode><AGVError></AGVError><UniqueCC>rDNDlh6XR8R6CVdGQyqDkZzdqE0=</UniqueCC><CustomData2></CustomData2><ThreeDFlow>1</ThreeDFlow><CreditCardInfo><IsPrepaid>0</IsPrepaid><CardType>Debit</CardType><CardProgram></CardProgram><CardProduct></CardProduct></CreditCardInfo><IsPartialApproval>0</IsPartialApproval><AmountInfo><RequestedAmount>1</RequestedAmount><RequestedCurrency>EUR</RequestedCurrency><ProcessedAmount>1</ProcessedAmount><ProcessedCurrency>EUR</ProcessedCurrency></AmountInfo><RRN></RRN><ICC></ICC><CVVReply></CVVReply></Response> ) end + + def successful_mpi_response + %( + <Response><Version>4.1.0</Version><ClientLoginID>SpreedlyTestTRX</ClientLoginID><ClientUniqueID>27822c1132eba4c731ebe24b6190646f</ClientUniqueID><TransactionID>1110000000009330260</TransactionID><Status>APPROVED</Status><AuthCode>111447</AuthCode><AVSCode></AVSCode><CVV2Reply></CVV2Reply><ReasonCodes><Reason code="0"></Reason></ReasonCodes><ErrCode>0</ErrCode><ExErrCode>0</ExErrCode><Token>UQBzAFEAdABvAG0ATgA5AEwAagBHAGwAPwA7AF0ANgA1AD4AfABOADUAdAA/AD4AZQA3AEcAXQBnAGgAQQA4AG4APABNACUARABFADgAMQBrAFIAMwA=</Token><CustomData></CustomData><ThreeDResponse><VerifyAuth3DResponse><Result></Result><ECI>5</ECI><CAVV>Vk83Y2t0cHRzRFZzRlZlR0JIQXo=</CAVV><WhitelistStatus></WhitelistStatus><XID>00000000000000000501</XID><ThreeDReason></ThreeDReason><ThreeDSVersion></ThreeDSVersion><ThreeDSServerTransID></ThreeDSServerTransID><AcsTransID></AcsTransID><DSTransID>c5b808e7-1de1-4069-a17b-f70d3b3b1645</DSTransID></VerifyAuth3DResponse></ThreeDResponse><AcquirerID>19</AcquirerID><IssuerBankName>Visa Production Support Client Bid 1</IssuerBankName><IssuerBankCountry>gb</IssuerBankCountry><Reference></Reference><AGVCode></AGVCode><AGVError></AGVError><UniqueCC>rDNDlh6XR8R6CVdGQyqDkZzdqE0=</UniqueCC><CustomData2></CustomData2><CreditCardInfo><IsPrepaid>0</IsPrepaid><CardType>Debit</CardType><CardProgram></CardProgram><CardProduct></CardProduct></CreditCardInfo><IsPartialApproval>0</IsPartialApproval><AmountInfo><RequestedAmount>1</RequestedAmount><RequestedCurrency>EUR</RequestedCurrency><ProcessedAmount>1</ProcessedAmount><ProcessedCurrency>EUR</ProcessedCurrency></AmountInfo><RRN></RRN><ICC></ICC><CVVReply></CVVReply><FraudResponse><FinalDecision>Accept</FinalDecision><Recommendations /><Rule /></FraudResponse></Response> + ) + end + + def successful_network_token_response + %( + <Response> + <Version>4.1.0</Version> + <ClientLoginID>SpreedlyTestTRX</ClientLoginID> + <ClientUniqueID>27822c1132eba4c731ebe24b6190646f</ClientUniqueID> + <TransactionID>1110000000009330260</TransactionID> + <Status>APPROVED</Status> + <AuthCode></AuthCode> + <AVSCode></AVSCode> + <CVV2Reply></CVV2Reply> + <ReasonCodes> + <Reason code="0"></Reason> + </ReasonCodes> + <ErrCode>0</ErrCode> + <ExErrCode>0</ExErrCode> + <Token>UQBzAFEAdABvAG0ATgA5AEwAagBHAGwAPwA7AF0ANgA1AD4AfABOADUAdAA/AD4AZQA3AEcAXQBnAGgAQQA4AG4APABNACUARABFADgAMQBrAFIAMwA=</Token> + <CustomData></CustomData> + <ThreeDResponse> + <VerifyAuth3DResponse> + <Result></Result> + <ECI>5</ECI> + <CAVV>Vk83Y2t0cHRzRFZzRlZlR0JIQXo=</CAVV> + <WhitelistStatus></WhitelistStatus> + <XID>00000000000000000501</XID> + <ThreeDReason></ThreeDReason> + <ThreeDSVersion></ThreeDSVersion> + <ThreeDSServerTransID></ThreeDSServerTransID> + <AcsTransID></AcsTransID> + <DSTransID>c5b808e7-1de1-4069-a17b-f70d3b3b1645</DSTransID> + </VerifyAuth3DResponse> + </ThreeDResponse> + <AcquirerID>19</AcquirerID> + <IssuerBankName>Visa Production Support Client Bid 1</IssuerBankName> + <IssuerBankCountry>gb</IssuerBankCountry> + <Reference></Reference> + <AGVCode></AGVCode> + <AGVError></AGVError> + <UniqueCC>rDNDlh6XR8R6CVdGQyqDkZzdqE0=</UniqueCC> + <CustomData2></CustomData2> + <CreditCardInfo> + <IsPrepaid>0</IsPrepaid> + <CardType>Debit</CardType> + <CardProgram></CardProgram> + <CardProduct></CardProduct> + </CreditCardInfo> + <IsPartialApproval>0</IsPartialApproval> + <AmountInfo> + <RequestedAmount>1</RequestedAmount> + <RequestedCurrency>EUR</RequestedCurrency> + <ProcessedAmount>1</ProcessedAmount> + <ProcessedCurrency>EUR</ProcessedCurrency> + </AmountInfo> + <RRN></RRN> + <ICC></ICC> + <CVVReply></CVVReply> +</Response> + ) + end + + def failed_mpi_response + %( + <Response><Version>4.1.0</Version><ClientLoginID>SpreedlyTestTRX</ClientLoginID><ClientUniqueID>040b37ca7af949daeb38a8cff0a16f1b</ClientUniqueID><TransactionID>1110000000009330310</TransactionID><Status>DECLINED</Status><AuthCode></AuthCode><AVSCode></AVSCode><CVV2Reply></CVV2Reply><ReasonCodes><Reason code="0">Decline</Reason></ReasonCodes><ErrCode>-1</ErrCode><ExErrCode>0</ExErrCode><Token>UQBLAEcAdABRAE8AWABPADUANgBCADAAcABGAEUANwArADgAewBTACcAcwAlAF8APABQAEEAXgBVACUAYQBLACMALwBWAEUANQApAD4ARQBqADsAMwA=</Token><CustomData></CustomData><ThreeDResponse><VerifyAuth3DResponse><Result></Result><ECI>5</ECI><CAVV>Vk83Y2t0cHRzRFZzRlZlR0JIQXo=</CAVV><WhitelistStatus></WhitelistStatus><XID>00000000000000000501</XID><ThreeDReason></ThreeDReason><ThreeDSVersion></ThreeDSVersion><ThreeDSServerTransID></ThreeDSServerTransID><AcsTransID></AcsTransID><DSTransID>c5b808e7-1de1-4069-a17b-f70d3b3b1645</DSTransID></VerifyAuth3DResponse></ThreeDResponse><AcquirerID>19</AcquirerID><IssuerBankName></IssuerBankName><IssuerBankCountry></IssuerBankCountry><Reference></Reference><AGVCode></AGVCode><AGVError></AGVError><UniqueCC>GyueFkuQqW+UL38d57fuA5/RqfQ=</UniqueCC><CustomData2></CustomData2><CreditCardInfo><IsPrepaid>0</IsPrepaid><CardType></CardType><CardProgram></CardProgram><CardProduct></CardProduct></CreditCardInfo><IsPartialApproval>0</IsPartialApproval><AmountInfo><RequestedAmount>1</RequestedAmount><RequestedCurrency>EUR</RequestedCurrency><ProcessedAmount>1</ProcessedAmount><ProcessedCurrency>EUR</ProcessedCurrency></AmountInfo><RRN></RRN><ICC></ICC><CVVReply></CVVReply><FraudResponse><FinalDecision>Accept</FinalDecision><Recommendations /><Rule /></FraudResponse></Response> + ) + end end diff --git a/test/unit/gateways/sage_pay_test.rb b/test/unit/gateways/sage_pay_test.rb index 256af606eff..e9ffa38f645 100644 --- a/test/unit/gateways/sage_pay_test.rb +++ b/test/unit/gateways/sage_pay_test.rb @@ -6,23 +6,23 @@ class SagePayTest < Test::Unit::TestCase def setup @gateway = SagePayGateway.new(login: 'X') - @credit_card = credit_card('4242424242424242', :brand => 'visa') - @electron_credit_card = credit_card('4245190000000000', :brand => 'visa') + @credit_card = credit_card('4242424242424242', brand: 'visa') + @electron_credit_card = credit_card('4245190000000000', brand: 'visa') @options = { - :billing_address => { - :name => 'Tekin Suleyman', - :address1 => 'Flat 10 Lapwing Court', - :address2 => 'West Didsbury', - :city => 'Manchester', - :county => 'Greater Manchester', - :country => 'GB', - :zip => 'M20 2PS' + billing_address: { + name: 'Tekin Suleyman', + address1: 'Flat 10 Lapwing Court', + address2: 'West Didsbury', + city: 'Manchester', + county: 'Greater Manchester', + country: 'GB', + zip: 'M20 2PS' }, - :order_id => '1', - :description => 'Store purchase', - :ip => '86.150.65.37', - :email => 'tekin@tekin.co.uk', - :phone => '0161 123 4567' + order_id: '1', + description: 'Store purchase', + ip: '86.150.65.37', + email: 'tekin@tekin.co.uk', + phone: '0161 123 4567' } @amount = 100 end @@ -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 @@ -92,18 +92,18 @@ def test_not_matched_cvv_result end def test_dont_send_fractional_amount_for_chinese_yen - @amount = 100_00 # 100 YEN + @amount = 100_00 # 100 YEN @options[:currency] = 'JPY' - @gateway.expects(:add_pair).with({}, :Amount, '100', :required => true) - @gateway.expects(:add_pair).with({}, :Currency, 'JPY', :required => true) + @gateway.expects(:add_pair).with({}, :Amount, '100', required: true) + @gateway.expects(:add_pair).with({}, :Currency, 'JPY', required: true) @gateway.send(:add_amount, {}, @amount, @options) end def test_send_fractional_amount_for_british_pounds - @gateway.expects(:add_pair).with({}, :Amount, '1.00', :required => true) - @gateway.expects(:add_pair).with({}, :Currency, 'GBP', :required => true) + @gateway.expects(:add_pair).with({}, :Amount, '1.00', required: true) + @gateway.expects(:add_pair).with({}, :Currency, 'GBP', required: true) @gateway.send(:add_amount, {}, @amount, @options) end @@ -111,7 +111,7 @@ def test_send_fractional_amount_for_british_pounds def test_paypal_callback_url_is_submitted stub_comms(@gateway, :ssl_request) do purchase_with_options(paypal_callback_url: 'callback.com') - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/PayPalCallbackURL=callback\.com/, data) end.respond_with(successful_purchase_response) end @@ -119,7 +119,7 @@ def test_paypal_callback_url_is_submitted def test_basket_is_submitted stub_comms(@gateway, :ssl_request) do purchase_with_options(basket: 'A1.2 Basket section') - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/Basket=A1\.2\+Basket\+section/, data) end.respond_with(successful_purchase_response) end @@ -127,7 +127,7 @@ def test_basket_is_submitted def test_gift_aid_payment_is_submitted stub_comms(@gateway, :ssl_request) do purchase_with_options(gift_aid_payment: 1) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/GiftAidPayment=1/, data) end.respond_with(successful_purchase_response) end @@ -135,7 +135,7 @@ def test_gift_aid_payment_is_submitted def test_apply_avscv2_is_submitted stub_comms(@gateway, :ssl_request) do purchase_with_options(apply_avscv2: 1) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/ApplyAVSCV2=1/, data) end.respond_with(successful_purchase_response) end @@ -143,7 +143,7 @@ def test_apply_avscv2_is_submitted def test_disable_3d_security_flag_is_submitted stub_comms(@gateway, :ssl_request) do purchase_with_options(apply_3d_secure: 1) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/Apply3DSecure=1/, data) end.respond_with(successful_purchase_response) end @@ -151,7 +151,7 @@ def test_disable_3d_security_flag_is_submitted def test_account_type_is_submitted stub_comms(@gateway, :ssl_request) do purchase_with_options(account_type: 'M') - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/AccountType=M/, data) end.respond_with(successful_purchase_response) end @@ -159,7 +159,7 @@ def test_account_type_is_submitted def test_billing_agreement_is_submitted stub_comms(@gateway, :ssl_request) do purchase_with_options(billing_agreement: 1) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/BillingAgreement=1/, data) end.respond_with(successful_purchase_response) end @@ -167,7 +167,7 @@ def test_billing_agreement_is_submitted def test_store_token_is_submitted stub_comms(@gateway, :ssl_request) do purchase_with_options(store: true) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/CreateToken=1/, data) end.respond_with(successful_purchase_response) end @@ -175,7 +175,7 @@ def test_store_token_is_submitted def test_basket_xml_is_submitted stub_comms(@gateway, :ssl_request) do purchase_with_options(basket_xml: 'A1.3 BasketXML section') - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/BasketXML=A1\.3\+BasketXML\+section/, data) end.respond_with(successful_purchase_response) end @@ -183,7 +183,7 @@ def test_basket_xml_is_submitted def test_customer_xml_is_submitted stub_comms(@gateway, :ssl_request) do purchase_with_options(customer_xml: 'A1.4 CustomerXML section') - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/CustomerXML=A1\.4\+CustomerXML\+section/, data) end.respond_with(successful_purchase_response) end @@ -191,7 +191,7 @@ def test_customer_xml_is_submitted def test_surcharge_xml_is_submitted stub_comms(@gateway, :ssl_request) do purchase_with_options(surcharge_xml: 'A1.1 SurchargeXML section') - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/SurchargeXML=A1\.1\+SurchargeXML\+section/, data) end.respond_with(successful_purchase_response) end @@ -199,7 +199,7 @@ def test_surcharge_xml_is_submitted def test_vendor_data_is_submitted stub_comms(@gateway, :ssl_request) do purchase_with_options(vendor_data: 'any data') - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/VendorData=any\+data/, data) end.respond_with(successful_purchase_response) end @@ -207,7 +207,7 @@ def test_vendor_data_is_submitted def test_language_is_submitted stub_comms(@gateway, :ssl_request) do purchase_with_options(language: 'FR') - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/Language=FR/, data) end.respond_with(successful_purchase_response) end @@ -215,7 +215,7 @@ def test_language_is_submitted def test_website_is_submitted stub_comms(@gateway, :ssl_request) do purchase_with_options(website: 'transaction-origin.com') - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/Website=transaction-origin\.com/, data) end.respond_with(successful_purchase_response) end @@ -223,9 +223,9 @@ def test_website_is_submitted def test_FIxxxx_optional_fields_are_submitted stub_comms(@gateway, :ssl_request) do purchase_with_options(recipient_account_number: '1234567890', - recipient_surname: 'Withnail', recipient_postcode: 'AB11AB', - recipient_dob: '19701223') - end.check_request do |method, endpoint, data, headers| + recipient_surname: 'Withnail', recipient_postcode: 'AB11AB', + recipient_dob: '19701223') + end.check_request do |_method, _endpoint, data, _headers| assert_match(/FIRecipientAcctNumber=1234567890/, data) assert_match(/FIRecipientSurname=Withnail/, data) assert_match(/FIRecipientPostcode=AB11AB/, data) @@ -234,10 +234,10 @@ def test_FIxxxx_optional_fields_are_submitted end def test_description_is_truncated - huge_description = 'SagePay transactions fail if the déscription is more than 100 characters. Therefore, we truncate it to 100 characters.' + ' Lots more text ' * 1000 + huge_description = 'SagePay transactions fail if the déscription is more than 100 characters. Therefore, we truncate it to 100 characters.' + (' Lots more text ' * 1000) stub_comms(@gateway, :ssl_request) do purchase_with_options(description: huge_description) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/&Description=SagePay\+transactions\+fail\+if\+the\+d%C3%A9scription\+is\+more\+than\+100\+characters.\+Therefore%2C\+we\+trunc&/, data) end.respond_with(successful_purchase_response) end @@ -247,16 +247,25 @@ def test_protocol_version_is_honoured stub_comms(gateway, :ssl_request) do gateway.purchase(@amount, @credit_card, @options) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/VPSProtocol=2.23/, data) 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 @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert data.include?('ReferrerID=00000000-0000-0000-0000-000000000001') end.respond_with(successful_purchase_response) ensure @@ -266,7 +275,7 @@ def test_referrer_id_is_added_to_post_data_parameters def test_successful_store response = stub_comms(@gateway, :ssl_request) do @gateway.store(@credit_card) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/TxType=TOKEN/, data) end.respond_with(successful_purchase_response) @@ -316,10 +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 @@ -327,12 +333,157 @@ def test_successful_authorization_and_capture_and_refund def test_repeat_purchase_with_reference_token stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, '1455548a8d178beecd88fe6a285f50ff;{0D2ACAF0-FA64-6DFF-3869-7ADDDC1E0474};15353766;BS231FNE14;purchase', @options) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/RelatedVPSTxId=%7B0D2ACAF0-FA64-6DFF-3869-7ADDDC1E0474%/, data) assert_match(/TxType=REPEAT/, data) end.respond_with(successful_purchase_response) end + def test_repeat_purchase_from_reference_purchase + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, '9a3c5a71ef733ce56a9b03754763da2c;{4B98024C-5D40-4F5C-4E19-A8D07EBFC5AD};14575233;7NJB98CZSG;repeat', @options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/RelatedVPSTxId=%7B4B98024C-5D40-4F5C-4E19-A8D07EBFC5AD%/, data) + assert_match(/TxType=REPEAT/, data) + end.respond_with(successful_purchase_response) + end + + 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) @@ -340,158 +491,156 @@ def purchase_with_options(optional) end def successful_purchase_response - <<-RESP -VPSProtocol=2.23 -Status=OK -StatusDetail=0000 : The Authorisation was Successful. -VPSTxId=B8AE1CF6-9DEF-C876-1BB4-9B382E6CE520 -SecurityKey=OHMETD7DFK -TxAuthNo=4193753 -AVSCV2=NO DATA MATCHES -AddressResult=NOTMATCHED -PostCodeResult=MATCHED -CV2Result=NOTMATCHED -3DSecureStatus=NOTCHECKED -Token=1 + <<~RESP + VPSProtocol=2.23 + Status=OK + StatusDetail=0000 : The Authorisation was Successful. + VPSTxId=B8AE1CF6-9DEF-C876-1BB4-9B382E6CE520 + SecurityKey=OHMETD7DFK + TxAuthNo=4193753 + AVSCV2=NO DATA MATCHES + AddressResult=NOTMATCHED + PostCodeResult=MATCHED + CV2Result=NOTMATCHED + 3DSecureStatus=NOTCHECKED + Token=1 RESP end def unsuccessful_purchase_response - <<-RESP -VPSProtocol=2.23 -Status=NOTAUTHED -StatusDetail=VSP Direct transaction from VSP Simulator. -VPSTxId=7BBA9078-8489-48CD-BF0D-10B0E6B0EF30 -SecurityKey=DKDYLDYLXV -AVSCV2=ALL MATCH -AddressResult=MATCHED -PostCodeResult=MATCHED -CV2Result=MATCHED + <<~RESP + VPSProtocol=2.23 + Status=NOTAUTHED + StatusDetail=VSP Direct transaction from VSP Simulator. + VPSTxId=7BBA9078-8489-48CD-BF0D-10B0E6B0EF30 + SecurityKey=DKDYLDYLXV + AVSCV2=ALL MATCH + AddressResult=MATCHED + PostCodeResult=MATCHED + CV2Result=MATCHED RESP end def successful_authorize_response - <<-RESP -VPSProtocol=2.23 -Status=OK -StatusDetail=0000 : The Authorisation was Successful. -VPSTxId=B8AE1CF6-9DEF-C876-1BB4-9B382E6CE520 -SecurityKey=OHMETD7DFK -TxAuthNo=4193753 -AVSCV2=NO DATA MATCHES -AddressResult=NOTMATCHED -PostCodeResult=MATCHED -CV2Result=NOTMATCHED -3DSecureStatus=NOTCHECKED -Token=1 + <<~RESP + VPSProtocol=2.23 + Status=OK + StatusDetail=0000 : The Authorisation was Successful. + VPSTxId=B8AE1CF6-9DEF-C876-1BB4-9B382E6CE520 + SecurityKey=OHMETD7DFK + TxAuthNo=4193753 + AVSCV2=NO DATA MATCHES + AddressResult=NOTMATCHED + PostCodeResult=MATCHED + CV2Result=NOTMATCHED + 3DSecureStatus=NOTCHECKED + Token=1 RESP end def successful_refund_response - <<-RESP -VPSProtocol=3.00 -Status=OK -StatusDetail=0000 : The Authorisation was Successful. -SecurityKey=KUMJBP02HM -TxAuthNo=15282432 -VPSTxId={08C870A9-1E53-3852-BA44-CBC91612CBCA} + <<~RESP + VPSProtocol=3.00 + Status=OK + StatusDetail=0000 : The Authorisation was Successful. + SecurityKey=KUMJBP02HM + TxAuthNo=15282432 + VPSTxId={08C870A9-1E53-3852-BA44-CBC91612CBCA} RESP end def successful_capture_response - <<-RESP -VPSProtocol=3.00 -Status=OK -StatusDetail=2004 : The Release was Successful. + <<~RESP + VPSProtocol=3.00 + Status=OK + StatusDetail=2004 : The Release was Successful. RESP end def unsuccessful_authorize_response - <<-RESP -VPSProtocol=2.23 -Status=NOTAUTHED -StatusDetail=VSP Direct transaction from VSP Simulator. -VPSTxId=7BBA9078-8489-48CD-BF0D-10B0E6B0EF30 -SecurityKey=DKDYLDYLXV -AVSCV2=ALL MATCH -AddressResult=MATCHED -PostCodeResult=MATCHED -CV2Result=MATCHED + <<~RESP + VPSProtocol=2.23 + Status=NOTAUTHED + StatusDetail=VSP Direct transaction from VSP Simulator. + VPSTxId=7BBA9078-8489-48CD-BF0D-10B0E6B0EF30 + SecurityKey=DKDYLDYLXV + AVSCV2=ALL MATCH + AddressResult=MATCHED + PostCodeResult=MATCHED + CV2Result=MATCHED RESP end def successful_void_response - <<-RESP -VPSProtocol=2.23 -Status=OK -StatusDetail=2006 : The Abort was Successful. -VPSTxId=B8AE1CF6-9DEF-C876-1BB4-9B382E6CE520 -SecurityKey=OHMETD7DFK -TxAuthNo=4193753 -AVSCV2=NO DATA MATCHES -AddressResult=NOTMATCHED -PostCodeResult=MATCHED -CV2Result=NOTMATCHED -3DSecureStatus=NOTCHECKED -Token=1 + <<~RESP + VPSProtocol=2.23 + Status=OK + StatusDetail=2006 : The Abort was Successful. + VPSTxId=B8AE1CF6-9DEF-C876-1BB4-9B382E6CE520 + SecurityKey=OHMETD7DFK + TxAuthNo=4193753 + AVSCV2=NO DATA MATCHES + AddressResult=NOTMATCHED + PostCodeResult=MATCHED + CV2Result=NOTMATCHED + 3DSecureStatus=NOTCHECKED + Token=1 RESP end def unsuccessful_void_response - <<-RESP -VPSProtocol=2.23 -Status=MALFORMED -StatusDetail=3046 : The VPSTxId field is missing. -VPSTxId=7BBA9078-8489-48CD-BF0D-10B0E6B0EF30 -SecurityKey=DKDYLDYLXV -AVSCV2=ALL MATCH -AddressResult=MATCHED -PostCodeResult=MATCHED -CV2Result=MATCHED + <<~RESP + VPSProtocol=2.23 + Status=MALFORMED + StatusDetail=3046 : The VPSTxId field is missing. + VPSTxId=7BBA9078-8489-48CD-BF0D-10B0E6B0EF30 + SecurityKey=DKDYLDYLXV + AVSCV2=ALL MATCH + AddressResult=MATCHED + PostCodeResult=MATCHED + CV2Result=MATCHED RESP end def transcript - <<-TRANSCRIPT - Amount=1.00&Currency=GBP&VendorTxCode=9094108b21f7b917e68d3e84b49ce9c4&Description=Store+purchase&CardHolder=Tekin+Suleyman&CardNumber=4929000000006&ExpiryDate=0616&CardType=VISA&CV2=123&BillingSurname=Suleyman&BillingFirstnames=Tekin&BillingAddress1=Flat+10+Lapwing+Court&BillingAddress2=West+Didsbury&BillingCity=Manchester&BillingCountry=GB&BillingPostCode=M20+2PS&DeliverySurname=Suleyman&DeliveryFirstnames=Tekin&DeliveryAddress1=120+Grosvenor+St&DeliveryCity=Manchester&DeliveryCountry=GB&DeliveryPostCode=M1+7QW&CustomerEMail=tekin%40tekin.co.uk&ClientIPAddress=86.150.65.37&Vendor=spreedly&TxType=PAYMENT&VPSProtocol=3.00 -I, [2015-07-22T17:16:49.292774 #97998] INFO -- : [ActiveMerchant::Billing::SagePayGateway] --> 200 OK (356 1.8635s) -D, [2015-07-22T17:16:49.292836 #97998] DEBUG -- : VPSProtocol=3.00 -Status=OK -StatusDetail=0000 : The Authorisation was Successful. -VPSTxId={D5B43220-E93C-ED13-6643-D22224BD1CDB} -SecurityKey=7OYK4OHM7Y -TxAuthNo=8769237 -AVSCV2=DATA NOT CHECKED -AddressResult=NOTPROVIDED -PostCodeResult=NOTPROVIDED -CV2Result=NOTPROVIDED -3DSecureStatus=NOTCHECKED -DeclineCode=00 -ExpiryDate=0616 -BankAuthCode=999777 - TRANSCRIPT - + <<~TRANSCRIPT + Amount=1.00&Currency=GBP&VendorTxCode=9094108b21f7b917e68d3e84b49ce9c4&Description=Store+purchase&CardHolder=Tekin+Suleyman&CardNumber=4929000000006&ExpiryDate=0616&CardType=VISA&CV2=123&BillingSurname=Suleyman&BillingFirstnames=Tekin&BillingAddress1=Flat+10+Lapwing+Court&BillingAddress2=West+Didsbury&BillingCity=Manchester&BillingCountry=GB&BillingPostCode=M20+2PS&DeliverySurname=Suleyman&DeliveryFirstnames=Tekin&DeliveryAddress1=120+Grosvenor+St&DeliveryCity=Manchester&DeliveryCountry=GB&DeliveryPostCode=M1+7QW&CustomerEMail=tekin%40tekin.co.uk&ClientIPAddress=86.150.65.37&Vendor=spreedly&TxType=PAYMENT&VPSProtocol=3.00 + I, [2015-07-22T17:16:49.292774 #97998] INFO -- : [ActiveMerchant::Billing::SagePayGateway] --> 200 OK (356 1.8635s) + D, [2015-07-22T17:16:49.292836 #97998] DEBUG -- : VPSProtocol=3.00 + Status=OK + StatusDetail=0000 : The Authorisation was Successful. + VPSTxId={D5B43220-E93C-ED13-6643-D22224BD1CDB} + SecurityKey=7OYK4OHM7Y + TxAuthNo=8769237 + AVSCV2=DATA NOT CHECKED + AddressResult=NOTPROVIDED + PostCodeResult=NOTPROVIDED + CV2Result=NOTPROVIDED + 3DSecureStatus=NOTCHECKED + DeclineCode=00 + ExpiryDate=0616 + BankAuthCode=999777 + TRANSCRIPT end def scrubbed_transcript - <<-TRANSCRIPT - Amount=1.00&Currency=GBP&VendorTxCode=9094108b21f7b917e68d3e84b49ce9c4&Description=Store+purchase&CardHolder=Tekin+Suleyman&CardNumber=[FILTERED]&ExpiryDate=0616&CardType=VISA&CV2=[FILTERED]&BillingSurname=Suleyman&BillingFirstnames=Tekin&BillingAddress1=Flat+10+Lapwing+Court&BillingAddress2=West+Didsbury&BillingCity=Manchester&BillingCountry=GB&BillingPostCode=M20+2PS&DeliverySurname=Suleyman&DeliveryFirstnames=Tekin&DeliveryAddress1=120+Grosvenor+St&DeliveryCity=Manchester&DeliveryCountry=GB&DeliveryPostCode=M1+7QW&CustomerEMail=tekin%40tekin.co.uk&ClientIPAddress=86.150.65.37&Vendor=spreedly&TxType=PAYMENT&VPSProtocol=3.00 -I, [2015-07-22T17:16:49.292774 #97998] INFO -- : [ActiveMerchant::Billing::SagePayGateway] --> 200 OK (356 1.8635s) -D, [2015-07-22T17:16:49.292836 #97998] DEBUG -- : VPSProtocol=3.00 -Status=OK -StatusDetail=0000 : The Authorisation was Successful. -VPSTxId={D5B43220-E93C-ED13-6643-D22224BD1CDB} -SecurityKey=7OYK4OHM7Y -TxAuthNo=8769237 -AVSCV2=DATA NOT CHECKED -AddressResult=NOTPROVIDED -PostCodeResult=NOTPROVIDED -CV2Result=NOTPROVIDED -3DSecureStatus=NOTCHECKED -DeclineCode=00 -ExpiryDate=0616 -BankAuthCode=999777 - TRANSCRIPT - + <<~TRANSCRIPT + Amount=1.00&Currency=GBP&VendorTxCode=9094108b21f7b917e68d3e84b49ce9c4&Description=Store+purchase&CardHolder=Tekin+Suleyman&CardNumber=[FILTERED]&ExpiryDate=0616&CardType=VISA&CV2=[FILTERED]&BillingSurname=Suleyman&BillingFirstnames=Tekin&BillingAddress1=Flat+10+Lapwing+Court&BillingAddress2=West+Didsbury&BillingCity=Manchester&BillingCountry=GB&BillingPostCode=M20+2PS&DeliverySurname=Suleyman&DeliveryFirstnames=Tekin&DeliveryAddress1=120+Grosvenor+St&DeliveryCity=Manchester&DeliveryCountry=GB&DeliveryPostCode=M1+7QW&CustomerEMail=tekin%40tekin.co.uk&ClientIPAddress=86.150.65.37&Vendor=spreedly&TxType=PAYMENT&VPSProtocol=3.00 + I, [2015-07-22T17:16:49.292774 #97998] INFO -- : [ActiveMerchant::Billing::SagePayGateway] --> 200 OK (356 1.8635s) + D, [2015-07-22T17:16:49.292836 #97998] DEBUG -- : VPSProtocol=3.00 + Status=OK + StatusDetail=0000 : The Authorisation was Successful. + VPSTxId={D5B43220-E93C-ED13-6643-D22224BD1CDB} + SecurityKey=7OYK4OHM7Y + TxAuthNo=8769237 + AVSCV2=DATA NOT CHECKED + AddressResult=NOTPROVIDED + PostCodeResult=NOTPROVIDED + CV2Result=NOTPROVIDED + 3DSecureStatus=NOTCHECKED + DeclineCode=00 + ExpiryDate=0616 + BankAuthCode=999777 + TRANSCRIPT end end diff --git a/test/unit/gateways/sage_test.rb b/test/unit/gateways/sage_test.rb index 9d7b2707929..69d3e98d876 100644 --- a/test/unit/gateways/sage_test.rb +++ b/test/unit/gateways/sage_test.rb @@ -5,29 +5,29 @@ class SageGatewayTest < Test::Unit::TestCase def setup @gateway = SageGateway.new( - :login => 'login', - :password => 'password' - ) + login: 'login', + password: 'password' + ) @credit_card = credit_card @check = check @amount = 100 @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase' + order_id: '1', + billing_address: address, + description: 'Store Purchase' } @check_options = { - :order_id => generate_unique_id, - :billing_address => address, - :shipping_address => address, - :email => 'longbob@example.com', - :drivers_license_state => 'CA', - :drivers_license_number => '12345689', - :date_of_birth => Date.new(1978, 8, 11), - :ssn => '078051120' + order_id: generate_unique_id, + billing_address: address, + shipping_address: address, + email: 'longbob@example.com', + drivers_license_state: 'CA', + drivers_license_number: '12345689', + date_of_birth: Date.new(1978, 8, 11), + ssn: '078051120' } end @@ -157,16 +157,16 @@ def test_invalid_login def test_include_customer_number_for_numeric_values stub_comms do - @gateway.purchase(@amount, @credit_card, @options.merge({:customer => '123'})) - end.check_request do |method, data| + @gateway.purchase(@amount, @credit_card, @options.merge({ customer: '123' })) + end.check_request do |_method, data| assert data =~ /T_customer_number=123/ end.respond_with(successful_authorization_response) end def test_dont_include_customer_number_for_numeric_values stub_comms do - @gateway.purchase(@amount, @credit_card, @options.merge({:customer => 'bob@test.com'})) - end.check_request do |method, data| + @gateway.purchase(@amount, @credit_card, @options.merge({ customer: 'bob@test.com' })) + end.check_request do |_method, data| assert data !~ /T_customer_number/ end.respond_with(successful_authorization_response) end @@ -188,7 +188,7 @@ def test_cvv_result def test_address_with_state post = {} options = { - :billing_address => { :country => 'US', :state => 'CA'} + billing_address: { country: 'US', state: 'CA' } } @gateway.send(:add_addresses, post, options) @@ -199,7 +199,7 @@ def test_address_with_state def test_address_without_state post = {} options = { - :billing_address => { :country => 'NZ', :state => ''} + billing_address: { country: 'NZ', state: '' } } @gateway.send(:add_addresses, post, options) @@ -249,7 +249,7 @@ def test_declined_check_purchase def test_successful_store response = stub_comms do @gateway.store(@credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, headers| assert_match(/<ns1:M_ID>login<\/ns1:M_ID>/, data) assert_match(/<ns1:M_KEY>password<\/ns1:M_KEY>/, data) assert_match(/<ns1:CARDNUMBER>#{credit_card.number}<\/ns1:CARDNUMBER>/, data) @@ -267,7 +267,7 @@ def test_successful_store def test_failed_store response = stub_comms do @gateway.store(@credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, headers| assert_match(/<ns1:M_ID>login<\/ns1:M_ID>/, data) assert_match(/<ns1:M_KEY>password<\/ns1:M_KEY>/, data) assert_match(/<ns1:CARDNUMBER>#{credit_card.number}<\/ns1:CARDNUMBER>/, data) @@ -285,7 +285,7 @@ def test_failed_store def test_successful_unstore response = stub_comms do @gateway.unstore('1234', @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, headers| assert_match(/<ns1:M_ID>login<\/ns1:M_ID>/, data) assert_match(/<ns1:M_KEY>password<\/ns1:M_KEY>/, data) assert_match(/<ns1:GUID>1234<\/ns1:GUID>/, data) @@ -301,7 +301,7 @@ def test_successful_unstore def test_failed_unstore response = stub_comms do @gateway.unstore('1234', @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, headers| assert_match(/<ns1:M_ID>login<\/ns1:M_ID>/, data) assert_match(/<ns1:M_KEY>password<\/ns1:M_KEY>/, data) assert_match(/<ns1:GUID>1234<\/ns1:GUID>/, data) @@ -324,6 +324,7 @@ def test_supports_scrubbing? end private + def successful_authorization_response "\002A911911APPROVED 00MX001234567890\0341000\0340\034\003" end @@ -365,175 +366,175 @@ def expected_expiration_date end def successful_store_response - <<-XML -<?xml version="1.0" encoding="utf-8" ?> -<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <soap:Body> - <INSERT_CREDIT_CARD_DATAResponse xmlns="https://www.sagepayments.net/web_services/wsVault/wsVault"> - <INSERT_CREDIT_CARD_DATAResult> - <!-- Bunch of xs:schema stuff. Then... --> - <diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1"> - <NewDataSet xmlns=""> - <Table1 diffgr:id="Table11" msdata:rowOrder="0" diffgr:hasChanges="inserted"> - <SUCCESS>true</SUCCESS> - <GUID>66234d2dfec24efe9fdcd4b751578c11</GUID> - <MESSAGE>SUCCESS</MESSAGE> - </Table1> - </NewDataSet> - </diffgr:diffgram> - </INSERT_CREDIT_CARD_DATAResult> - </INSERT_CREDIT_CARD_DATAResponse> - </soap:Body> -</soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8" ?> + <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> + <soap:Body> + <INSERT_CREDIT_CARD_DATAResponse xmlns="https://www.sagepayments.net/web_services/wsVault/wsVault"> + <INSERT_CREDIT_CARD_DATAResult> + <!-- Bunch of xs:schema stuff. Then... --> + <diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1"> + <NewDataSet xmlns=""> + <Table1 diffgr:id="Table11" msdata:rowOrder="0" diffgr:hasChanges="inserted"> + <SUCCESS>true</SUCCESS> + <GUID>66234d2dfec24efe9fdcd4b751578c11</GUID> + <MESSAGE>SUCCESS</MESSAGE> + </Table1> + </NewDataSet> + </diffgr:diffgram> + </INSERT_CREDIT_CARD_DATAResult> + </INSERT_CREDIT_CARD_DATAResponse> + </soap:Body> + </soap:Envelope> XML end def failed_store_response - <<-XML -<?xml version="1.0" encoding="utf-8" ?> -<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <soap:Body> - <INSERT_CREDIT_CARD_DATAResponse xmlns="https://www.sagepayments.net/web_services/wsVault/wsVault"> - <INSERT_CREDIT_CARD_DATAResult> - <!-- Bunch of xs:schema stuff. Then... --> - <diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1"> - <NewDataSet xmlns=""> - <Table1 diffgr:id="Table11" msdata:rowOrder="0" diffgr:hasChanges="inserted"> - <SUCCESS>false</SUCCESS> - <GUID /> - <MESSAGE>UNABLE TO VERIFY VAULT SERVICE</MESSAGE> - </Table1> - </NewDataSet> - </diffgr:diffgram> - </INSERT_CREDIT_CARD_DATAResult> - </INSERT_CREDIT_CARD_DATAResponse> - </soap:Body> -</soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8" ?> + <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> + <soap:Body> + <INSERT_CREDIT_CARD_DATAResponse xmlns="https://www.sagepayments.net/web_services/wsVault/wsVault"> + <INSERT_CREDIT_CARD_DATAResult> + <!-- Bunch of xs:schema stuff. Then... --> + <diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1"> + <NewDataSet xmlns=""> + <Table1 diffgr:id="Table11" msdata:rowOrder="0" diffgr:hasChanges="inserted"> + <SUCCESS>false</SUCCESS> + <GUID /> + <MESSAGE>UNABLE TO VERIFY VAULT SERVICE</MESSAGE> + </Table1> + </NewDataSet> + </diffgr:diffgram> + </INSERT_CREDIT_CARD_DATAResult> + </INSERT_CREDIT_CARD_DATAResponse> + </soap:Body> + </soap:Envelope> XML end def successful_unstore_response - <<-XML -<?xml version="1.0" encoding="utf-8" ?> -<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <soap:Body> - <DELETE_DATAResponse xmlns="https://www.sagepayments.net/web_services/wsVault/wsVault"> - <DELETE_DATAResult>true</DELETE_DATAResult> - </DELETE_DATAResponse> - </soap:Body> -</soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8" ?> + <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> + <soap:Body> + <DELETE_DATAResponse xmlns="https://www.sagepayments.net/web_services/wsVault/wsVault"> + <DELETE_DATAResult>true</DELETE_DATAResult> + </DELETE_DATAResponse> + </soap:Body> + </soap:Envelope> XML end def failed_unstore_response - <<-XML -<?xml version="1.0" encoding="utf-8" ?> -<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <soap:Body> - <DELETE_DATAResponse xmlns="https://www.sagepayments.net/web_services/wsVault/wsVault"> - <DELETE_DATAResult>false</DELETE_DATAResult> - </DELETE_DATAResponse> - </soap:Body> -</soap:Envelope> + <<~XML + <?xml version="1.0" encoding="utf-8" ?> + <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> + <soap:Body> + <DELETE_DATAResponse xmlns="https://www.sagepayments.net/web_services/wsVault/wsVault"> + <DELETE_DATAResult>false</DELETE_DATAResult> + </DELETE_DATAResponse> + </soap:Body> + </soap:Envelope> XML end def pre_scrubbed - <<-PRE_SCRUBBED -opening connection to www.sagepayments.net:443... -opened -starting SSL for www.sagepayments.net:443... -SSL established -<- "POST /cgi-bin/eftBankcard.dll?transaction 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: www.sagepayments.net\r\nContent-Length: 444\r\n\r\n" -<- "C_name=Longbob+Longsen&C_cardnumber=4111111111111111&C_exp=0917&C_cvv=123&T_amt=1.00&T_ordernum=1741a24e00a5a5f11653&C_address=456+My+Street&C_city=Ottawa&C_state=ON&C_zip=K1C2N6&C_country=CA&C_telephone=%28555%29555-5555&C_fax=%28555%29555-6666&C_email=longbob%40example.com&C_ship_name=Jim+Smith&C_ship_address=456+My+Street&C_ship_city=Ottawa&C_ship_state=ON&C_ship_zip=K1C2N6&C_ship_country=CA&M_id=214282982451&M_key=Z5W2S8J7X8T5&T_code=01" --> "HTTP/1.1 200 OK\r\n" --> "Content-Type: text/html\r\n" --> "Content-Encoding: gzip\r\n" --> "Vary: Accept-Encoding\r\n" --> "Server: \r\n" --> "X-AspNet-Version: \r\n" --> "X-Powered-By: ASP.NET\r\n" --> "Date: Thu, 30 Jun 2016 02:58:40 GMT\r\n" --> "Connection: close\r\n" --> "Content-Length: 185\r\n" --> "\r\n" -reading 185 bytes... --> "\x1F\x8B\b\x00\x00\x00\x00\x00\x04\x00\xED\xBD\a`\x1CI\x96%&/m\xCA{\x7FJ\xF5J\xD7\xE0t\xA1\b\x80`\x13$\xD8\x90@\x10\xEC\xC1\x88\xCD\xE6\x92\xEC\x1DiG#)\xAB*\x81\xCAeVe]f\x16@\xCC\xED\x9D\xBC\xF7\xDE{\xEF\xBD\xF7\xDE{\xEF\xBD\xF7\xBA;\x9DN'\xF7\xDF\xFF?\\fd\x01l\xF6\xCEJ\xDA\xC9\x9E!\x80\xAA\xC8\x1F?~|\x1F?\"~\xAD\xE3\x1D<\xBB\xC7/_\xBE\xFA\xF2'O\x9F\xA6\xF4\a\xFD\x99v\x9F\xDD\x9D/\xE8\xAB\xCF?}\xF3\xF9\x17\xEF\xCE\xBF;\xDF\xF9\x9Dv\x1F\xEC\xEFf{\xFB\xF9\xCENv?\xBB\x7F\xBE\xBB\xFB\xE9\xFD{\xBF\xD3\xCE\xEF\xF4k\xFF?XI\x04rQ\x00\x00\x00" -read 185 bytes -Conn close + <<~PRE_SCRUBBED + opening connection to www.sagepayments.net:443... + opened + starting SSL for www.sagepayments.net:443... + SSL established + <- "POST /cgi-bin/eftBankcard.dll?transaction 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: www.sagepayments.net\r\nContent-Length: 444\r\n\r\n" + <- "C_name=Longbob+Longsen&C_cardnumber=4111111111111111&C_exp=0917&C_cvv=123&T_amt=1.00&T_ordernum=1741a24e00a5a5f11653&C_address=456+My+Street&C_city=Ottawa&C_state=ON&C_zip=K1C2N6&C_country=CA&C_telephone=%28555%29555-5555&C_fax=%28555%29555-6666&C_email=longbob%40example.com&C_ship_name=Jim+Smith&C_ship_address=456+My+Street&C_ship_city=Ottawa&C_ship_state=ON&C_ship_zip=K1C2N6&C_ship_country=CA&M_id=214282982451&M_key=Z5W2S8J7X8T5&T_code=01" + -> "HTTP/1.1 200 OK\r\n" + -> "Content-Type: text/html\r\n" + -> "Content-Encoding: gzip\r\n" + -> "Vary: Accept-Encoding\r\n" + -> "Server: \r\n" + -> "X-AspNet-Version: \r\n" + -> "X-Powered-By: ASP.NET\r\n" + -> "Date: Thu, 30 Jun 2016 02:58:40 GMT\r\n" + -> "Connection: close\r\n" + -> "Content-Length: 185\r\n" + -> "\r\n" + reading 185 bytes... + -> "\x1F\x8B\b\x00\x00\x00\x00\x00\x04\x00\xED\xBD\a`\x1CI\x96%&/m\xCA{\x7FJ\xF5J\xD7\xE0t\xA1\b\x80`\x13$\xD8\x90@\x10\xEC\xC1\x88\xCD\xE6\x92\xEC\x1DiG#)\xAB*\x81\xCAeVe]f\x16@\xCC\xED\x9D\xBC\xF7\xDE{\xEF\xBD\xF7\xDE{\xEF\xBD\xF7\xBA;\x9DN'\xF7\xDF\xFF?\\fd\x01l\xF6\xCEJ\xDA\xC9\x9E!\x80\xAA\xC8\x1F?~|\x1F?\"~\xAD\xE3\x1D<\xBB\xC7/_\xBE\xFA\xF2'O\x9F\xA6\xF4\a\xFD\x99v\x9F\xDD\x9D/\xE8\xAB\xCF?}\xF3\xF9\x17\xEF\xCE\xBF;\xDF\xF9\x9Dv\x1F\xEC\xEFf{\xFB\xF9\xCENv?\xBB\x7F\xBE\xBB\xFB\xE9\xFD{\xBF\xD3\xCE\xEF\xF4k\xFF?XI\x04rQ\x00\x00\x00" + read 185 bytes + Conn close PRE_SCRUBBED end def post_scrubbed - <<-POST_SCRUBBED -opening connection to www.sagepayments.net:443... -opened -starting SSL for www.sagepayments.net:443... -SSL established -<- "POST /cgi-bin/eftBankcard.dll?transaction 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: www.sagepayments.net\r\nContent-Length: 444\r\n\r\n" -<- "C_name=Longbob+Longsen&C_cardnumber=[FILTERED]&C_exp=0917&C_cvv=[FILTERED]&T_amt=1.00&T_ordernum=1741a24e00a5a5f11653&C_address=456+My+Street&C_city=Ottawa&C_state=ON&C_zip=K1C2N6&C_country=CA&C_telephone=%28555%29555-5555&C_fax=%28555%29555-6666&C_email=longbob%40example.com&C_ship_name=Jim+Smith&C_ship_address=456+My+Street&C_ship_city=Ottawa&C_ship_state=ON&C_ship_zip=K1C2N6&C_ship_country=CA&M_id=[FILTERED]&M_key=[FILTERED]&T_code=01" --> "HTTP/1.1 200 OK\r\n" --> "Content-Type: text/html\r\n" --> "Content-Encoding: gzip\r\n" --> "Vary: Accept-Encoding\r\n" --> "Server: \r\n" --> "X-AspNet-Version: \r\n" --> "X-Powered-By: ASP.NET\r\n" --> "Date: Thu, 30 Jun 2016 02:58:40 GMT\r\n" --> "Connection: close\r\n" --> "Content-Length: 185\r\n" --> "\r\n" -reading 185 bytes... --> \"\u001F?\b\u0000\u0000\u0000\u0000\u0000\u0004\u0000??\a`\u001CI?%&/m?{\u007FJ?J??t?\b?`\u0013$?@\u0010??????\u001DiG#)?*??eVe]f\u0016@????{???{???;?N'????\\fd\u0001l??J??!???\u001F?~|\u001F?\"~??\u001D<??/_???'O???\a??v??/???}??\u0017??;???v\u001F??f{???Nv??\u007F?????{?????k??XI\u0004rQ\u0000\u0000\u0000\" -read 185 bytes -Conn close + <<~POST_SCRUBBED + opening connection to www.sagepayments.net:443... + opened + starting SSL for www.sagepayments.net:443... + SSL established + <- "POST /cgi-bin/eftBankcard.dll?transaction 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: www.sagepayments.net\r\nContent-Length: 444\r\n\r\n" + <- "C_name=Longbob+Longsen&C_cardnumber=[FILTERED]&C_exp=0917&C_cvv=[FILTERED]&T_amt=1.00&T_ordernum=1741a24e00a5a5f11653&C_address=456+My+Street&C_city=Ottawa&C_state=ON&C_zip=K1C2N6&C_country=CA&C_telephone=%28555%29555-5555&C_fax=%28555%29555-6666&C_email=longbob%40example.com&C_ship_name=Jim+Smith&C_ship_address=456+My+Street&C_ship_city=Ottawa&C_ship_state=ON&C_ship_zip=K1C2N6&C_ship_country=CA&M_id=[FILTERED]&M_key=[FILTERED]&T_code=01" + -> "HTTP/1.1 200 OK\r\n" + -> "Content-Type: text/html\r\n" + -> "Content-Encoding: gzip\r\n" + -> "Vary: Accept-Encoding\r\n" + -> "Server: \r\n" + -> "X-AspNet-Version: \r\n" + -> "X-Powered-By: ASP.NET\r\n" + -> "Date: Thu, 30 Jun 2016 02:58:40 GMT\r\n" + -> "Connection: close\r\n" + -> "Content-Length: 185\r\n" + -> "\r\n" + reading 185 bytes... + -> \"\u001F?\b\u0000\u0000\u0000\u0000\u0000\u0004\u0000??\a`\u001CI?%&/m?{\u007FJ?J??t?\b?`\u0013$?@\u0010??????\u001DiG#)?*??eVe]f\u0016@????{???{???;?N'????\\fd\u0001l??J??!???\u001F?~|\u001F?\"~??\u001D<??/_???'O???\a??v??/???}??\u0017??;???v\u001F??f{???Nv??\u007F?????{?????k??XI\u0004rQ\u0000\u0000\u0000\" + read 185 bytes + Conn close POST_SCRUBBED end def pre_scrubbed_echeck - <<-PRE_SCRUBBED -opening connection to www.sagepayments.net:443... -opened -starting SSL for www.sagepayments.net:443... -SSL established -<- "POST /cgi-bin/eftVirtualCheck.dll?transaction 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: www.sagepayments.net\r\nContent-Length: 562\r\n\r\n" -<- "C_first_name=Jim&C_last_name=Smith&C_rte=244183602&C_acct=15378535&C_check_number=1&C_acct_type=DDA&C_customer_type=WEB&C_originator_id=&T_addenda=&C_ssn=&C_dl_state_code=&C_dl_number=&C_dob=&T_amt=1.00&T_ordernum=0ac6fd1f74a98de94bf9&C_address=456+My+Street&C_city=Ottawa&C_state=ON&C_zip=K1C2N6&C_country=CA&C_telephone=%28555%29555-5555&C_fax=%28555%29555-6666&C_email=longbob%40example.com&C_ship_name=Jim+Smith&C_ship_address=456+My+Street&C_ship_city=Ottawa&C_ship_state=ON&C_ship_zip=K1C2N6&C_ship_country=CA&M_id=562313162894&M_key=J6U9B3G2F6L3&T_code=01" --> "HTTP/1.1 200 OK\r\n" --> "Cache-Control: no-cache\r\n" --> "Pragma: no-cache\r\n" --> "Transfer-Encoding: chunked\r\n" --> "Content-Type: text/html; charset=us-ascii\r\n" --> "Content-Encoding: gzip\r\n" --> "Expires: -1\r\n" --> "Vary: Accept-Encoding\r\n" --> "Server: Microsoft-IIS/7.5\r\n" --> "X-Powered-By: ASP.NET\r\n" --> "Date: Thu, 02 Nov 2017 13:26:30 GMT\r\n" --> "Connection: close\r\n" --> "\r\n" --> "ac\r\n" -reading 172 bytes... --> "\x1F\x8B\b\x00\x00\x00\x00\x00\x04\x00\xED\xBD\a`\x1CI\x96%&/m\xCA{\x7FJ\xF5J\xD7\xE0t\xA1\b\x80`\x13$\xD8\x90@\x10\xEC\xC1\x88\xCD\xE6\x92\xEC\x1DiG#)\xAB*\x81\xCAeVe]f\x16@\xCC\xED\x9D\xBC\xF7\xDE{\xEF\xBD\xF7\xDE{\xEF\xBD\xF7\xBA;\x9DN'\xF7\xDF\xFF?\\fd\x01l\xF6\xCEJ\xDA\xC9\x9E!\x80\xAA\xC8\x1F?~|\x1F?\"~\xAD\xE3\x94\x9F\xE3\x93\x93\xD3\x97oN\x9F\xD2\xAF\xD1gg\xE7\xD9\x93\xBDo?\xFC\x89\xAF\x16\xF7v~\xA7\x9Dl\xFA\xE9\xF9l\xF7\xFC\xC1~\xF6\xF0`\x96?\xDC\x9F\x9C?\xFC\x9Dv~\xA7\x17_\xBE8\xA5\x1F\xBF" -read 172 bytes -reading 2 bytes... --> "\r\n" -read 2 bytes --> "b\r\n" -reading 11 bytes... --> "\xF6\xFF\x03\x90\xEB\x1E T\x00\x00\x00" -read 11 bytes -reading 2 bytes... --> "\r\n" -read 2 bytes --> "0\r\n" --> "\r\n" -Conn close + <<~PRE_SCRUBBED + opening connection to www.sagepayments.net:443... + opened + starting SSL for www.sagepayments.net:443... + SSL established + <- "POST /cgi-bin/eftVirtualCheck.dll?transaction 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: www.sagepayments.net\r\nContent-Length: 562\r\n\r\n" + <- "C_first_name=Jim&C_last_name=Smith&C_rte=244183602&C_acct=15378535&C_check_number=1&C_acct_type=DDA&C_customer_type=WEB&C_originator_id=&T_addenda=&C_ssn=&C_dl_state_code=&C_dl_number=&C_dob=&T_amt=1.00&T_ordernum=0ac6fd1f74a98de94bf9&C_address=456+My+Street&C_city=Ottawa&C_state=ON&C_zip=K1C2N6&C_country=CA&C_telephone=%28555%29555-5555&C_fax=%28555%29555-6666&C_email=longbob%40example.com&C_ship_name=Jim+Smith&C_ship_address=456+My+Street&C_ship_city=Ottawa&C_ship_state=ON&C_ship_zip=K1C2N6&C_ship_country=CA&M_id=562313162894&M_key=J6U9B3G2F6L3&T_code=01" + -> "HTTP/1.1 200 OK\r\n" + -> "Cache-Control: no-cache\r\n" + -> "Pragma: no-cache\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Content-Type: text/html; charset=us-ascii\r\n" + -> "Content-Encoding: gzip\r\n" + -> "Expires: -1\r\n" + -> "Vary: Accept-Encoding\r\n" + -> "Server: Microsoft-IIS/7.5\r\n" + -> "X-Powered-By: ASP.NET\r\n" + -> "Date: Thu, 02 Nov 2017 13:26:30 GMT\r\n" + -> "Connection: close\r\n" + -> "\r\n" + -> "ac\r\n" + reading 172 bytes... + -> "\x1F\x8B\b\x00\x00\x00\x00\x00\x04\x00\xED\xBD\a`\x1CI\x96%&/m\xCA{\x7FJ\xF5J\xD7\xE0t\xA1\b\x80`\x13$\xD8\x90@\x10\xEC\xC1\x88\xCD\xE6\x92\xEC\x1DiG#)\xAB*\x81\xCAeVe]f\x16@\xCC\xED\x9D\xBC\xF7\xDE{\xEF\xBD\xF7\xDE{\xEF\xBD\xF7\xBA;\x9DN'\xF7\xDF\xFF?\\fd\x01l\xF6\xCEJ\xDA\xC9\x9E!\x80\xAA\xC8\x1F?~|\x1F?\"~\xAD\xE3\x94\x9F\xE3\x93\x93\xD3\x97oN\x9F\xD2\xAF\xD1gg\xE7\xD9\x93\xBDo?\xFC\x89\xAF\x16\xF7v~\xA7\x9Dl\xFA\xE9\xF9l\xF7\xFC\xC1~\xF6\xF0`\x96?\xDC\x9F\x9C?\xFC\x9Dv~\xA7\x17_\xBE8\xA5\x1F\xBF" + read 172 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "b\r\n" + reading 11 bytes... + -> "\xF6\xFF\x03\x90\xEB\x1E T\x00\x00\x00" + read 11 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close PRE_SCRUBBED end def post_scrubbed_echeck - <<-POST_SCRUBBED -opening connection to www.sagepayments.net:443...\nopened\nstarting SSL for www.sagepayments.net:443...\nSSL established\n<- \"POST /cgi-bin/eftVirtualCheck.dll?transaction 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: www.sagepayments.net\r\nContent-Length: 562\r\n\r\n\"\n<- \"C_first_name=Jim&C_last_name=Smith&C_rte=[FILTERED]&C_acct=[FILTERED]&C_check_number=1&C_acct_type=DDA&C_customer_type=WEB&C_originator_id=&T_addenda=&C_ssn=[FILTERED]&C_dl_state_code=&C_dl_number=&C_dob=&T_amt=1.00&T_ordernum=0ac6fd1f74a98de94bf9&C_address=456+My+Street&C_city=Ottawa&C_state=ON&C_zip=K1C2N6&C_country=CA&C_telephone=%28555%29555-5555&C_fax=%28555%29555-6666&C_email=longbob%40example.com&C_ship_name=Jim+Smith&C_ship_address=456+My+Street&C_ship_city=Ottawa&C_ship_state=ON&C_ship_zip=K1C2N6&C_ship_country=CA&M_id=[FILTERED]&M_key=[FILTERED]&T_code=01\"\n-> \"HTTP/1.1 200 OK\r\n\"\n-> \"Cache-Control: no-cache\r\n\"\n-> \"Pragma: no-cache\r\n\"\n-> \"Transfer-Encoding: chunked\r\n\"\n-> \"Content-Type: text/html; charset=us-ascii\r\n\"\n-> \"Content-Encoding: gzip\r\n\"\n-> \"Expires: -1\r\n\"\n-> \"Vary: Accept-Encoding\r\n\"\n-> \"Server: Microsoft-IIS/7.5\r\n\"\n-> \"X-Powered-By: ASP.NET\r\n\"\n-> \"Date: Thu, 02 Nov 2017 13:26:30 GMT\r\n\"\n-> \"Connection: close\r\n\"\n-> \"\r\n\"\n-> \"ac\r\n\"\nreading 172 bytes...\n-> \"\u001F?\b\u0000\u0000\u0000\u0000\u0000\u0004\u0000??\a`\u001CI?%&/m?{\u007FJ?J??t?\b?`\u0013$?@\u0010??????\u001DiG#)?*??eVe]f\u0016@????{???{???;?N'????\\fd\u0001l??J??!???\u001F?~|\u001F?\"~????oN???gg???o????\u0016?v~??l???l???~??`???????v~?\u0017_?8?\u001F?\"\nread 172 bytes\nreading 2 bytes...\n-> \"\r\n\"\nread 2 bytes\n-> \"b\r\n\"\nreading 11 bytes...\n-> \"??\u0003??\u001E T\u0000\u0000\u0000\"\nread 11 bytes\nreading 2 bytes...\n-> \"\r\n\"\nread 2 bytes\n-> \"0\r\n\"\n-> \"\r\n\"\nConn close + <<~POST_SCRUBBED + opening connection to www.sagepayments.net:443...\nopened\nstarting SSL for www.sagepayments.net:443...\nSSL established\n<- \"POST /cgi-bin/eftVirtualCheck.dll?transaction 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: www.sagepayments.net\r\nContent-Length: 562\r\n\r\n\"\n<- \"C_first_name=Jim&C_last_name=Smith&C_rte=[FILTERED]&C_acct=[FILTERED]&C_check_number=1&C_acct_type=DDA&C_customer_type=WEB&C_originator_id=&T_addenda=&C_ssn=[FILTERED]&C_dl_state_code=&C_dl_number=&C_dob=&T_amt=1.00&T_ordernum=0ac6fd1f74a98de94bf9&C_address=456+My+Street&C_city=Ottawa&C_state=ON&C_zip=K1C2N6&C_country=CA&C_telephone=%28555%29555-5555&C_fax=%28555%29555-6666&C_email=longbob%40example.com&C_ship_name=Jim+Smith&C_ship_address=456+My+Street&C_ship_city=Ottawa&C_ship_state=ON&C_ship_zip=K1C2N6&C_ship_country=CA&M_id=[FILTERED]&M_key=[FILTERED]&T_code=01\"\n-> \"HTTP/1.1 200 OK\r\n\"\n-> \"Cache-Control: no-cache\r\n\"\n-> \"Pragma: no-cache\r\n\"\n-> \"Transfer-Encoding: chunked\r\n\"\n-> \"Content-Type: text/html; charset=us-ascii\r\n\"\n-> \"Content-Encoding: gzip\r\n\"\n-> \"Expires: -1\r\n\"\n-> \"Vary: Accept-Encoding\r\n\"\n-> \"Server: Microsoft-IIS/7.5\r\n\"\n-> \"X-Powered-By: ASP.NET\r\n\"\n-> \"Date: Thu, 02 Nov 2017 13:26:30 GMT\r\n\"\n-> \"Connection: close\r\n\"\n-> \"\r\n\"\n-> \"ac\r\n\"\nreading 172 bytes...\n-> \"\u001F?\b\u0000\u0000\u0000\u0000\u0000\u0004\u0000??\a`\u001CI?%&/m?{\u007FJ?J??t?\b?`\u0013$?@\u0010??????\u001DiG#)?*??eVe]f\u0016@????{???{???;?N'????\\fd\u0001l??J??!???\u001F?~|\u001F?\"~????oN???gg???o????\u0016?v~??l???l???~??`???????v~?\u0017_?8?\u001F?\"\nread 172 bytes\nreading 2 bytes...\n-> \"\r\n\"\nread 2 bytes\n-> \"b\r\n\"\nreading 11 bytes...\n-> \"??\u0003??\u001E T\u0000\u0000\u0000\"\nread 11 bytes\nreading 2 bytes...\n-> \"\r\n\"\nread 2 bytes\n-> \"0\r\n\"\n-> \"\r\n\"\nConn close POST_SCRUBBED end end diff --git a/test/unit/gateways/sallie_mae_test.rb b/test/unit/gateways/sallie_mae_test.rb index 40194a47164..b47bb928923 100644 --- a/test/unit/gateways/sallie_mae_test.rb +++ b/test/unit/gateways/sallie_mae_test.rb @@ -3,16 +3,16 @@ class SallieMaeTest < Test::Unit::TestCase def setup @gateway = SallieMaeGateway.new( - :login => 'FAKEACCOUNT' - ) + login: 'FAKEACCOUNT' + ) @credit_card = credit_card @amount = 100 @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase' + order_id: '1', + billing_address: address, + description: 'Store Purchase' } end @@ -35,7 +35,7 @@ def test_non_test_account end def test_test_account - gateway = SallieMaeGateway.new(:login => 'TEST0') + gateway = SallieMaeGateway.new(login: 'TEST0') assert gateway.test? end diff --git a/test/unit/gateways/secure_net_test.rb b/test/unit/gateways/secure_net_test.rb index 01c9b55c70d..064ebf8cd5e 100644 --- a/test/unit/gateways/secure_net_test.rb +++ b/test/unit/gateways/secure_net_test.rb @@ -5,17 +5,17 @@ class SecureNetTest < Test::Unit::TestCase def setup @gateway = SecureNetGateway.new( - :login => 'X', - :password => 'Y' - ) + login: 'X', + password: 'Y' + ) @credit_card = credit_card @amount = 100 @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase' + order_id: '1', + billing_address: address, + description: 'Store Purchase' } end @@ -117,8 +117,8 @@ def test_failed_refund def test_order_id_is_truncated order_id = "SecureNet doesn't like order_ids greater than 25 characters." stub_comms do - @gateway.purchase(@amount, @credit_card, order_id: order_id) - end.check_request do |endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, order_id:) + end.check_request do |_endpoint, data, _headers| assert_match(/ORDERID>SecureNet doesn't like or</, data) end.respond_with(successful_purchase_response) end @@ -133,7 +133,7 @@ def test_passes_optional_fields options = { description: 'Good Stuff', invoice_description: 'Sweet Invoice', invoice_number: '48' } stub_comms do @gateway.purchase(@amount, @credit_card, options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(%r{NOTE>Good Stuff<}, data) assert_match(%r{INVOICEDESC>Sweet Invoice<}, data) assert_match(%r{INVOICENUM>48<}, data) @@ -143,7 +143,7 @@ def test_passes_optional_fields def test_only_passes_optional_fields_if_specified stub_comms do @gateway.purchase(@amount, @credit_card, {}) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_no_match(%r{NOTE}, data) assert_no_match(%r{INVOICEDESC}, data) assert_no_match(%r{INVOICENUM}, data) @@ -153,7 +153,7 @@ def test_only_passes_optional_fields_if_specified def test_passes_with_no_developer_id stub_comms do @gateway.purchase(@amount, @credit_card, {}) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_no_match(%r{DEVELOPERID}, data) end.respond_with(successful_purchase_response) end @@ -161,7 +161,7 @@ def test_passes_with_no_developer_id def test_passes_with_developer_id stub_comms do @gateway.purchase(@amount, @credit_card, developer_id: '1234') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(%r{DEVELOPERID}, data) end.respond_with(successful_purchase_response) end @@ -169,7 +169,7 @@ def test_passes_with_developer_id def test_passes_with_test_mode stub_comms do @gateway.purchase(@amount, @credit_card, test_mode: false) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(%r{<TEST>FALSE</TEST>}, data) end.respond_with(successful_purchase_response) end @@ -177,7 +177,7 @@ def test_passes_with_test_mode def test_passes_without_test_mode stub_comms do @gateway.purchase(@amount, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(%r{<TEST>TRUE</TEST>}, data) end.respond_with(successful_purchase_response) end @@ -231,48 +231,48 @@ def failed_refund_response end def pre_scrubbed - <<-EOS -opening connection to certify.securenet.com:443... -opened -starting SSL for certify.securenet.com:443... -SSL established -<- "POST /API/gateway.svc/webHttp/ProcessTransaction HTTP/1.1\r\nContent-Type: text/xml\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: certify.securenet.com\r\nContent-Length: 1044\r\n\r\n" -<- "<?xml version=\"1.0\" encoding=\"UTF-8\"?><TRANSACTION xmlns=\"http://gateway.securenet.com/API/Contracts\" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\"><AMOUNT>1.00</AMOUNT><CARD><CARDCODE>123</CARDCODE><CARDNUMBER>4000100011112224</CARDNUMBER><EXPDATE>0919</EXPDATE></CARD><CODE>0100</CODE><CUSTOMER_BILL><ADDRESS>456 My Street</ADDRESS><CITY>Ottawa</CITY><COMPANY>Widgets Inc</COMPANY><COUNTRY>CA</COUNTRY><FIRSTNAME>Longbob</FIRSTNAME><LASTNAME>Longsen</LASTNAME><PHONE>(555)555-5555</PHONE><STATE>ON</STATE><ZIP>K1C2N6</ZIP></CUSTOMER_BILL><CUSTOMER_SHIP i:nil=\"true\"></CUSTOMER_SHIP><DCI>0</DCI><INSTALLMENT_SEQUENCENUM>1</INSTALLMENT_SEQUENCENUM><MERCHANT_KEY><GROUPID>0</GROUPID><SECUREKEY>BI8gL8HO1dKP</SECUREKEY><SECURENETID>7001218</SECURENETID></MERCHANT_KEY><METHOD>CC</METHOD><NOTE>Store Purchase</NOTE><ORDERID>1519921868962609</ORDERID><OVERRIDE_FROM>0</OVERRIDE_FROM><RETAIL_LANENUM>0</RETAIL_LANENUM><TEST>TRUE</TEST><TOTAL_INSTALLMENTCOUNT>0</TOTAL_INSTALLMENTCOUNT><TRANSACTION_SERVICE>0</TRANSACTION_SERVICE></TRANSACTION>" --> "HTTP/1.1 200 OK\r\n" --> "Content-Length: 2547\r\n" --> "Content-Type: application/xml; charset=utf-8\r\n" --> "X-Powered-By: ASP.NET\r\n" --> "Date: Thu, 01 Mar 2018 16:31:01 GMT\r\n" --> "Connection: close\r\n" --> "Set-Cookie: TS01e56b0e=010bfb2c76b6671aabf6f176a4e5aefd8e7a6ce7f697d82dfcfd424edede4ae7d4dba7557a4a7a13a539cfc1c5c061e08d5040811a; Path=/\r\n" --> "\r\n" -reading 2547 bytes... --> "<GATEWAYRESPONSE xmlns=\"http://gateway.securenet.com/API/Contracts\" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\"><ABRESPONSE i:nil=\"true\"/><TRANSACTIONRESPONSE><RESPONSE_CODE>1</RESPONSE_CODE><RESPONSE_REASON_CODE>0000</RESPONSE_REASON_CODE><RESPONSE_REASON_TEXT>Approved</RESPONSE_REASON_TEXT><RESPONSE_SUBCODE/><ADDITIONALAMOUNT>0</ADDITIONALAMOUNT><ADDITIONALDATA1/><ADDITIONALDATA2/><ADDITIONALDATA3/><ADDITIONALDATA4/><ADDITIONALDATA5/><AUTHCODE>JUJQLQ</AUTHCODE><AUTHORIZATIONMODE i:nil=\"true\"/><AUTHORIZEDAMOUNT>1.00</AUTHORIZEDAMOUNT><AVS_RESULT_CODE>Y</AVS_RESULT_CODE><BANK_ACCOUNTNAME/><BANK_ACCOUNTTYPE/><BATCHID>0</BATCHID><CALLID/><CARDENTRYMODE i:nil=\"true\"/><CARDHOLDERVERIFICATION i:nil=\"true\"/><CARDHOLDER_FIRSTNAME>Longbob</CARDHOLDER_FIRSTNAME><CARDHOLDER_LASTNAME>Longsen</CARDHOLDER_LASTNAME><CARDLEVEL_RESULTS/><CARDTYPE>VI</CARDTYPE><CARD_CODE_RESPONSE_CODE>M</CARD_CODE_RESPONSE_CODE><CASHBACK_AMOUNT>0</CASHBACK_AMOUNT><CATINDICATOR>0</CATINDICATOR><CAVV_RESPONSE_CODE/><CHECKNUM i:nil=\"true\"/><CODE>0100</CODE><CUSTOMERID/><CUSTOMER_BILL><ADDRESS>456 My Street</ADDRESS><CITY>Ottawa</CITY><COMPANY>Widgets Inc</COMPANY><COUNTRY>CA</COUNTRY><EMAIL/><EMAILRECEIPT>FALSE</EMAILRECEIPT><FIRSTNAME>Longbob</FIRSTNAME><LASTNAME>Longsen</LASTNAME><PHONE>(555)555-5555</PHONE><STATE>ON</STATE><ZIP>K1C2N6</ZIP></CUSTOMER_BILL><DYNAMICMCC i:nil=\"true\"/><EMVRESPONSE><ISSUERAUTHENTICATIONDATA i:nil=\"true\"/><ISSUERSCRIPTTEMPLATE1 i:nil=\"true\"/><ISSUERSCRIPTTEMPLATE2 i:nil=\"true\"/></EMVRESPONSE><EXPIRYDATE>0919</EXPIRYDATE><GRATUITY>0</GRATUITY><INDUSTRYSPECIFICDATA>P</INDUSTRYSPECIFICDATA><INVOICEDESCRIPTION i:nil=\"true\"/><LAST4DIGITS>2224</LAST4DIGITS><LEVEL2_VALID>FALSE</LEVEL2_VALID><LEVEL3_VALID>FALSE</LEVEL3_VALID><MARKETSPECIFICDATA/><METHOD>CC</METHOD><NETWORKCODE/><NETWORKID/><NOTES>Store Purchase</NOTES><ORDERID>1519921868962609</ORDERID><PAYMENTID/><RETREFERENCENUM/><RISK_CATEGORY i:nil=\"true\"/><RISK_REASON1 i:nil=\"true\"/><RISK_REASON2 i:nil=\"true\"/><RISK_REASON3 i:nil=\"true\"/><RISK_REASON4 i:nil=\"true\"/><RISK_REASON5 i:nil=\"true\"/><SECURENETID>7001218</SECURENETID><SETTLEMENTAMOUNT>1.00</SETTLEMENTAMOUNT><SETTLEMENTDATETIME>03012018113101</SETTLEMENTDATETIME><SOFTDESCRIPTOR/><SYSTEM_TRACENUM/><TRACKTYPE>0</TRACKTYPE><TRANSACTIONAMOUNT>1.00</TRANSACTIONAMOUNT><TRANSACTIONDATETIME>03012018113101</TRANSACTIONDATETIME><TRANSACTIONID>116186071</TRANSACTIONID><USERDEFINED i:nil=\"true\"/></TRANSACTIONRESPONSE><VAULTACCOUNTRESPONSE i:nil=\"true\"/><VAULTCUSTOMERRESPONSE i:nil=\"true\"/></GATEWAYRESPONSE>" -read 2547 bytes -Conn close - EOS + <<~REQUEST + opening connection to certify.securenet.com:443... + opened + starting SSL for certify.securenet.com:443... + SSL established + <- "POST /API/gateway.svc/webHttp/ProcessTransaction HTTP/1.1\r\nContent-Type: text/xml\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: certify.securenet.com\r\nContent-Length: 1044\r\n\r\n" + <- "<?xml version=\"1.0\" encoding=\"UTF-8\"?><TRANSACTION xmlns=\"http://gateway.securenet.com/API/Contracts\" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\"><AMOUNT>1.00</AMOUNT><CARD><CARDCODE>123</CARDCODE><CARDNUMBER>4000100011112224</CARDNUMBER><EXPDATE>0919</EXPDATE></CARD><CODE>0100</CODE><CUSTOMER_BILL><ADDRESS>456 My Street</ADDRESS><CITY>Ottawa</CITY><COMPANY>Widgets Inc</COMPANY><COUNTRY>CA</COUNTRY><FIRSTNAME>Longbob</FIRSTNAME><LASTNAME>Longsen</LASTNAME><PHONE>(555)555-5555</PHONE><STATE>ON</STATE><ZIP>K1C2N6</ZIP></CUSTOMER_BILL><CUSTOMER_SHIP i:nil=\"true\"></CUSTOMER_SHIP><DCI>0</DCI><INSTALLMENT_SEQUENCENUM>1</INSTALLMENT_SEQUENCENUM><MERCHANT_KEY><GROUPID>0</GROUPID><SECUREKEY>BI8gL8HO1dKP</SECUREKEY><SECURENETID>7001218</SECURENETID></MERCHANT_KEY><METHOD>CC</METHOD><NOTE>Store Purchase</NOTE><ORDERID>1519921868962609</ORDERID><OVERRIDE_FROM>0</OVERRIDE_FROM><RETAIL_LANENUM>0</RETAIL_LANENUM><TEST>TRUE</TEST><TOTAL_INSTALLMENTCOUNT>0</TOTAL_INSTALLMENTCOUNT><TRANSACTION_SERVICE>0</TRANSACTION_SERVICE></TRANSACTION>" + -> "HTTP/1.1 200 OK\r\n" + -> "Content-Length: 2547\r\n" + -> "Content-Type: application/xml; charset=utf-8\r\n" + -> "X-Powered-By: ASP.NET\r\n" + -> "Date: Thu, 01 Mar 2018 16:31:01 GMT\r\n" + -> "Connection: close\r\n" + -> "Set-Cookie: TS01e56b0e=010bfb2c76b6671aabf6f176a4e5aefd8e7a6ce7f697d82dfcfd424edede4ae7d4dba7557a4a7a13a539cfc1c5c061e08d5040811a; Path=/\r\n" + -> "\r\n" + reading 2547 bytes... + -> "<GATEWAYRESPONSE xmlns=\"http://gateway.securenet.com/API/Contracts\" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\"><ABRESPONSE i:nil=\"true\"/><TRANSACTIONRESPONSE><RESPONSE_CODE>1</RESPONSE_CODE><RESPONSE_REASON_CODE>0000</RESPONSE_REASON_CODE><RESPONSE_REASON_TEXT>Approved</RESPONSE_REASON_TEXT><RESPONSE_SUBCODE/><ADDITIONALAMOUNT>0</ADDITIONALAMOUNT><ADDITIONALDATA1/><ADDITIONALDATA2/><ADDITIONALDATA3/><ADDITIONALDATA4/><ADDITIONALDATA5/><AUTHCODE>JUJQLQ</AUTHCODE><AUTHORIZATIONMODE i:nil=\"true\"/><AUTHORIZEDAMOUNT>1.00</AUTHORIZEDAMOUNT><AVS_RESULT_CODE>Y</AVS_RESULT_CODE><BANK_ACCOUNTNAME/><BANK_ACCOUNTTYPE/><BATCHID>0</BATCHID><CALLID/><CARDENTRYMODE i:nil=\"true\"/><CARDHOLDERVERIFICATION i:nil=\"true\"/><CARDHOLDER_FIRSTNAME>Longbob</CARDHOLDER_FIRSTNAME><CARDHOLDER_LASTNAME>Longsen</CARDHOLDER_LASTNAME><CARDLEVEL_RESULTS/><CARDTYPE>VI</CARDTYPE><CARD_CODE_RESPONSE_CODE>M</CARD_CODE_RESPONSE_CODE><CASHBACK_AMOUNT>0</CASHBACK_AMOUNT><CATINDICATOR>0</CATINDICATOR><CAVV_RESPONSE_CODE/><CHECKNUM i:nil=\"true\"/><CODE>0100</CODE><CUSTOMERID/><CUSTOMER_BILL><ADDRESS>456 My Street</ADDRESS><CITY>Ottawa</CITY><COMPANY>Widgets Inc</COMPANY><COUNTRY>CA</COUNTRY><EMAIL/><EMAILRECEIPT>FALSE</EMAILRECEIPT><FIRSTNAME>Longbob</FIRSTNAME><LASTNAME>Longsen</LASTNAME><PHONE>(555)555-5555</PHONE><STATE>ON</STATE><ZIP>K1C2N6</ZIP></CUSTOMER_BILL><DYNAMICMCC i:nil=\"true\"/><EMVRESPONSE><ISSUERAUTHENTICATIONDATA i:nil=\"true\"/><ISSUERSCRIPTTEMPLATE1 i:nil=\"true\"/><ISSUERSCRIPTTEMPLATE2 i:nil=\"true\"/></EMVRESPONSE><EXPIRYDATE>0919</EXPIRYDATE><GRATUITY>0</GRATUITY><INDUSTRYSPECIFICDATA>P</INDUSTRYSPECIFICDATA><INVOICEDESCRIPTION i:nil=\"true\"/><LAST4DIGITS>2224</LAST4DIGITS><LEVEL2_VALID>FALSE</LEVEL2_VALID><LEVEL3_VALID>FALSE</LEVEL3_VALID><MARKETSPECIFICDATA/><METHOD>CC</METHOD><NETWORKCODE/><NETWORKID/><NOTES>Store Purchase</NOTES><ORDERID>1519921868962609</ORDERID><PAYMENTID/><RETREFERENCENUM/><RISK_CATEGORY i:nil=\"true\"/><RISK_REASON1 i:nil=\"true\"/><RISK_REASON2 i:nil=\"true\"/><RISK_REASON3 i:nil=\"true\"/><RISK_REASON4 i:nil=\"true\"/><RISK_REASON5 i:nil=\"true\"/><SECURENETID>7001218</SECURENETID><SETTLEMENTAMOUNT>1.00</SETTLEMENTAMOUNT><SETTLEMENTDATETIME>03012018113101</SETTLEMENTDATETIME><SOFTDESCRIPTOR/><SYSTEM_TRACENUM/><TRACKTYPE>0</TRACKTYPE><TRANSACTIONAMOUNT>1.00</TRANSACTIONAMOUNT><TRANSACTIONDATETIME>03012018113101</TRANSACTIONDATETIME><TRANSACTIONID>116186071</TRANSACTIONID><USERDEFINED i:nil=\"true\"/></TRANSACTIONRESPONSE><VAULTACCOUNTRESPONSE i:nil=\"true\"/><VAULTCUSTOMERRESPONSE i:nil=\"true\"/></GATEWAYRESPONSE>" + read 2547 bytes + Conn close + REQUEST end def post_scrubbed - <<-EOS -opening connection to certify.securenet.com:443... -opened -starting SSL for certify.securenet.com:443... -SSL established -<- "POST /API/gateway.svc/webHttp/ProcessTransaction HTTP/1.1\r\nContent-Type: text/xml\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: certify.securenet.com\r\nContent-Length: 1044\r\n\r\n" -<- "<?xml version=\"1.0\" encoding=\"UTF-8\"?><TRANSACTION xmlns=\"http://gateway.securenet.com/API/Contracts\" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\"><AMOUNT>1.00</AMOUNT><CARD><CARDCODE>[FILTERED]</CARDCODE><CARDNUMBER>[FILTERED]</CARDNUMBER><EXPDATE>0919</EXPDATE></CARD><CODE>0100</CODE><CUSTOMER_BILL><ADDRESS>456 My Street</ADDRESS><CITY>Ottawa</CITY><COMPANY>Widgets Inc</COMPANY><COUNTRY>CA</COUNTRY><FIRSTNAME>Longbob</FIRSTNAME><LASTNAME>Longsen</LASTNAME><PHONE>(555)555-5555</PHONE><STATE>ON</STATE><ZIP>K1C2N6</ZIP></CUSTOMER_BILL><CUSTOMER_SHIP i:nil=\"true\"></CUSTOMER_SHIP><DCI>0</DCI><INSTALLMENT_SEQUENCENUM>1</INSTALLMENT_SEQUENCENUM><MERCHANT_KEY><GROUPID>0</GROUPID><SECUREKEY>[FILTERED]</SECUREKEY><SECURENETID>7001218</SECURENETID></MERCHANT_KEY><METHOD>CC</METHOD><NOTE>Store Purchase</NOTE><ORDERID>1519921868962609</ORDERID><OVERRIDE_FROM>0</OVERRIDE_FROM><RETAIL_LANENUM>0</RETAIL_LANENUM><TEST>TRUE</TEST><TOTAL_INSTALLMENTCOUNT>0</TOTAL_INSTALLMENTCOUNT><TRANSACTION_SERVICE>0</TRANSACTION_SERVICE></TRANSACTION>" --> "HTTP/1.1 200 OK\r\n" --> "Content-Length: 2547\r\n" --> "Content-Type: application/xml; charset=utf-8\r\n" --> "X-Powered-By: ASP.NET\r\n" --> "Date: Thu, 01 Mar 2018 16:31:01 GMT\r\n" --> "Connection: close\r\n" --> "Set-Cookie: TS01e56b0e=010bfb2c76b6671aabf6f176a4e5aefd8e7a6ce7f697d82dfcfd424edede4ae7d4dba7557a4a7a13a539cfc1c5c061e08d5040811a; Path=/\r\n" --> "\r\n" -reading 2547 bytes... --> "<GATEWAYRESPONSE xmlns=\"http://gateway.securenet.com/API/Contracts\" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\"><ABRESPONSE i:nil=\"true\"/><TRANSACTIONRESPONSE><RESPONSE_CODE>1</RESPONSE_CODE><RESPONSE_REASON_CODE>0000</RESPONSE_REASON_CODE><RESPONSE_REASON_TEXT>Approved</RESPONSE_REASON_TEXT><RESPONSE_SUBCODE/><ADDITIONALAMOUNT>0</ADDITIONALAMOUNT><ADDITIONALDATA1/><ADDITIONALDATA2/><ADDITIONALDATA3/><ADDITIONALDATA4/><ADDITIONALDATA5/><AUTHCODE>JUJQLQ</AUTHCODE><AUTHORIZATIONMODE i:nil=\"true\"/><AUTHORIZEDAMOUNT>1.00</AUTHORIZEDAMOUNT><AVS_RESULT_CODE>Y</AVS_RESULT_CODE><BANK_ACCOUNTNAME/><BANK_ACCOUNTTYPE/><BATCHID>0</BATCHID><CALLID/><CARDENTRYMODE i:nil=\"true\"/><CARDHOLDERVERIFICATION i:nil=\"true\"/><CARDHOLDER_FIRSTNAME>Longbob</CARDHOLDER_FIRSTNAME><CARDHOLDER_LASTNAME>Longsen</CARDHOLDER_LASTNAME><CARDLEVEL_RESULTS/><CARDTYPE>VI</CARDTYPE><CARD_CODE_RESPONSE_CODE>M</CARD_CODE_RESPONSE_CODE><CASHBACK_AMOUNT>0</CASHBACK_AMOUNT><CATINDICATOR>0</CATINDICATOR><CAVV_RESPONSE_CODE/><CHECKNUM i:nil=\"true\"/><CODE>0100</CODE><CUSTOMERID/><CUSTOMER_BILL><ADDRESS>456 My Street</ADDRESS><CITY>Ottawa</CITY><COMPANY>Widgets Inc</COMPANY><COUNTRY>CA</COUNTRY><EMAIL/><EMAILRECEIPT>FALSE</EMAILRECEIPT><FIRSTNAME>Longbob</FIRSTNAME><LASTNAME>Longsen</LASTNAME><PHONE>(555)555-5555</PHONE><STATE>ON</STATE><ZIP>K1C2N6</ZIP></CUSTOMER_BILL><DYNAMICMCC i:nil=\"true\"/><EMVRESPONSE><ISSUERAUTHENTICATIONDATA i:nil=\"true\"/><ISSUERSCRIPTTEMPLATE1 i:nil=\"true\"/><ISSUERSCRIPTTEMPLATE2 i:nil=\"true\"/></EMVRESPONSE><EXPIRYDATE>0919</EXPIRYDATE><GRATUITY>0</GRATUITY><INDUSTRYSPECIFICDATA>P</INDUSTRYSPECIFICDATA><INVOICEDESCRIPTION i:nil=\"true\"/><LAST4DIGITS>2224</LAST4DIGITS><LEVEL2_VALID>FALSE</LEVEL2_VALID><LEVEL3_VALID>FALSE</LEVEL3_VALID><MARKETSPECIFICDATA/><METHOD>CC</METHOD><NETWORKCODE/><NETWORKID/><NOTES>Store Purchase</NOTES><ORDERID>1519921868962609</ORDERID><PAYMENTID/><RETREFERENCENUM/><RISK_CATEGORY i:nil=\"true\"/><RISK_REASON1 i:nil=\"true\"/><RISK_REASON2 i:nil=\"true\"/><RISK_REASON3 i:nil=\"true\"/><RISK_REASON4 i:nil=\"true\"/><RISK_REASON5 i:nil=\"true\"/><SECURENETID>7001218</SECURENETID><SETTLEMENTAMOUNT>1.00</SETTLEMENTAMOUNT><SETTLEMENTDATETIME>03012018113101</SETTLEMENTDATETIME><SOFTDESCRIPTOR/><SYSTEM_TRACENUM/><TRACKTYPE>0</TRACKTYPE><TRANSACTIONAMOUNT>1.00</TRANSACTIONAMOUNT><TRANSACTIONDATETIME>03012018113101</TRANSACTIONDATETIME><TRANSACTIONID>116186071</TRANSACTIONID><USERDEFINED i:nil=\"true\"/></TRANSACTIONRESPONSE><VAULTACCOUNTRESPONSE i:nil=\"true\"/><VAULTCUSTOMERRESPONSE i:nil=\"true\"/></GATEWAYRESPONSE>" -read 2547 bytes -Conn close - EOS + <<~REQUEST + opening connection to certify.securenet.com:443... + opened + starting SSL for certify.securenet.com:443... + SSL established + <- "POST /API/gateway.svc/webHttp/ProcessTransaction HTTP/1.1\r\nContent-Type: text/xml\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: certify.securenet.com\r\nContent-Length: 1044\r\n\r\n" + <- "<?xml version=\"1.0\" encoding=\"UTF-8\"?><TRANSACTION xmlns=\"http://gateway.securenet.com/API/Contracts\" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\"><AMOUNT>1.00</AMOUNT><CARD><CARDCODE>[FILTERED]</CARDCODE><CARDNUMBER>[FILTERED]</CARDNUMBER><EXPDATE>0919</EXPDATE></CARD><CODE>0100</CODE><CUSTOMER_BILL><ADDRESS>456 My Street</ADDRESS><CITY>Ottawa</CITY><COMPANY>Widgets Inc</COMPANY><COUNTRY>CA</COUNTRY><FIRSTNAME>Longbob</FIRSTNAME><LASTNAME>Longsen</LASTNAME><PHONE>(555)555-5555</PHONE><STATE>ON</STATE><ZIP>K1C2N6</ZIP></CUSTOMER_BILL><CUSTOMER_SHIP i:nil=\"true\"></CUSTOMER_SHIP><DCI>0</DCI><INSTALLMENT_SEQUENCENUM>1</INSTALLMENT_SEQUENCENUM><MERCHANT_KEY><GROUPID>0</GROUPID><SECUREKEY>[FILTERED]</SECUREKEY><SECURENETID>7001218</SECURENETID></MERCHANT_KEY><METHOD>CC</METHOD><NOTE>Store Purchase</NOTE><ORDERID>1519921868962609</ORDERID><OVERRIDE_FROM>0</OVERRIDE_FROM><RETAIL_LANENUM>0</RETAIL_LANENUM><TEST>TRUE</TEST><TOTAL_INSTALLMENTCOUNT>0</TOTAL_INSTALLMENTCOUNT><TRANSACTION_SERVICE>0</TRANSACTION_SERVICE></TRANSACTION>" + -> "HTTP/1.1 200 OK\r\n" + -> "Content-Length: 2547\r\n" + -> "Content-Type: application/xml; charset=utf-8\r\n" + -> "X-Powered-By: ASP.NET\r\n" + -> "Date: Thu, 01 Mar 2018 16:31:01 GMT\r\n" + -> "Connection: close\r\n" + -> "Set-Cookie: TS01e56b0e=010bfb2c76b6671aabf6f176a4e5aefd8e7a6ce7f697d82dfcfd424edede4ae7d4dba7557a4a7a13a539cfc1c5c061e08d5040811a; Path=/\r\n" + -> "\r\n" + reading 2547 bytes... + -> "<GATEWAYRESPONSE xmlns=\"http://gateway.securenet.com/API/Contracts\" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\"><ABRESPONSE i:nil=\"true\"/><TRANSACTIONRESPONSE><RESPONSE_CODE>1</RESPONSE_CODE><RESPONSE_REASON_CODE>0000</RESPONSE_REASON_CODE><RESPONSE_REASON_TEXT>Approved</RESPONSE_REASON_TEXT><RESPONSE_SUBCODE/><ADDITIONALAMOUNT>0</ADDITIONALAMOUNT><ADDITIONALDATA1/><ADDITIONALDATA2/><ADDITIONALDATA3/><ADDITIONALDATA4/><ADDITIONALDATA5/><AUTHCODE>JUJQLQ</AUTHCODE><AUTHORIZATIONMODE i:nil=\"true\"/><AUTHORIZEDAMOUNT>1.00</AUTHORIZEDAMOUNT><AVS_RESULT_CODE>Y</AVS_RESULT_CODE><BANK_ACCOUNTNAME/><BANK_ACCOUNTTYPE/><BATCHID>0</BATCHID><CALLID/><CARDENTRYMODE i:nil=\"true\"/><CARDHOLDERVERIFICATION i:nil=\"true\"/><CARDHOLDER_FIRSTNAME>Longbob</CARDHOLDER_FIRSTNAME><CARDHOLDER_LASTNAME>Longsen</CARDHOLDER_LASTNAME><CARDLEVEL_RESULTS/><CARDTYPE>VI</CARDTYPE><CARD_CODE_RESPONSE_CODE>M</CARD_CODE_RESPONSE_CODE><CASHBACK_AMOUNT>0</CASHBACK_AMOUNT><CATINDICATOR>0</CATINDICATOR><CAVV_RESPONSE_CODE/><CHECKNUM i:nil=\"true\"/><CODE>0100</CODE><CUSTOMERID/><CUSTOMER_BILL><ADDRESS>456 My Street</ADDRESS><CITY>Ottawa</CITY><COMPANY>Widgets Inc</COMPANY><COUNTRY>CA</COUNTRY><EMAIL/><EMAILRECEIPT>FALSE</EMAILRECEIPT><FIRSTNAME>Longbob</FIRSTNAME><LASTNAME>Longsen</LASTNAME><PHONE>(555)555-5555</PHONE><STATE>ON</STATE><ZIP>K1C2N6</ZIP></CUSTOMER_BILL><DYNAMICMCC i:nil=\"true\"/><EMVRESPONSE><ISSUERAUTHENTICATIONDATA i:nil=\"true\"/><ISSUERSCRIPTTEMPLATE1 i:nil=\"true\"/><ISSUERSCRIPTTEMPLATE2 i:nil=\"true\"/></EMVRESPONSE><EXPIRYDATE>0919</EXPIRYDATE><GRATUITY>0</GRATUITY><INDUSTRYSPECIFICDATA>P</INDUSTRYSPECIFICDATA><INVOICEDESCRIPTION i:nil=\"true\"/><LAST4DIGITS>2224</LAST4DIGITS><LEVEL2_VALID>FALSE</LEVEL2_VALID><LEVEL3_VALID>FALSE</LEVEL3_VALID><MARKETSPECIFICDATA/><METHOD>CC</METHOD><NETWORKCODE/><NETWORKID/><NOTES>Store Purchase</NOTES><ORDERID>1519921868962609</ORDERID><PAYMENTID/><RETREFERENCENUM/><RISK_CATEGORY i:nil=\"true\"/><RISK_REASON1 i:nil=\"true\"/><RISK_REASON2 i:nil=\"true\"/><RISK_REASON3 i:nil=\"true\"/><RISK_REASON4 i:nil=\"true\"/><RISK_REASON5 i:nil=\"true\"/><SECURENETID>7001218</SECURENETID><SETTLEMENTAMOUNT>1.00</SETTLEMENTAMOUNT><SETTLEMENTDATETIME>03012018113101</SETTLEMENTDATETIME><SOFTDESCRIPTOR/><SYSTEM_TRACENUM/><TRACKTYPE>0</TRACKTYPE><TRANSACTIONAMOUNT>1.00</TRANSACTIONAMOUNT><TRANSACTIONDATETIME>03012018113101</TRANSACTIONDATETIME><TRANSACTIONID>116186071</TRANSACTIONID><USERDEFINED i:nil=\"true\"/></TRANSACTIONRESPONSE><VAULTACCOUNTRESPONSE i:nil=\"true\"/><VAULTCUSTOMERRESPONSE i:nil=\"true\"/></GATEWAYRESPONSE>" + read 2547 bytes + Conn close + REQUEST end end diff --git a/test/unit/gateways/secure_pay_au_test.rb b/test/unit/gateways/secure_pay_au_test.rb index 163c41fe0fa..6d30a2129d9 100644 --- a/test/unit/gateways/secure_pay_au_test.rb +++ b/test/unit/gateways/secure_pay_au_test.rb @@ -5,17 +5,17 @@ class SecurePayAuTest < Test::Unit::TestCase def setup @gateway = SecurePayAuGateway.new( - :login => 'login', - :password => 'password' - ) + login: 'login', + password: 'password' + ) @credit_card = credit_card @amount = 100 @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase' + order_id: 'order123', + billing_address: address, + description: 'Store Purchase' } end @@ -24,10 +24,9 @@ def test_supported_countries end def test_supported_card_types - assert_equal [:visa, :master, :american_express, :diners_club, :jcb], SecurePayAuGateway.supported_cardtypes + assert_equal %i[visa master american_express diners_club jcb], SecurePayAuGateway.supported_cardtypes end - def test_successful_purchase_with_live_data @gateway.expects(:ssl_post).returns(successful_live_purchase_response) @@ -52,14 +51,14 @@ def test_successful_purchase def test_localized_currency stub_comms do - @gateway.purchase(100, @credit_card, @options.merge(:currency => 'CAD')) - end.check_request do |endpoint, data, headers| + @gateway.purchase(100, @credit_card, @options.merge(currency: 'CAD')) + end.check_request do |_endpoint, data, _headers| assert_match %r{<amount>100<\/amount>}, data end.respond_with(successful_purchase_response) stub_comms do - @gateway.purchase(100, @credit_card, @options.merge(:currency => 'JPY')) - end.check_request do |endpoint, data, headers| + @gateway.purchase(100, @credit_card, @options.merge(currency: 'JPY')) + end.check_request do |_endpoint, data, _headers| assert_match %r{<amount>1<\/amount>}, data end.respond_with(successful_purchase_response) end @@ -80,6 +79,14 @@ def test_purchase_with_stored_id_calls_commit_periodic @gateway.purchase(@amount, '123', @options) end + def test_periodic_payment_submits_order_id + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, '123', @options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/<transactionReference>order123<\/transactionReference>/, data) + end.respond_with(successful_purchase_response) + end + def test_purchase_with_creditcard_calls_commit_with_purchase @gateway.expects(:commit).with(:purchase, anything) @@ -167,7 +174,7 @@ def test_failed_login def test_successful_store @gateway.expects(:ssl_post).returns(successful_store_response) - assert response = @gateway.store(@credit_card, {:billing_id => 'test3', :amount => 123}) + assert response = @gateway.store(@credit_card, { billing_id: 'test3', amount: 123 }) assert_instance_of Response, response assert_equal 'Successful', response.message assert_equal 'test3', response.params['client_id'] @@ -191,6 +198,23 @@ def test_successful_triggered_payment assert_equal 'test3', response.params['client_id'] end + def test_request_timeout_default + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/<timeoutValue>60/, data) + end.respond_with(successful_purchase_response) + end + + def test_override_request_timeout + gateway = SecurePayAuGateway.new(login: 'login', password: 'password', request_timeout: 44) + stub_comms(gateway, :ssl_request) do + gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/<timeoutValue>44/, data) + end.respond_with(successful_purchase_response) + end + def test_scrub assert_equal @gateway.scrub(pre_scrub), post_scrub end @@ -202,7 +226,7 @@ def test_supports_scrubbing? private def successful_store_response - <<-XML.gsub(/^\s{4}/,'') + <<-XML.gsub(/^\s{4}/, '') <?xml version="1.0" encoding="UTF-8"?> <SecurePayMessage> <MessageInfo> @@ -241,7 +265,7 @@ def successful_store_response end def successful_unstore_response - <<-XML.gsub(/^\s{4}/,'') + <<-XML.gsub(/^\s{4}/, '') <?xml version="1.0" encoding="UTF-8"?> <SecurePayMessage> <MessageInfo> @@ -273,7 +297,7 @@ def successful_unstore_response end def successful_triggered_payment_response - <<-XML.gsub(/^\s{4}/,'') + <<-XML.gsub(/^\s{4}/, '') <?xml version="1.0" encoding="UTF-8"?> <SecurePayMessage> <MessageInfo> @@ -319,7 +343,7 @@ def failed_login_response end def successful_purchase_response - <<-XML.gsub(/^\s{4}/,'') + <<-XML.gsub(/^\s{4}/, '') <?xml version="1.0" encoding="UTF-8"?> <SecurePayMessage> <MessageInfo> @@ -366,7 +390,7 @@ def successful_purchase_response end def failed_purchase_response - <<-XML.gsub(/^\s{4}/,'') + <<-XML.gsub(/^\s{4}/, '') <?xml version="1.0" encoding="UTF-8"?> <SecurePayMessage> <MessageInfo> @@ -413,7 +437,7 @@ def failed_purchase_response end def successful_live_purchase_response - <<-XML.gsub(/^\s{4}/,'') + <<-XML.gsub(/^\s{4}/, '') <?xml version="1.0" encoding="UTF-8"?> <SecurePayMessage> <MessageInfo> diff --git a/test/unit/gateways/secure_pay_tech_test.rb b/test/unit/gateways/secure_pay_tech_test.rb index 008f447cbc7..6ebe85049ff 100644 --- a/test/unit/gateways/secure_pay_tech_test.rb +++ b/test/unit/gateways/secure_pay_tech_test.rb @@ -3,41 +3,42 @@ class SecurePayTechTest < Test::Unit::TestCase def setup @gateway = SecurePayTechGateway.new( - :login => 'x', - :password => 'y' - ) + login: 'x', + password: 'y' + ) @amount = 100 @credit_card = credit_card('4987654321098769') @options = { - :billing_address => address + billing_address: address } end - + def test_successful_purchase @gateway.expects(:ssl_post).returns(successful_purchase_response) - + assert response = @gateway.purchase(@amount, @credit_card, @options) assert_instance_of Response, response assert_success response assert response.test? assert_equal '4--120119220646821', response.authorization end - + def test_unsuccessful_purchase @gateway.expects(:ssl_post).returns(unsuccessful_purchase_response) - + assert response = @gateway.purchase(@amount, @credit_card, @options) assert_instance_of Response, response assert_failure response assert response.test? end - + private + def successful_purchase_response "1,4--120119220646821,000000014511,23284,014511,20080125\r\n" end - + def unsuccessful_purchase_response "4,4--120119180936527,000000014510,23283,014510,20080125\r\n" end diff --git a/test/unit/gateways/secure_pay_test.rb b/test/unit/gateways/secure_pay_test.rb index 2a897b6a2fd..71cd4e7fa00 100644 --- a/test/unit/gateways/secure_pay_test.rb +++ b/test/unit/gateways/secure_pay_test.rb @@ -3,16 +3,16 @@ class SecurePayTest < Test::Unit::TestCase def setup @gateway = SecurePayGateway.new( - :login => 'X', - :password => 'Y' + login: 'X', + password: 'Y' ) @credit_card = credit_card @options = { - :order_id => generate_unique_id, - :description => 'Store purchase', - :billing_address => address + order_id: generate_unique_id, + description: 'Store purchase', + billing_address: address } @amount = 100 @@ -48,7 +48,6 @@ def test_successful_purchase assert response.authorization end - def test_avs_result @gateway.expects(:ssl_post).returns(successful_purchase_response) diff --git a/test/unit/gateways/securion_pay_test.rb b/test/unit/gateways/securion_pay_test.rb index ea2a5d85025..b37509b5f66 100644 --- a/test/unit/gateways/securion_pay_test.rb +++ b/test/unit/gateways/securion_pay_test.rb @@ -5,7 +5,7 @@ class SecurionPayTest < Test::Unit::TestCase def setup @gateway = SecurionPayGateway.new( - secret_key: 'pr_test_SyMyCpIJosFIAESEsZUd3TgN', + secret_key: 'pr_test_SyMyCpIJosFIAESEsZUd3TgN' ) @credit_card = credit_card @@ -18,6 +18,15 @@ def setup billing_address: address, description: 'Store Purchase' } + + @three_ds_secure = { + version: '1.0.2', + cavv: '3q2+78r+ur7erb7vyv66vv\/\/\/\/8=', + eci: '05', + xid: 'ODUzNTYzOTcwODU5NzY3Qw==', + enrolled: 'true', + authentication_response_status: 'Y' + } end def test_successful_store @@ -27,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'] @@ -35,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'] @@ -63,10 +72,93 @@ def test_successful_purchase assert response.test? end + def test_three_ds_v1_object_construction + post = {} + @options[:three_d_secure] = @three_ds_secure + @gateway.send(:add_external_three_ds, post, @options) + assert post[:threeDSecure] + ds_data = post[:threeDSecure][:external] + ds_options = @options[:three_d_secure] + assert_equal ds_options[:version], ds_data[:version] + assert_equal ds_options[:cavv], ds_data[:authenticationValue] + assert_equal ds_options[:eci], ds_data[:eci] + assert_equal ds_options[:xid], ds_data[:xid] + assert_equal nil, ds_data[:ds_transaction_id] + assert_equal ds_options[:authentication_response_status], ds_data[:status] + end + + def test_three_ds_v2_object_construction + post = {} + @three_ds_secure[:ds_transaction_id] = 'ODUzNTYzOTcwODU5NzY3Qw==' + @three_ds_secure[:version] = '2.2.0' + @options[:three_d_secure] = @three_ds_secure + + @gateway.send(:add_external_three_ds, post, @options) + + assert post[:threeDSecure] + ds_data = post[:threeDSecure][:external] + ds_options = @options[:three_d_secure] + + assert_equal ds_options[:version], ds_data[:version] + assert_equal ds_options[:cavv], ds_data[:authenticationValue] + assert_equal ds_options[:eci], ds_data[:eci] + assert_equal nil, ds_data[:xid] + assert_equal ds_options[:ds_transaction_id], ds_data[:dsTransactionId] + assert_equal ds_options[:authentication_response_status], ds_data[:status] + end + + def test_three_ds_version_validation + post = {} + @options[:three_d_secure] = @three_ds_secure + @gateway.send(:add_external_three_ds, post, @options) + resp = @gateway.send(:validate_three_ds_params, @three_ds_secure) + + assert_equal nil, resp + post[:threeDSecure][:external][:version] = '4.0' + resp = @gateway.send(:validate_three_ds_params, post[:threeDSecure]) + + assert_equal 'ThreeDs data is invalid', resp.message + assert_equal 'ThreeDs version not supported', resp.params['three_ds_version'] + end + + def test_three_ds_auth_response_validation + post = {} + @options[:three_d_secure] = @three_ds_secure + @gateway.send(:add_external_three_ds, post, @options) + post[:threeDSecure][:external][:status] = 'P' + resp = @gateway.send(:validate_three_ds_params, post[:threeDSecure][:external]) + assert_equal 'ThreeDs data is invalid', resp.message + assert_equal 'Authentication response value not supported', resp.params['auth_response'] + end + + def test_purchase_with_three_ds + @options[:three_d_secure] = @three_ds_secure + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request(skip_response: true) do |_endpoint, data, _headers| + three_ds_params = JSON.parse(data)['three_dsecure'] + assert_equal '1.0', three_ds_params['three_dsecure_version'] + assert_equal '3q2+78r+ur7erb7vyv66vv\/\/\/\/8=', three_ds_params['cavv'] + assert_equal '05', three_ds_params['eci'] + assert_equal 'ODUzNTYzOTcwODU5NzY3Qw==', three_ds_params['xid'] + assert_equal 'Y', three_ds_params['enrollment_response'] + assert_equal 'Y', three_ds_params['authentication_response'] + end + end + + def test_unsuccessfully_purchase_with_wrong_three_ds_data + @three_ds_secure.delete(:version) + @options[:three_d_secure] = @three_ds_secure + resp = @gateway.purchase(@amount, @credit_card, @options) + + assert_equal 'ThreeDs data is invalid', resp.message + assert_equal 'ThreeDs version not supported', resp.params['three_ds_version'] + end + def test_successful_purchase_with_token response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, 'tok_xxx') - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/card=tok_xxx/, data) refute_match(/card\[number\]/, data) end.respond_with(successful_purchase_response) @@ -87,8 +179,8 @@ def test_invalid_raw_response def test_client_data_submitted_with_purchase stub_comms(@gateway, :ssl_request) do updated_options = @options.merge({ description: 'test charge', ip: '127.127.127.127', user_agent: 'browser XXX', referrer: 'http://www.foobar.com', email: 'foo@bar.com' }) - @gateway.purchase(@amount,@credit_card,updated_options) - end.check_request do |method, endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, updated_options) + end.check_request do |_method, _endpoint, data, _headers| assert_match(/description=test\+charge/, data) assert_match(/ip=127\.127\.127\.127/, data) assert_match(/user_agent=browser\+XXX/, data) @@ -100,8 +192,8 @@ def test_client_data_submitted_with_purchase def test_client_data_submitted_with_purchase_without_email_or_order stub_comms(@gateway, :ssl_request) do updated_options = @options.merge({ description: 'test charge', ip: '127.127.127.127', user_agent: 'browser XXX', referrer: 'http://www.foobar.com' }) - @gateway.purchase(@amount,@credit_card,updated_options) - end.check_request do |method, endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, updated_options) + end.check_request do |_method, _endpoint, data, _headers| assert_match(/description=test\+charge/, data) assert_match(/ip=127\.127\.127\.127/, data) assert_match(/user_agent=browser\+XXX/, data) @@ -122,7 +214,7 @@ def test_successful_authorization end def test_add_address - post = { card: { } } + post = { card: {} } @gateway.send(:add_address, post, @options) assert_equal @options[:billing_address][:zip], post[:card][:addressZip] assert_equal @options[:billing_address][:state], post[:card][:addressState] @@ -139,7 +231,7 @@ def test_ensure_does_not_respond_to_credit def test_address_is_included_with_card_data stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert data =~ /card\[addressLine1\]/ end.respond_with(successful_purchase_response) end @@ -170,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 @@ -202,7 +294,7 @@ def test_successful_full_refund assert response.params['refunded'] assert_equal 0, response.params['amount'] assert_equal 1, response.params['refunds'].size - assert_equal @amount, response.params['refunds'].map{|r| r['amount']}.sum + assert_equal @amount, response.params['refunds'].map { |r| r['amount'] }.sum assert_equal 'char_DQca5ZjbewP2Oe0lIsNe4EXP', response.authorization assert response.test? end @@ -215,7 +307,7 @@ def test_successful_partially_refund assert_success response assert response.params['refunded'] assert_equal @amount - @refund_amount, response.params['amount'] - assert_equal @refund_amount, response.params['refunds'].map{|r| r['amount']}.sum + assert_equal @refund_amount, response.params['refunds'].map { |r| r['amount'] }.sum assert_equal 'char_oVnJ1j6fZqOvnopBBvlnpEuX', response.authorization assert response.test? end @@ -302,6 +394,15 @@ def test_declined_request assert_equal 'char_mApucpvVbCJgo7x09Je4n9gC', response.params['error']['chargeId'] end + def test_amount_currency_gets_downcased + @options[:currency] = 'USD' + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_equal 'usd', CGI.parse(data)['currency'].first + end.respond_with(successful_purchase_response) + end + private def pre_scrubbed diff --git a/test/unit/gateways/shift4_test.rb b/test/unit/gateways/shift4_test.rb new file mode 100644 index 00000000000..7990d6e5914 --- /dev/null +++ b/test/unit/gateways/shift4_test.rb @@ -0,0 +1,1396 @@ +require 'test_helper' + +class Shift4Test < Test::Unit::TestCase + include CommStub + def setup + @gateway = Shift4Gateway.new(client_guid: '123456', auth_token: 'abcder123') + @credit_card = credit_card('4000100011112224', verification_value: '333', first_name: 'John', last_name: 'Doe') + @amount = 5 + @options = {} + @extra_options = { + clerk_id: '1576', + notes: 'test notes', + tax: '2', + customer_reference: 'D019D09309F2', + destination_postal_code: '94719', + product_descriptors: %w(Hamburger Fries Soda Cookie), + order_id: '123456' + } + @customer_address = { + address1: '123 Street', + zip: '94901' + } + end + + def test_successful_capture + response = stub_comms do + @gateway.capture(@amount, '1111g66gw3ryke06', @options) + end.check_request do |_endpoint, data, headers| + request = JSON.parse(data) + assert_nil request['card']['present'], 'N' + assert_nil request['card']['entryMode'] + assert_nil headers['Invoice'] + end.respond_with(successful_capture_response) + + assert response.success? + assert_equal response.message, 'Transaction successful' + assert_equal response_result(response)['card']['token']['value'].present?, true + end + + def test_successful_authorize + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.respond_with(successful_authorize_response) + + assert response.success? + assert_equal response.message, 'Transaction successful' + assert_equal response_result(response)['card']['token']['value'].present?, true + end + + def test_successful_purchase + response = stub_comms do + @gateway.purchase(@amount, '1111g66gw3ryke06', @options) + end.respond_with(successful_purchase_response) + + assert response.success? + assert_equal response.message, 'Transaction successful' + assert_equal response_result(response)['card']['token']['value'].present?, true + end + + def test_successful_purchase_with_extra_fields + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(@extra_options)) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['clerk']['numericId'], @extra_options[:clerk_id] + assert_equal request['transaction']['notes'], @extra_options[:notes] + assert_equal request['transaction']['vendorReference'], @extra_options[:order_id] + assert_equal request['amount']['tax'], @extra_options[:tax].to_f + assert_equal request['amount']['total'], (@amount / 100.0).to_s + assert_equal request['transaction']['purchaseCard']['customerReference'], @extra_options[:customer_reference] + assert_equal request['transaction']['purchaseCard']['destinationPostalCode'], @extra_options[:destination_postal_code] + assert_equal request['transaction']['purchaseCard']['productDescriptors'], @extra_options[:product_descriptors] + end.respond_with(successful_purchase_response) + + assert response.success? + assert_equal response.message, 'Transaction successful' + end + + def test_successful_purchase_with_customer_details + customer = { billing_address: @customer_address, ip: '127.0.0.1', email: 'test@test.com' } + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(customer)) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['customer']['addressLine1'], @customer_address[:address1] + assert_equal request['customer']['postalCode'], @customer_address[:zip] + assert_equal request['customer']['emailAddress'], customer[:email] + assert_equal request['customer']['ipAddress'], customer[:ip] + assert_equal request['customer']['firstName'], @credit_card.first_name + assert_equal request['customer']['lastName'], @credit_card.last_name + end.respond_with(successful_purchase_response) + + customer[:billing_address][:zip] = nil + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(customer)) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_nil request['customer']['postalCode'] + end.respond_with(successful_purchase_response) + + customer[:billing_address][:zip] = '' + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(customer)) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_nil request['customer']['postalCode'] + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_stored_credential_framework + stored_credential_options = { + initial_transaction: true, + reason_type: 'recurring' + } + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_options })) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data)['transaction'] + assert_equal request['cardOnFile']['usageIndicator'], '01' + assert_equal request['cardOnFile']['indicator'], '01' + assert_equal request['cardOnFile']['scheduledIndicator'], '01' + assert_nil request['cardOnFile']['transactionId'] + end.respond_with(successful_purchase_response) + + assert response.success? + assert_equal response.message, 'Transaction successful' + + stored_credential_options = { + reason_type: 'recurring', + network_transaction_id: '123abcdefg' + } + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_options })) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data)['transaction'] + assert_equal request['cardOnFile']['usageIndicator'], '02' + assert_equal request['cardOnFile']['indicator'], '01' + assert_equal request['cardOnFile']['scheduledIndicator'], '01' + assert_equal request['cardOnFile']['transactionId'], stored_credential_options[:network_transaction_id] + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_card_on_file_fields + card_on_file_fields = { + usage_indicator: '01', + indicator: '02', + scheduled_indicator: '01' + } + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(card_on_file_fields)) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data)['transaction'] + assert_equal request['cardOnFile']['usageIndicator'], card_on_file_fields[:usage_indicator] + assert_equal request['cardOnFile']['indicator'], card_on_file_fields[:indicator] + assert_equal request['cardOnFile']['scheduledIndicator'], card_on_file_fields[:scheduled_indicator] + assert_nil request['cardOnFile']['transactionId'] + end.respond_with(successful_purchase_response) + + card_on_file_fields = { + usage_indicator: '02', + indicator: '01', + scheduled_indicator: '02', + transaction_id: 'TXID00001293' + } + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(card_on_file_fields)) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data)['transaction'] + assert_equal request['cardOnFile']['usageIndicator'], card_on_file_fields[:usage_indicator] + assert_equal request['cardOnFile']['indicator'], card_on_file_fields[:indicator] + assert_equal request['cardOnFile']['scheduledIndicator'], card_on_file_fields[:scheduled_indicator] + assert_equal request['cardOnFile']['transactionId'], card_on_file_fields[:transaction_id] + end.respond_with(successful_purchase_response) + end + + def test_card_on_file_fields_and_stored_credential_framework_combined + card_on_file_fields = { + usage_indicator: '02', + indicator: '02', + scheduled_indicator: '02' + } + stored_credential_options = { + initial_transaction: true, + reason_type: 'recurring' + } + @options[:stored_credential] = stored_credential_options + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(card_on_file_fields)) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data)['transaction'] + assert_equal request['cardOnFile']['usageIndicator'], card_on_file_fields[:usage_indicator] + assert_equal request['cardOnFile']['indicator'], card_on_file_fields[:indicator] + assert_equal request['cardOnFile']['scheduledIndicator'], card_on_file_fields[:scheduled_indicator] + assert_nil request['cardOnFile']['transactionId'] + end.respond_with(successful_purchase_response) + end + + def test_successful_store + response = stub_comms do + @gateway.store(@credit_card, @options.merge(@extra_options.except(:tax))) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_nil request['card']['entryMode'] + assert_nil request['clerk'] + end.respond_with(successful_store_response) + + assert response.success? + assert_equal response.message, 'Transaction successful' + end + + def test_successful_refund + response = stub_comms do + @gateway.refund(@amount, '1111g66gw3ryke06', @options.merge!(invoice: '4666309473', expiration_date: '1235')) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['card']['present'], 'N' + assert_equal request['card']['expirationDate'], '1235' + assert_nil request['card']['entryMode'] + assert_nil request['customer'] + end.respond_with(successful_refund_response) + + assert response.success? + assert_equal response.message, 'Transaction successful' + end + + def test_successful_credit + stub_comms do + @gateway.refund(@amount, @credit_card, @options.merge!(invoice: '4666309473', expiration_date: '1235')) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['card']['present'], 'N' + assert_equal request['card']['expirationDate'], @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') + + assert response.success? + assert_equal response.message, 'Transaction successful' + end + + def test_failed_purchase + @gateway.expects(:ssl_request).returns(failed_purchase_response) + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_failure response + assert_equal response.message, 'Transaction declined' + assert_equal 'D', response.error_code + 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 + + def test_failed_authorize + @gateway.expects(:ssl_request).returns(failed_authorize_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert_nil response.authorization + assert_equal 'GTV Msg: ERROR{0} 20018: no default category found, UC, Mod10=N TOKEN01CE ENGINE29CE', response.message + assert_equal 9100, response.error_code + 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_equal 'N7', response.error_code + assert response.test? + end + + def test_failed_authorize_with_alternate_host_response + response = stub_comms do + @gateway.authorize(@amount, @credit_card) + end.respond_with(failed_authorize_with_alternate_host_response) + + assert_failure response + assert_equal 'Invalid Merchant', response.message + assert_equal '03', response.error_code + 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) + + response = @gateway.capture(@amount, 'abc', @options) + assert_failure response + assert_nil response.authorization + assert_equal 'INTERNET FAILURE: Timeout waiting for response across the Internet UTGAPI05CE', response.message + assert_equal 9961, response.error_code + assert response.test? + end + + def test_failed_refund + @gateway.expects(:ssl_request).returns(failed_refund_response) + + response = @gateway.refund(1919, @credit_card, @options) + assert_failure response + assert_equal response.error_code, 'D' + assert_equal response.message, 'Transaction declined' + assert response.test? + end + + def test_failed_void + @gateway.expects(:ssl_request).returns(failed_void_response) + + response = @gateway.void('', @options) + assert_failure response + assert_nil response.authorization + assert_equal 'Invoice Not Found 00000000kl 0008628968 ENGINE29CE', response.message + assert_equal 9815, response.error_code + assert response.test? + end + + def test_successful_verify_fields + card_on_file_fields = { + usage_indicator: '02', + indicator: '01', + scheduled_indicator: '02', + transaction_id: 'TXID00001293' + } + response = stub_comms do + @gateway.verify(@credit_card, @options.merge(card_on_file_fields)) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['transaction']['cardOnFile']['usageIndicator'], card_on_file_fields[:usage_indicator] + assert_equal request['transaction']['cardOnFile']['indicator'], card_on_file_fields[:indicator] + assert_equal request['transaction']['cardOnFile']['scheduledIndicator'], card_on_file_fields[:scheduled_indicator] + assert_equal request['transaction']['cardOnFile']['transactionId'], card_on_file_fields[:transaction_id] + assert_not_nil request['dateTime'] + assert !request['customer'].nil? && !request['customer'].empty? + assert_nil request['card']['entryMode'] + end.respond_with(successful_verify_response) + + assert_success response + end + + def test_successful_verify_with_stored_credential_framework + stored_credential_options = { + reason_type: 'recurring', + network_transaction_id: '123abcdefg' + } + stub_comms do + @gateway.verify(@credit_card, @options.merge({ stored_credential: stored_credential_options })) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data)['transaction'] + assert_equal request['cardOnFile']['usageIndicator'], '02' + assert_equal request['cardOnFile']['indicator'], '01' + assert_equal request['cardOnFile']['scheduledIndicator'], '01' + assert_equal request['cardOnFile']['transactionId'], stored_credential_options[:network_transaction_id] + end.respond_with(successful_verify_response) + end + + def test_card_present_field + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['card']['present'], 'N' + end.respond_with(successful_purchase_response) + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge({ card_present: 'Y' })) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['card']['present'], 'Y' + end.respond_with(successful_purchase_response) + end + + def test_successful_header_fields + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, _data, headers| + assert_equal headers['CompanyName'], 'Spreedly' + assert_equal headers['InterfaceVersion'], '1' + assert_equal headers['InterfaceName'], 'Spreedly' + end.respond_with(successful_purchase_response) + end + + def test_successful_time_zone_offset + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge!(merchant_time_zone: 'EST')) + end.check_request do |_endpoint, data, _headers| + assert_equal DateTime.parse(JSON.parse(data)['dateTime']).formatted_offset, Time.now.in_time_zone(@options[:merchant_time_zone]).formatted_offset + end.respond_with(successful_purchase_response) + end + + def test_support_scrub + assert @gateway.supports_scrubbing? + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + def test_setup_access_token_should_rise_an_exception_under_unsuccessful_request + @gateway.expects(:ssl_post).returns(failed_auth_response) + + error = assert_raises(ActiveMerchant::OAuthResponseError) do + @gateway.setup_access_token + end + + assert_match(/Failed with AuthToken not valid ENGINE22CE/, error.message) + end + + def test_setup_access_token_should_successfully_extract_the_token_from_response + @gateway.expects(:ssl_post).returns(sucess_auth_response) + + assert_equal 'abc123', @gateway.setup_access_token + end + + private + + def response_result(response) + response.params['result'].first + end + + def pre_scrubbed + <<-PRE_SCRUBBED + opening connection to utgapi.shift4test.com:443... + opened + starting SSL for utgapi.shift4test.com:443... + SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES128-GCM-SHA256 + <- "POST /api/rest/v1/transactions/authorization HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nCompanyname: Spreedly\r\nAccesstoken: 4902FAD2-E88F-4A8D-98C2-EED2A73DBBE2\r\nInvoice: 1\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: utgapi.shift4test.com\r\nContent-Length: 498\r\n\r\n" + <- "{\"dateTime\":\"2022-06-09T14:03:36.413505000+14:03\",\"amount\":{\"total\":5.0,\"tax\":1.0},\"clerk\":{\"numericId\":24},\"transaction\":{\"invoice\":\"1\",\"purchaseCard\":{\"customerReference\":\"457\",\"destinationPostalCode\":\"89123\",\"productDescriptors\":[\"Potential\",\"Wrong\"]}},\"card\":{\"expirationDate\":\"0923\",\"number\":\"4000100011112224\",\"entryMode\":null,\"present\":null,\"securityCode\":{\"indicator\":1,\"value\":\"4444\"}},\"customer\":{\"addressLine1\":\"89 Main Street\",\"firstName\":\"XYZ\",\"lastName\":\"RON\",\"postalCode\":\"89000\"}}" + -> "HTTP/1.1 200 OK\r\n" + -> "Connection: close\r\n" + -> "Content-Type: text/json; charset=ISO-8859-1\r\n" + -> "Content-Length: 1074\r\n" + -> "Date: Thu, 09 Jun 2022 09:03:40 GMT\r\n" + -> "Pragma: no-cache\r\n" + -> "X-Frame-Options: deny\r\n" + -> "Content-Security-Policy: default-src 'none';base-uri 'none';frame-ancestors 'none';object-src 'none';sandbox;\r\n" + -> "Strict-Transport-Security: max-age=31536000; includeSubDomains\r\n" + -> "X-XSS-Protection: 1; mode=block\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "X-Permitted-Cross-Domain-Policies: none\r\n" + -> "Referrer-Policy: no-referrer\r\n" + -> "X-Powered-By: Electricity\r\n" + -> "Expires: 0\r\n" + -> "Cache-Control: private, no-cache, no-store, max-age=0, no-transform\r\n" + -> "Server: DatasnapHTTPService/2011\r\n" + -> "\r\n" + reading 1074 bytes... + -> "" + -> "{\"result\":[{\"dateTime\":\"2022-06-09T14:03:36.000-07:00\",\"receiptColumns\":30,\"amount\":{\"tax\":1,\"total\":5},\"card\":{\"type\":\"VS\",\"entryMode\":\"M\",\"number\":\"XXXXXXXXXXXX2224\",\"present\":\"Y\",\"securityCode\":{\"result\":\"N\",\"valid\":\"N\"},\"token\":{\"value\":\"8042728003772224\"}},\"clerk\":{\"numericId\":24},\"customer\":{\"addressLine1\":\"89 Main Street\",\"firstName\":\"XYZ\",\"lastName\":\"RON\",\"postalCode\":\"89000\"},\"device\":{\"capability\":{\"magstripe\":\"Y\",\"manualEntry\":\"Y\"}},\"merchant\":{\"mid\":8504672,\"name\":\"Zippin - Retail\"},\"receipt\":[{\"key\":\"MaskedPAN\",\"printValue\":\"XXXXXXXXXXXX2224\"},{\"key\":\"CardEntryMode\",\"printName\":\"ENTRY METHOD\",\"printValue\":\"KEYED\"},{\"key\":\"SignatureRequired\",\"printValue\":\"N\"}],\"server\":{\"name\":\"UTGAPI05CE\"},\"transaction\":{\"authSource\":\"E\",\"avs\":{\"postalCodeVerified\":\"Y\",\"result\":\"Y\",\"streetVerified\":\"Y\",\"valid\":\"Y\"},\"invoice\":\"0000000001\",\"purchaseCard\":{\"customerReference\":\"457\",\"destinationPostalCode\":\"89123\",\"productDescriptors\":[\"Potential\",\"Wrong\"]},\"responseCode\":\"D\",\"saleFlag\":\"S\"},\"universalToken\":{\"value\":\"400010-2F1AA405-001AA4-000026B7-1766C44E9E8\"}}]}" + read 1074 bytes + Conn close + PRE_SCRUBBED + end + + def post_scrubbed + <<-POST_SCRUBBED + opening connection to utgapi.shift4test.com:443... + opened + starting SSL for utgapi.shift4test.com:443... + SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES128-GCM-SHA256 + <- "POST /api/rest/v1/transactions/authorization HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nCompanyname: Spreedly\r\nAccesstoken: 4902FAD2-E88F-4A8D-98C2-EED2A73DBBE2\r\nInvoice: 1\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: utgapi.shift4test.com\r\nContent-Length: 498\r\n\r\n" + <- "{\"dateTime\":\"2022-06-09T14:03:36.413505000+14:03\",\"amount\":{\"total\":5.0,\"tax\":1.0},\"clerk\":{\"numericId\":24},\"transaction\":{\"invoice\":\"1\",\"purchaseCard\":{\"customerReference\":\"457\",\"destinationPostalCode\":\"89123\",\"productDescriptors\":[\"Potential\",\"Wrong\"]}},\"card\":{\"expirationDate\":\"[FILTERED]",\"number\":\"[FILTERED]",\"entryMode\":null,\"present\":null,\"securityCode\":{\"indicator\":1,\"value\":\"[FILTERED]\"}},\"customer\":{\"addressLine1\":\"89 Main Street\",\"firstName\":\"[FILTERED]",\"lastName\":\"[FILTERED]",\"postalCode\":\"89000\"}}" + -> "HTTP/1.1 200 OK\r\n" + -> "Connection: close\r\n" + -> "Content-Type: text/json; charset=ISO-8859-1\r\n" + -> "Content-Length: 1074\r\n" + -> "Date: Thu, 09 Jun 2022 09:03:40 GMT\r\n" + -> "Pragma: no-cache\r\n" + -> "X-Frame-Options: deny\r\n" + -> "Content-Security-Policy: default-src 'none';base-uri 'none';frame-ancestors 'none';object-src 'none';sandbox;\r\n" + -> "Strict-Transport-Security: max-age=31536000; includeSubDomains\r\n" + -> "X-XSS-Protection: 1; mode=block\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "X-Permitted-Cross-Domain-Policies: none\r\n" + -> "Referrer-Policy: no-referrer\r\n" + -> "X-Powered-By: Electricity\r\n" + -> "Expires: 0\r\n" + -> "Cache-Control: private, no-cache, no-store, max-age=0, no-transform\r\n" + -> "Server: DatasnapHTTPService/2011\r\n" + -> "\r\n" + reading 1074 bytes... + -> "" + -> "{\"result\":[{\"dateTime\":\"2022-06-09T14:03:36.000-07:00\",\"receiptColumns\":30,\"amount\":{\"tax\":1,\"total\":5},\"card\":{\"type\":\"VS\",\"entryMode\":\"M\",\"number\":\"[FILTERED]",\"present\":\"Y\",\"securityCode\":{\"result\":\"N\",\"valid\":\"N\"},\"token\":{\"value\":\"8042728003772224\"}},\"clerk\":{\"numericId\":24},\"customer\":{\"addressLine1\":\"89 Main Street\",\"firstName\":\"[FILTERED]",\"lastName\":\"[FILTERED]",\"postalCode\":\"89000\"},\"device\":{\"capability\":{\"magstripe\":\"Y\",\"manualEntry\":\"Y\"}},\"merchant\":{\"mid\":8504672,\"name\":\"Zippin - Retail\"},\"receipt\":[{\"key\":\"MaskedPAN\",\"printValue\":\"XXXXXXXXXXXX2224\"},{\"key\":\"CardEntryMode\",\"printName\":\"ENTRY METHOD\",\"printValue\":\"KEYED\"},{\"key\":\"SignatureRequired\",\"printValue\":\"N\"}],\"server\":{\"name\":\"UTGAPI05CE\"},\"transaction\":{\"authSource\":\"E\",\"avs\":{\"postalCodeVerified\":\"Y\",\"result\":\"Y\",\"streetVerified\":\"Y\",\"valid\":\"Y\"},\"invoice\":\"0000000001\",\"purchaseCard\":{\"customerReference\":\"457\",\"destinationPostalCode\":\"89123\",\"productDescriptors\":[\"Potential\",\"Wrong\"]},\"responseCode\":\"D\",\"saleFlag\":\"S\"},\"universalToken\":{\"value\":\"400010-2F1AA405-001AA4-000026B7-1766C44E9E8\"}}]}" + read 1074 bytes + Conn close + POST_SCRUBBED + end + + def successful_purchase_response + <<-RESPONSE + { + "result": [ + { + "dateTime": "2022-02-09T05:11:54.000-08:00", + "receiptColumns": 30, + "amount": { + "total": 5 + }, + "card": { + "type": "VS", + "entryMode": "M", + "number": "XXXXXXXXXXXX1111", + "present": "N", + "securityCode": { + "result": "N", + "valid": "N" + }, + "token": { + "value": "8042714004661111" + } + }, + "clerk": { + "numericId": 16 + }, + "device": { + "capability": { + "magstripe": "Y", + "manualEntry": "Y" + } + }, + "merchant": { + "mid": 8585812, + "name": "RealtimePOS - Retail" + }, + "receipt": [ + { + "key": "MaskedPAN", + "printValue": "XXXXXXXXXXXX1111" + }, + { + "key": "CardEntryMode", + "printName": "ENTRY METHOD", + "printValue": "KEYED" + }, + { + "key": "SignatureRequired", + "printValue": "N" + } + ], + "server": { + "name": "UTGAPI12CE" + }, + "transaction": { + "authSource": "E", + "invoice": "4666309473", + "purchaseCard": { + "customerReference": "1234567", + "destinationPostalCode": "89123", + "productDescriptors": [ + "Test" + ] + }, + "responseCode": "A", + "saleFlag": "S" + }, + "universalToken": { + "value": "444433-2D5C1A5C-001624-00001621-16BAAF4ACC6" + } + } + ] + } + RESPONSE + end + + def successful_store_response + <<-RESPONSE + { + "result": [ + { + "dateTime": "2022-06-27T13:06:07.000-07:00", + "receiptColumns": 30, + "card": { + "type": "VS", + "number": "XXXXXXXXXXXX2224", + "securityCode": {}, + "token": { + "value": "22243v5f0vkezpej" + } + }, + "merchant": { + "mid": 8628968 + }, + "server": { + "name": "UTGAPI11CE" + }, + "universalToken": { + "value": "400010-2F1AA405-001AA4-000026B7-1766C44E9E8" + } + } + ] + } + RESPONSE + end + + def successful_authorize_response + <<-RESPONSE + { + "result": [ + { + "dateTime": "2022-05-02T02:19:38.000-07:00", + "receiptColumns": 30, + "amount": { + "total": 5 + }, + "card": { + "type": "VS", + "entryMode": "M", + "number": "XXXXXXXXXXXX1111", + "present": "N", + "securityCode": {}, + "token": { + "value": "8042677003331111" + } + }, + "clerk": { + "numericId": 24 + }, + "customer": { + "addressLine1": "89 Main Street", + "firstName": "XYZ", + "lastName": "RON", + "postalCode": "89000" + }, + "device": { + "capability": { + "magstripe": "Y", + "manualEntry": "Y" + } + }, + "merchant": { + "mid": 8504672 + }, + "receipt": [ + { + "key": "MaskedPAN", + "printValue": "XXXXXXXXXXXX1111" + }, + { + "key": "CardEntryMode", + "printName": "ENTRY METHOD", + "printValue": "KEYED" + }, + { + "key": "SignatureRequired", + "printValue": "Y" + } + ], + "server": { + "name": "UTGAPI12CE" + }, + "transaction": { + "authorizationCode": "OK168Z", + "authSource": "E", + "avs": { + "postalCodeVerified":"Y", + "result":"Y", + "streetVerified":"Y", + "valid":"Y" + }, + "invoice": "3333333309", + "purchaseCard": { + "customerReference": "457", + "destinationPostalCode": "89123", + "productDescriptors": [ + "Potential", + "Wrong" + ] + }, + "responseCode": "A", + "saleFlag": "S" + }, + "universalToken": { + "value": "444433-2D5C1A5C-001624-00001621-16BAAF4ACC6" + } + } + ] + } + RESPONSE + end + + def successful_capture_response + <<-RESPONSE + { + "result": [ + { + "dateTime": "2022-05-08T01:18:22.000-07:00", + "receiptColumns": 30, + "amount": { + "total": 5 + }, + "card": { + "type": "VS", + "entryMode": "M", + "number": "XXXXXXXXXXXX1111", + "present": "N", + "token": { + "value": "1111x19h4cryk231" + } + }, + "clerk": { + "numericId": 24 + }, + "device": { + "capability": { + "magstripe": "Y", + "manualEntry": "Y" + } + }, + "merchant": { + "mid": 8628968 + }, + "receipt": [ + { + "key": "MaskedPAN", + "printValue": "XXXXXXXXXXXX1111" + }, + { + "key": "CardEntryMode", + "printName": "ENTRY METHOD", + "printValue": "KEYED" + }, + { + "key": "SignatureRequired", + "printValue": "Y" + } + ], + "server": { + "name": "UTGAPI03CE" + }, + "transaction": { + "authorizationCode": "OK207Z", + "authSource": "E", + "invoice": "3333333309", + "responseCode": "A", + "saleFlag": "S" + }, + "universalToken": { + "value": "444433-2D5C1A5C-001624-00001621-16BAAF4ACC6" + } + } + ] + } + RESPONSE + end + + def successful_refund_response + <<-RESPONSE + { + "result": [ + { + "dateTime": "2022-02-09T05:11:54.000-08:00", + "receiptColumns": 30, + "amount": { + "total": 5 + }, + "card": { + "type": "VS", + "entryMode": "M", + "number": "XXXXXXXXXXXX1111", + "present": "N", + "securityCode": { + "result": "N", + "valid": "N" + }, + "token": { + "value": "8042714004661111" + } + }, + "clerk": { + "numericId": 16 + }, + "device": { + "capability": { + "magstripe": "Y", + "manualEntry": "Y" + } + }, + "merchant": { + "mid": 8585812, + "name": "RealtimePOS - Retail" + }, + "receipt": [ + { + "key": "MaskedPAN", + "printValue": "XXXXXXXXXXXX1111" + }, + { + "key": "CardEntryMode", + "printName": "ENTRY METHOD", + "printValue": "KEYED" + }, + { + "key": "SignatureRequired", + "printValue": "N" + } + ], + "server": { + "name": "UTGAPI12CE" + }, + "transaction": { + "authSource": "E", + "invoice": "4666309473", + "purchaseCard": { + "customerReference": "1234567", + "destinationPostalCode": "89123", + "productDescriptors": [ + "Test" + ] + }, + "responseCode": "A", + "saleFlag": "S" + }, + "universalToken": { + "value": "444433-2D5C1A5C-001624-00001621-16BAAF4ACC6" + } + } + ] + } + RESPONSE + end + + def successful_void_response + <<-RESPONSE + { + "result": [ + { + "dateTime": "2022-05-16T14:59:54.000-07:00", + "receiptColumns": 30, + "amount": { + "total": 5 + }, + "card": { + "type": "VS", + "entryMode": "M", + "number": "XXXXXXXXXXXX2224", + "token": { + "value": "2224kz7vybyv1gs3" + } + }, + "device": { + "capability": { + "magstripe": "Y", + "manualEntry": "Y" + } + }, + "merchant": { + "mid": 8628968 + }, + "receipt": [ + { + "key": "TransactionResponse", + "printName": "Response", + "printValue": "SALE CORRECTION" + }, + { + "key": "MaskedPAN", + "printValue": "XXXXXXXXXXXX2224" + }, + { + "key": "CardEntryMode", + "printName": "ENTRY METHOD", + "printValue": "KEYED" + }, + { + "key": "SignatureRequired", + "printValue": "N" + } + ], + "server": { + "name": "UTGAPI07CE" + }, + "transaction": { + "authSource": "E", + "invoice": "0000000001", + "responseCode": "A", + "saleFlag": "S" + } + } + ] + } + RESPONSE + end + + def successful_verify_response + <<-RESPONSE + { + "result": [ + { + "dateTime": "2022-09-16T01:40:51.000-07:00", + "card": { + "type": "VS", + "entryMode": "M", + "number": "XXXXXXXXXXXX2224", + "present": "N", + "securityCode": { + "result": "M", + "valid": "Y" + }, + "token": { + "value": "2224xzsetmjksx13" + } + }, + "customer": { + "firstName": "John", + "lastName": "Smith" + }, + "device": { + "capability": { + "magstripe": "Y", + "manualEntry": "Y" + } + }, + "merchant": { + "name": "Spreedly - ECom" + }, + "server": { + "name": "UTGAPI12CE" + }, + "transaction": { + "authorizationCode": "OK684Z", + "authSource": "E", + "responseCode": "A", + "saleFlag": "S" + }, + "universalToken": { + "value": "400010-2F1AA405-001AA4-000026B7-1766C44E9E8" + } + } + ] + } + RESPONSE + end + + def failed_authorize_response + <<-RESPONSE + { + "result": [ + { + "error": { + "longText": "GTV Msg: ERROR{0} 20018: no default category found, UC, Mod10=N TOKEN01CE ENGINE29CE", + "primaryCode": 9100, + "shortText": "SYSTEM ERROR" + }, + "server": { + "name": "UTGAPI12CE" + } + } + ] + } + RESPONSE + end + + def failed_purchase_response + <<-RESPONSE + { + "result": [ + { + "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": [ + { + "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 + + def failed_capture_response + <<-RESPONSE + { + "result": [ + { + "error": { + "longText": "INTERNET FAILURE: Timeout waiting for response across the Internet UTGAPI05CE", + "primaryCode": 9961, + "shortText": "INTERNET FAILURE" + }, + "server": { + "name": "UTGAPI05CE" + } + } + ] + } + RESPONSE + end + + def failed_refund_response + <<-RESPONSE + { + "result": + [ + { + "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 + + def failed_void_response + <<-RESPONSE + { + "result": [ + { + "error": { + "longText": "Invoice Not Found 00000000kl 0008628968 ENGINE29CE", + "primaryCode": 9815, + "shortText": "NO INV" + }, + "server": { + "name": "UTGAPI13CE" + } + } + ] + } + RESPONSE + end + + def successful_access_token_response + <<-RESPONSE + { + "result": [ + { + "dateTime": "2022-06-22T15:27:51.000-07:00", + "receiptColumns": 30, + "credential": { + "accessToken": "3F6A334E-01E5-4EDB-B4CE-0B1BEFC13518" + }, + "device": { + "capability": { + "magstripe": "Y", + "manualEntry": "Y" + } + }, + "server": { + "name": "UTGAPI09CE" + } + } + ] + } + RESPONSE + end + + def failed_auth_response + <<-RESPONSE + { + "result": [ + { + "error": { + "longText": "AuthToken not valid ENGINE22CE", + "primaryCode": 9862, + "secondaryCode": 4, + "shortText ": "AuthToken" + }, + "server": { + "name": "UTGAPI03CE" + } + } + ] + } + RESPONSE + end + + def failed_auth_response_no_message + <<-RESPONSE + { + "result": [ + { + "error": { + "secondaryCode": 4, + "shortText ": "AuthToken" + }, + "server": { + "name": "UTGAPI03CE" + } + } + ] + } + RESPONSE + end + + def sucess_auth_response + <<-RESPONSE + { + "result": [ + { + "credential": { + "accessToken": "abc123" + } + } + ] + } + RESPONSE + end + + 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 + + def failed_authorize_with_alternate_host_response + <<~RESPONSE + { + "result": [ + { + "dateTime": "2024-09-06T12:46:05.000-07:00", + "receiptColumns": 30, + "correlationId": "A6D33AD9-29BE-44A3-B6B4-8FC6354A0514", + "amount": { + "total": 2118.37 + }, + "card": { + "type": "VS", + "entryMode": "M", + "number": "[FILTERED]", + "present": "N", + "token": { + "value": "657492f6d9cx5qmf" + } + }, + "clerk": { + "numericId": 1 + }, + "customer": { + "addressLine1": "13238 N 101st Pl", + "emailAddress": "howardholleb@everonsolutions.com", + "firstName": "[FILTERED]", + "lastName": "[FILTERED]", + "postalCode": "85260" + }, + "device": { + "capability": { + "magstripe": "Y", + "manualEntry": "Y" + } + }, + "merchant": { + "mid": 8723645, + "name": "NEW CARDINALS STADIUM" + }, + "receipt": [ + { + "key": "MaskedPAN", + "printValue": "XXXXXXXXXXXX6574" + }, + { + "key": "CardEntryMode", + "printName": "ENTRY METHOD", + "printValue": "KEYED" + }, + { + "key": "SignatureRequired", + "printValue": "N" + }, + { + "key": "TerminalID", + "printName": "TID", + "printValue": "78084447" + } + ], + "server": { + "name": "UTGAPI04S7" + }, + "transaction": { + "authSource": "E", + "HEY": "WHOA", + "avs": { + "postalCodeVerified": "Y", + "result": "Y", + "streetVerified": "Y", + "valid": "Y" + }, + "cardOnFile": { + "indicator": "01", + "scheduledIndicator": "02", + "usageIndicator": "01" + }, + "invoice": "0725417280", + "hostresponse": { + "reasonCode": "03", + "reattemptPermission": "Reattempt permitted 15 times in 30 days", + "reasonDescription": "Invalid Merchant" + }, + "responseCode": "D", + "retrievalReference": "425019365998", + "saleFlag": "S", + "vendorReference": "19026022674001" + }, + "universalToken": { + "value": "480709-62ADAB16-000B58-00004E9E-191C7407826" + } + } + ] + } + 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&currency=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&currency=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 new file mode 100644 index 00000000000..82b91e5ac09 --- /dev/null +++ b/test/unit/gateways/simetrik_test.rb @@ -0,0 +1,970 @@ +require 'test_helper' + +class SimetrikTest < Test::Unit::TestCase + def setup + @token_acquirer = 'ea890fd1-49f3-4a34-a150-192bf9a59205' + @datetime = Time.new.strftime('%Y-%m-%dT%H:%M:%S.%L%:z') + @gateway = SimetrikGateway.new( + client_id: 'client_id', + client_secret: 'client_secret_key', + audience: 'audience_url', + access_token: { expires_at: Time.new.to_i } + ) + @credit_card = CreditCard.new( + first_name: 'sergiod', + last_name: 'lobob', + number: '4551478422045511', + month: 12, + year: 2029, + verification_value: '111' + ) + @amount = 1000 + @trace_id = SecureRandom.uuid + @order_id = SecureRandom.uuid[0..7] + + @sub_merchant = { + address: 'string', + extra_params: {}, + mcc: 'string', + merchant_id: 'string', + name: 'string', + phone_number: 'string', + postal_code: 'string', + url: 'string' + } + + @authorize_capture_options = { + acquire_extra_options: {}, + trace_id: @trace_id, + user: { + id: '123', + email: 's@example.com' + }, + order: { + id: @order_id, + datetime_local_transaction: @datetime, + description: 'a popsicle', + installments: 1 + }, + currency: 'USD', + vat: 190, + 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: @sub_merchant, + token_acquirer: @token_acquirer + } + + @authorize_capture_expected_body = { + 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 + } + }, + 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' + } + }, + 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 + + def test_successful_purchase + @gateway.expects(:ssl_request).returns(successful_purchase_response_body) + + response = @gateway.purchase(@amount, @credit_card, @authorize_capture_options) + assert_success response + assert_instance_of Response, response + + assert_equal response.message, 'successful charge' + assert_equal response.error_code, nil, 'Should expected error code equal to nil ' + assert_equal response.avs_result['code'], 'G' + assert_equal response.cvv_result['code'], 'P' + assert response.test? + end + + def test_success_purchase_with_billing_address + expected_body = JSON.parse(@authorize_capture_expected_body.dup) + expected_body['forward_payload']['payment_method']['card']['billing_address'] = address + + @gateway.expects(:ssl_request).returns(successful_purchase_response_body) + + options = @authorize_capture_options.clone() + options[:billing_address] = address + + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_instance_of Response, response + + assert_equal response.message, 'successful charge' + assert_equal response.error_code, nil, 'Should expected error code equal to nil ' + assert_equal response.avs_result['code'], 'G' + assert_equal response.cvv_result['code'], 'P' + assert response.test? + end + + def test_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 + + @gateway.expects(:ssl_request).returns(successful_purchase_response_body) + + options = @authorize_capture_options.clone() + options[:shipping_address] = address + + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_instance_of Response, response + + assert_equal response.message, 'successful charge' + assert_equal response.error_code, nil, 'Should expected error code equal to nil ' + assert_equal response.avs_result['code'], 'G' + assert_equal response.cvv_result['code'], 'P' + assert response.test? + end + + def test_failed_purchase + @gateway.expects(:ssl_request).returns(failed_purchase_response_body) + + response = @gateway.purchase(@amount, @credit_card, @authorize_capture_options) + assert_failure response + assert_instance_of Response, response + assert response.test? + assert_equal response.avs_result['code'], 'I' + assert_equal response.cvv_result['code'], 'P' + assert_equal response.error_code, 'incorrect_number' + assert response.test? + end + + def test_successful_authorize + @gateway.expects(:ssl_request).returns(successful_authorize_response_body) + + response = @gateway.authorize(@amount, @credit_card, @authorize_capture_options) + assert_success response + assert_instance_of Response, response + assert_equal response.message, 'successful authorize' + assert_equal response.error_code, nil, 'Should expected error code equal to nil ' + assert_equal response.avs_result['code'], 'G' + assert_equal response.cvv_result['code'], 'P' + assert response.test? + end + + def test_failed_authorize + @gateway.expects(:ssl_request).returns(failed_authorize_response_body) + + response = @gateway.authorize(@amount, @credit_card, @authorize_capture_options) + assert_failure response + assert_instance_of Response, response + assert response.test? + + assert_equal response.avs_result['code'], 'I' + assert_equal response.cvv_result['code'], 'P' + assert_equal response.error_code, 'incorrect_number' + assert response.test? + end + + def test_successful_capture + @gateway.expects(:ssl_request).returns(successful_capture_response_body) + + response = @gateway.capture(@amount, 'fdb52e6a0e794b039de097e815a982fd', { + vat: @authorize_capture_options[:vat], + currency: 'USD', + transaction_id: 'fdb52e6a0e794b039de097e815a982fd', + token_acquirer: @token_acquirer, + trace_id: @trace_id + }) + + assert_success response + assert_instance_of Response, response + assert response.test? + assert_equal 'successful capture', response.message + assert_equal response.message, 'successful capture' + end + + def test_failed_capture + @gateway.expects(:ssl_request).returns(failed_capture_response_body) + + response = @gateway.capture(@amount, 'SI-226', { + vat: 190, + currency: 'USD', + token_acquirer: @token_acquirer, + trace_id: @trace_id + }) + + assert_failure response + assert_instance_of Response, response + assert_equal response.avs_result['code'], 'I' + assert_equal response.cvv_result['code'], 'P' + assert_equal response.error_code, 'processing_error' + assert response.test? + end + + def test_successful_refund + @gateway.expects(:ssl_request).returns(successful_refund_response_body) + + response = @gateway.refund(@amount, 'SI-226', { + amount: { + currency: 'USD' + }, + token_acquirer: @token_acquirer, + comment: 'A Comment', + acquire_extra_options: { + ruc: 123 + }, + trace_id: @trace_id + }) + + assert_success response + assert_instance_of Response, response + assert response.test? + + assert_equal 'successful refund', response.message + assert_equal response.message, 'successful refund' + assert_equal response.error_code, nil, 'Should expected error code equal to nil' + end + + def test_failed_refund + @gateway.expects(:ssl_request).returns(failed_refund_response_body) + response = @gateway.refund(@amount, 'SI-226', { + amount: { + currency: 'USD' + }, + token_acquirer: @token_acquirer, + comment: 'A Comment', + trace_id: @trace_id + }) + assert_failure response + assert_instance_of Response, response + assert_equal response.avs_result['code'], 'I' + assert_equal response.cvv_result['code'], 'P' + assert_equal response.error_code, 'processing_error' + assert response.test? + end + + def test_successful_void + @gateway.expects(:ssl_request).returns(successful_void_response_body) + response = @gateway.void('a17f70f9-82de-4c47-8d9c-7743dac6a561', { + token_acquirer: @token_acquirer, + trace_id: @trace_id + + }) + + assert_success response + assert_instance_of Response, response + assert response.test? + assert_equal 'successful void', response.message + assert_equal response.message, 'successful void' + end + + def test_failed_void + @gateway.expects(:ssl_request).returns(failed_void_response_body) + response = @gateway.void('a17f70f9-82de-4c47-8d9c-7743dac6a561', { + token_acquirer: @token_acquirer, + trace_id: @trace_id + }) + assert_failure response + assert_instance_of Response, response + assert_equal response.avs_result['code'], 'I' + assert_equal response.cvv_result['code'], 'P' + assert_equal response.error_code, 'processing_error' + assert response.test? + end + + # no assertion for client_secret because it is included in a private method, if there + # is refactoring of sign_access_token method ensure that it is properly scrubbed + def test_scrub + transcript = @gateway.scrub(pre_scrubbed()) + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + assert_scrubbed('4551478422045511', transcript) + end + + private + + def pre_scrubbed + <<-PRESCRUBBED + opening connection to payments.sta.simetrik.com:443... + opened + starting SSL for payments.sta.simetrik.com:443... + SSL established + <- "POST /v1/bc4c0f26-a357-4294-9b9e-a90e6c868c6e/charge HTTP/1.1\r\nContent-Type: application/json\r\nAuthorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InYwZ0ZNM1QwY1BpZ3J2OTBrS1dEZSJ9. eyJpc3MiOiJodHRwczovL3RlbmFudC1wYXltZW50cy1kZXYudXMuYXV0aDAuY29tLyIsInN1YiI6IndOaEpCZHJLRGszdlRta1FNQVdpNXpXTjd5MjFhZE8zQGNsaWVudHMiL CJhdWQiOiJodHRwczovL3RlbmFudC1wYXltZW50cy1kZXYudXMuYXV0aDAuY29tL2FwaS92Mi8iLCJpYXQiOjE2NTExNjk1OTYsImV4cCI6MTY1MTI1NTk5NiwiYXpwIjoid0 5oSkJkcktEazN2VG1rUU1BV2k1eldON3kyMWFkTzMiLCJzY29wZSI6InJlYWQ6Y2xpZW50X2dyYW50cyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9. mAaWcAiq0t_UnXQGMv2uHcOfFoxclfPBU9Wa_Tmzmps3jIZnCggGxptAjaxn_Hj7Mteni4u9t7QVDUA6pQ1nVT4hfuFbFiC3CcvB8AKb6_PgIYLHuZ1i0VKyS6VtdB04_Sl8u kbBcnXXt2GrRps23OPwwBjdJOKzXhz0pLeiDeBA_0SkF6LXqmbMuFB5PPGC2hyQUOOlkrqBjXH8meIMfnBh4GooF3AnsuhDT3hSu8t0gpQAVmQatxqPQwce8WXkD6qnCM6Q81 LnZCBjfzyF2T_LN4q9GmFkcy3NGkEXXNC1UigPxqbgDmf448biCKiMv1NnXyMhfknxH_kR2f6QdQ\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: payments.sta.simetrik.com\r\nContent-Length: 720\r\n\r\n" + <- "{\"forward_route\":{\"trace_id\":\"ce4091cf-3656-4c78-b835-f9fcf2b2cb11\",\"psp_extra_fields\":{}},\"forward_payload\":{\"user\": {\"id\":\"123\",\"email\":\"s@example.com\"},\"order\":{\"id\":\"191885304068\",\"description\":\"apopsicle\",\"installments\":1, \"datetime_local_transaction\":\"2022-04-28T14:13:16.117-04:00\",\"amount\":{\"total_amount\":1.0,\"currency\":\"USD\",\"vat\":19.0}} ,\"payment_method\":{\"card\":{\"number\":\"4551708161768059\",\"exp_month\":7,\"exp_year\":2022,\"security_code\":\"111\", \"type\":\"visa\",\"holder_first_name\":\"Joe\",\"holder_last_name\":\"Doe\"}},\"sub_merchant\":{\"merchant_id\":\"400000008\", \"extra_params\":{},\"mcc\":\"5816\",\"name\":\"885.519.237\",\"address\":\"None\",\"postal_code\":\"None\",\"url\":\"string\", \"phone_number\":\"3434343\"},\"acquire_extra_options\":{}}}" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: nginx\r\n" + -> "Date: Thu, 28 Apr 2022 18:13:34 GMT\r\n" + -> "Content-Type: text/plain; charset=utf-8\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "Apigw-Requestid: RTbhkj7GoAMETbA=\r\n" + -> "Via: 1.1 reverse-proxy-02-797bd8c84-8jv96\r\n" + -> "access-control-allow-origin: *\r\n" + -> "VGS-Request-Id: 8cdb14abe5f9fb04b3d3a5690930a418\r\n" + -> "Content-Encoding: gzip\r\n" + -> "\r\n" + -> "235\r\n" + reading 565 bytes... + read 565 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + PRESCRUBBED + end + + def post_scrubbed + <<-POSTSCRUBBED + opening connection to payments.sta.simetrik.com:443... + opened + starting SSL for payments.sta.simetrik.com:443... + SSL established + <- "POST /v1/bc4c0f26-a357-4294-9b9e-a90e6c868c6e/charge HTTP/1.1\r\nContent-Type: application/json\r\nAuthorization: Bearer [FILTERED]\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: payments.sta.simetrik.com\r\nContent-Length: 720\r\n\r\n" + <- "{\"forward_route\":{\"trace_id\":\"ce4091cf-3656-4c78-b835-f9fcf2b2cb11\",\"psp_extra_fields\":{}},\"forward_payload\":{\"user\": {\"id\":\"123\",\"email\":\"s@example.com\"},\"order\":{\"id\":\"191885304068\",\"description\":\"apopsicle\",\"installments\":1, \"datetime_local_transaction\":\"2022-04-28T14:13:16.117-04:00\",\"amount\":{\"total_amount\":1.0,\"currency\":\"USD\",\"vat\":19.0}} ,\"payment_method\":{\"card\":{\"number\":\"[FILTERED]\",\"exp_month\":7,\"exp_year\":2022,\"security_code\":\"[FILTERED]\", \"type\":\"visa\",\"holder_first_name\":\"Joe\",\"holder_last_name\":\"Doe\"}},\"sub_merchant\":{\"merchant_id\":\"400000008\", \"extra_params\":{},\"mcc\":\"5816\",\"name\":\"885.519.237\",\"address\":\"None\",\"postal_code\":\"None\",\"url\":\"string\", \"phone_number\":\"3434343\"},\"acquire_extra_options\":{}}}" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: nginx\r\n" + -> "Date: Thu, 28 Apr 2022 18:13:34 GMT\r\n" + -> "Content-Type: text/plain; charset=utf-8\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "Apigw-Requestid: RTbhkj7GoAMETbA=\r\n" + -> "Via: 1.1 reverse-proxy-02-797bd8c84-8jv96\r\n" + -> "access-control-allow-origin: *\r\n" + -> "VGS-Request-Id: 8cdb14abe5f9fb04b3d3a5690930a418\r\n" + -> "Content-Encoding: gzip\r\n" + -> "\r\n" + -> "235\r\n" + reading 565 bytes... + read 565 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + POSTSCRUBBED + end + + def successful_purchase_response_body + '{ + "code": "S001", + "message": "successful charge", + "acquirer_body": { + "dataMap": { + "ACTION_CODE": "000", + "MERCHANT": "400000008", + "STATUS": "Authorized", + "CARD": "455170******8059", + "INSTALLMENTS_INFO": "03000000000", + "QUOTA_NUMBER": "03", + "QUOTA_AMOUNT": "0.00", + "QUOTA_DEFERRED": "0" + }, + "order": { + "purchaseNumber": "56700001", + "amount": 1000.0, + "currency": "USD", + "authorizedAmount": 1000.0, + "authorizationCode": "105604", + "actionCode": "000", + "traceNumber": "75763", + "transactionId": "984220460014549", + "transactionDate": "220215105559" + }, + "fulfillment": { + "merchantId": "400000008", + "captureType": "manual", + "countable": false, + "signature": "6168ebd4-9798-477c-80d4-b80971820b51" + } + }, + "avs_result": "G", + "cvv_result": "P", + "simetrik_authorization_id": "a870eeca1b1c46b39b6fd76fde7c32b6", + "trace_id": "00866583c3c24a36b0270f1e38568c77" + }' + end + + def failed_purchase_response_body + '{ + "trace_id": 50300, + "code": "R101", + "message": "incorrect_number", + "simetrik_authorization_id": "S-1205", + "avs_result": "I", + "cvv_result": "P", + "acquirer_body": { + "header": { + "ecoreTransactionUUID": "8d2dfc73-ec1f-43af-aa0f-b2c123fd25ea", + "ecoreTransactionDate": 1603054418778, + "millis": 4752 + }, + "fulfillment": { + "channel": "web", + "merchantId": "341198210", + "terminalId": "1", + "captureType": "manual", + "countable": true, + "fastPayment": false, + "signature": "2e2cba40-a914-4e79-b4d3-8a2f2737eb73" + }, + "order": { + "tokenId": "99E9BF92C69A4799A9BF92C69AF79979", + "purchaseNumber": "2020100901", + "amount": 10.5, + "installment": 2, + "currency": "PEN", + "authorizedAmount": 10.5, + "authorizationCode": "173424", + "actionCode": "000", + "traceNumber": "177159", + "transactionDate": "201010173430", + "transactionId": "993202840246052" + }, + "token": { + "tokenId": "7000010038706267", + "ownerId": "abc@mail.com", + "expireOn": "240702123548" + }, + "dataMap": { + "TERMINAL": "00000001", + "TRACE_NUMBER": "177159", + "ECI_DESCRIPTION": "Transaccion no autenticada pero enviada en canal seguro", + "SIGNATURE": "2e2cba40-a914-4e79-b4d3-8a2f2737eb73", + "CARD": "455170******8329", + "MERCHANT": "341198210", + "STATUS": "Authorized", + "INSTALLMENTS_INFO": "02000000000", + "ACTION_DESCRIPTION": "Aprobado y completado con exito", + "ID_UNICO": "993202840246052", + "AMOUNT": "10.50", + "QUOTA_NUMBER": "02", + "AUTHORIZATION_CODE": "173424", + "CURRENCY": "0604", + "TRANSACTION_DATE": "201010173430", + "ACTION_CODE": "000", + "CARD_TOKEN": "7000010038706267", + "ECI": "07", + "BRAND": "visa", + "ADQUIRENTE": "570002", + "QUOTA_AMOUNT": "0.00", + "PROCESS_CODE": "000000", + "VAULT_BLOCK": "abc@mail.com", + "TRANSACTION_ID": "993202840246052", + "QUOTA_DEFERRED": "0" + } + } + }' + end + + def successful_authorize_response_body + '{ + "trace_id": 50300, + "code": "S001", + "message": "successful authorize", + "simetrik_authorization_id": "S-1205", + "avs_result": "G", + "cvv_result": "P", + "acquirer_body": { + "header": { + "ecoreTransactionUUID": "8d2dfc73-ec1f-43af-aa0f-b2c123fd25ea", + "ecoreTransactionDate": 1603054418778, + "millis": 4752 + }, + "fulfillment": { + "channel": "web", + "merchantId": "341198210", + "terminalId": "1", + "captureType": "manual", + "countable": true, + "fastPayment": false, + "signature": "2e2cba40-a914-4e79-b4d3-8a2f2737eb73" + }, + "order": { + "tokenId": "99E9BF92C69A4799A9BF92C69AF79979", + "purchaseNumber": "2020100901", + "amount": 10.5, + "installment": 2, + "currency": "PEN", + "authorizedAmount": 10.5, + "authorizationCode": "173424", + "actionCode": "000", + "traceNumber": "177159", + "transactionDate": "201010173430", + "transactionId": "993202840246052" + }, + "token": { + "tokenId": "7000010038706267", + "ownerId": "abc@mail.com", + "expireOn": "240702123548" + }, + "dataMap": { + "TERMINAL": "00000001", + "TRACE_NUMBER": "177159", + "ECI_DESCRIPTION": "Transaccion no autenticada pero enviada en canal seguro", + "SIGNATURE": "2e2cba40-a914-4e79-b4d3-8a2f2737eb73", + "CARD": "455170******8329", + "MERCHANT": "341198210", + "STATUS": "Authorized", + "INSTALLMENTS_INFO": "02000000000", + "ACTION_DESCRIPTION": "Aprobado y completado con exito", + "ID_UNICO": "993202840246052", + "AMOUNT": "10.50", + "QUOTA_NUMBER": "02", + "AUTHORIZATION_CODE": "173424", + "CURRENCY": "0604", + "TRANSACTION_DATE": "201010173430", + "ACTION_CODE": "000", + "CARD_TOKEN": "7000010038706267", + "ECI": "07", + "BRAND": "visa", + "ADQUIRENTE": "570002", + "QUOTA_AMOUNT": "0.00", + "PROCESS_CODE": "000000", + "VAULT_BLOCK": "abc@mail.com", + "TRANSACTION_ID": "993202840246052", + "QUOTA_DEFERRED": "0" + } + } + }' + end + + def failed_authorize_response_body + '{ + "trace_id": 50300, + "code": "R101", + "message": "incorrect_number", + "simetrik_authorization_id": "S-1205", + "avs_result": "I", + "cvv_result": "P", + "acquirer_body": { + "header": { + "ecoreTransactionUUID": "8d2dfc73-ec1f-43af-aa0f-b2c123fd25ea", + "ecoreTransactionDate": 1603054418778, + "millis": 4752 + }, + "fulfillment": { + "channel": "web", + "merchantId": "341198210", + "terminalId": "1", + "captureType": "manual", + "countable": true, + "fastPayment": false, + "signature": "2e2cba40-a914-4e79-b4d3-8a2f2737eb73" + }, + "order": { + "tokenId": "99E9BF92C69A4799A9BF92C69AF79979", + "purchaseNumber": "2020100901", + "amount": 10.5, + "installment": 2, + "currency": "PEN", + "authorizedAmount": 10.5, + "authorizationCode": "173424", + "actionCode": "000", + "traceNumber": "177159", + "transactionDate": "201010173430", + "transactionId": "993202840246052" + }, + "token": { + "tokenId": "7000010038706267", + "ownerId": "abc@mail.com", + "expireOn": "240702123548" + }, + "dataMap": { + "TERMINAL": "00000001", + "TRACE_NUMBER": "177159", + "ECI_DESCRIPTION": "Transaccion no autenticada pero enviada en canal seguro", + "SIGNATURE": "2e2cba40-a914-4e79-b4d3-8a2f2737eb73", + "CARD": "455170******8329", + "MERCHANT": "341198210", + "STATUS": "Authorized", + "INSTALLMENTS_INFO": "02000000000", + "ACTION_DESCRIPTION": "Aprobado y completado con exito", + "ID_UNICO": "993202840246052", + "AMOUNT": "10.50", + "QUOTA_NUMBER": "02", + "AUTHORIZATION_CODE": "173424", + "CURRENCY": "0604", + "TRANSACTION_DATE": "201010173430", + "ACTION_CODE": "000", + "CARD_TOKEN": "7000010038706267", + "ECI": "07", + "BRAND": "visa", + "ADQUIRENTE": "570002", + "QUOTA_AMOUNT": "0.00", + "PROCESS_CODE": "000000", + "VAULT_BLOCK": "abc@mail.com", + "TRANSACTION_ID": "993202840246052", + "QUOTA_DEFERRED": "0" + } + } + }' + end + + def successful_capture_response_body + '{"code": "S001", "message": "successful capture", "acquirer_body": {"dataMap": {"ACTION_CODE": "000", "AUTHORIZATION_CODE": "121742", "ECI_DESCRIPTION": "Transaccion no autenticada pero enviada en canal seguro", "ID_UNICO": "984221100087087", "MERCHANT": "400000008", "STATUS": "Confirmed", "TRACE_NUMBER": "134626", "ACTION_DESCRIPTION": "Aprobado y completado con exito"}, "order": {"purchaseNumber": "112226091072", "amount": 1.0, "currency": "USD", "authorizedAmount": 1.0, "authorizationCode": "121742", "actionCode": "000", "traceNumber": "134626", "transactionId": "984221100087087", "transactionDate": "220420121813"}}, "simetrik_authorization_id": "0ef9f1f07e304bd7969d8282d230f072", "trace_id": "5273214580359436090"}' + end + + def failed_capture_response_body + '{ + "trace_id": 50300, + "code": "R302", + "message": "processing_error", + "simetrik_authorization_id": "S-1205", + "avs_result": "I", + "cvv_result": "P", + "acquirer_body": { + "header": { + "ecoreTransactionUUID": "8d2dfc73-ec1f-43af-aa0f-b2c123fd25ea", + "ecoreTransactionDate": 1603054418778, + "millis": 4752 + }, + "fulfillment": { + "channel": "web", + "merchantId": "341198210", + "terminalId": "1", + "captureType": "manual", + "countable": true, + "fastPayment": false, + "signature": "2e2cba40-a914-4e79-b4d3-8a2f2737eb73" + }, + "order": { + "tokenId": "99E9BF92C69A4799A9BF92C69AF79979", + "purchaseNumber": "2020100901", + "amount": 10.5, + "installment": 2, + "currency": "PEN", + "authorizedAmount": 10.5, + "authorizationCode": "173424", + "actionCode": "000", + "traceNumber": "177159", + "transactionDate": "201010173430", + "transactionId": "993202840246052" + }, + "token": { + "tokenId": "7000010038706267", + "ownerId": "abc@mail.com", + "expireOn": "240702123548" + }, + "dataMap": { + "TERMINAL": "00000001", + "TRACE_NUMBER": "177159", + "ECI_DESCRIPTION": "Transaccion no autenticada pero enviada en canal seguro", + "SIGNATURE": "2e2cba40-a914-4e79-b4d3-8a2f2737eb73", + "CARD": "455170******8329", + "MERCHANT": "341198210", + "STATUS": "Authorized", + "INSTALLMENTS_INFO": "02000000000", + "ACTION_DESCRIPTION": "Aprobado y completado con exito", + "ID_UNICO": "993202840246052", + "AMOUNT": "10.50", + "QUOTA_NUMBER": "02", + "AUTHORIZATION_CODE": "173424", + "CURRENCY": "0604", + "TRANSACTION_DATE": "201010173430", + "ACTION_CODE": "000", + "CARD_TOKEN": "7000010038706267", + "ECI": "07", + "BRAND": "visa", + "ADQUIRENTE": "570002", + "QUOTA_AMOUNT": "0.00", + "PROCESS_CODE": "000000", + "VAULT_BLOCK": "abc@mail.com", + "TRANSACTION_ID": "993202840246052", + "QUOTA_DEFERRED": "0" + } + } + }' + end + + def successful_refund_response_body + '{ + "trace_id": 50300, + "code": "S001", + "message": "successful refund", + "simetrik_authorization_id": "S-1205", + "acquirer_body": { + "header": { + "ecoreTransactionUUID": "8d2dfc73-ec1f-43af-aa0f-b2c123fd25ea", + "ecoreTransactionDate": 1603054418778, + "millis": 4752 + }, + "fulfillment": { + "channel": "web", + "merchantId": "341198210", + "terminalId": "1", + "captureType": "manual", + "countable": true, + "fastPayment": false, + "signature": "2e2cba40-a914-4e79-b4d3-8a2f2737eb73" + }, + "order": { + "tokenId": "99E9BF92C69A4799A9BF92C69AF79979", + "purchaseNumber": "2020100901", + "amount": 10.5, + "installment": 2, + "currency": "PEN", + "authorizedAmount": 10.5, + "authorizationCode": "173424", + "actionCode": "000", + "traceNumber": "177159", + "transactionDate": "201010173430", + "transactionId": "993202840246052" + }, + "token": { + "tokenId": "7000010038706267", + "ownerId": "abc@mail.com", + "expireOn": "240702123548" + }, + "dataMap": { + "TERMINAL": "00000001", + "TRACE_NUMBER": "177159", + "ECI_DESCRIPTION": "Transaccion no autenticada pero enviada en canal seguro", + "SIGNATURE": "2e2cba40-a914-4e79-b4d3-8a2f2737eb73", + "CARD": "455170******8329", + "MERCHANT": "341198210", + "STATUS": "Authorized", + "INSTALLMENTS_INFO": "02000000000", + "ACTION_DESCRIPTION": "Aprobado y completado con exito", + "ID_UNICO": "993202840246052", + "AMOUNT": "10.50", + "QUOTA_NUMBER": "02", + "AUTHORIZATION_CODE": "173424", + "CURRENCY": "0604", + "TRANSACTION_DATE": "201010173430", + "ACTION_CODE": "000", + "CARD_TOKEN": "7000010038706267", + "ECI": "07", + "BRAND": "visa", + "ADQUIRENTE": "570002", + "QUOTA_AMOUNT": "0.00", + "PROCESS_CODE": "000000", + "VAULT_BLOCK": "abc@mail.com", + "TRANSACTION_ID": "993202840246052", + "QUOTA_DEFERRED": "0" + } + } + }' + end + + def failed_refund_response_body + '{ + "trace_id": 50300, + "code": "R302", + "message": "processing_error", + "simetrik_authorization_id": "S-1205", + "avs_result": "I", + "cvv_result": "P", + "acquirer_body": { + "header": { + "ecoreTransactionUUID": "8d2dfc73-ec1f-43af-aa0f-b2c123fd25ea", + "ecoreTransactionDate": 1603054418778, + "millis": 4752 + }, + "fulfillment": { + "channel": "web", + "merchantId": "341198210", + "terminalId": "1", + "captureType": "manual", + "countable": true, + "fastPayment": false, + "signature": "2e2cba40-a914-4e79-b4d3-8a2f2737eb73" + }, + "order": { + "tokenId": "99E9BF92C69A4799A9BF92C69AF79979", + "purchaseNumber": "2020100901", + "amount": 10.5, + "installment": 2, + "currency": "PEN", + "authorizedAmount": 10.5, + "authorizationCode": "173424", + "actionCode": "000", + "traceNumber": "177159", + "transactionDate": "201010173430", + "transactionId": "993202840246052" + }, + "token": { + "tokenId": "7000010038706267", + "ownerId": "abc@mail.com", + "expireOn": "240702123548" + }, + "dataMap": { + "TERMINAL": "00000001", + "TRACE_NUMBER": "177159", + "ECI_DESCRIPTION": "Transaccion no autenticada pero enviada en canal seguro", + "SIGNATURE": "2e2cba40-a914-4e79-b4d3-8a2f2737eb73", + "CARD": "455170******8329", + "MERCHANT": "341198210", + "STATUS": "Authorized", + "INSTALLMENTS_INFO": "02000000000", + "ACTION_DESCRIPTION": "Aprobado y completado con exito", + "ID_UNICO": "993202840246052", + "AMOUNT": "10.50", + "QUOTA_NUMBER": "02", + "AUTHORIZATION_CODE": "173424", + "CURRENCY": "0604", + "TRANSACTION_DATE": "201010173430", + "ACTION_CODE": "000", + "CARD_TOKEN": "7000010038706267", + "ECI": "07", + "BRAND": "visa", + "ADQUIRENTE": "570002", + "QUOTA_AMOUNT": "0.00", + "PROCESS_CODE": "000000", + "VAULT_BLOCK": "abc@mail.com", + "TRANSACTION_ID": "993202840246052", + "QUOTA_DEFERRED": "0" + } + } + }' + end + + def successful_void_response_body + '{ + "trace_id": 50300, + "code": "S001", + "message": "successful void", + "simetrik_authorization_id": "S-1205", + "acquirer_body": {} + }' + end + + def failed_void_response_body + '{ + "trace_id": 50300, + "code": "R302", + "message": "processing_error", + "simetrik_authorization_id": "S-1205", + "avs_result": "I", + "cvv_result": "P", + "acquirer_body": { + "header": { + "ecoreTransactionUUID": "8d2dfc73-ec1f-43af-aa0f-b2c123fd25ea", + "ecoreTransactionDate": 1603054418778, + "millis": 4752 + }, + "fulfillment": { + "channel": "web", + "merchantId": "341198210", + "terminalId": "1", + "captureType": "manual", + "countable": true, + "fastPayment": false, + "signature": "2e2cba40-a914-4e79-b4d3-8a2f2737eb73" + }, + "order": { + "tokenId": "99E9BF92C69A4799A9BF92C69AF79979", + "purchaseNumber": "2020100901", + "amount": 10.5, + "installment": 2, + "currency": "PEN", + "authorizedAmount": 10.5, + "authorizationCode": "173424", + "actionCode": "000", + "traceNumber": "177159", + "transactionDate": "201010173430", + "transactionId": "993202840246052" + }, + "token": { + "tokenId": "7000010038706267", + "ownerId": "abc@mail.com", + "expireOn": "240702123548" + }, + "dataMap": { + "TERMINAL": "00000001", + "TRACE_NUMBER": "177159", + "ECI_DESCRIPTION": "Transaccion no autenticada pero enviada en canal seguro", + "SIGNATURE": "2e2cba40-a914-4e79-b4d3-8a2f2737eb73", + "CARD": "455170******8329", + "MERCHANT": "341198210", + "STATUS": "Authorized", + "INSTALLMENTS_INFO": "02000000000", + "ACTION_DESCRIPTION": "Aprobado y completado con exito", + "ID_UNICO": "993202840246052", + "AMOUNT": "10.50", + "QUOTA_NUMBER": "02", + "AUTHORIZATION_CODE": "173424", + "CURRENCY": "0604", + "TRANSACTION_DATE": "201010173430", + "ACTION_CODE": "000", + "CARD_TOKEN": "7000010038706267", + "ECI": "07", + "BRAND": "visa", + "ADQUIRENTE": "570002", + "QUOTA_AMOUNT": "0.00", + "PROCESS_CODE": "000000", + "VAULT_BLOCK": "abc@mail.com", + "TRANSACTION_ID": "993202840246052", + "QUOTA_DEFERRED": "0" + } + } + }' + end +end diff --git a/test/unit/gateways/skip_jack_test.rb b/test/unit/gateways/skip_jack_test.rb index 10cc562bbfa..362efffb520 100644 --- a/test/unit/gateways/skip_jack_test.rb +++ b/test/unit/gateways/skip_jack_test.rb @@ -1,38 +1,37 @@ require 'test_helper' class SkipJackTest < Test::Unit::TestCase - def setup Base.mode = :test - @gateway = SkipJackGateway.new(:login => 'X', :password => 'Y') + @gateway = SkipJackGateway.new(login: 'X', password: 'Y') @credit_card = credit_card('4242424242424242') @billing_address = { - :address1 => '123 Any St.', - :address2 => 'Apt. B', - :city => 'Anytown', - :state => 'ST', - :country => 'US', - :zip => '51511-1234', - :phone => '616-555-1212', - :fax => '616-555-2121' + address1: '123 Any St.', + address2: 'Apt. B', + city: 'Anytown', + state: 'ST', + country: 'US', + zip: '51511-1234', + phone: '616-555-1212', + fax: '616-555-2121' } @shipping_address = { - :name => 'Stew Packman', - :address1 => 'Company', - :address2 => '321 No RD', - :city => 'Nowhereton', - :state => 'ZC', - :country => 'MX', - :phone => '0123231212' + name: 'Stew Packman', + address1: 'Company', + address2: '321 No RD', + city: 'Nowhereton', + state: 'ZC', + country: 'MX', + phone: '0123231212' } @options = { - :order_id => 1, - :email => 'cody@example.com' + order_id: 1, + email: 'cody@example.com' } @amount = 100 @@ -103,10 +102,10 @@ def test_split_line end def test_turn_authorizeapi_response_into_hash - body = <<-EOS -"AUTHCODE","szSerialNumber","szTransactionAmount","szAuthorizationDeclinedMessage","szAVSResponseCode","szAVSResponseMessage","szOrderNumber","szAuthorizationResponseCode","szIsApproved","szCVV2ResponseCode","szCVV2ResponseMessage","szReturnCode","szTransactionFileName","szCAVVResponseCode" -"000067","999888777666","1900","","N","Card authorized, exact address match with 5 digit zipcode.","1","000067","1","","","1","10138083786558.009","" - EOS + body = <<~RESPONSE + "AUTHCODE","szSerialNumber","szTransactionAmount","szAuthorizationDeclinedMessage","szAVSResponseCode","szAVSResponseMessage","szOrderNumber","szAuthorizationResponseCode","szIsApproved","szCVV2ResponseCode","szCVV2ResponseMessage","szReturnCode","szTransactionFileName","szCAVVResponseCode" + "000067","999888777666","1900","","N","Card authorized, exact address match with 5 digit zipcode.","1","000067","1","","","1","10138083786558.009","" + RESPONSE map = @gateway.send(:authorize_response_map, body) @@ -193,9 +192,8 @@ def test_paymentech_authorization_failure assert_failure response end - def test_serial_number_is_added_before_developer_serial_number_for_authorization - expected ="Year=#{Time.now.year + 1}&TransactionAmount=1.00&ShipToPhone=&SerialNumber=X&SJName=Longbob+Longsen&OrderString=1~None~0.00~0~N~%7C%7C&OrderNumber=1&OrderDescription=&Month=9&InvoiceNumber=&Email=cody%40example.com&DeveloperSerialNumber=Y&CustomerCode=&CVV2=123&AccountNumber=4242424242424242" + expected = "Year=#{Time.now.year + 1}&TransactionAmount=1.00&ShipToPhone=&SerialNumber=X&SJName=Longbob+Longsen&OrderString=1~None~0.00~0~N~%7C%7C&OrderNumber=1&OrderDescription=&Month=9&InvoiceNumber=&Email=cody%40example.com&DeveloperSerialNumber=Y&CustomerCode=&CVV2=123&AccountNumber=4242424242424242" expected = expected.gsub('~', '%7E') if RUBY_VERSION < '2.5.0' @gateway.expects(:ssl_post).with('https://developer.skipjackic.com/scripts/evolvcc.dll?AuthorizeAPI', expected).returns(successful_authorization_response) @@ -207,7 +205,7 @@ def test_serial_number_is_added_before_developer_serial_number_for_capture response = @gateway.authorize(@amount, @credit_card, @options) @gateway.expects(:ssl_post).with('https://developer.skipjackic.com/scripts/evolvcc.dll?SJAPI_TransactionChangeStatusRequest', "szTransactionId=#{response.authorization}&szSerialNumber=X&szForceSettlement=0&szDeveloperSerialNumber=Y&szDesiredStatus=SETTLE&szAmount=1.00").returns(successful_capture_response) - response = @gateway.capture(@amount, response.authorization) + @gateway.capture(@amount, response.authorization) end def test_successful_partial_capture @@ -216,7 +214,7 @@ def test_successful_partial_capture response = @gateway.authorize(@amount, @credit_card, @options) @gateway.expects(:ssl_post).with('https://developer.skipjackic.com/scripts/evolvcc.dll?SJAPI_TransactionChangeStatusRequest', "szTransactionId=#{response.authorization}&szSerialNumber=X&szForceSettlement=0&szDeveloperSerialNumber=Y&szDesiredStatus=SETTLE&szAmount=1.00").returns(successful_capture_response) - response = @gateway.capture(@amount/2, response.authorization) + response = @gateway.capture(@amount / 2, response.authorization) assert_equal '1.0000', response.params['TransactionAmount'] end @@ -227,53 +225,53 @@ def test_dont_send_blank_state @options[:shipping_address] = @shipping_address @gateway.expects(:ssl_post).with do |url, params| url == 'https://developer.skipjackic.com/scripts/evolvcc.dll?AuthorizeAPI' && - CGI.parse(params)['State'].first == 'XX' && - CGI.parse(params)['ShipToState'].first == 'XX' + CGI.parse(params)['State'].first == 'XX' && + CGI.parse(params)['ShipToState'].first == 'XX' end.returns(successful_authorization_response) @gateway.authorize(@amount, @credit_card, @options) end private + def successful_authorization_response - <<-CSV -"AUTHCODE","szSerialNumber","szTransactionAmount","szAuthorizationDeclinedMessage","szAVSResponseCode","szAVSResponseMessage","szOrderNumber","szAuthorizationResponseCode","szIsApproved","szCVV2ResponseCode","szCVV2ResponseMessage","szReturnCode","szTransactionFileName","szCAVVResponseCode" -"TAS204","000386891209","100","","Y","Card authorized, exact address match with 5 digit zip code.","107a0fdb21ba42cf04f60274908085ea","TAS204","1","M","Match","1","9802853155172.022","" + <<~CSV + "AUTHCODE","szSerialNumber","szTransactionAmount","szAuthorizationDeclinedMessage","szAVSResponseCode","szAVSResponseMessage","szOrderNumber","szAuthorizationResponseCode","szIsApproved","szCVV2ResponseCode","szCVV2ResponseMessage","szReturnCode","szTransactionFileName","szCAVVResponseCode" + "TAS204","000386891209","100","","Y","Card authorized, exact address match with 5 digit zip code.","107a0fdb21ba42cf04f60274908085ea","TAS204","1","M","Match","1","9802853155172.022","" CSV end def successful_capture_response - <<-CSV -"000386891209","0","1","","","","","","","","","" -"000386891209","1.0000","SETTLE","SUCCESSFUL","Valid","618844630c5fad658e95abfd5e1d4e22","9802853156029.022" + <<~CSV + "000386891209","0","1","","","","","","","","","" + "000386891209","1.0000","SETTLE","SUCCESSFUL","Valid","618844630c5fad658e95abfd5e1d4e22","9802853156029.022" CSV end def successful_refund_response - <<-CSV -"AUTHCODE","szSerialNumber","szTransactionAmount","szAuthorizationDeclinedMessage","szAVSResponseCode","szAVSResponseMessage","szOrderNumber","szAuthorizationResponseCode","szIsApproved","szCVV2ResponseCode","szCVV2ResponseMessage","szReturnCode","szTransactionFileName","szCAVVResponseCode" -"TAS204","000386891209","100","","Y","Card authorized, exact address match with 5 digit zip code.","107a0fdb21ba42cf04f60274908085ea","TAS204","1","M","Match","1","9802853155172.022","" + <<~CSV + "AUTHCODE","szSerialNumber","szTransactionAmount","szAuthorizationDeclinedMessage","szAVSResponseCode","szAVSResponseMessage","szOrderNumber","szAuthorizationResponseCode","szIsApproved","szCVV2ResponseCode","szCVV2ResponseMessage","szReturnCode","szTransactionFileName","szCAVVResponseCode" + "TAS204","000386891209","100","","Y","Card authorized, exact address match with 5 digit zip code.","107a0fdb21ba42cf04f60274908085ea","TAS204","1","M","Match","1","9802853155172.022","" CSV end def unsuccessful_authorization_response - <<-CSV -"AUTHCODE","szSerialNumber","szTransactionAmount","szAuthorizationDeclinedMessage","szAVSResponseCode","szAVSResponseMessage","szOrderNumber","szAuthorizationResponseCode","szIsApproved","szCVV2ResponseCode","szCVV2ResponseMessage","szReturnCode","szTransactionFileName","szCAVVResponseCode"\r\n"EMPTY","000386891209","100","","","","b1eec256d0182f29375e0cbae685092d","","0","","","-35","","" + <<~CSV + "AUTHCODE","szSerialNumber","szTransactionAmount","szAuthorizationDeclinedMessage","szAVSResponseCode","szAVSResponseMessage","szOrderNumber","szAuthorizationResponseCode","szIsApproved","szCVV2ResponseCode","szCVV2ResponseMessage","szReturnCode","szTransactionFileName","szCAVVResponseCode"\r\n"EMPTY","000386891209","100","","","","b1eec256d0182f29375e0cbae685092d","","0","","","-35","","" CSV end def unsuccessful_paymentech_authorization_response - <<-CSV -"AUTHCODE","szSerialNumber","szTransactionAmount","szAuthorizationDeclinedMessage","szAVSResponseCode","szAVSResponseMessage","szOrderNumber","szAuthorizationResponseCode","szIsApproved","szCVV2ResponseCode","szCVV2ResponseMessage","szReturnCode","szTransactionFileName","szCAVVResponseCode", -"EMPTY","000000000000","1.00","","","","43985b7953199d1f02c3017f948e9f13","","0","","","-83","","", + <<~CSV + "AUTHCODE","szSerialNumber","szTransactionAmount","szAuthorizationDeclinedMessage","szAVSResponseCode","szAVSResponseMessage","szOrderNumber","szAuthorizationResponseCode","szIsApproved","szCVV2ResponseCode","szCVV2ResponseMessage","szReturnCode","szTransactionFileName","szCAVVResponseCode", + "EMPTY","000000000000","1.00","","","","43985b7953199d1f02c3017f948e9f13","","0","","","-83","","", CSV end def successful_paymentech_authorization_response - <<-CSV -"AUTHCODE","szSerialNumber","szTransactionAmount","szAuthorizationDeclinedMessage","szAVSResponseCode","szAVSResponseMessage","szOrderNumber","szAuthorizationResponseCode","szIsApproved","szCVV2ResponseCode","szCVV2ResponseMessage","szReturnCode","szTransactionFileName","szCAVVResponseCode", -"093223","000000000000","1.00","","Y","Card authorized, exact address match with 5 digit zip code.","5ac0f04e737baea5a5370037afe827f6","093223","1","M","Match","1","40000024585892.109","", + <<~CSV + "AUTHCODE","szSerialNumber","szTransactionAmount","szAuthorizationDeclinedMessage","szAVSResponseCode","szAVSResponseMessage","szOrderNumber","szAuthorizationResponseCode","szIsApproved","szCVV2ResponseCode","szCVV2ResponseMessage","szReturnCode","szTransactionFileName","szCAVVResponseCode", + "093223","000000000000","1.00","","Y","Card authorized, exact address match with 5 digit zip code.","5ac0f04e737baea5a5370037afe827f6","093223","1","M","Match","1","40000024585892.109","", CSV end end - diff --git a/test/unit/gateways/so_easy_pay_test.rb b/test/unit/gateways/so_easy_pay_test.rb index 1dead1bd716..ae4c9579feb 100644 --- a/test/unit/gateways/so_easy_pay_test.rb +++ b/test/unit/gateways/so_easy_pay_test.rb @@ -3,17 +3,17 @@ class SoEasyPayTest < Test::Unit::TestCase def setup @gateway = SoEasyPayGateway.new( - :login => 'login', - :password => 'password' - ) + login: 'login', + password: 'password' + ) @credit_card = credit_card @amount = 100 @options = { - :order_id => '1', - :billing_address => address, - :description => 'Store Purchase' + order_id: '1', + billing_address: address, + description: 'Store Purchase' } end @@ -87,7 +87,7 @@ def test_do_not_depend_on_expiry_date_class def test_use_ducktyping_for_credit_card @gateway.expects(:ssl_post).returns(successful_purchase_response) - credit_card = stub(:number => '4242424242424242', :verification_value => '123', :name => 'Hans Tester', :year => 2012, :month => 1) + credit_card = stub(number: '4242424242424242', verification_value: '123', name: 'Hans Tester', year: 2012, month: 1) assert_nothing_raised do assert_success @gateway.purchase(@amount, credit_card, @options) @@ -220,6 +220,4 @@ def failed_credit_response </soap:Body> </soap:Envelope>) end - end - diff --git a/test/unit/gateways/spreedly_core_test.rb b/test/unit/gateways/spreedly_core_test.rb index afc5d4dd961..2b2b0b1de49 100644 --- a/test/unit/gateways/spreedly_core_test.rb +++ b/test/unit/gateways/spreedly_core_test.rb @@ -1,12 +1,12 @@ require 'test_helper' class SpreedlyCoreTest < Test::Unit::TestCase - def setup - @gateway = SpreedlyCoreGateway.new(:login => 'api_login', :password => 'api_secret', :gateway_token => 'token') + @gateway = SpreedlyCoreGateway.new(login: 'api_login', password: 'api_secret', gateway_token: 'token') @payment_method_token = 'E3eQGR3E0xiosj7FOJRtIKbF8Ch' @credit_card = credit_card + @check = check @amount = 103 @existing_transaction = 'LKA3RchoqYO0njAfhHVw60ohjrC' @not_found_transaction = 'AdyQXaG0SVpSoMPdmFlvd3aA3uz' @@ -41,7 +41,7 @@ def test_failed_purchase_with_payment_method_token end def test_successful_purchase_with_credit_card - @gateway.stubs(:raw_ssl_request).returns(successful_store_response, successful_purchase_response) + @gateway.stubs(:raw_ssl_request).returns(successful_purchase_response) response = @gateway.purchase(@amount, @credit_card) assert_success response @@ -57,6 +57,21 @@ def test_successful_purchase_with_credit_card assert_equal 'used', response.params['payment_method_storage_state'] end + def test_successful_purchase_with_check + @gateway.stubs(:raw_ssl_request).returns(successful_check_purchase_response) + response = @gateway.purchase(@amount, @check) + + assert_success response + assert !response.test? + + assert_equal 'ZwnfZs3Qy4gRDPWXHopamNuarCJ', response.authorization + assert_equal 'Succeeded!', response.message + assert_equal 'Purchase', response.params['transaction_type'] + assert_equal 'HtCrYfW17wEzWWfrMbwDX4TwPVW', response.params['payment_method_token'] + assert_equal '021*', response.params['payment_method_routing_number'] + assert_equal '*3210', response.params['payment_method_account_number'] + end + def test_failed_purchase_with_invalid_credit_card @gateway.expects(:raw_ssl_request).returns(failed_store_response) response = @gateway.purchase(@amount, @credit_card) @@ -65,7 +80,7 @@ def test_failed_purchase_with_invalid_credit_card end def test_failed_purchase_with_credit_card - @gateway.stubs(:raw_ssl_request).returns(successful_store_response, failed_purchase_response) + @gateway.stubs(:raw_ssl_request).returns(failed_purchase_response) response = @gateway.purchase(@amount, @credit_card) assert_failure response @@ -121,7 +136,7 @@ def test_failed_authorize_with_token end def test_successful_authorize_with_credit_card_and_capture - @gateway.stubs(:raw_ssl_request).returns(successful_store_response, successful_authorize_response) + @gateway.stubs(:raw_ssl_request).returns(successful_authorize_response) response = @gateway.authorize(@amount, @credit_card) assert_success response @@ -144,7 +159,7 @@ def test_successful_authorize_with_credit_card_and_capture end def test_failed_authorize_with_credit_card - @gateway.stubs(:raw_ssl_request).returns(successful_store_response, failed_authorize_response) + @gateway.stubs(:raw_ssl_request).returns(failed_authorize_response) response = @gateway.authorize(@amount, @credit_card) assert_failure response assert_equal 'This transaction cannot be processed.', response.message @@ -279,6 +294,21 @@ def test_failed_find assert_match %r(#{@not_found_transaction}), response.message end + def test_gateway_specific_response_fileds_returned_correctly + @gateway.expects(:raw_ssl_request).returns(successful_purchase_response) + + response = @gateway.purchase(@amount, @payment_method_token) + assert_success response + + assert_not_empty response.params['gateway_specific_response_fields'] + assert_includes response.params['gateway_specific_response_fields'].keys, 'migs' + + migs_response_fields = response.params.dig('gateway_specific_response_fields', 'migs') + assert_equal migs_response_fields['batch_no'], '20122018' + assert_equal migs_response_fields['receipt_no'], 'rxI320t' + assert_equal migs_response_fields['authorize_id'], '800385' + end + def test_scrubbing assert @gateway.supports_scrubbing? assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed @@ -317,6 +347,13 @@ def successful_purchase_response <created_at type="datetime">2012-12-06T20:28:14Z</created_at> <updated_at type="datetime">2012-12-06T20:28:14Z</updated_at> </response> + <gateway_specific_response_fields> + <migs> + <batch_no>20122018</batch_no> + <authorize_id>800385</authorize_id> + <receipt_no>rxI320t</receipt_no> + </migs> + </gateway_specific_response_fields> <payment_method> <token>5WxC03VQ0LmmkYvIHl7XsPKIpUb</token> <created_at type="datetime">2012-12-06T20:20:29Z</created_at> @@ -351,6 +388,97 @@ def successful_purchase_response XML end + def successful_check_purchase_response + MockResponse.succeeded <<-XML + <transaction> + <on_test_gateway type="boolean">false</on_test_gateway> + <created_at type="dateTime">2019-01-06T18:24:33Z</created_at> + <updated_at type="dateTime">2019-01-06T18:24:33Z</updated_at> + <succeeded type="boolean">true</succeeded> + <state>succeeded</state> + <token>ZwnfZs3Qy4gRDPWXHopamNuarCJ</token> + <transaction_type>Purchase</transaction_type> + <order_id nil="true"/> + <ip nil="true"/> + <description nil="true"/> + <email nil="true"/> + <merchant_name_descriptor nil="true"/> + <merchant_location_descriptor nil="true"/> + <gateway_specific_fields nil="true"/> + <gateway_specific_response_fields> + </gateway_specific_response_fields> + <gateway_transaction_id>49</gateway_transaction_id> + <gateway_latency_ms type="integer">0</gateway_latency_ms> + <amount type="integer">100</amount> + <currency_code>USD</currency_code> + <retain_on_success type="boolean">false</retain_on_success> + <payment_method_added type="boolean">true</payment_method_added> + <message key="messages.transaction_succeeded">Succeeded!</message> + <gateway_token>3gLeg4726V5P0HK7cq7QzHsL0a6</gateway_token> + <gateway_type>test</gateway_type> + <shipping_address> + <name nil="true"/> + <address1 nil="true"/> + <address2 nil="true"/> + <city nil="true"/> + <state nil="true"/> + <zip nil="true"/> + <country nil="true"/> + <phone_number nil="true"/> + </shipping_address> + <response> + <success type="boolean">true</success> + <message>Successful purchase</message> + <avs_code nil="true"/> + <avs_message nil="true"/> + <cvv_code nil="true"/> + <cvv_message nil="true"/> + <pending type="boolean">false</pending> + <result_unknown type="boolean">false</result_unknown> + <error_code nil="true"/> + <error_detail nil="true"/> + <cancelled type="boolean">false</cancelled> + <fraud_review nil="true"/> + <created_at type="dateTime">2019-01-06T18:24:33Z</created_at> + <updated_at type="dateTime">2019-01-06T18:24:33Z</updated_at> + </response> + <api_urls> + </api_urls> + <payment_method> + <token>HtCrYfW17wEzWWfrMbwDX4TwPVW</token> + <created_at type="dateTime">2019-01-06T18:24:33Z</created_at> + <updated_at type="dateTime">2019-01-06T18:24:33Z</updated_at> + <email nil="true"/> + <data nil="true"/> + <storage_state>cached</storage_state> + <test type="boolean">true</test> + <metadata nil="true"/> + <full_name>Jim Smith</full_name> + <bank_name nil="true"/> + <account_type>checking</account_type> + <account_holder_type>personal</account_holder_type> + <routing_number_display_digits>021</routing_number_display_digits> + <account_number_display_digits>3210</account_number_display_digits> + <first_name>Jim</first_name> + <last_name>Smith</last_name> + <address1 nil="true"/> + <address2 nil="true"/> + <city nil="true"/> + <state nil="true"/> + <zip nil="true"/> + <country nil="true"/> + <phone_number nil="true"/> + <company nil="true"/> + <payment_method_type>bank_account</payment_method_type> + <errors> + </errors> + <routing_number>021*</routing_number> + <account_number>*3210</account_number> + </payment_method> + </transaction> + XML + end + def failed_purchase_response MockResponse.failed <<-XML <transaction> @@ -843,7 +971,7 @@ def successful_unstore_response end def pre_scrubbed - <<-EOS + <<-REQUEST opening connection to core.spreedly.com:443... opened starting SSL for core.spreedly.com:443... @@ -868,11 +996,11 @@ def pre_scrubbed -> "<transaction>\n <token>NRBpydUCWn658GHV8h2kVlUzB0i</token>\n <created_at type=\"dateTime\">2018-03-10T22:04:06Z</created_at>\n <updated_at type=\"dateTime\">2018-03-10T22:04:06Z</updated_at>\n <succeeded type=\"boolean\">true</succeeded>\n <transaction_type>AddPaymentMethod</transaction_type>\n <retained type=\"boolean\">false</retained>\n <state>succeeded</state>\n <message key=\"messages.transaction_succeeded\">Succeeded!</message>\n <payment_method>\n <token>Wd25UIrH1uopTkZZ4UDdb5XmSDd</token>\n <created_at type=\"dateTime\">2018-03-10T22:04:06Z</created_at>\n <updated_at type=\"dateTime\">2018-03-10T22:04:06Z</updated_at>\n <email nil=\"true\"/>\n <data nil=\"true\"/>\n <storage_state>cached</storage_state>\n <test type=\"boolean\">true</test>\n <last_four_digits>4444</last_four_digits>\n <first_six_digits>555555</first_six_digits>\n <card_type>master</card_type>\n <first_name>Longbob</first_name>\n <last_name>Longsen</last_name>\n <month type=\"integer\">9</month>\n <year type=\"integer\">2019</year>\n <address1 nil=\"true\"/>\n <address2 nil=\"true\"/>\n <city nil=\"true\"/>\n <state nil=\"true\"/>\n <zip nil=\"true\"/>\n <country nil=\"true\"/>\n <phone_number nil=\"true\"/>\n <company nil=\"true\"/>\n <full_name>Longbob Longsen</full_name>\n <eligible_for_card_updater type=\"boolean\">true</eligible_for_card_updater>\n <shipping_address1 nil=\"true\"/>\n <shipping_address2 nil=\"true\"/>\n <shipping_city nil=\"true\"/>\n <shipping_state nil=\"true\"/>\n <shipping_zip nil=\"true\"/>\n <shipping_country nil=\"true\"/>\n <shipping_phone_number nil=\"true\"/>\n <payment_method_type>credit_card</payment_method_type>\n <errors>\n </errors>\n <verification_value>XXX</verification_value>\n <number>XXXX-XXXX-XXXX-4444</number>\n <fingerprint>125370bb396dff6fed4f581f85a91a9e5317</fingerprint>\n </payment_method>\n</transaction>\n" read 1875 bytes Conn close - EOS + REQUEST end def post_scrubbed - <<-EOS + <<-REQUEST opening connection to core.spreedly.com:443... opened starting SSL for core.spreedly.com:443... @@ -897,7 +1025,7 @@ def post_scrubbed -> "<transaction>\n <token>NRBpydUCWn658GHV8h2kVlUzB0i</token>\n <created_at type=\"dateTime\">2018-03-10T22:04:06Z</created_at>\n <updated_at type=\"dateTime\">2018-03-10T22:04:06Z</updated_at>\n <succeeded type=\"boolean\">true</succeeded>\n <transaction_type>AddPaymentMethod</transaction_type>\n <retained type=\"boolean\">false</retained>\n <state>succeeded</state>\n <message key=\"messages.transaction_succeeded\">Succeeded!</message>\n <payment_method>\n <token>Wd25UIrH1uopTkZZ4UDdb5XmSDd</token>\n <created_at type=\"dateTime\">2018-03-10T22:04:06Z</created_at>\n <updated_at type=\"dateTime\">2018-03-10T22:04:06Z</updated_at>\n <email nil=\"true\"/>\n <data nil=\"true\"/>\n <storage_state>cached</storage_state>\n <test type=\"boolean\">true</test>\n <last_four_digits>4444</last_four_digits>\n <first_six_digits>555555</first_six_digits>\n <card_type>master</card_type>\n <first_name>Longbob</first_name>\n <last_name>Longsen</last_name>\n <month type=\"integer\">9</month>\n <year type=\"integer\">2019</year>\n <address1 nil=\"true\"/>\n <address2 nil=\"true\"/>\n <city nil=\"true\"/>\n <state nil=\"true\"/>\n <zip nil=\"true\"/>\n <country nil=\"true\"/>\n <phone_number nil=\"true\"/>\n <company nil=\"true\"/>\n <full_name>Longbob Longsen</full_name>\n <eligible_for_card_updater type=\"boolean\">true</eligible_for_card_updater>\n <shipping_address1 nil=\"true\"/>\n <shipping_address2 nil=\"true\"/>\n <shipping_city nil=\"true\"/>\n <shipping_state nil=\"true\"/>\n <shipping_zip nil=\"true\"/>\n <shipping_country nil=\"true\"/>\n <shipping_phone_number nil=\"true\"/>\n <payment_method_type>credit_card</payment_method_type>\n <errors>\n </errors>\n <verification_value>[FILTERED]</verification_value>\n <number>[FILTERED]</number>\n <fingerprint>125370bb396dff6fed4f581f85a91a9e5317</fingerprint>\n </payment_method>\n</transaction>\n" read 1875 bytes Conn close - EOS + REQUEST end def successful_verify_response diff --git a/test/unit/gateways/stripe_payment_intents_test.rb b/test/unit/gateways/stripe_payment_intents_test.rb new file mode 100644 index 00000000000..c20547a0160 --- /dev/null +++ b/test/unit/gateways/stripe_payment_intents_test.rb @@ -0,0 +1,2431 @@ +require 'test_helper' + +class StripePaymentIntentsTest < Test::Unit::TestCase + include CommStub + + def setup + @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', + verification_value: '737', + month: 10, + year: 2022 + ) + + @amount = 2020 + @update_amount = 2050 + + @options = { + currency: 'GBP', + confirmation_method: 'manual' + } + + @google_pay = network_tokenization_credit_card( + '4242424242424242', + payment_cryptogram: 'dGVzdGNyeXB0b2dyYW1YWFhYWFhYWFhYWFg9PQ==', + source: :google_pay, + brand: 'visa', + eci: '05', + month: '09', + year: '2030' + ) + + @apple_pay = network_tokenization_credit_card( + '4242424242424242', + payment_cryptogram: 'dGVzdGNyeXB0b2dyYW1YWFhYWFhYWFhYWFg9PQ==', + source: :apple_pay, + brand: 'visa', + eci: '05', + month: '09', + year: '2030', + 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' + ) + + @network_transaction_id = '1098510912210968' + end + + def test_successful_create_and_confirm_intent + @gateway.expects(:ssl_request).times(3).returns(successful_create_3ds2_payment_method, successful_create_3ds2_intent_response, successful_confirm_3ds2_intent_response) + + assert create = @gateway.create_intent(@amount, @threeds_2_card, @options.merge(return_url: 'https://www.example.com', capture_method: 'manual')) + assert_instance_of MultiResponse, create + assert_success create + + assert_equal 'pi_1F1wpFAWOtgoysog8nTulYGk', create.authorization + assert_equal 'requires_confirmation', create.params['status'] + assert create.test? + + assert confirm = @gateway.confirm_intent(create.params['id'], nil, @options.merge(return_url: 'https://example.com/return-to-me', payment_method_types: 'card')) + assert_equal 'redirect_to_url', confirm.params.dig('next_action', 'type') + assert_equal 'card', confirm.params.dig('payment_method_types')[0] + end + + def test_successful_create_and_capture_intent + options = @options.merge(capture_method: 'manual', confirm: true) + @gateway.expects(:ssl_request).twice.returns(successful_create_intent_response, successful_capture_response) + assert create = @gateway.create_intent(@amount, @visa_token, 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_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')) + + assert update = @gateway.update_intent(@update_amount, create.params['id'], nil, @options.merge(capture_method: 'manual')) + assert_equal @update_amount, update.params['amount'] + assert_equal 'requires_confirmation', update.params['status'] + end + + def test_contains_statement_descriptor_suffix + options = @options.merge(capture_method: 'manual', statement_descriptor_suffix: 'suffix') + + stub_comms(@gateway, :ssl_request) do + @gateway.create_intent(@amount, @visa_token, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/statement_descriptor_suffix=suffix/, data) + end.respond_with(successful_create_intent_response) + end + + def test_successful_create_and_void_intent + @gateway.expects(:ssl_request).twice.returns(successful_create_intent_response, successful_void_response) + assert create = @gateway.create_intent(@amount, @visa_token, @options.merge(capture_method: 'manual', confirm: true)) + + assert cancel = @gateway.void(create.params['id']) + assert_equal @amount, cancel.params.dig('charges', 'data')[0].dig('amount_refunded') + assert_equal 'canceled', cancel.params['status'] + end + + def test_create_intent_with_optional_idempotency_key_header + idempotency_key = 'test123' + options = @options.merge(idempotency_key:) + + create_intent = stub_comms(@gateway, :ssl_request) do + @gateway.create_intent(@amount, @visa_token, options) + end.check_request do |_method, _endpoint, _data, headers| + assert_equal idempotency_key, headers['Idempotency-Key'] + end.respond_with(successful_create_intent_response) + + stub_comms(@gateway, :ssl_request) do + @gateway.show_intent(create_intent.authorization, options) + end.check_request do |_method, _endpoint, _data, headers| + assert_not_equal idempotency_key, headers['Idempotency-Key'] + end.respond_with(successful_create_intent_response) + end + + def test_request_three_d_secure + request_three_d_secure = 'any' + options = @options.merge(request_three_d_secure:) + + stub_comms(@gateway, :ssl_request) do + @gateway.create_intent(@amount, @visa_token, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/\[request_three_d_secure\]=any/, data) + end.respond_with(successful_request_three_d_secure_response) + + request_three_d_secure = 'automatic' + options = @options.merge(request_three_d_secure:) + + stub_comms(@gateway, :ssl_request) do + @gateway.create_intent(@amount, @visa_token, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/\[request_three_d_secure\]=automatic/, data) + end.respond_with(successful_request_three_d_secure_response) + + request_three_d_secure = 'challenge' + options = @options.merge(request_three_d_secure:) + + stub_comms(@gateway, :ssl_request) do + @gateway.create_intent(@amount, @visa_token, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/\[request_three_d_secure\]=challenge/, data) + end.respond_with(successful_request_three_d_secure_response) + + request_three_d_secure = true + options = @options.merge(request_three_d_secure:) + + stub_comms(@gateway, :ssl_request) do + @gateway.create_intent(@amount, @visa_token, options) + end.check_request do |_method, _endpoint, data, _headers| + refute_match(/\[request_three_d_secure\]/, data) + end.respond_with(successful_request_three_d_secure_response) + end + + def test_external_three_d_secure_auth_data + options = @options.merge( + three_d_secure: { + eci: '05', + cavv: '4BQwsg4yuKt0S1LI1nDZTcO9vUM=', + xid: 'd+NEBKSpEMauwleRhdrDY06qj4A=' + } + ) + + 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\]\[three_d_secure\]/, data) + assert_match(/three_d_secure\]\[version\]=1.0.2/, data) + assert_match(/three_d_secure\]\[electronic_commerce_indicator\]=05/, data) + assert_match(/three_d_secure\]\[cryptogram\]=4BQwsg4yuKt0S1LI1nDZTcO9vUM%3D/, data) + assert_match(/three_d_secure\]\[transaction_id\]=d%2BNEBKSpEMauwleRhdrDY06qj4A%3D/, data) + end.respond_with(successful_request_three_d_secure_response) + + options = @options.merge( + three_d_secure: { + version: '2.1.0', + eci: '02', + cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA=', + ds_transaction_id: 'f879ea1c-aa2c-4441-806d-e30406466d79' + } + ) + + 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\]\[three_d_secure\]/, data) + assert_match(/three_d_secure\]\[version\]=2.1.0/, data) + assert_match(/three_d_secure\]\[electronic_commerce_indicator\]=02/, data) + assert_match(/three_d_secure\]\[cryptogram\]=jJ81HADVRtXfCBATEp01CJUAAAA%3D/, data) + assert_match(/three_d_secure\]\[transaction_id\]=f879ea1c-aa2c-4441-806d-e30406466d79/, data) + end.respond_with(successful_request_three_d_secure_response) + end + + def test_failed_capture_after_creation + @gateway.expects(:ssl_request).returns(failed_capture_response) + + 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 'pi_1F2MB5AWOtgoysogCMt8BaxR', create.authorization + end + + def test_failed_void_after_capture + @gateway.expects(:ssl_request).twice.returns(successful_capture_response, failed_cancel_response) + assert create = @gateway.create_intent(@amount, @visa_token, @options.merge(confirm: true)) + assert_equal 'succeeded', create.params['status'] + intent_id = create.params['id'] + + assert cancel = @gateway.void(intent_id, cancellation_reason: 'requested_by_customer') + assert_equal 'You cannot cancel this PaymentIntent because ' \ + 'it has a status of succeeded. Only a PaymentIntent with ' \ + 'one of the following statuses may be canceled: ' \ + '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 + on_behalf_of = 'account_27704' + transfer_group = 'TG1000' + application_fee_amount = 100 + + options = @options.merge( + transfer_destination: destination, + transfer_amount: amount, + on_behalf_of:, + transfer_group:, + application_fee: application_fee_amount + ) + + stub_comms(@gateway, :ssl_request) do + @gateway.create_intent(@amount, @visa_token, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/transfer_data\[destination\]=#{destination}/, data) + assert_match(/transfer_data\[amount\]=#{amount}/, data) + assert_match(/on_behalf_of=#{on_behalf_of}/, data) + assert_match(/transfer_group=#{transfer_group}/, data) + assert_match(/application_fee_amount=#{application_fee_amount}/, data) + end.respond_with(successful_create_intent_response) + end + + def test_on_behalf_of + on_behalf_of = 'account_27704' + + options = @options.merge(on_behalf_of:) + + stub_comms(@gateway, :ssl_request) do + @gateway.create_intent(@amount, @visa_token, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_no_match(/transfer_data\[destination\]/, data) + assert_no_match(/transfer_data\[amount\]/, data) + assert_match(/on_behalf_of=#{on_behalf_of}/, data) + assert_no_match(/transfer_group/, data) + assert_no_match(/application_fee_amount/, data) + end.respond_with(successful_create_intent_response) + end + + def test_failed_payment_methods_post + @gateway.expects(:ssl_request).returns(failed_payment_method_response) + + assert create = @gateway.create_intent(@amount, 'pm_failed', @options) + assert_equal 'validation_error', create.params.dig('error', 'code') + assert_equal '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.', create.params.dig('error', 'message') + 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_authorize_with_idempotent_replayed + @gateway.instance_variable_set(:@response_headers, { 'idempotent-replayed' => 'true' }) + @gateway.expects(:ssl_request).returns(failed_payment_method_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert response.params['response_headers']['idempotent_replayed'], 'true' + end + + def test_failed_error_on_requires_action + @gateway.expects(:ssl_request).returns(failed_with_set_error_on_requires_action_response) + + assert create = @gateway.create_intent(@amount, 'pm_failed', @options) + assert_equal 'This payment required an authentication action to complete, but `error_on_requires_action` was set. When you\'re ready, you can upgrade your integration to handle actions at https://stripe.com/docs/payments/payment-intents/upgrade-to-handle-actions.', create.params.dig('error', 'message') + assert_equal 'card_error', create.params.dig('error', 'type') + end + + def test_failed_refund_due_to_service_unavailability + @gateway.expects(:ssl_request).returns(failed_service_response) + + assert refund = @gateway.refund(@amount, 'pi_123') + assert_failure refund + assert_match(/Error while communicating with one of our backends/, refund.params.dig('error', 'message')) + end + + def test_failed_refund_due_to_pending_3ds_auth + @gateway.expects(:ssl_request).returns(successful_confirm_3ds2_intent_response) + + assert refund = @gateway.refund(@amount, 'pi_123') + assert_failure refund + assert_equal 'requires_action', refund.params['status'] + assert_match(/payment_intent has a status of requires_action/, refund.message) + end + + def test_successful_verify + @gateway.expects(:ssl_request).returns(successful_verify_response) + assert verify = @gateway.verify(@visa_token) + assert_success verify + assert_equal 'succeeded', verify.params['status'] + end + + def test_successful_verify_returns_card_three_3d_supported + @gateway.instance_variable_set(:@card_3d_supported, true) + @gateway.expects(:ssl_request).returns(successful_verify_response) + + assert verify = @gateway.verify(@visa_token) + assert_success verify + assert_equal 'succeeded', verify.params['status'] + assert_equal true, verify.params['three_d_secure_usage_supported'] + end + + def test_successful_verify_google_pay + stub_comms(@gateway, :ssl_request) do + @gateway.verify(@google_pay, @options.merge(new_ap_gp_route: true)) + end.check_request do |_method, _endpoint, data, _headers| + assert_match('payment_method_data[card][network_token][tokenization_method]=google_pay_dpan', data) + assert_match("payment_method_data[card][network_token][number]=#{@google_pay.number}", data) + assert_match('payment_method_options[card][network_token][cryptogram]', data) + assert_match("payment_method_options[card][network_token][electronic_commerce_indicator]=#{@google_pay.eci}", data) + end.respond_with(successful_verify_response) + end + + def test_successful_verify_non_tokenized_google_pay + stub_comms(@gateway, :ssl_request) do + @gateway.verify(@credit_card, @options.merge!(wallet_type: :non_tokenized_google_pay)) + end.check_request do |_method, _endpoint, data, _headers| + assert_match('metadata[input_method]=GooglePay', data) + end.respond_with(successful_verify_response) + end + + def test_successful_purchase_with_level3_data + @options[:merchant_reference] = 123 + @options[:customer_reference] = 456 + @options[:shipping_address_zip] = 98765 + @options[:shipping_from_zip] = 54321 + @options[:shipping_amount] = 40 + @options[:line_items] = [ + { + 'product_code' => 1234, + 'product_description' => 'An item', + 'unit_cost' => 60, + 'quantity' => 7, + 'tax_amount' => 0 + }, + { + 'product_code' => 999, + 'tax_amount' => 888 + } + ] + + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @visa_token, @options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match('level3[merchant_reference]=123', data) + assert_match('level3[customer_reference]=456', data) + assert_match('level3[shipping_address_zip]=98765', data) + assert_match('level3[shipping_amount]=40', data) + assert_match('level3[shipping_from_zip]=54321', data) + assert_match('level3[line_items][0][product_description]=An+item', data) + assert_match('level3[line_items][1][product_code]=999', data) + end.respond_with(successful_create_intent_response) + end + + def test_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| + 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| + 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| + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, card_to_use, { + currency: 'USD', + execute_threed: true, + confirm: true, + off_session: true, + stored_credential: { + 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_match(%r{payment_method_options\[card\]\[mit_exemption\]\[network_transaction_id\]=#{@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_without_optional_ds_transaction_id + [@three_ds_off_session_credit_card, @three_ds_authentication_required_setup_for_off_session].each do |card_to_use| + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, card_to_use, { + currency: 'USD', + execute_threed: true, + confirm: true, + off_session: true, + stored_credential: { + network_transaction_id: @network_transaction_id # TEST env seems happy with any value :/ + } + }) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(%r{payment_method_options\[card\]\[mit_exemption\]\[network_transaction_id\]=#{@network_transaction_id}}, data) + assert_no_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_without_stored_credentials_introduces_no_exemption_fields + [@three_ds_off_session_credit_card, @three_ds_authentication_required_setup_for_off_session].each do |card_to_use| + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, card_to_use, { + currency: 'USD' + }) + end.check_request do |_method, _endpoint, data, _headers| + assert_no_match(%r{payment_method_options\[card\]\[mit_exemption\]\[network_transaction_id\]=}, data) + assert_no_match(%r{payment_method_options\[card\]\[mit_exemption\]\[ds_transaction_id\]=null}, data) + end.respond_with(successful_create_intent_response) + end + end + + def test_sends_network_transaction_id_separate_from_stored_creds + options = @options.merge(network_transaction_id: @network_transaction_id) + + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @visa_token, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(%r{payment_method_options\[card\]\[mit_exemption\]\[network_transaction_id\]=#{@network_transaction_id}}, data) + 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', + 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("payment_method_data[card][network_token][number]=#{@google_pay.number}", data) + 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_non_tokenized + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge(wallet_type: :non_tokenized_google_pay)) + end.check_request do |_method, _endpoint, data, _headers| + assert_match('metadata[input_method]=GooglePay', 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_network_token_cc + options = { + currency: 'USD' + } + + 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][last4]=5556', data) + assert_match('[card][network_token][number]=4000056655665556', data) + end.respond_with(successful_create_intent_response_with_network_token_fields) + end + + def test_purchase_with_shipping_options + options = { + currency: 'GBP', + customer: 'abc123', + shipping_address: { + name: 'John Adam', + phone_number: '+0018313818368', + city: 'San Diego', + country: 'USA', + address1: 'block C', + address2: 'street 48', + zip: '22400', + state: 'California', + email: 'test@email.com' + } + } + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @visa_token, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match('shipping[address][city]=San+Diego', data) + assert_match('shipping[address][country]=USA', data) + assert_match('shipping[address][line1]=block+C', data) + assert_match('shipping[address][line2]=street+48', data) + assert_match('shipping[address][postal_code]=22400', data) + 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', + shipping_address: { + name: 'John Adam', + address1: 'block C' + }, + 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| + assert_match('shipping[address][line1]=block+C', data) + assert_match('shipping[name]=John+Adam', data) + assert_match('shipping[carrier]=FEDEX', data) + assert_match('shipping[tracking_number]=TXNABC123', data) + end.respond_with(successful_create_intent_response) + end + + def test_authorize_with_apple_pay + options = { + 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('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 + sc_network_transaction_id = '1078784111114777' + options = @options.merge( + network_transaction_id: @network_transaction_id, + stored_credential: { + network_transaction_id: sc_network_transaction_id, + ds_transaction_id: 'null' + } + ) + + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @visa_token, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(%r{payment_method_options\[card\]\[mit_exemption\]\[network_transaction_id\]=#{@network_transaction_id}}, data) + end.respond_with(successful_create_intent_response) + end + + def test_successful_off_session_intent_creation_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| + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, card_to_use, { + currency: 'USD', + execute_threed: true, + confirm: true, + off_session: true, + claim_without_transaction_id: true + }) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(%r{payment_method_options\[card\]\[mit_exemption\]\[claim_without_transaction_id\]=true}, data) + end.respond_with(successful_create_intent_response) + end + end + + def test_successful_off_session_intent_creation_when_claim_without_transaction_id_is_false + [@three_ds_off_session_credit_card, @three_ds_authentication_required_setup_for_off_session].each do |card_to_use| + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, card_to_use, { + currency: 'USD', + execute_threed: true, + confirm: true, + off_session: true, + claim_without_transaction_id: false + }) + end.check_request do |_method, _endpoint, data, _headers| + assert_no_match(%r{payment_method_options\[card\]\[mit_exemption\]\[claim_without_transaction_id\]}, data) + end.respond_with(successful_create_intent_response) + end + end + + def test_successful_off_session_intent_creation_without_claim_without_transaction_id + [@three_ds_off_session_credit_card, @three_ds_authentication_required_setup_for_off_session].each do |card_to_use| + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, card_to_use, { + currency: 'USD', + execute_threed: true, + confirm: true, + off_session: true + }) + end.check_request do |_method, _endpoint, data, _headers| + assert_no_match(%r{payment_method_options\[card\]\[mit_exemption\]\[claim_without_transaction_id\]}, data) + end.respond_with(successful_create_intent_response) + end + end + + def test_store_does_not_pass_validation_to_attach_by_default + stub_comms(@gateway, :ssl_request) do + @gateway.store(@credit_card) + end.check_request do |_method, endpoint, data, _headers| + assert_no_match(/validate=/, data) if /attach/.match?(endpoint) + end.respond_with(successful_payment_method_response, successful_create_customer_response, successful_payment_method_attach_response) + end + + def test_store_sets_validation_on_attach_to_false_when_false_in_options + options = @options.merge( + validate: false + ) + + stub_comms(@gateway, :ssl_request) do + @gateway.store(@credit_card, options) + end.check_request do |_method, endpoint, data, _headers| + assert_match(/validate=false/, data) if /attach/.match?(endpoint) + end.respond_with(successful_payment_method_response, successful_create_customer_response, successful_payment_method_attach_response) + end + + def test_store_sets_validationon_attach_to_true_when_true_in_options + options = @options.merge( + validate: true + ) + + stub_comms(@gateway, :ssl_request) do + @gateway.store(@credit_card, options) + end.check_request do |_method, endpoint, data, _headers| + assert_match(/validate=true/, data) if /attach/.match?(endpoint) + end.respond_with(successful_payment_method_response, successful_create_customer_response, successful_payment_method_attach_response) + end + + def test_succesful_purchase_with_radar_session + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, { + radar_session_id: 'test_radar_session_id' + }) + end.check_request do |_method, endpoint, data, _headers| + assert_match(/radar_options\[session\]=test_radar_session_id/, data) if /payment_intents/.match?(endpoint) + end.respond_with(successful_create_intent_response) + end + + def test_succesful_authorize_with_radar_session + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, { + radar_session_id: 'test_radar_session_id' + }) + end.check_request do |_method, endpoint, data, _headers| + assert_match(/radar_options\[session\]=test_radar_session_id/, data) if /payment_intents/.match?(endpoint) + end.respond_with(successful_create_intent_response) + end + + def test_successful_authorize_with_skip_radar_rules + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, { + skip_radar_rules: true + }) + end.check_request do |_method, endpoint, data, _headers| + assert_match(/radar_options\[skip_rules\]\[0\]=all/, data) if /payment_intents/.match?(endpoint) + end.respond_with(successful_create_intent_response) + end + + def test_successful_authorization_with_event_type_metadata + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, { + email: 'wow@example.com', + event_type: 'concert' + }) + end.check_request do |_method, endpoint, data, _headers| + if /payment_intents/.match?(endpoint) + assert_match(/metadata\[email\]=wow%40example.com/, data) + assert_match(/metadata\[event_type\]=concert/, data) + end + end.respond_with(successful_create_intent_response) + end + + def test_successful_setup_purchase + stub_comms(@gateway, :ssl_request) do + @gateway.setup_purchase(@amount, { payment_method_types: %w[afterpay_clearpay card] }) + end.check_request do |_method, endpoint, data, _headers| + assert_match(/payment_method_types\[0\]=afterpay_clearpay&payment_method_types\[1\]=card/, data) if /payment_intents/.match?(endpoint) + end.respond_with(successful_setup_purchase) + end + + def test_supported_countries + countries = %w(AE AT AU BE BG BR CA CH CY CZ DE DK EE ES FI FR GB GR HK HU IE IN IT JP LT LU LV MT MX MY NL NO NZ PL PT RO SE SG SI SK US) + assert_equal countries.sort, StripePaymentIntentsGateway.supported_countries.sort + end + + 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', + network_transaction_id: '1098510912210968' + } + }) + end.check_request do |_method, _endpoint, data, _headers| + assert_match('payment_method_options[card][stored_credential_transaction_type]=stored_on_session', data) + assert_not_match('payment_method_options[card][mit_exemption][network_transaction_id]=1098510912210968', 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', + network_transaction_id: '1098510912210968' + } + }) + end.check_request do |_method, _endpoint, data, _headers| + assert_match('payment_method_options[card][stored_credential_transaction_type]=stored_off_session_recurring', data) + assert_match('payment_method_options[card][mit_exemption][network_transaction_id]=1098510912210968', 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 + + def test_create_setup_intent_with_moto_exemption + idempotency_key = 'test123' + options = @options.merge(moto: true, confirm: true, idempotency_key:) + + create_intent = stub_comms(@gateway, :ssl_request) do + @gateway.create_setup_intent(@visa_token, options) + end.check_request do |_method, _endpoint, data, headers| + assert_equal(idempotency_key, headers['Idempotency-Key']) + assert_match(/\[moto\]=true/, data) + end.respond_with(successful_verify_response) + + stub_comms(@gateway, :ssl_request) do + @gateway.retrieve_setup_intent(create_intent.authorization, options) + end.check_request do |_method, _endpoint, _data, headers| + assert_not_equal(idempotency_key, headers['Idempotency-Key']) + end.respond_with(successful_verify_response) + end + + def test_add_network_token_cryptogram_and_eci_for_apple_pay_cit + options = { + 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. + } + } + + stub_comms(@gateway, :ssl_request) do + @gateway.create_intent(@amount, @apple_pay, options) + end.check_request do |_method, endpoint, data, _headers| + if /payment_intents/.match?(endpoint) + assert_match(/payment_method_options\[card\]\[stored_credential_transaction_type\]=setup_on_session/, data) + assert_match(/card\[eci\]=05/, data) + assert_match(/card\[cryptogram\]=dGVzdGNyeXB0b2dyYW1YWFhYWFhYWFhYWFg9PQ%3D%3D/, data) + end + end.respond_with(successful_create_intent_response_with_apple_pay_and_billing_address) + end + + def test_skip_network_token_cryptogram_and_eci_for_apple_pay_mit + options = { + 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: @network_transaction_id, + off_session: 'true' + } + } + + stub_comms(@gateway, :ssl_request) do + @gateway.create_intent(@amount, @apple_pay, options) + end.check_request do |_method, endpoint, data, _headers| + if /payment_intents/.match?(endpoint) + assert_match(/payment_method_options\[card\]\[stored_credential_transaction_type\]=stored_off_session_recurring/, data) + assert_not_match(/card\[eci\]/, data) + assert_not_match(/card\[cryptogram\]/, data) + end + end.respond_with(successful_verify_response) + end + + private + + def successful_setup_purchase + <<-RESPONSE + { + "id": "pi_3Jr0wXAWOtgoysog2Sp0iKjo", + "object": "payment_intent", + "amount": 2000, + "amount_capturable": 0, + "amount_received": 0, + "application": null, + "application_fee_amount": null, + "canceled_at": null, + "cancellation_reason": null, + "capture_method": "automatic", + "charges": { + "object": "list", + "data": [ + + ], + "has_more": false, + "total_count": 0, + "url": "/v1/charges?payment_intent=pi_3Jr0wXAWOtgoysog2Sp0iKjo" + }, + "client_secret": "pi_3Jr0wXAWOtgoysog2Sp0iKjo_secret_1l5cE3MskZ8AMOZaNdpmgZDCn", + "confirmation_method": "automatic", + "created": 1635774777, + "currency": "usd", + "customer": null, + "description": null, + "invoice": null, + "last_payment_error": null, + "livemode": false, + "metadata": { + }, + "next_action": null, + "on_behalf_of": null, + "payment_method": null, + "payment_method_options": { + "afterpay_clearpay": { + "reference": null + }, + "card": { + "installments": null, + "network": null, + "request_three_d_secure": "automatic" + } + }, + "payment_method_types": [ + "afterpay_clearpay", + "card" + ], + "receipt_email": null, + "review": null, + "setup_future_usage": null, + "shipping": null, + "source": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "requires_payment_method", + "transfer_data": null, + "transfer_group": null + } + RESPONSE + end + + def successful_create_intent_response + <<-RESPONSE + {"id":"pi_1F1xauAWOtgoysogIfHO8jGi","object":"payment_intent","amount":2020,"amount_capturable":2020,"amount_received":0,"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":null,"billing_details":{"address":{"city":null,"country":null,"line1":null,"line2":null,"postal_code":null,"state":null},"email":null,"name":null,"phone":null},"captured":false,"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":"requires_capture","transfer_data":null,"transfer_group":null} + 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} + RESPONSE + end + + def successful_void_response + <<-RESPONSE + {"id":"pi_1F1yBVAWOtgoysogearamRvl","object":"payment_intent","amount":2020,"amount_capturable":0,"amount_received":0,"application":null,"application_fee_amount":null,"canceled_at":1564504103,"cancellation_reason":"requested_by_customer","capture_method":"manual","charges":{"object":"list","data":[{"id":"ch_1F1yBWAWOtgoysog1MQfDpJH","object":"charge","amount":2020,"amount_refunded":2020,"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":null,"phone":null},"captured":false,"created":1564504102,"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":46,"seller_message":"Payment complete.","type":"authorized"},"paid":true,"payment_intent":"pi_1F1yBVAWOtgoysogearamRvl","payment_method":"pm_1F1yBVAWOtgoysogddy4E3hL","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_1F1yBWAWOtgoysog1MQfDpJH/rcpt_FX2Go3YHBqAYQPJuKGMeab3nyCU0Kks","refunded":true,"refunds":{"object":"list","data":[{"id":"re_1F1yBXAWOtgoysog0PU371Yz","object":"refund","amount":2020,"balance_transaction":null,"charge":"ch_1F1yBWAWOtgoysog1MQfDpJH","created":1564504103,"currency":"gbp","metadata":{},"reason":"requested_by_customer","receipt_number":null,"source_transfer_reversal":null,"status":"succeeded","transfer_reversal":null}],"has_more":false,"total_count":1,"url":"/v1/charges/ch_1F1yBWAWOtgoysog1MQfDpJH/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_1F1yBVAWOtgoysogearamRvl"},"client_secret":"pi_1F1yBVAWOtgoysogearamRvl_secret_oCnlR2t0GPclqACgHt2rst4gM","confirmation_method":"manual","created":1564504101,"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_1F1yBVAWOtgoysogddy4E3hL","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":"canceled","transfer_data":null,"transfer_group":null} + RESPONSE + end + + def successful_update_intent_response + <<-RESPONSE + {"id":"pi_1F1yBbAWOtgoysog52J88BuO","object":"payment_intent","amount":2050,"amount_capturable":0,"amount_received":0,"application":null,"application_fee_amount":null,"canceled_at":null,"cancellation_reason":null,"capture_method":"manual","charges":{"object":"list","data":[],"has_more":false,"total_count":0,"url":"/v1/charges?payment_intent=pi_1F1yBbAWOtgoysog52J88BuO"},"client_secret":"pi_1F1yBbAWOtgoysog52J88BuO_secret_olw5rmbtm7cd72S9JfbKjTJJv","confirmation_method":"manual","created":1564504107,"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_1F1yBbAWOtgoysoguJQsDdYj","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":"requires_confirmation","transfer_data":null,"transfer_group":null} + RESPONSE + end + + def successful_create_3ds2_payment_method + <<-RESPONSE + { + "id": "pm_1F1xK0AWOtgoysogfPuRKN1d", + "object": "payment_method", + "billing_details": { + "address": {"city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null}, + "email": null, + "name": null, + "phone": null}, + "card": { + "brand": "visa", + "checks": {"address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": "unchecked"}, + "country": null, + "exp_month": 10, + "exp_year": 2020, + "fingerprint": "l3J0NJaGgv0jAGLV", + "funding": "credit", + "generated_from": null, + "last4": "3220", + "three_d_secure_usage": {"supported": true}, + "wallet": null}, + "created": 1564500784, + "customer": null, + "livemode": false, + "metadata": {}, + "type": "card" + } + RESPONSE + end + + def successful_create_3ds2_intent_response + <<-RESPONSE + { + "id": "pi_1F1wpFAWOtgoysog8nTulYGk", + "object": "payment_intent", + "amount": 2020, + "amount_capturable": 0, + "amount_received": 0, + "application": null, + "application_fee_amount": null, + "canceled_at": null, + "cancellation_reason": null, + "capture_method": "manual", + "charges": { + "object": "list", + "data": [], + "has_more": false, + "total_count": 0, + "url": "/v1/charges?payment_intent=pi_1F1wpFAWOtgoysog8nTulYGk" + }, + "client_secret": "pi_1F1wpFAWOtgoysog8nTulYGk_secret_75qf7rjBDsTTz279LfS1feXUj", + "confirmation_method": "manual", + "created": 1564498877, + "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_1F1wpFAWOtgoysogJ8zQ8K07", + "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": "requires_confirmation", + "transfer_data": null, + "transfer_group": null + } + RESPONSE + end + + def successful_confirm_3ds2_intent_response + <<-RESPONSE + { + "id": "pi_1F1wpFAWOtgoysog8nTulYGk", + "object": "payment_intent", + "amount": 2020, + "amount_capturable": 0, + "amount_received": 0, + "application": null, + "application_fee_amount": null, + "canceled_at": null, + "cancellation_reason": null, + "capture_method": "manual", + "charges": { + "object": "list", + "data": [], + "has_more": false, + "total_count": 0, + "url": "/v1/charges?payment_intent=pi_1F1wpFAWOtgoysog8nTulYGk"}, + "client_secret": "pi_1F1wpFAWOtgoysog8nTulYGk_secret_75qf7rjBDsTTz279LfS1feXUj", + "confirmation_method": "manual", + "created": 1564498877, + "currency": "gbp", + "customer": "cus_7s22nNueP2Hjj6", + "description": null, + "invoice": null, + "last_payment_error": null, + "livemode": false, + "metadata": {}, + "next_action": { + "redirect_to_url": { + "return_url": "https://example.com/return-to-me", + "url": "https://hooks.stripe.com/3d_secure_2_eap/begin_test/src_1F1wpGAWOtgoysog4f00umCp/src_client_secret_FX0qk3uQ04woFWgdJbN3pnHD"}, + "type": "redirect_to_url"}, + "on_behalf_of": null, + "payment_method": "pm_1F1wpFAWOtgoysogJ8zQ8K07", + "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": "requires_action", + "transfer_data": null, + "transfer_group": null + } + RESPONSE + end + + def successful_request_three_d_secure_response + <<-RESPONSE + {"id"=>"pi_1HZJGPAWOtgoysogrKURP11Q", + "object"=>"payment_intent", + "amount"=>2000, + "amount_capturable"=>0, + "amount_received"=>2000, + "application"=>nil, + "application_fee_amount"=>nil, + "canceled_at"=>nil, + "cancellation_reason"=>nil, + "capture_method"=>"automatic", + "charges"=> + {"object"=>"list", + "data"=> + [{"id"=>"ch_1HZJGQAWOtgoysogEpbZTGIl", + "object"=>"charge", + "amount"=>2000, + "amount_captured"=>2000, + "amount_refunded"=>0, + "application"=>nil, + "application_fee"=>nil, + "application_fee_amount"=>nil, + "balance_transaction"=>"txn_1HZJGQAWOtgoysogEKwV2r5N", + "billing_details"=> + {"address"=>{"city"=>nil, "country"=>nil, "line1"=>nil, "line2"=>nil, "postal_code"=>nil, "state"=>nil}, "email"=>nil, "name"=>nil, "phone"=>nil}, + "calculated_statement_descriptor"=>"SPREEDLY", + "captured"=>true, + "created"=>1602002626, + "currency"=>"gbp", + "customer"=>nil, + "description"=>nil, + "destination"=>nil, + "dispute"=>nil, + "disputed"=>false, + "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"=>16, + "seller_message"=>"Payment complete.", + "type"=>"authorized"}, + "paid"=>true, + "payment_intent"=>"pi_1HZJGPAWOtgoysogrKURP11Q", + "payment_method"=>"pm_1HZJGOAWOtgoysogvnMsnnG1", + "payment_method_details"=> + {"card"=> + {"brand"=>"visa", + "checks"=>{"address_line1_check"=>nil, "address_postal_code_check"=>nil, "cvc_check"=>"pass"}, + "country"=>"US", + "ds_transaction_id"=>nil, + "exp_month"=>10, + "exp_year"=>2020, + "fingerprint"=>"hfaVNMiXc0dYSiC5", + "funding"=>"credit", + "installments"=>nil, + "last4"=>"4242", + "moto"=>nil, + "network"=>"visa", + "network_transaction_id"=>"1041029786787710", + "three_d_secure"=> + {"authenticated"=>false, + "authentication_flow"=>nil, + "electronic_commerce_indicator"=>"06", + "result"=>"attempt_acknowledged", + "result_reason"=>nil, + "succeeded"=>true, + "transaction_id"=>"d1VlRVF6a1BVNXN1cjMzZVl0RU0=", + "version"=>"1.0.2"}, + "wallet"=>nil}, + "type"=>"card"}, + "receipt_email"=>nil, + "receipt_number"=>nil, + "receipt_url"=>"https://pay.stripe.com/receipts/acct_160DX6AWOtgoysog/ch_1HZJGQAWOtgoysogEpbZTGIl/rcpt_I9cVpN9xAeS39FhMqTS33Fj8gHsjjuX", + "refunded"=>false, + "refunds"=>{"object"=>"list", "data"=>[], "has_more"=>false, "total_count"=>0, "url"=>"/v1/charges/ch_1HZJGQAWOtgoysogEpbZTGIl/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_1HZJGPAWOtgoysogrKURP11Q"}, + "client_secret"=>"pi_1HZJGPAWOtgoysogrKURP11Q_secret_dJNY00dYXC22Fc9nPscAmhFMt", + "confirmation_method"=>"automatic", + "created"=>1602002625, + "currency"=>"gbp", + "customer"=>nil, + "description"=>nil, + "invoice"=>nil, + "last_payment_error"=>nil, + "livemode"=>false, + "metadata"=>{}, + "next_action"=>nil, + "on_behalf_of"=>nil, + "payment_method"=>"pm_1HZJGOAWOtgoysogvnMsnnG1", + "payment_method_options"=>{"card"=>{"installments"=>nil, "network"=>nil, "request_three_d_secure"=>"any"}}, + "payment_method_types"=>["card"], + "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 failed_capture_response + <<-RESPONSE + {"error":{"charge":"ch_1F2MB6AWOtgoysogAIvNV32Z","code":"card_declined","decline_code":"generic_decline","doc_url":"https://stripe.com/docs/error-codes/card-declined","message":"Your card was declined.","payment_intent":{"id":"pi_1F2MB5AWOtgoysogCMt8BaxR","object":"payment_intent","amount":2020,"amount_capturable":0,"amount_received":0,"application":null,"application_fee_amount":null,"canceled_at":null,"cancellation_reason":null,"capture_method":"automatic","charges":{"object":"list","data":[{"id":"ch_1F2MB6AWOtgoysogAIvNV32Z","object":"charge","amount":2020,"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":null,"phone":null},"captured":false,"created":1564596332,"currency":"gbp","customer":"cus_7s22nNueP2Hjj6","description":null,"destination":null,"dispute":null,"failure_code":"card_declined","failure_message":"Your card was declined.","fraud_details":{},"invoice":null,"livemode":false,"metadata":{},"on_behalf_of":null,"order":null,"outcome":{"network_status":"declined_by_network","reason":"generic_decline","risk_level":"normal","risk_score":41,"seller_message":"The bank did not return any further details with this decline.","type":"issuer_declined"},"paid":false,"payment_intent":"pi_1F2MB5AWOtgoysogCMt8BaxR","payment_method":"pm_1F2MB5AWOtgoysogq3yXZ98h","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":"1VUoWMvHnqtngyrD","funding":"credit","last4":"0002","three_d_secure":null,"wallet":null},"type":"card"},"receipt_email":null,"receipt_number":null,"receipt_url":"https://pay.stripe.com/receipts/acct_160DX6AWOtgoysog/ch_1F2MB6AWOtgoysogAIvNV32Z/rcpt_FXR3PjBGluHmHsnLmp0S2KQiHl3yg6W","refunded":false,"refunds":{"object":"list","data":[],"has_more":false,"total_count":0,"url":"/v1/charges/ch_1F2MB6AWOtgoysogAIvNV32Z/refunds"},"review":null,"shipping":null,"source":null,"source_transfer":null,"statement_descriptor":null,"status":"failed","transfer_data":null,"transfer_group":null}],"has_more":false,"total_count":1,"url":"/v1/charges?payment_intent=pi_1F2MB5AWOtgoysogCMt8BaxR"},"client_secret":"pi_1F2MB5AWOtgoysogCMt8BaxR_secret_fOHryjtjBE4gACiHTcREraXSQ","confirmation_method":"manual","created":1564596331,"currency":"gbp","customer":"cus_7s22nNueP2Hjj6","description":null,"invoice":null,"last_payment_error":{"charge":"ch_1F2MB6AWOtgoysogAIvNV32Z","code":"card_declined","decline_code":"generic_decline","doc_url":"https://stripe.com/docs/error-codes/card-declined","message":"Your card was declined.","payment_method":{"id":"pm_1F2MB5AWOtgoysogq3yXZ98h","object":"payment_method","billing_details":{"address":{"city":null,"country":null,"line1":null,"line2":null,"postal_code":null,"state":null},"email":null,"name":null,"phone":null},"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":"1VUoWMvHnqtngyrD","funding":"credit","generated_from":null,"last4":"0002","three_d_secure_usage":{"supported":true},"wallet":null},"created":1564596331,"customer":null,"livemode":false,"metadata":{},"type":"card"},"type":"card_error"},"livemode":false,"metadata":{},"next_action":null,"on_behalf_of":null,"payment_method":null,"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":"requires_payment_method","transfer_data":null,"transfer_group":null},"payment_method":{"id":"pm_1F2MB5AWOtgoysogq3yXZ98h","object":"payment_method","billing_details":{"address":{"city":null,"country":null,"line1":null,"line2":null,"postal_code":null,"state":null},"email":null,"name":null,"phone":null},"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":"1VUoWMvHnqtngyrD","funding":"credit","generated_from":null,"last4":"0002","three_d_secure_usage":{"supported":true},"wallet":null},"created":1564596331,"customer":null,"livemode":false,"metadata":{},"type":"card"},"type":"card_error"}} + RESPONSE + end + + def failed_cancel_response + <<-RESPONSE + {"error":{"code":"payment_intent_unexpected_state","doc_url":"https://stripe.com/docs/error-codes/payment-intent-unexpected-state","message":"You cannot cancel this PaymentIntent because it has a status of succeeded. Only a PaymentIntent with one of the following statuses may be canceled: requires_payment_method, requires_capture, requires_confirmation, requires_action.","payment_intent":{"id":"pi_1F2McmAWOtgoysoglFLDRWab","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":"automatic","charges":{"object":"list","data":[{"id":"ch_1F2McmAWOtgoysogQgUS1YtH","object":"charge","amount":2020,"amount_refunded":0,"application":null,"application_fee":null,"application_fee_amount":null,"balance_transaction":"txn_1F2McmAWOtgoysog8uxBEJ30","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":1564598048,"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":53,"seller_message":"Payment complete.","type":"authorized"},"paid":true,"payment_intent":"pi_1F2McmAWOtgoysoglFLDRWab","payment_method":"pm_1F2MclAWOtgoysogq80GBBMO","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_1F2McmAWOtgoysogQgUS1YtH/rcpt_FXRVzyFnf7aCS1r13N3uym1u8AaboOJ","refunded":false,"refunds":{"object":"list","data":[],"has_more":false,"total_count":0,"url":"/v1/charges/ch_1F2McmAWOtgoysogQgUS1YtH/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_1F2McmAWOtgoysoglFLDRWab"},"client_secret":"pi_1F2McmAWOtgoysoglFLDRWab_secret_z4faDF0Cv0JZJ6pxK3bdIodkD","confirmation_method":"manual","created":1564598048,"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_1F2MclAWOtgoysogq80GBBMO","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},"type":"invalid_request_error"}} + 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"}} + RESPONSE + end + + def failed_service_response + <<-RESPONSE + {"error": {"message": "Error while communicating with one of our backends. Sorry about that! We have been notified of the problem. If you have any questions, we can help at https://support.stripe.com/.", "type": "api_error" }} + RESPONSE + end + + def failed_with_set_error_on_requires_action_response + <<-RESPONSE + {"error": {"message": "This payment required an authentication action to complete, but `error_on_requires_action` was set. When you're ready, you can upgrade your integration to handle actions at https://stripe.com/docs/payments/payment-intents/upgrade-to-handle-actions.", "type": "card_error" }} + RESPONSE + end + + def successful_verify_response + <<-RESPONSE + { + "id": "seti_1Gsw0aAWOtgoysog0XjSBPVX", + "object": "setup_intent", + "application": null, + "cancellation_reason": null, + "client_secret": "seti_1Gsw0aAWOtgoysog0XjSBPVX_secret_HRpfHkvewAdYQJgee27ihJfm4E4zWmW", + "created": 1591903456, + "customer": "cus_GkjsDZC58SgUcY", + "description": null, + "last_setup_error": null, + "livemode": false, + "mandate": null, + "metadata": { + }, + "next_action": null, + "on_behalf_of": null, + "payment_method": "pm_1Gsw0aAWOtgoysog304wX4J9", + "payment_method_options": { + "card": { + "request_three_d_secure": "automatic" + } + }, + "payment_method_types": [ + "card" + ], + "single_use_mandate": null, + "status": "succeeded", + "usage": "off_session" + } + RESPONSE + end + + def successful_payment_method_response + <<-RESPONSE + { + "id": "pm_1IQ3OhAWOtgoysogUkVwJ5MT", + "object": "payment_method", + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": null, + "phone": null + }, + "card": { + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": "unchecked" + }, + "country": "US", + "exp_month": 10, + "exp_year": 2021, + "fingerprint": "hfaVNMiXc0dYSiC5", + "funding": "credit", + "generated_from": null, + "last4": "4242", + "networks": { + "available": [ + "visa" + ], + "preferred": null + }, + "three_d_secure_usage": { + "supported": true + }, + "wallet": null + }, + "created": 1614573020, + "customer": null, + "livemode": false, + "metadata": { + }, + "type": "card" + } + RESPONSE + end + + def successful_create_customer_response + <<-RESPONSE + { + "id": "cus_J27e2tthifSmpm", + "object": "customer", + "account_balance": 0, + "address": null, + "balance": 0, + "created": 1614573020, + "currency": null, + "default_source": null, + "delinquent": false, + "description": null, + "discount": null, + "email": null, + "invoice_prefix": "B0C3D1B5", + "invoice_settings": { + "custom_fields": null, + "default_payment_method": null, + "footer": null + }, + "livemode": false, + "metadata": { + }, + "name": null, + "next_invoice_sequence": 1, + "phone": null, + "preferred_locales": [], + "shipping": null, + "sources": { + "object": "list", + "data": [], + "has_more": false, + "total_count": 0, + "url": "/v1/customers/cus_J27e2tthifSmpm/sources" + }, + "subscriptions": { + "object": "list", + "data": [], + "has_more": false, + "total_count": 0, + "url": "/v1/customers/cus_J27e2tthifSmpm/subscriptions" + }, + "tax_exempt": "none", + "tax_ids": { + "object": "list", + "data": [], + "has_more": false, + "total_count": 0, + "url": "/v1/customers/cus_J27e2tthifSmpm/tax_ids" + }, + "tax_info": null, + "tax_info_verification": null + } + RESPONSE + end + + def successful_payment_method_attach_response + <<-RESPONSE + { + "id": "pm_1IQ3AYAWOtgoysogcvbllgNa", + "object": "payment_method", + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": null, + "phone": null + }, + "card": { + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": "unchecked" + }, + "country": "US", + "exp_month": 10, + "exp_year": 2021, + "fingerprint": "hfaVNMiXc0dYSiC5", + "funding": "credit", + "generated_from": null, + "last4": "4242", + "networks": { + "available": [ + "visa" + ], + "preferred": null + }, + "three_d_secure_usage": { + "supported": true + }, + "wallet": null + }, + "created": 1614572142, + "customer": "cus_J27PL9krZlnw82", + "livemode": false, + "metadata": { + }, + "type": "card" + } + RESPONSE + end + + def pre_scrubbed + <<-PRE_SCRUBBED + opening connection to api.stripe.com:443... + opened + starting SSL for api.stripe.com:443... + SSL established + <- "POST /v1/charges HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAuthorization: Basic c2tfdGVzdF9oQkwwTXF6ZGZ6Rnk3OXU0cFloUmVhQlo6\r\nUser-Agent: Stripe/v1 ActiveMerchantBindings/1.45.0\r\nX-Stripe-Client-User-Agent: {\"bindings_version\":\"1.45.0\",\"lang\":\"ruby\",\"lang_version\":\"2.1.3 p242 (2014-09-19)\",\"platform\":\"x86_64-linux\",\"publisher\":\"active_merchant\"}\r\nX-Stripe-Client-User-Metadata: {\"ip\":null}\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nConnection: close\r\nHost: api.stripe.com\r\nContent-Length: 270\r\n\r\n" + <- "amount=100&currency=usd&card[number]=4242424242424242&card[exp_month]=9&card[exp_year]=2015&card[tokenization_method]=android_pay&card[eci]=07&capture_method=automatic&card[name]=Longbob+Longsen&description=ActiveMerchant+Test+Purchase&payment_user_agent=Stripe%2Fv1+ActiveMerchantBindings%2F1.45.0&metadata[email]=wow%40example.com&card[cryptogram]=sensitive_data&payment_method_types[0]=card&payment_method_data[type]=card&payment_method_data[card][token]=tok_1KHrnVAWOtgoysogWbF1jrM9&metadata[connect_agent]=placeholder&metadata[transaction_token]=Coe7nlopnvhfcNRXhJMH5DTVusU&metadata[email]=john.smith%40example.com&metadata[order_id]=order_id-xxxxxx-x&confirm=true&return_url=http%3A%2F%2Fexaple.com%2Ftransaction%transaction_idxxxx%2Fredirect" + -> "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" + -> "Strict-Transport-Security: max-age=31556926; includeSubDomains; preload\r\n" + -> "\r\n" + reading 5204 bytes... + -> "{\n \"id\": \"pi_3KHrnWAWOtgoysog1Y5qMLqc\",\n \"object\": \"payment_intent\",\n \"amount\": 100,\n \"amount_capturable\": 0,\n \"amount_received\": 100,\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_3KHrnWAWOtgoysog1noj1iU9\",\n \"object\": \"charge\",\n \"amount\": 100,\n \"amount_captured\": 100,\n \"amount_refunded\": 0,\n \"application\": null,\n \"application_fee\": null,\n \"application_fee_amount\": null,\n \"balance_transaction\": \"txn_3KHrnWAWOtgoysog1vy6pmxk\",\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\": \"SPREEDLY\",\n \"captured\": true,\n \"created\": 1642174478,\n \"currency\": \"usd\",\n \"customer\": null,\n \"description\": null,\n \"destination\": null,\n \"dispute\": null,\n \"disputed\": false,\n \"failure_code\": null,\n \"failure_message\": null,\n \"fraud_details\": {\n },\n \"invoice\": null,\n \"livemode\": false,\n \"metadata\": {\n \"connect_agent\": \"placeholder\",\n \"transaction_token\": \"Coe7nlopnvhfcNRXhJMH5DTVusU\",\n \"email\": \"john.smith@example.com\",\n \"order_id\": \"AH2EjtfMGoZkWNEwLU90sq7VzcDlzWH_KugIYT4aVWEtJF9AwmqiXqsBs2l9q6F2Ruq9WKkUBbuLWNmA3P22ShFXFCZosTwkoflaDeTD2xeiMvmYv29VPINEDtLdSAoJ-DDlRKnsxa-n\"\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\": 36,\n \"seller_message\": \"Payment complete.\",\n \"type\": \"authorized\"\n },\n \"paid\": true,\n \"payment_intent\": \"pi_3KHrnWAWOtgoysog1Y5qMLqc\",\n \"payment_method\": \"pm_1KHrnWAWOtgoysogqXkTXrCb\",\n \"payment_method_details\": {\n \"card\": {\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\": 12,\n \"exp_year\": 2027,\n \"fingerprint\": \"sUdMrygQwzOKqwSm\",\n \"funding\": \"debit\",\n \"installments\": null,\n \"last4\": \"0000\",\n \"mandate\": null,\n \"moto\": null,\n \"network\": \"visa\",\n \"network_transaction_id\": \"1158510077114121\",\n \"three_d_secure\": null,\n \"wallet\": {\n \"dynamic_last4\": \"3478\",\n \"google_pay\": {\n },\n \"type\": \"google_pay\"\n }\n },\n \"type\": \"card\"\n },\n \"receipt_email\": null,\n \"receipt_number\": null,\n \"receipt_url\": \"https://pay.stripe.com/receipts/acct_160DX6AWOtgoysog/ch_3KHrnWAWOtgoysog1noj1iU9/rcpt_KxnOefAivglRgWZmxp0PLOJUQg0VhS9\",\n \"refunded\": false,\n \"refunds\": {\n \"object\": \"list\",\n \"data\": [\n\n ],\n \"has_more\": false,\n \"total_count\": 0,\n \"url\": \"/v1/charges/ch_3KHrnWAWOtgoysog1noj1iU9/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_3KHrnWAWOtgoysog1Y5qMLqc\"\n },\n \"client_secret\": \"pi_3KHrnWAWOtgoysog1Y5qMLqc_secret_5ZEt4fzM7YCi1zdMzs4iQXLjC\",\n \"confirmation_method\": \"automatic\",\n \"created\": 1642174478,\n \"currency\": \"usd\",\n \"customer\": null,\n \"description\": null,\n \"invoice\": null,\n \"last_payment_error\": null,\n \"livemode\": false,\n \"metadata\": {\n \"connect_agent\": \"placeholder\",\n \"transaction_token\": \"Coe7nlopnvhfcNRXhJMH5DTVusU\",\n \"email\": \"john.smith@example.com\",\n \"order_id\": \"AH2EjtfMGoZkWNEwLU90sq7VzcDlzWH_KugIYT4aVWEtJF9AwmqiXqsBs2l9q6F2Ruq9WKkUBbuLWNmA3P22ShFXFCZosTwkoflaDeTD2xeiMvmYv29VPINEDtLdSAoJ-DDlRKnsxa-n\"\n },\n \"next_action\": null,\n \"on_behalf_of\": null,\n \"payment_method\": \"pm_1KHrnWAWOtgoysogqXkTXrCb\",\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}\n" + read 5204 bytes + Conn close + PRE_SCRUBBED + end + + def scrubbed + <<-SCRUBBED + opening connection to api.stripe.com:443... + opened + starting SSL for api.stripe.com:443... + SSL established + <- "POST /v1/charges HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAuthorization: Basic [FILTERED]\r\nUser-Agent: Stripe/v1 ActiveMerchantBindings/1.45.0\r\nX-Stripe-Client-User-Agent: {\"bindings_version\":\"1.45.0\",\"lang\":\"ruby\",\"lang_version\":\"2.1.3 p242 (2014-09-19)\",\"platform\":\"x86_64-linux\",\"publisher\":\"active_merchant\"}\r\nX-Stripe-Client-User-Metadata: {\"ip\":null}\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nConnection: close\r\nHost: api.stripe.com\r\nContent-Length: 270\r\n\r\n" + <- "amount=100&currency=usd&card[number]=[FILTERED]&card[exp_month]=9&card[exp_year]=2015&card[tokenization_method]=android_pay&card[eci]=07&capture_method=automatic&card[name]=Longbob+Longsen&description=ActiveMerchant+Test+Purchase&payment_user_agent=Stripe%2Fv1+ActiveMerchantBindings%2F1.45.0&metadata[email]=wow%40example.com&card[cryptogram]=[FILTERED]&payment_method_types[0]=card&payment_method_data[type]=card&payment_method_data[card][token]=[FILTERED]&metadata[connect_agent]=placeholder&metadata[transaction_token]=Coe7nlopnvhfcNRXhJMH5DTVusU&metadata[email]=john.smith%40example.com&metadata[order_id]=order_id-xxxxxx-x&confirm=true&return_url=http%3A%2F%2Fexaple.com%2Ftransaction%transaction_idxxxx%2Fredirect" + -> "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" + -> "Strict-Transport-Security: max-age=31556926; includeSubDomains; preload\r\n" + -> "\r\n" + reading 5204 bytes... + -> "{\n \"id\": \"pi_3KHrnWAWOtgoysog1Y5qMLqc\",\n \"object\": \"payment_intent\",\n \"amount\": 100,\n \"amount_capturable\": 0,\n \"amount_received\": 100,\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_3KHrnWAWOtgoysog1noj1iU9\",\n \"object\": \"charge\",\n \"amount\": 100,\n \"amount_captured\": 100,\n \"amount_refunded\": 0,\n \"application\": null,\n \"application_fee\": null,\n \"application_fee_amount\": null,\n \"balance_transaction\": \"txn_3KHrnWAWOtgoysog1vy6pmxk\",\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\": \"SPREEDLY\",\n \"captured\": true,\n \"created\": 1642174478,\n \"currency\": \"usd\",\n \"customer\": null,\n \"description\": null,\n \"destination\": null,\n \"dispute\": null,\n \"disputed\": false,\n \"failure_code\": null,\n \"failure_message\": null,\n \"fraud_details\": {\n },\n \"invoice\": null,\n \"livemode\": false,\n \"metadata\": {\n \"connect_agent\": \"placeholder\",\n \"transaction_token\": \"Coe7nlopnvhfcNRXhJMH5DTVusU\",\n \"email\": \"john.smith@example.com\",\n \"order_id\": \"AH2EjtfMGoZkWNEwLU90sq7VzcDlzWH_KugIYT4aVWEtJF9AwmqiXqsBs2l9q6F2Ruq9WKkUBbuLWNmA3P22ShFXFCZosTwkoflaDeTD2xeiMvmYv29VPINEDtLdSAoJ-DDlRKnsxa-n\"\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\": 36,\n \"seller_message\": \"Payment complete.\",\n \"type\": \"authorized\"\n },\n \"paid\": true,\n \"payment_intent\": \"pi_3KHrnWAWOtgoysog1Y5qMLqc\",\n \"payment_method\": \"pm_1KHrnWAWOtgoysogqXkTXrCb\",\n \"payment_method_details\": {\n \"card\": {\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\": 12,\n \"exp_year\": 2027,\n \"fingerprint\": \"sUdMrygQwzOKqwSm\",\n \"funding\": \"debit\",\n \"installments\": null,\n \"last4\": \"0000\",\n \"mandate\": null,\n \"moto\": null,\n \"network\": \"visa\",\n \"network_transaction_id\": \"1158510077114121\",\n \"three_d_secure\": null,\n \"wallet\": {\n \"dynamic_last4\": \"3478\",\n \"google_pay\": {\n },\n \"type\": \"google_pay\"\n }\n },\n \"type\": \"card\"\n },\n \"receipt_email\": null,\n \"receipt_number\": null,\n \"receipt_url\": \"https://pay.stripe.com/receipts/acct_160DX6AWOtgoysog/ch_3KHrnWAWOtgoysog1noj1iU9/rcpt_KxnOefAivglRgWZmxp0PLOJUQg0VhS9\",\n \"refunded\": false,\n \"refunds\": {\n \"object\": \"list\",\n \"data\": [\n\n ],\n \"has_more\": false,\n \"total_count\": 0,\n \"url\": \"/v1/charges/ch_3KHrnWAWOtgoysog1noj1iU9/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_3KHrnWAWOtgoysog1Y5qMLqc\"\n },\n \"client_secret\": \"pi_3KHrnWAWOtgoysog1Y5qMLqc_secret_5ZEt4fzM7YCi1zdMzs4iQXLjC\",\n \"confirmation_method\": \"automatic\",\n \"created\": 1642174478,\n \"currency\": \"usd\",\n \"customer\": null,\n \"description\": null,\n \"invoice\": null,\n \"last_payment_error\": null,\n \"livemode\": false,\n \"metadata\": {\n \"connect_agent\": \"placeholder\",\n \"transaction_token\": \"Coe7nlopnvhfcNRXhJMH5DTVusU\",\n \"email\": \"john.smith@example.com\",\n \"order_id\": \"AH2EjtfMGoZkWNEwLU90sq7VzcDlzWH_KugIYT4aVWEtJF9AwmqiXqsBs2l9q6F2Ruq9WKkUBbuLWNmA3P22ShFXFCZosTwkoflaDeTD2xeiMvmYv29VPINEDtLdSAoJ-DDlRKnsxa-n\"\n },\n \"next_action\": null,\n \"on_behalf_of\": null,\n \"payment_method\": \"pm_1KHrnWAWOtgoysogqXkTXrCb\",\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}\n" + read 5204 bytes + 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&currency=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&currency=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 c5b2b0ba2c4..090dd24e056 100644 --- a/test/unit/gateways/stripe_test.rb +++ b/test/unit/gateways/stripe_test.rb @@ -4,27 +4,33 @@ 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') + @non_3ds_card = credit_card('378282246310005') @amount = 400 @refund_amount = 200 @options = { - :billing_address => address(), - :statement_address => statement_address(), - :description => 'Test Purchase' + billing_address: address(), + statement_address: statement_address(), + shipping_address: shipping_address(), + description: 'Test Purchase' + } + + @threeds_options = { + execute_threed: true, + callback_url: 'http://www.example.com/callback' } - @apple_pay_payment_token = apple_pay_payment_token @emv_credit_card = credit_card_with_icc_data - @payment_token = StripeGateway::StripePaymentToken.new(token_params) - @token_string = @payment_token.payment_data['id'] + @token_string = 'tok_14uq3k2gKyKnHxtYUAZZZlH3' @check = check({ bank_name: 'STRIPE TEST BANK', account_number: '000123456789', - routing_number: '110000000', + routing_number: '110000000' }) end @@ -40,18 +46,6 @@ def test_successful_new_customer_with_card assert response.test? end - def test_successful_new_customer_with_apple_pay_payment_token - @gateway.expects(:ssl_request).returns(successful_new_customer_response) - @gateway.expects(:tokenize_apple_pay_token).returns(Response.new(true, nil, token: successful_apple_pay_token_exchange)) - - assert response = @gateway.store(@apple_pay_payment_token, @options) - assert_instance_of Response, response - assert_success response - - assert_equal 'cus_3sgheFxeBgTQ3M|card_483etw4er9fg4vF3sQdrt3FG', response.authorization - assert response.test? - end - def test_successful_new_customer_with_emv_credit_card @gateway.expects(:ssl_request).returns(successful_new_customer_response) @@ -75,19 +69,7 @@ def test_successful_new_card @gateway.expects(:ssl_request).returns(successful_new_card_response) @gateway.expects(:add_creditcard) - assert response = @gateway.store(@credit_card, :customer => 'cus_3sgheFxeBgTQ3M') - assert_instance_of MultiResponse, response - assert_success response - - assert_equal 'cus_3sgheFxeBgTQ3M|card_483etw4er9fg4vF3sQdrt3FG', response.authorization - assert response.test? - end - - def test_successful_new_card_via_apple_pay_payment_token - @gateway.expects(:ssl_request).returns(successful_new_card_response) - @gateway.expects(:tokenize_apple_pay_token).returns(Response.new(true, nil, token: successful_apple_pay_token_exchange)) - - assert response = @gateway.store(@apple_pay_payment_token, :customer => 'cus_3sgheFxeBgTQ3M') + assert response = @gateway.store(@credit_card, customer: 'cus_3sgheFxeBgTQ3M') assert_instance_of MultiResponse, response assert_success response @@ -99,7 +81,7 @@ def test_successful_new_card_with_emv_credit_card @gateway.expects(:ssl_request).returns(successful_new_card_response) @gateway.expects(:add_creditcard) - assert response = @gateway.store(@emv_credit_card, :customer => 'cus_3sgheFxeBgTQ3M') + assert response = @gateway.store(@emv_credit_card, customer: 'cus_3sgheFxeBgTQ3M') assert_instance_of MultiResponse, response assert_success response @@ -111,19 +93,7 @@ def test_successful_new_card_with_token_string @gateway.expects(:ssl_request).returns(successful_new_card_response) @gateway.expects(:add_creditcard) - assert response = @gateway.store(@token_string, :customer => 'cus_3sgheFxeBgTQ3M') - assert_instance_of MultiResponse, response - assert_success response - - assert_equal 'cus_3sgheFxeBgTQ3M|card_483etw4er9fg4vF3sQdrt3FG', response.authorization - assert response.test? - end - - def test_successful_new_card_with_payment_token - @gateway.expects(:ssl_request).returns(successful_new_card_response) - @gateway.expects(:add_payment_token) - - assert response = @gateway.store(@payment_token, :customer => 'cus_3sgheFxeBgTQ3M') + assert response = @gateway.store(@token_string, customer: 'cus_3sgheFxeBgTQ3M') assert_instance_of MultiResponse, response assert_success response @@ -135,22 +105,7 @@ def test_successful_new_card_and_customer_update @gateway.expects(:ssl_request).twice.returns(successful_new_card_response, successful_new_customer_response) @gateway.expects(:add_creditcard) - assert response = @gateway.store(@credit_card, :customer => 'cus_3sgheFxeBgTQ3M', :email => 'test@test.com') - assert_instance_of MultiResponse, response - assert_success response - - assert_equal 'cus_3sgheFxeBgTQ3M|card_483etw4er9fg4vF3sQdrt3FG', response.authorization - assert_equal 2, response.responses.size - assert_equal 'cus_3sgheFxeBgTQ3M|card_483etw4er9fg4vF3sQdrt3FG', response.responses[0].authorization - assert_equal 'cus_3sgheFxeBgTQ3M', response.responses[1].authorization - assert response.test? - end - - def test_successful_new_card_and_customer_update_via_apple_pay_payment_token - @gateway.expects(:ssl_request).twice.returns(successful_new_card_response, successful_new_customer_response) - @gateway.expects(:tokenize_apple_pay_token).returns(Response.new(true, nil, token: successful_apple_pay_token_exchange)) - - assert response = @gateway.store(@apple_pay_payment_token, :customer => 'cus_3sgheFxeBgTQ3M', :email => 'test@test.com') + assert response = @gateway.store(@credit_card, customer: 'cus_3sgheFxeBgTQ3M', email: 'test@test.com') assert_instance_of MultiResponse, response assert_success response @@ -164,7 +119,7 @@ def test_successful_new_card_and_customer_update_via_apple_pay_payment_token def test_successful_new_card_and_customer_update_with_emv_credit_card @gateway.expects(:ssl_request).twice.returns(successful_new_card_response, successful_new_customer_response) - assert response = @gateway.store(@emv_credit_card, :customer => 'cus_3sgheFxeBgTQ3M', :email => 'test@test.com') + assert response = @gateway.store(@emv_credit_card, customer: 'cus_3sgheFxeBgTQ3M', email: 'test@test.com') assert_instance_of MultiResponse, response assert_success response @@ -178,21 +133,7 @@ def test_successful_new_card_and_customer_update_with_emv_credit_card def test_successful_new_card_and_customer_update_with_token_string @gateway.expects(:ssl_request).twice.returns(successful_new_card_response, successful_new_customer_response) - assert response = @gateway.store(@token_string, :customer => 'cus_3sgheFxeBgTQ3M', :email => 'test@test.com') - assert_instance_of MultiResponse, response - assert_success response - - assert_equal 'cus_3sgheFxeBgTQ3M|card_483etw4er9fg4vF3sQdrt3FG', response.authorization - assert_equal 2, response.responses.size - assert_equal 'cus_3sgheFxeBgTQ3M|card_483etw4er9fg4vF3sQdrt3FG', response.responses[0].authorization - assert_equal 'cus_3sgheFxeBgTQ3M', response.responses[1].authorization - assert response.test? - end - - def test_successful_new_card_and_customer_update_with_payment_token - @gateway.expects(:ssl_request).twice.returns(successful_new_card_response, successful_new_customer_response) - - assert response = @gateway.store(@payment_token, :customer => 'cus_3sgheFxeBgTQ3M', :email => 'test@test.com') + assert response = @gateway.store(@token_string, customer: 'cus_3sgheFxeBgTQ3M', email: 'test@test.com') assert_instance_of MultiResponse, response assert_success response @@ -207,7 +148,7 @@ def test_successful_new_default_card @gateway.expects(:ssl_request).twice.returns(successful_new_card_response, successful_new_customer_response) @gateway.expects(:add_creditcard) - assert response = @gateway.store(@credit_card, @options.merge(:customer => 'cus_3sgheFxeBgTQ3M', :set_default => true)) + assert response = @gateway.store(@credit_card, @options.merge(customer: 'cus_3sgheFxeBgTQ3M', set_default: true)) assert_instance_of MultiResponse, response assert_success response @@ -218,25 +159,10 @@ def test_successful_new_default_card assert response.test? end - def test_successful_new_default_card_via_apple_pay_payment_token - @gateway.expects(:ssl_request).twice.returns(successful_new_card_response, successful_new_customer_response) - @gateway.expects(:tokenize_apple_pay_token).returns(Response.new(true, nil, token: successful_apple_pay_token_exchange)) - - assert response = @gateway.store(@apple_pay_payment_token, @options.merge(:customer => 'cus_3sgheFxeBgTQ3M', :set_default => true)) - assert_instance_of MultiResponse, response - assert_success response - - assert_equal 'cus_3sgheFxeBgTQ3M|card_483etw4er9fg4vF3sQdrt3FG', response.authorization - assert_equal 'cus_3sgheFxeBgTQ3M|card_483etw4er9fg4vF3sQdrt3FG', response.responses[0].authorization - assert_equal 2, response.responses.size - assert_equal 'cus_3sgheFxeBgTQ3M', response.responses[1].authorization - assert response.test? - end - def test_successful_new_default_card_with_emv_credit_card @gateway.expects(:ssl_request).twice.returns(successful_new_card_response, successful_new_customer_response) - assert response = @gateway.store(@emv_credit_card, @options.merge(:customer => 'cus_3sgheFxeBgTQ3M', :set_default => true)) + assert response = @gateway.store(@emv_credit_card, @options.merge(customer: 'cus_3sgheFxeBgTQ3M', set_default: true)) assert_instance_of MultiResponse, response assert_success response @@ -251,22 +177,7 @@ def test_successful_new_default_card_with_token_string @gateway.expects(:ssl_request).twice.returns(successful_new_card_response, successful_new_customer_response) @gateway.expects(:add_creditcard) - assert response = @gateway.store(@token_string, @options.merge(:customer => 'cus_3sgheFxeBgTQ3M', :set_default => true)) - assert_instance_of MultiResponse, response - assert_success response - - assert_equal 'cus_3sgheFxeBgTQ3M|card_483etw4er9fg4vF3sQdrt3FG', response.authorization - assert_equal 2, response.responses.size - assert_equal 'cus_3sgheFxeBgTQ3M|card_483etw4er9fg4vF3sQdrt3FG', response.responses[0].authorization - assert_equal 'cus_3sgheFxeBgTQ3M', response.responses[1].authorization - assert response.test? - end - - def test_successful_new_default_card_with_payment_token - @gateway.expects(:ssl_request).twice.returns(successful_new_card_response, successful_new_customer_response) - @gateway.expects(:add_payment_token) - - assert response = @gateway.store(@payment_token, @options.merge(:customer => 'cus_3sgheFxeBgTQ3M', :set_default => true)) + assert response = @gateway.store(@token_string, @options.merge(customer: 'cus_3sgheFxeBgTQ3M', set_default: true)) assert_instance_of MultiResponse, response assert_success response @@ -280,7 +191,7 @@ def test_successful_new_default_card_with_payment_token def test_passing_validate_false_on_store response = stub_comms(@gateway, :ssl_request) do @gateway.store(@credit_card, validate: false) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/validate=false/, data) end.respond_with(successful_new_customer_response) @@ -290,7 +201,7 @@ def test_passing_validate_false_on_store def test_empty_values_not_sent response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card, referrer: '') - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| refute_match(/referrer/, data) end.respond_with(successful_purchase_response) @@ -321,39 +232,51 @@ def test_successful_authorization_with_token_string assert response.test? end - def test_successful_authorization_with_payment_token - @gateway.expects(:add_payment_token) - @gateway.expects(:ssl_request).returns(successful_authorization_response) + def test_successful_authorization_with_emv_credit_card + @gateway.expects(:ssl_request).returns(successful_authorization_response_with_icc_data) - assert response = @gateway.authorize(@amount, @payment_token, @options) + assert response = @gateway.authorize(@amount, @emv_credit_card, @options) assert_instance_of Response, response assert_success response - assert_equal 'ch_test_charge', response.authorization - assert response.test? + assert_equal 'ch_test_emv_charge', response.authorization + assert response.emv_authorization, 'Response should include emv_authorization containing the EMV ARPC' end - def test_successful_authorization_with_apple_pay_token_exchange - @gateway.expects(:tokenize_apple_pay_token).returns(Response.new(true, nil, token: successful_apple_pay_token_exchange)) - @gateway.expects(:ssl_request).returns(successful_authorization_response) + def test_contains_statement_descriptor_suffix + options = @options.merge(statement_descriptor_suffix: 'suffix') - assert response = @gateway.authorize(@amount, @apple_pay_payment_token, @options) - assert_instance_of Response, response - assert_success response - - assert_equal 'ch_test_charge', response.authorization - assert response.test? + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/statement_descriptor_suffix=suffix/, data) + end.respond_with(successful_purchase_response) end - def test_successful_authorization_with_emv_credit_card - @gateway.expects(:ssl_request).returns(successful_authorization_response_with_icc_data) + def test_connected_account + destination = 'account_27701' + amount = 8000 + on_behalf_of = 'account_27704' + transfer_group = 'TG1000' + application_fee_amount = 100 - assert response = @gateway.authorize(@amount, @emv_credit_card, @options) - assert_instance_of Response, response - assert_success response + options = @options.merge( + transfer_destination: destination, + transfer_amount: amount, + on_behalf_of:, + transfer_group:, + application_fee_amount: + ) - assert_equal 'ch_test_emv_charge', response.authorization - assert response.emv_authorization, 'Response should include emv_authorization containing the EMV ARPC' + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/transfer_data\[destination\]=#{destination}/, data) + assert_match(/transfer_data\[amount\]=#{amount}/, data) + assert_match(/on_behalf_of=#{on_behalf_of}/, data) + assert_match(/transfer_group=#{transfer_group}/, data) + assert_match(/application_fee_amount=#{application_fee_amount}/, data) + end.respond_with(successful_purchase_response) end def test_declined_authorization_with_emv_credit_card @@ -376,9 +299,23 @@ def test_successful_capture end def test_successful_capture_with_emv_credit_card_tc - @gateway.expects(:ssl_request).returns(successful_capture_response_with_icc_data) - - assert response = @gateway.capture(@amount, 'ch_test_emv_charge') + stripe_charge_id = 'ch_test_emv_charge' + tc_emv_response = 'mock_icc_data' + @gateway. + expects(:ssl_request). + with( + :post, + "https://api.stripe.com/v1/charges/#{stripe_charge_id}", + "card[emv_approval_data]=#{tc_emv_response}", + anything + ).returns(successful_capture_response_with_icc_data) + + @gateway. + expects(:ssl_request). + with(:post, 'https://api.stripe.com/v1/charges/ch_test_emv_charge/capture', '', anything). + returns(successful_capture_response_without_icc_data) + + assert response = @gateway.capture(@amount, stripe_charge_id, icc_data: tc_emv_response) assert_success response assert response.emv_authorization, 'Response should include emv_authorization containing the EMV TC' end @@ -406,26 +343,43 @@ def test_successful_purchase_with_token_string assert response.test? end - def test_successful_purchase_with_payment_token - @gateway.expects(:add_payment_token) - @gateway.expects(:ssl_request).returns(successful_purchase_response) - - assert response = @gateway.purchase(@amount, @payment_token, @options) - assert_success response + def test_successful_purchase_with_level3_data + @gateway.expects(:add_creditcard) - assert_equal 'ch_test_charge', response.authorization - assert response.test? - end + @options[:merchant_reference] = 123 + @options[:customer_reference] = 456 + @options[:shipping_address_zip] = 98765 + @options[:shipping_from_zip] = 54321 + @options[:shipping_amount] = 40 + @options[:line_items] = [ + { + 'product_code' => 1234, + 'product_description' => 'An item', + 'unit_cost' => 60, + 'quantity' => 7, + 'tax_amount' => 0 + }, + { + 'product_code' => 999, + 'tax_amount' => 888 + } + ] - def test_successful_purchase_with_apple_pay_token_exchange - @gateway.expects(:tokenize_apple_pay_token).returns(Response.new(true, nil, token: successful_apple_pay_token_exchange)) - @gateway.expects(:ssl_request).returns(successful_purchase_response) + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_method, endpoint, data, _headers| + if %r{/charges}.match?(endpoint) + assert_match('level3[merchant_reference]=123', data) + assert_match('level3[customer_reference]=456', data) + assert_match('level3[shipping_address_zip]=98765', data) + assert_match('level3[shipping_amount]=40', data) + assert_match('level3[shipping_from_zip]=54321', data) + assert_match('level3[line_items][0][product_description]=An+item', data) + assert_match('level3[line_items][1][product_code]=999', data) + end + end.respond_with(successful_purchase_response) - assert response = @gateway.purchase(@amount, @apple_pay_payment_token, @options) - assert_instance_of Response, response assert_success response - - assert_equal 'ch_test_charge', response.authorization assert response.test? end @@ -448,8 +402,8 @@ def test_adds_application_to_x_stripe_client_user_agent_header } response = stub_comms(@gateway, :ssl_request) do - @gateway.purchase(@amount, 'cus_xxx|card_xxx', @options.merge({application: application})) - end.check_request do |method, endpoint, data, headers| + @gateway.purchase(@amount, 'cus_xxx|card_xxx', @options.merge({ application: })) + end.check_request do |_method, _endpoint, _data, headers| assert_match(/\"application\"/, headers['X-Stripe-Client-User-Agent']) assert_match(/\"name\":\"app\"/, headers['X-Stripe-Client-User-Agent']) assert_match(/\"version\":\"1.0\"/, headers['X-Stripe-Client-User-Agent']) @@ -462,7 +416,7 @@ def test_adds_application_to_x_stripe_client_user_agent_header def test_successful_purchase_with_token_including_customer response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, 'cus_xxx|card_xxx') - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/customer=cus_xxx/, data) assert_match(/card=card_xxx/, data) end.respond_with(successful_purchase_response) @@ -473,7 +427,7 @@ def test_successful_purchase_with_token_including_customer def test_successful_purchase_with_token response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, 'card_xxx') - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/card=card_xxx/, data) end.respond_with(successful_purchase_response) @@ -483,7 +437,7 @@ def test_successful_purchase_with_token def test_successful_purchase_with_statement_description stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card, statement_description: '5K RACE TICKET') - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/statement_descriptor=5K\+RACE\+TICKET/, data) end.respond_with(successful_purchase_response) end @@ -501,7 +455,7 @@ def test_successful_void def test_void_contains_charge_expand @gateway.expects(:ssl_request).with do |_, _, post, _| - post.include?('expand[]=charge') + post.include?('expand[0]=charge') end.returns(successful_purchase_response(true)) assert response = @gateway.void('ch_test_charge') @@ -511,7 +465,8 @@ def test_void_contains_charge_expand def test_void_with_additional_expand_contains_two_expands @gateway.expects(:ssl_request).with do |_, _, post, _| parsed = CGI.parse(post) - parsed['expand[]'].sort == ['balance_transaction', 'charge'].sort + parsed['expand[0]'] = 'balance_transaction' + parsed['expand[1]'] = 'charge' end.returns(successful_purchase_response(true)) assert response = @gateway.void('ch_test_charge', expand: :balance_transaction) @@ -521,7 +476,7 @@ def test_void_with_additional_expand_contains_two_expands def test_void_with_expand_charge_only_sends_one_charge_expand @gateway.expects(:ssl_request).with do |_, _, post, _| parsed = CGI.parse(post) - parsed['expand[]'] == ['charge'] + parsed['expand[0]'] == ['charge'] end.returns(successful_purchase_response(true)) assert response = @gateway.void('ch_test_charge', expand: ['charge']) @@ -533,7 +488,25 @@ def test_successful_void_with_metadata post.include?('metadata[first_value]=true') end.returns(successful_purchase_response(true)) - assert response = @gateway.void('ch_test_charge', {metadata: {first_value: true}}) + assert response = @gateway.void('ch_test_charge', { metadata: { first_value: true } }) + assert_success response + end + + def test_successful_void_with_reason + @gateway.expects(:ssl_request).with do |_, _, post, _| + post.include?('reason=fraudulent') + end.returns(successful_purchase_response(true)) + + assert response = @gateway.void('ch_test_charge', { reason: 'fraudulent' }) + assert_success response + end + + def test_successful_void_with_reverse_transfer + @gateway.expects(:ssl_request).with do |_, _, post, _| + post.include?('reverse_transfer=true') + end.returns(successful_purchase_response(true)) + + assert response = @gateway.void('ch_test_charge', { reverse_transfer: true }) assert_success response end @@ -546,6 +519,15 @@ def test_successful_refund assert_equal 're_test_refund', response.authorization end + def test_successful_refund_with_reason + @gateway.expects(:ssl_request).returns(successful_partially_refunded_response) + + assert response = @gateway.refund(@refund_amount, 'ch_test_charge', reason: 'fraudulent') + assert_success response + + assert_equal 're_test_refund', response.authorization + end + def test_unsuccessful_refund @gateway.expects(:ssl_request).returns(generic_error_response) @@ -554,17 +536,17 @@ def test_unsuccessful_refund end def test_successful_refund_with_refund_application_fee - @gateway.expects(:ssl_request).with do |method, url, post, headers| + @gateway.expects(:ssl_request).with do |_method, _url, post, _headers| post.include?('refund_application_fee=true') end.returns(successful_partially_refunded_response) - assert response = @gateway.refund(@refund_amount, 'ch_test_charge', :refund_application_fee => true) + assert response = @gateway.refund(@refund_amount, 'ch_test_charge', refund_application_fee: true) assert_success response end def test_refund_contains_charge_expand @gateway.expects(:ssl_request).with do |_, _, post, _| - post.include?('expand[]=charge') + post.include?('expand[0]=charge') end.returns(successful_partially_refunded_response) assert response = @gateway.refund(@refund_amount, 'ch_test_charge') @@ -574,7 +556,8 @@ def test_refund_contains_charge_expand def test_refund_with_additional_expand_contains_two_expands @gateway.expects(:ssl_request).with do |_, _, post, _| parsed = CGI.parse(post) - parsed['expand[]'].sort == ['balance_transaction', 'charge'].sort + parsed['expand[0]'] = 'balance_transaction' + parsed['expand[1]'] = 'charge' end.returns(successful_partially_refunded_response) assert response = @gateway.refund(@refund_amount, 'ch_test_charge', expand: :balance_transaction) @@ -584,7 +567,7 @@ def test_refund_with_additional_expand_contains_two_expands def test_refund_with_expand_charge_only_sends_one_charge_expand @gateway.expects(:ssl_request).with do |_, _, post, _| parsed = CGI.parse(post) - parsed['expand[]'] == ['charge'] + parsed['expand[0]'] == ['charge'] end.returns(successful_partially_refunded_response) assert response = @gateway.refund(@refund_amount, 'ch_test_charge', expand: ['charge']) @@ -592,18 +575,18 @@ def test_refund_with_expand_charge_only_sends_one_charge_expand end def test_successful_refund_with_metadata - @gateway.expects(:ssl_request).with do |method, url, post, headers| + @gateway.expects(:ssl_request).with do |_method, _url, post, _headers| post.include?('metadata[first_value]=true') end.returns(successful_partially_refunded_response) - assert response = @gateway.refund(@refund_amount, 'ch_test_charge', {metadata: {first_value: true}}) + assert response = @gateway.refund(@refund_amount, 'ch_test_charge', { metadata: { first_value: true } }) assert_success response end def test_successful_refund_with_reverse_transfer stub_comms(@gateway, :ssl_request) do @gateway.refund(@amount, 'auth', reverse_transfer: true) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/reverse_transfer=true/, data) end.respond_with(successful_partially_refunded_response) end @@ -614,39 +597,45 @@ def test_successful_refund_with_refund_fee_amount @gateway.expects(:ssl_request).returns(successful_fetch_application_fee_response).in_sequence(s) @gateway.expects(:ssl_request).returns(successful_partially_refunded_application_fee_response).in_sequence(s) - assert response = @gateway.refund(@refund_amount, 'ch_test_charge', :refund_fee_amount => 100) + assert response = @gateway.refund(@refund_amount, 'ch_test_charge', refund_fee_amount: 100) assert_success response end - # What is the significance of this??? it's to test that the first response is used as primary. so identical to above with an extra assertion - def test_refund_with_fee_response_gives_a_charge_authorization + def test_refund_with_fee_response_responds_with_the_refund_authorization s = sequence('request') @gateway.expects(:ssl_request).returns(successful_partially_refunded_response).in_sequence(s) @gateway.expects(:ssl_request).returns(successful_fetch_application_fee_response).in_sequence(s) @gateway.expects(:ssl_request).returns(successful_partially_refunded_application_fee_response).in_sequence(s) - assert response = @gateway.refund(@refund_amount, 'ch_test_charge', :refund_fee_amount => 100) + assert response = @gateway.refund(@refund_amount, 'ch_test_charge', refund_fee_amount: 100) assert_success response assert_equal 're_test_refund', response.authorization end - def test_unsuccessful_refund_with_refund_fee_amount_when_application_fee_id_not_found + def test_successful_refund_with_failed_fee_refund_fetch s = sequence('request') @gateway.expects(:ssl_request).returns(successful_partially_refunded_response).in_sequence(s) @gateway.expects(:ssl_request).returns(unsuccessful_fetch_application_fee_response).in_sequence(s) - assert response = @gateway.refund(@refund_amount, 'ch_test_charge', :refund_fee_amount => 100) - assert_failure response - assert_match(/^Application fee id could not be retrieved/, response.message) + assert response = @gateway.refund(@refund_amount, 'ch_test_charge', refund_fee_amount: 100) + assert_success response end - def test_unsuccessful_refund_with_refund_fee_amount_when_refunding_application_fee + def test_successful_refund_with_failed_fee_refund s = sequence('request') @gateway.expects(:ssl_request).returns(successful_partially_refunded_response).in_sequence(s) @gateway.expects(:ssl_request).returns(successful_fetch_application_fee_response).in_sequence(s) @gateway.expects(:ssl_request).returns(generic_error_response).in_sequence(s) - assert response = @gateway.refund(@refund_amount, 'ch_test_charge', :refund_fee_amount => 100) + assert response = @gateway.refund(@refund_amount, 'ch_test_charge', refund_fee_amount: 100) + assert_success response + end + + def test_unsuccessful_refund_does_not_refund_fee + s = sequence('request') + @gateway.expects(:ssl_request).returns(generic_error_response).in_sequence(s) + + assert response = @gateway.refund(@refund_amount, 'ch_test_charge', refund_fee_amount: 100) assert_failure response end @@ -674,7 +663,7 @@ def test_unsuccessful_verify end def test_successful_request_always_uses_live_mode_to_determine_test_request - @gateway.expects(:ssl_request).returns(successful_partially_refunded_response(:livemode => true)) + @gateway.expects(:ssl_request).returns(successful_partially_refunded_response) assert response = @gateway.refund(@refund_amount, 'ch_test_charge') assert_success response @@ -702,6 +691,19 @@ def test_declined_request assert_equal 'ch_test_charge', response.authorization end + def test_declined_request_returns_header_response + @gateway.instance_variable_set(:@response_headers, { 'idempotent-replayed' => 'true' }) + @gateway.expects(:ssl_request).returns(declined_purchase_response) + + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_failure response + + assert_equal Gateway::STANDARD_ERROR_CODE[:card_declined], response.error_code + refute response.test? # unsuccessful request defaults to live + assert_equal 'ch_test_charge', response.authorization + assert response.params['response_headers']['idempotent_replayed'], 'true' + end + def test_declined_request_advanced_decline_codes @gateway.expects(:ssl_request).returns(declined_call_issuer_purchase_response) @@ -743,6 +745,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, {}) @@ -813,7 +822,7 @@ def test_add_creditcard_with_card_token_and_customer def test_add_creditcard_with_card_token_and_track_data post = {} credit_card_token = 'card_2iD4AezYnNNzkW' - @gateway.send(:add_creditcard, post, credit_card_token, :track_data => 'Tracking data') + @gateway.send(:add_creditcard, post, credit_card_token, track_data: 'Tracking data') assert_equal 'Tracking data', post[:card][:swipe_data] end @@ -826,7 +835,8 @@ 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' @@ -839,57 +849,87 @@ def test_add_creditcard_pads_eci_value def test_application_fee_is_submitted_for_purchase stub_comms(@gateway, :ssl_request) do - @gateway.purchase(@amount, @credit_card, @options.merge({:application_fee => 144})) - end.check_request do |method, endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, @options.merge({ application_fee: 144 })) + end.check_request do |_method, _endpoint, data, _headers| assert_match(/application_fee=144/, data) end.respond_with(successful_purchase_response) end def test_application_fee_is_submitted_for_capture stub_comms(@gateway, :ssl_request) do - @gateway.capture(@amount, 'ch_test_charge', @options.merge({:application_fee => 144})) - end.check_request do |method, endpoint, data, headers| + @gateway.capture(@amount, 'ch_test_charge', @options.merge({ application_fee: 144 })) + end.check_request do |_method, _endpoint, data, _headers| assert_match(/application_fee=144/, data) end.respond_with(successful_capture_response) end def test_exchange_rate_is_submitted_for_purchase stub_comms(@gateway, :ssl_request) do - @gateway.purchase(@amount, @credit_card, @options.merge({:exchange_rate => 0.96251})) - end.check_request do |method, endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, @options.merge({ exchange_rate: 0.96251 })) + end.check_request do |_method, _endpoint, data, _headers| assert_match(/exchange_rate=0.96251/, data) end.respond_with(successful_purchase_response) end def test_exchange_rate_is_submitted_for_capture stub_comms(@gateway, :ssl_request) do - @gateway.capture(@amount, 'ch_test_charge', @options.merge({:exchange_rate => 0.96251})) - end.check_request do |method, endpoint, data, headers| + @gateway.capture(@amount, 'ch_test_charge', @options.merge({ exchange_rate: 0.96251 })) + end.check_request do |_method, _endpoint, data, _headers| assert_match(/exchange_rate=0.96251/, data) end.respond_with(successful_capture_response) end def test_destination_is_submitted_for_purchase stub_comms(@gateway, :ssl_request) do - @gateway.purchase(@amount, @credit_card, @options.merge({:destination => 'subaccountid'})) - end.check_request do |method, endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, @options.merge({ destination: 'subaccountid' })) + end.check_request do |_method, _endpoint, data, _headers| assert_match(/destination\[account\]=subaccountid/, data) end.respond_with(successful_purchase_response) end def test_destination_amount_is_submitted_for_purchase stub_comms(@gateway, :ssl_request) do - @gateway.purchase(@amount, @credit_card, @options.merge({:destination => 'subaccountid', :destination_amount => @amount - 20})) - end.check_request do |method, endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, @options.merge({ destination: 'subaccountid', destination_amount: @amount - 20 })) + end.check_request do |_method, _endpoint, data, _headers| assert_match(/destination\[amount\]=#{@amount - 20}/, data) end.respond_with(successful_purchase_response) end + def test_radar_session_is_submitted_for_purchase + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, { + radar_session_id: 'test_radar_session_id' + }) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/radar_options\[session\]=test_radar_session_id/, data) + end.respond_with(successful_purchase_response) + end + + def test_radar_session_is_submitted_for_authorize + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, { + radar_session_id: 'test_radar_session_id' + }) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/radar_options\[session\]=test_radar_session_id/, data) + end.respond_with(successful_authorization_response) + end + + def test_skip_rules_is_submitted_for_purchase + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, { + skip_radar_rules: true + }) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/radar_options\[skip_rules\]\[0\]=all/, data) + end.respond_with(successful_authorization_response) + end + def test_client_data_submitted_with_purchase stub_comms(@gateway, :ssl_request) do - updated_options = @options.merge({:description => 'a test customer',:ip => '127.127.127.127', :user_agent => 'some browser', :order_id => '42', :email => 'foo@wonderfullyfakedomain.com', :receipt_email => 'receipt-receiver@wonderfullyfakedomain.com', :referrer =>'http://www.shopify.com'}) - @gateway.purchase(@amount,@credit_card,updated_options) - end.check_request do |method, endpoint, data, headers| + updated_options = @options.merge({ description: 'a test customer', ip: '127.127.127.127', user_agent: 'some browser', order_id: '42', email: 'foo@wonderfullyfakedomain.com', receipt_email: 'receipt-receiver@wonderfullyfakedomain.com', referrer: 'http://www.shopify.com' }) + @gateway.purchase(@amount, @credit_card, updated_options) + end.check_request do |_method, _endpoint, data, _headers| assert_match(/description=a\+test\+customer/, data) assert_match(/ip=127\.127\.127\.127/, data) assert_match(/user_agent=some\+browser/, data) @@ -904,9 +944,9 @@ def test_client_data_submitted_with_purchase def test_client_data_submitted_with_purchase_without_email_or_order stub_comms(@gateway, :ssl_request) do - updated_options = @options.merge({:description => 'a test customer',:ip => '127.127.127.127', :user_agent => 'some browser', :referrer =>'http://www.shopify.com'}) - @gateway.purchase(@amount,@credit_card,updated_options) - end.check_request do |method, endpoint, data, headers| + updated_options = @options.merge({ description: 'a test customer', ip: '127.127.127.127', user_agent: 'some browser', referrer: 'http://www.shopify.com' }) + @gateway.purchase(@amount, @credit_card, updated_options) + end.check_request do |_method, _endpoint, data, _headers| assert_match(/description=a\+test\+customer/, data) assert_match(/ip=127\.127\.127\.127/, data) assert_match(/user_agent=some\+browser/, data) @@ -918,9 +958,9 @@ def test_client_data_submitted_with_purchase_without_email_or_order def test_client_data_submitted_with_metadata_in_options stub_comms(@gateway, :ssl_request) do - updated_options = @options.merge({:metadata => {:this_is_a_random_key_name => 'with a random value', :i_made_up_this_key_too => 'canyoutell'}, :order_id => '42', :email => 'foo@wonderfullyfakedomain.com'}) - @gateway.purchase(@amount,@credit_card,updated_options) - end.check_request do |method, endpoint, data, headers| + updated_options = @options.merge({ metadata: { this_is_a_random_key_name: 'with a random value', i_made_up_this_key_too: 'canyoutell' }, order_id: '42', email: 'foo@wonderfullyfakedomain.com' }) + @gateway.purchase(@amount, @credit_card, updated_options) + end.check_request do |_method, _endpoint, data, _headers| assert_match(/metadata\[this_is_a_random_key_name\]=with\+a\+random\+value/, data) assert_match(/metadata\[i_made_up_this_key_too\]=canyoutell/, data) assert_match(/metadata\[email\]=foo\%40wonderfullyfakedomain\.com/, data) @@ -930,9 +970,9 @@ def test_client_data_submitted_with_metadata_in_options def test_client_data_submitted_with_metadata_in_options_with_emv_credit_card_purchase stub_comms(@gateway, :ssl_request) do - updated_options = @options.merge({:metadata => {:this_is_a_random_key_name => 'with a random value', :i_made_up_this_key_too => 'canyoutell'}, :order_id => '42', :email => 'foo@wonderfullyfakedomain.com'}) + updated_options = @options.merge({ metadata: { this_is_a_random_key_name: 'with a random value', i_made_up_this_key_too: 'canyoutell' }, order_id: '42', email: 'foo@wonderfullyfakedomain.com' }) @gateway.purchase(@amount, @emv_credit_card, updated_options) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/metadata\[this_is_a_random_key_name\]=with\+a\+random\+value/, data) assert_match(/metadata\[i_made_up_this_key_too\]=canyoutell/, data) assert_match(/metadata\[email\]=foo\%40wonderfullyfakedomain\.com/, data) @@ -943,9 +983,9 @@ def test_client_data_submitted_with_metadata_in_options_with_emv_credit_card_pur def test_client_data_submitted_with_metadata_in_options_with_emv_credit_card_authorize stub_comms(@gateway, :ssl_request) do - updated_options = @options.merge({:metadata => {:this_is_a_random_key_name => 'with a random value', :i_made_up_this_key_too => 'canyoutell'}, :order_id => '42', :email => 'foo@wonderfullyfakedomain.com'}) + updated_options = @options.merge({ metadata: { this_is_a_random_key_name: 'with a random value', i_made_up_this_key_too: 'canyoutell' }, order_id: '42', email: 'foo@wonderfullyfakedomain.com' }) @gateway.authorize(@amount, @emv_credit_card, updated_options) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/metadata\[this_is_a_random_key_name\]=with\+a\+random\+value/, data) assert_match(/metadata\[i_made_up_this_key_too\]=canyoutell/, data) assert_match(/metadata\[email\]=foo\%40wonderfullyfakedomain\.com/, data) @@ -958,7 +998,7 @@ def test_quickchip_is_set_on_purchase stub_comms(@gateway, :ssl_request) do @emv_credit_card.read_method = 'contact_quickchip' @gateway.purchase(@amount, @emv_credit_card, @options) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/card\[processing_method\]=quick_chip/, data) end.respond_with(successful_purchase_response) end @@ -967,13 +1007,13 @@ def test_quickchip_is_not_set_on_authorize stub_comms(@gateway, :ssl_request) do @emv_credit_card.read_method = 'contact_quickchip' @gateway.authorize(@amount, @emv_credit_card, @options) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| refute_match(/card\[processing_method\]=quick_chip/, data) end.respond_with(successful_purchase_response) end def test_add_address - post = {:card => {}} + post = { card: {} } @gateway.send(:add_address, post, @options) assert_equal @options[:billing_address][:zip], post[:card][:address_zip] assert_equal @options[:billing_address][:state], post[:card][:address_state] @@ -996,9 +1036,34 @@ def test_add_statement_address assert_equal @options[:statement_address][:city], post[:statement_address][:city] end + def test_add_shipping_address + post = {} + + @gateway.send(:add_shipping_address, post, @credit_card, @options) + + assert_equal @options[:shipping_address][:zip], post[:shipping][:address][:postal_code] + assert_equal @options[:shipping_address][:state], post[:shipping][:address][:state] + assert_equal @options[:shipping_address][:address1], post[:shipping][:address][:line1] + assert_equal @options[:shipping_address][:address2], post[:shipping][:address][:line2] + assert_equal @options[:shipping_address][:country], post[:shipping][:address][:country] + assert_equal @options[:shipping_address][:city], post[:shipping][:address][:city] + assert_equal @options[:shipping_address][:name], post[:shipping][:name] + assert_equal @options[:shipping_address][:phone_number], post[:shipping][:phone] + end + + def test_shipping_address_not_added_if_no_name_present + post = {} + + options = @options.dup + options[:shipping_address] = options[:shipping_address].except(:name) + @gateway.send(:add_shipping_address, post, @credit_card, options) + + assert_empty post + end + def test_add_statement_address_returns_nil_if_required_fields_missing post = {} - [:address1, :city, :zip, :state].each do |required_key| + %i[address1 city zip state].each do |required_key| missing_required = @options.tap do |options| options[:statement_address].delete_if { |k| k == required_key } end @@ -1020,55 +1085,55 @@ def test_gateway_without_credentials end def test_metadata_header - @gateway.expects(:ssl_request).once.with {|method, url, post, headers| - headers && headers['X-Stripe-Client-User-Metadata'] == {:ip => '1.1.1.1'}.to_json + @gateway.expects(:ssl_request).once.with { |_method, _url, _post, headers| + headers && headers['X-Stripe-Client-User-Metadata'] == { ip: '1.1.1.1' }.to_json }.returns(successful_purchase_response) - @gateway.purchase(@amount, @credit_card, @options.merge(:ip => '1.1.1.1')) + @gateway.purchase(@amount, @credit_card, @options.merge(ip: '1.1.1.1')) end def test_optional_version_header - @gateway.expects(:ssl_request).once.with {|method, url, post, headers| + @gateway.expects(:ssl_request).once.with { |_method, _url, _post, headers| headers && headers['Stripe-Version'] == '2013-10-29' }.returns(successful_purchase_response) - @gateway.purchase(@amount, @credit_card, @options.merge(:version => '2013-10-29')) + @gateway.purchase(@amount, @credit_card, @options.merge(version: '2013-10-29')) end def test_optional_idempotency_key_header - @gateway.expects(:ssl_request).once.with {|method, url, post, headers| + @gateway.expects(:ssl_request).once.with { |_method, _url, _post, headers| headers && headers['Idempotency-Key'] == 'test123' }.returns(successful_purchase_response) - response = @gateway.purchase(@amount, @credit_card, @options.merge(:idempotency_key => 'test123')) + response = @gateway.purchase(@amount, @credit_card, @options.merge(idempotency_key: 'test123')) assert_success response end def test_optional_idempotency_on_void - @gateway.expects(:ssl_request).once.with {|method, url, post, headers| + @gateway.expects(:ssl_request).once.with { |_method, _url, _post, headers| headers && headers['Idempotency-Key'] == 'test123' }.returns(successful_purchase_response(true)) - response = @gateway.void('ch_test_charge', @options.merge(:idempotency_key => 'test123')) + response = @gateway.void('ch_test_charge', @options.merge(idempotency_key: 'test123')) assert_success response end def test_optional_idempotency_on_verify - @gateway.expects(:ssl_request).with do |method, url, post, headers| + @gateway.expects(:ssl_request).with do |_method, _url, _post, headers| headers && headers['Idempotency-Key'] == nil end.returns(successful_void_response) - @gateway.expects(:ssl_request).with do |method, url, post, headers| + @gateway.expects(:ssl_request).with do |_method, _url, _post, headers| headers && headers['Idempotency-Key'] == 'test123' end.returns(successful_authorization_response) - response = @gateway.verify(@credit_card, @options.merge(:idempotency_key => 'test123')) + response = @gateway.verify(@credit_card, @options.merge(idempotency_key: 'test123')) assert_success response end def test_initialize_gateway_with_version - @gateway = StripeGateway.new(:login => 'login', :version => '2013-12-03') - @gateway.expects(:ssl_request).once.with {|method, url, post, headers| + @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) @@ -1078,7 +1143,7 @@ def test_initialize_gateway_with_version def test_track_data_and_traditional_should_be_mutually_exclusive stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert data =~ /card\[name\]/ assert data !~ /card\[swipe_data\]/ end.respond_with(successful_purchase_response) @@ -1086,7 +1151,7 @@ def test_track_data_and_traditional_should_be_mutually_exclusive stub_comms(@gateway, :ssl_request) do @credit_card.track_data = '%B378282246310005^LONGSON/LONGBOB^1705101130504392?' @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert data !~ /card\[name\]/ assert data =~ /card\[swipe_data\]/ end.respond_with(successful_purchase_response) @@ -1095,7 +1160,7 @@ def test_track_data_and_traditional_should_be_mutually_exclusive def test_address_is_included_with_card_data stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert data =~ /card\[address_line1\]/ end.respond_with(successful_purchase_response) end @@ -1104,8 +1169,8 @@ def test_contactless_flag_is_included_with_emv_card_data stub_comms(@gateway, :ssl_request) do @emv_credit_card.read_method = 'contactless' @gateway.purchase(@amount, @emv_credit_card, @options) - end.check_request do |method, endpoint, data, headers| - data =~ /card\[read_method\]=contactless/ + end.check_request do |_method, _endpoint, data, _headers| + assert data =~ /card\[read_method\]=contactless/ end.respond_with(successful_purchase_response) end @@ -1113,16 +1178,16 @@ def test_contactless_magstripe_flag_is_included_with_emv_card_data stub_comms(@gateway, :ssl_request) do @emv_credit_card.read_method = 'contactless_magstripe' @gateway.purchase(@amount, @emv_credit_card, @options) - end.check_request do |method, endpoint, data, headers| - data =~ /card\[read_method\]=contactless_magstripe_mode/ + end.check_request do |_method, _endpoint, data, _headers| + assert data =~ /card\[read_method\]=contactless_magstripe_mode/ end.respond_with(successful_purchase_response) end def test_contactless_flag_is_not_included_with_emv_card_data_by_default stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @emv_credit_card, @options) - end.check_request do |method, endpoint, data, headers| - data !~ /card\[read_method\]=contactless/ && data !~ /card\[read_method\]=contactless_magstripe_mode/ + end.check_request do |_method, _endpoint, data, _headers| + assert data !~ /card\[read_method\]=contactless/ && data !~ /card\[read_method\]=contactless_magstripe_mode/ end.respond_with(successful_purchase_response) end @@ -1131,38 +1196,38 @@ def test_encrypted_pin_is_included_with_emv_card_data @emv_credit_card.encrypted_pin_cryptogram = '8b68af72199529b8' @emv_credit_card.encrypted_pin_ksn = 'ffff0102628d12000001' @gateway.purchase(@amount, @emv_credit_card, @options) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert data =~ /card\[encrypted_pin\]=8b68af72199529b8/ assert data =~ /card\[encrypted_pin_key_id\]=ffff0102628d12000001/ end.respond_with(successful_purchase_response) end def generate_options_should_allow_key - assert_equal({:key => '12345'}, generate_options({:key => '12345'})) + assert_equal({ key: '12345' }, generate_options({ key: '12345' })) end def test_passing_expand_parameters - @gateway.expects(:ssl_request).with do |method, url, post, headers| - post.include?('expand[]=balance_transaction') + @gateway.expects(:ssl_request).with do |_method, _url, post, _headers| + post.include?('expand[0]=balance_transaction') end.returns(successful_authorization_response) - @options.merge!(:expand => :balance_transaction) + @options[:expand] = :balance_transaction @gateway.authorize(@amount, @credit_card, @options) end def test_passing_expand_parameters_as_array - @gateway.expects(:ssl_request).with do |method, url, post, headers| - post.include?('expand[]=balance_transaction&expand[]=customer') + @gateway.expects(:ssl_request).with do |_method, _url, post, _headers| + post.include?('expand[0]=balance_transaction&expand[1]=customer') end.returns(successful_authorization_response) - @options.merge!(:expand => [:balance_transaction, :customer]) + @options[:expand] = %i[balance_transaction customer] @gateway.authorize(@amount, @credit_card, @options) end def test_recurring_flag_not_set_by_default - @gateway.expects(:ssl_request).with do |method, url, post, headers| + @gateway.expects(:ssl_request).with do |_method, _url, post, _headers| !post.include?('recurring') end.returns(successful_authorization_response) @@ -1170,49 +1235,49 @@ def test_recurring_flag_not_set_by_default end def test_passing_recurring_eci_sets_recurring_flag - @gateway.expects(:ssl_request).with do |method, url, post, headers| + @gateway.expects(:ssl_request).with do |_method, _url, post, _headers| post.include?('recurring=true') end.returns(successful_authorization_response) - @options.merge!(eci: 'recurring') + @options[:eci] = 'recurring' @gateway.authorize(@amount, @credit_card, @options) end def test_passing_unknown_eci_does_not_set_recurring_flag - @gateway.expects(:ssl_request).with do |method, url, post, headers| + @gateway.expects(:ssl_request).with do |_method, _url, post, _headers| !post.include?('recurring') end.returns(successful_authorization_response) - @options.merge!(eci: 'installment') + @options[:eci] = 'installment' @gateway.authorize(@amount, @credit_card, @options) end def test_passing_recurring_true_option_sets_recurring_flag - @gateway.expects(:ssl_request).with do |method, url, post, headers| + @gateway.expects(:ssl_request).with do |_method, _url, post, _headers| post.include?('recurring=true') end.returns(successful_authorization_response) - @options.merge!(recurring: true) + @options[:recurring] = true @gateway.authorize(@amount, @credit_card, @options) end def test_passing_recurring_false_option_does_not_set_recurring_flag - @gateway.expects(:ssl_request).with do |method, url, post, headers| + @gateway.expects(:ssl_request).with do |_method, _url, post, _headers| !post.include?('recurring') end.returns(successful_authorization_response) - @options.merge!(recurring: false) + @options[:recurring] = false @gateway.authorize(@amount, @credit_card, @options) end def test_new_attributes_are_included_in_update stub_comms(@gateway, :ssl_request) do - @gateway.send(:update, 'cus_3sgheFxeBgTQ3M', 'card_483etw4er9fg4vF3sQdrt3FG', { :name => 'John Smith', :exp_year => 2021, :exp_month => 6 }) - end.check_request do |method, endpoint, data, headers| + @gateway.send(:update, 'cus_3sgheFxeBgTQ3M', 'card_483etw4er9fg4vF3sQdrt3FG', { name: 'John Smith', exp_year: 2021, exp_month: 6 }) + end.check_request do |_method, endpoint, data, _headers| assert data == 'name=John+Smith&exp_year=2021&exp_month=6' assert endpoint.include? '/customers/cus_3sgheFxeBgTQ3M/cards/card_483etw4er9fg4vF3sQdrt3FG' end.respond_with(successful_update_credit_card_response) @@ -1220,6 +1285,7 @@ def test_new_attributes_are_included_in_update def test_scrub assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + assert_equal @gateway.scrub(pre_scrubbed_nested_payment_method_data), post_scrubbed_nested_payment_method_data end def test_scrubs_track_data @@ -1230,18 +1296,23 @@ def test_scrubs_emv_data assert_equal @gateway.scrub(pre_scrubbed_with_emv_data), post_scrubbed_with_emv_data end + def test_scrubs_account_number + assert_equal @gateway.scrub(pre_scrubbed_with_account_number), post_scrubbed_with_account_number + end + def test_supports_scrubbing? assert @gateway.supports_scrubbing? end def test_successful_auth_with_network_tokenization_apple_pay - @gateway.expects(:ssl_request).with do |method, endpoint, data, headers| + @gateway.expects(:ssl_request).with do |method, _endpoint, data, _headers| assert_equal :post, method assert_match %r'card\[cryptogram\]=111111111100cryptogram&card\[eci\]=05&card\[tokenization_method\]=apple_pay', data 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' @@ -1256,13 +1327,14 @@ def test_successful_auth_with_network_tokenization_apple_pay end def test_successful_auth_with_network_tokenization_android_pay - @gateway.expects(:ssl_request).with do |method, endpoint, data, headers| + @gateway.expects(:ssl_request).with do |method, _endpoint, data, _headers| assert_equal :post, method assert_match %r'card\[cryptogram\]=111111111100cryptogram&card\[eci\]=05&card\[tokenization_method\]=android_pay', data 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', @@ -1278,13 +1350,14 @@ def test_successful_auth_with_network_tokenization_android_pay end def test_successful_purchase_with_network_tokenization_apple_pay - @gateway.expects(:ssl_request).with do |method, endpoint, data, headers| + @gateway.expects(:ssl_request).with do |method, _endpoint, data, _headers| assert_equal :post, method assert_match %r'card\[cryptogram\]=111111111100cryptogram&card\[eci\]=05&card\[tokenization_method\]=apple_pay', data 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' @@ -1299,13 +1372,14 @@ def test_successful_purchase_with_network_tokenization_apple_pay end def test_successful_purchase_with_network_tokenization_android_pay - @gateway.expects(:ssl_request).with do |method, endpoint, data, headers| + @gateway.expects(:ssl_request).with do |method, _endpoint, data, _headers| assert_equal :post, method assert_match %r'card\[cryptogram\]=111111111100cryptogram&card\[eci\]=05&card\[tokenization_method\]=android_pay', data 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', @@ -1327,7 +1401,7 @@ def test_supports_network_tokenization def test_emv_capture_application_fee_ignored response = stub_comms(@gateway, :ssl_request) do @gateway.capture(@amount, 'ch_test_charge', application_fee: 100, icc_data: @emv_credit_card.icc_data) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert data !~ /application_fee/, 'request should not include application_fee' end.respond_with(successful_capture_response_with_icc_data) @@ -1337,24 +1411,68 @@ def test_emv_capture_application_fee_ignored def test_authorization_with_emv_payment_application_fee_included response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@amount, 'ch_test_charge', application_fee: 100, icc_data: @emv_credit_card.icc_data) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert data =~ /application_fee/, 'request should include application_fee' end.respond_with(successful_capture_response_with_icc_data) assert_success response end + def test_authorization_with_emv_payment_sets_capture_to_false + response = stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, 'ch_test_charge', application_fee: 100, icc_data: @emv_credit_card.icc_data) + end.check_request do |_method, _endpoint, data, _headers| + assert data =~ /capture\=false/, 'request should set capture to false' + end.respond_with(successful_capture_response_with_icc_data) + + assert_success response + end def test_passing_stripe_account_header - @gateway.expects(:ssl_request).with do |method, url, post, headers| + @gateway.expects(:ssl_request).with do |_method, _url, _post, headers| headers.include?('Stripe-Account') end.returns(successful_authorization_response) - @options.merge!(stripe_account: fixtures(:stripe_destination)[:stripe_user_id]) + @options[:stripe_account] = fixtures(:stripe_destination)[:stripe_user_id] @gateway.purchase(@amount, @credit_card, @options) end + def test_3ds_source_creation + @gateway.expects(:ssl_request).twice.returns(threeds_first_sources_created_response, threeds_second_sources_created_response) + card_source = @gateway.send(:create_source, @amount, @threeds_card, 'card', @options.merge(@threeds_options)) + assert_success card_source + response = @gateway.send(:create_source, @amount, card_source.params['id'], 'three_d_secure', @options) + assert_equal 'source', response.params['object'] + assert_equal 'pending', response.params['status'] + assert_equal 'three_d_secure', response.params['type'] + assert_equal false, response.params['three_d_secure']['authenticated'] + end + + def test_non3ds_card_source_creation + @gateway.expects(:ssl_request).returns(non_3ds_sources_create_response) + response = @gateway.send(:create_source, @amount, @non_3ds_card, 'card', @options.merge(@threeds_options)) + assert_equal 'source', response.params['object'] + assert_equal 'chargeable', response.params['status'] + assert_equal 'card', response.params['type'] + assert_equal 'not_supported', response.params['card']['three_d_secure'] + end + + def test_webhook_creation + @gateway.expects(:ssl_request).returns(webhook_event_creation_response) + response = @gateway.send(:create_webhook_endpoint, @options.merge(@threeds_options), ['source.chargeable']) + assert_includes response.params['enabled_events'], 'source.chargeable' + assert_equal @options.merge(@threeds_options)[:callback_url], response.params['url'] + end + + def test_webhook_deletion + @gateway.expects(:ssl_request).twice.returns(webhook_event_creation_response, webhook_event_deletion_response) + webhook = @gateway.send(:create_webhook_endpoint, @options.merge(@threeds_options), ['source.chargeable']) + response = @gateway.send(:delete_webhook_endpoint, @options.merge(webhook_id: webhook.params['id'])) + assert_equal response.params['id'], webhook.params['id'] + assert_equal true, response.params['deleted'] + end + def test_verify_good_credentials @gateway.expects(:raw_ssl_request).returns(credentials_are_legit_response) assert @gateway.verify_credentials @@ -1412,6 +1530,35 @@ def pre_scrubbed PRE_SCRUBBED end + def pre_scrubbed_nested_payment_method_data + <<-PRE_SCRUBBED + opening connection to api.stripe.com:443... + opened + starting SSL for api.stripe.com:443... + SSL established + <- "POST /v1/charges HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAuthorization: Basic c2tfdGVzdF9oQkwwTXF6ZGZ6Rnk3OXU0cFloUmVhQlo6\r\nUser-Agent: Stripe/v1 ActiveMerchantBindings/1.45.0\r\nX-Stripe-Client-User-Agent: {\"bindings_version\":\"1.45.0\",\"lang\":\"ruby\",\"lang_version\":\"2.1.3 p242 (2014-09-19)\",\"platform\":\"x86_64-linux\",\"publisher\":\"active_merchant\"}\r\nX-Stripe-Client-User-Metadata: {\"ip\":null}\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nConnection: close\r\nHost: api.stripe.com\r\nContent-Length: 270\r\n\r\n" + <- "amount=100&currency=usd&payment_method_data[card][number]=4242424242424242&payment_method_data[card][exp_month]=9&payment_method_data[card][exp_year]=2015&payment_method_data[card][cvc]=123&payment_method_data[card][name]=Longbob+Longsen&description=ActiveMerchant+Test+Purchase&payment_user_agent=Stripe%2Fv1+ActiveMerchantBindings%2F1.45.0&metadata[email]=wow%40example.com&payment_method_data[card][cryptogram]=sensitive_data&three_d_secure[cryptogram]=123456789abcdefghijklmnop&three_d_secure[apple_pay]=true" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: nginx\r\n" + -> "Date: Tue, 02 Dec 2014 19:44:17 GMT\r\n" + -> "Content-Type: application/json;charset=utf-8\r\n" + -> "Content-Length: 1303\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-Max-Age: 300\r\n" + -> "Cache-Control: no-cache, no-store\r\n" + -> "Request-Id: 89de951c-f880-4c39-93b0-832b3cc6dd32\r\n" + -> "Stripe-Version: 2013-12-03\r\n" + -> "Strict-Transport-Security: max-age=31556926; includeSubDomains\r\n" + -> "\r\n" + reading 1303 bytes... + -> "{\n \"id\": \"ch_155MZJ2gKyKnHxtY1dGqFhSb\",\n \"object\": \"charge\",\n \"created\": 1417549457,\n \"livemode\": false,\n \"paid\": true,\n \"amount\": 100,\n \"currency\": \"usd\",\n \"refunded\": false,\n \"captured\": true,\n \"refunds\": [],\n \"card\": {\n \"id\": \"card_155MZJ2gKyKnHxtYihrJ8z94\",\n \"object\": \"card\",\n \"last4\": \"4242\",\n \"brand\": \"Visa\",\n \"funding\": \"credit\",\n \"exp_month\": 9,\n \"exp_year\": 2015,\n \"fingerprint\": \"944LvWcY01HVTbVc\",\n \"country\": \"US\",\n \"name\": \"Longbob Longsen\",\n \"address_line1\": null,\n \"address_line2\": null,\n \"address_city\": null,\n \"address_state\": null,\n \"address_zip\": null,\n \"address_country\": null,\n \"cvc_check\": \"pass\",\n \"address_line1_check\": null,\n \"address_zip_check\": null,\n \"dynamic_last4\": null,\n \"customer\": null,\n \"type\": \"Visa\"\n },\n \"balance_transaction\": \"txn_155MZJ2gKyKnHxtYxpYDI5OW\",\n \"failure_message\": null,\n \"failure_code\": null,\n \"amount_refunded\": 0,\n \"customer\": null,\n \"invoice\": null,\n \"description\": \"ActiveMerchant Test Purchase\",\n \"dispute\": null,\n \"metadata\": {\n \"email\": \"wow@example.com\"\n },\n \"statement_description\": null,\n \"fraud_details\": {\n \"stripe_report\": \"unavailable\",\n \"user_report\": null\n },\n \"receipt_email\": null,\n \"receipt_number\": null,\n \"shipping\": null\n}\n" + read 1303 bytes + Conn close + PRE_SCRUBBED + end + def pre_scrubbed_with_track_data <<-PRE_SCRUBBED opening connection to api.stripe.com:443... @@ -1471,6 +1618,63 @@ def pre_scrubbed_with_emv_data PRE_SCRUBBED end + def pre_scrubbed_with_account_number + <<-PRE_SCRUBBED + opening connection to api.stripe.com:443... + opened + starting SSL for api.stripe.com:443... + SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES128-GCM-SHA256 + <- "POST /v1/tokens?bank_account[account_number]=000123456789&bank_account[country]=US&bank_account[currency]=usd&bank_account[routing_number]=110000000&bank_account[name]=Jim+Smith&bank_account[account_holder_type]=individual HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAuthorization: Basic c2tfdGVzdF8zT0Q0VGRLU0lPaERPTDIxNDZKSmNDNzk6\r\nUser-Agent: Stripe/v1 ActiveMerchantBindings/1.123.0\r\nStripe-Version: 2015-04-07\r\nX-Stripe-Client-User-Agent: {\"bindings_version\":\"1.123.0\",\"lang\":\"ruby\",\"lang_version\":\"2.6.6 p146 (2020-03-31)\",\"platform\":\"x86_64-darwin20\",\"publisher\":\"active_merchant\"}\r\nX-Stripe-Client-User-Metadata: {\"ip\":null}\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: 0\r\n\r\n" + <- "" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: nginx\r\n" + -> "Date: Wed, 06 Oct 2021 18:51:30 GMT\r\n" + -> "Content-Type: application/json\r\n" + -> "Content-Length: 610\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" + -> "request-id: req_cueTxQR09SjIOA\r\n" + -> "stripe-version: 2015-04-07\r\n" + -> "Strict-Transport-Security: max-age=31556926; includeSubDomains; preload\r\n" + -> "\r\n" + reading 610 bytes... + -> "{\n \"id\": \"btok_1JhfDCAWOtgoysogF0IbYRWH\",\n \"object\": \"token\",\n \"bank_account\": {\n \"id\": \"ba_1JhfDCAWOtgoysogLB5vljcp\",\n \"object\": \"bank_account\",\n \"account_holder_name\": \"Jim Smith\",\n \"account_holder_type\": \"individual\",\n \"account_type\": null,\n \"bank_name\": \"STRIPE TEST BANK\",\n \"country\": \"US\",\n \"currency\": \"usd\",\n \"fingerprint\": \"uCkXlMFxqys7GosR\",\n \"last4\": \"6789\",\n \"name\": \"Jim Smith\",\n \"routing_number\": \"110000000\",\n \"status\": \"new\"\n },\n \"client_ip\": \"172.74.90.160\",\n \"created\": 1633546290,\n \"livemode\": false,\n \"type\": \"bank_account\",\n \"used\": false\n}\n" + read 610 bytes + Conn close + opening connection to api.stripe.com:443... + opened + starting SSL for api.stripe.com:443... + SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES128-GCM-SHA256 + <- "POST /v1/customers HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAuthorization: Basic c2tfdGVzdF8zT0Q0VGRLU0lPaERPTDIxNDZKSmNDNzk6\r\nUser-Agent: Stripe/v1 ActiveMerchantBindings/1.123.0\r\nStripe-Version: 2015-04-07\r\nX-Stripe-Client-User-Agent: {\"bindings_version\":\"1.123.0\",\"lang\":\"ruby\",\"lang_version\":\"2.6.6 p146 (2020-03-31)\",\"platform\":\"x86_64-darwin20\",\"publisher\":\"active_merchant\"}\r\nX-Stripe-Client-User-Metadata: {\"ip\":null}\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: 36\r\n\r\n" + <- "source=btok_1JhfDCAWOtgoysogF0IbYRWH" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: nginx\r\n" + -> "Date: Wed, 06 Oct 2021 18:51:31 GMT\r\n" + -> "Content-Type: application/json\r\n" + -> "Content-Length: 1713\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" + -> "request-id: req_CfMAs2B351RDr0\r\n" + -> "stripe-version: 2015-04-07\r\n" + -> "Strict-Transport-Security: max-age=31556926; includeSubDomains; preload\r\n" + -> "\r\n" + reading 1713 bytes... + -> "{\n \"id\": \"cus_KMNzQZK4SN7Agn\",\n \"object\": \"customer\",\n \"account_balance\": 0,\n \"address\": null,\n \"balance\": 0,\n \"created\": 1633546290,\n \"currency\": null,\n \"default_source\": \"ba_1JhfDCAWOtgoysogLB5vljcp\",\n \"delinquent\": false,\n \"description\": null,\n \"discount\": null,\n \"email\": null,\n \"invoice_prefix\": \"02D58981\",\n \"invoice_settings\": {\n \"custom_fields\": null,\n \"default_payment_method\": null,\n \"footer\": null\n },\n \"livemode\": false,\n \"metadata\": {\n },\n \"name\": null,\n \"next_invoice_sequence\": 1,\n \"phone\": null,\n \"preferred_locales\": [\n\n ],\n \"shipping\": null,\n \"sources\": {\n \"object\": \"list\",\n \"data\": [\n {\n \"id\": \"ba_1JhfDCAWOtgoysogLB5vljcp\",\n \"object\": \"bank_account\",\n \"account_holder_name\": \"Jim Smith\",\n \"account_holder_type\": \"individual\",\n \"account_type\": null,\n \"bank_name\": \"STRIPE TEST BANK\",\n \"country\": \"US\",\n \"currency\": \"usd\",\n \"customer\": \"cus_KMNzQZK4SN7Agn\",\n \"fingerprint\": \"uCkXlMFxqys7GosR\",\n \"last4\": \"6789\",\n \"metadata\": {\n },\n \"name\": \"Jim Smith\",\n \"routing_number\": \"110000000\",\n \"status\": \"new\"\n }\n ],\n \"has_more\": false,\n \"total_count\": 1,\n \"url\": \"/v1/customers/cus_KMNzQZK4SN7Agn/sources\"\n },\n \"subscriptions\": {\n \"object\": \"list\",\n \"data\": [\n\n ],\n \"has_more\": false,\n \"total_count\": 0,\n \"url\": \"/v1/customers/cus_KMNzQZK4SN7Agn/subscriptions\"\n },\n \"tax_exempt\": \"none\",\n \"tax_ids\": {\n \"object\": \"list\",\n \"data\": [\n\n ],\n \"has_more\": false,\n \"total_count\": 0,\n \"url\": \"/v1/customers/cus_KMNzQZK4SN7Agn/tax_ids\"\n },\n \"tax_info\": null,\n \"tax_info_verification\": null\n}\n" + read 1713 bytes + Conn close + PRE_SCRUBBED + end + def post_scrubbed_with_emv_data <<-POST_SCRUBBED opening connection to api.stripe.com:443... @@ -1559,6 +1763,92 @@ def post_scrubbed POST_SCRUBBED end + def post_scrubbed_nested_payment_method_data + <<-POST_SCRUBBED + opening connection to api.stripe.com:443... + opened + starting SSL for api.stripe.com:443... + SSL established + <- "POST /v1/charges HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAuthorization: Basic [FILTERED]\r\nUser-Agent: Stripe/v1 ActiveMerchantBindings/1.45.0\r\nX-Stripe-Client-User-Agent: {\"bindings_version\":\"1.45.0\",\"lang\":\"ruby\",\"lang_version\":\"2.1.3 p242 (2014-09-19)\",\"platform\":\"x86_64-linux\",\"publisher\":\"active_merchant\"}\r\nX-Stripe-Client-User-Metadata: {\"ip\":null}\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nConnection: close\r\nHost: api.stripe.com\r\nContent-Length: 270\r\n\r\n" + <- "amount=100&currency=usd&payment_method_data[card][number]=[FILTERED]&payment_method_data[card][exp_month]=9&payment_method_data[card][exp_year]=2015&payment_method_data[card][cvc]=[FILTERED]&payment_method_data[card][name]=Longbob+Longsen&description=ActiveMerchant+Test+Purchase&payment_user_agent=Stripe%2Fv1+ActiveMerchantBindings%2F1.45.0&metadata[email]=wow%40example.com&payment_method_data[card][cryptogram]=[FILTERED]&three_d_secure[cryptogram]=[FILTERED]&three_d_secure[apple_pay]=true" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: nginx\r\n" + -> "Date: Tue, 02 Dec 2014 19:44:17 GMT\r\n" + -> "Content-Type: application/json;charset=utf-8\r\n" + -> "Content-Length: 1303\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-Max-Age: 300\r\n" + -> "Cache-Control: no-cache, no-store\r\n" + -> "Request-Id: 89de951c-f880-4c39-93b0-832b3cc6dd32\r\n" + -> "Stripe-Version: 2013-12-03\r\n" + -> "Strict-Transport-Security: max-age=31556926; includeSubDomains\r\n" + -> "\r\n" + reading 1303 bytes... + -> "{\n \"id\": \"ch_155MZJ2gKyKnHxtY1dGqFhSb\",\n \"object\": \"charge\",\n \"created\": 1417549457,\n \"livemode\": false,\n \"paid\": true,\n \"amount\": 100,\n \"currency\": \"usd\",\n \"refunded\": false,\n \"captured\": true,\n \"refunds\": [],\n \"card\": {\n \"id\": \"card_155MZJ2gKyKnHxtYihrJ8z94\",\n \"object\": \"card\",\n \"last4\": \"4242\",\n \"brand\": \"Visa\",\n \"funding\": \"credit\",\n \"exp_month\": 9,\n \"exp_year\": 2015,\n \"fingerprint\": \"944LvWcY01HVTbVc\",\n \"country\": \"US\",\n \"name\": \"Longbob Longsen\",\n \"address_line1\": null,\n \"address_line2\": null,\n \"address_city\": null,\n \"address_state\": null,\n \"address_zip\": null,\n \"address_country\": null,\n \"cvc_check\": \"pass\",\n \"address_line1_check\": null,\n \"address_zip_check\": null,\n \"dynamic_last4\": null,\n \"customer\": null,\n \"type\": \"Visa\"\n },\n \"balance_transaction\": \"txn_155MZJ2gKyKnHxtYxpYDI5OW\",\n \"failure_message\": null,\n \"failure_code\": null,\n \"amount_refunded\": 0,\n \"customer\": null,\n \"invoice\": null,\n \"description\": \"ActiveMerchant Test Purchase\",\n \"dispute\": null,\n \"metadata\": {\n \"email\": \"wow@example.com\"\n },\n \"statement_description\": null,\n \"fraud_details\": {\n \"stripe_report\": \"unavailable\",\n \"user_report\": null\n },\n \"receipt_email\": null,\n \"receipt_number\": null,\n \"shipping\": null\n}\n" + read 1303 bytes + Conn close + POST_SCRUBBED + end + + def post_scrubbed_with_account_number + <<-POST_SCRUBBED + opening connection to api.stripe.com:443... + opened + starting SSL for api.stripe.com:443... + SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES128-GCM-SHA256 + <- "POST /v1/tokens?bank_account[account_number]=[FILTERED]&bank_account[country]=US&bank_account[currency]=usd&bank_account[routing_number]=110000000&bank_account[name]=Jim+Smith&bank_account[account_holder_type]=individual HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAuthorization: Basic [FILTERED]\r\nUser-Agent: Stripe/v1 ActiveMerchantBindings/1.123.0\r\nStripe-Version: 2015-04-07\r\nX-Stripe-Client-User-Agent: {\"bindings_version\":\"1.123.0\",\"lang\":\"ruby\",\"lang_version\":\"2.6.6 p146 (2020-03-31)\",\"platform\":\"x86_64-darwin20\",\"publisher\":\"active_merchant\"}\r\nX-Stripe-Client-User-Metadata: {\"ip\":null}\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: 0\r\n\r\n" + <- "" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: nginx\r\n" + -> "Date: Wed, 06 Oct 2021 18:51:30 GMT\r\n" + -> "Content-Type: application/json\r\n" + -> "Content-Length: 610\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" + -> "request-id: req_cueTxQR09SjIOA\r\n" + -> "stripe-version: 2015-04-07\r\n" + -> "Strict-Transport-Security: max-age=31556926; includeSubDomains; preload\r\n" + -> "\r\n" + reading 610 bytes... + -> "{\n \"id\": \"btok_1JhfDCAWOtgoysogF0IbYRWH\",\n \"object\": \"token\",\n \"bank_account\": {\n \"id\": \"ba_1JhfDCAWOtgoysogLB5vljcp\",\n \"object\": \"bank_account\",\n \"account_holder_name\": \"Jim Smith\",\n \"account_holder_type\": \"individual\",\n \"account_type\": null,\n \"bank_name\": \"STRIPE TEST BANK\",\n \"country\": \"US\",\n \"currency\": \"usd\",\n \"fingerprint\": \"uCkXlMFxqys7GosR\",\n \"last4\": \"6789\",\n \"name\": \"Jim Smith\",\n \"routing_number\": \"110000000\",\n \"status\": \"new\"\n },\n \"client_ip\": \"172.74.90.160\",\n \"created\": 1633546290,\n \"livemode\": false,\n \"type\": \"bank_account\",\n \"used\": false\n}\n" + read 610 bytes + Conn close + opening connection to api.stripe.com:443... + opened + starting SSL for api.stripe.com:443... + SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES128-GCM-SHA256 + <- "POST /v1/customers HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAuthorization: Basic [FILTERED]\r\nUser-Agent: Stripe/v1 ActiveMerchantBindings/1.123.0\r\nStripe-Version: 2015-04-07\r\nX-Stripe-Client-User-Agent: {\"bindings_version\":\"1.123.0\",\"lang\":\"ruby\",\"lang_version\":\"2.6.6 p146 (2020-03-31)\",\"platform\":\"x86_64-darwin20\",\"publisher\":\"active_merchant\"}\r\nX-Stripe-Client-User-Metadata: {\"ip\":null}\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: 36\r\n\r\n" + <- "source=btok_1JhfDCAWOtgoysogF0IbYRWH" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: nginx\r\n" + -> "Date: Wed, 06 Oct 2021 18:51:31 GMT\r\n" + -> "Content-Type: application/json\r\n" + -> "Content-Length: 1713\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" + -> "request-id: req_CfMAs2B351RDr0\r\n" + -> "stripe-version: 2015-04-07\r\n" + -> "Strict-Transport-Security: max-age=31556926; includeSubDomains; preload\r\n" + -> "\r\n" + reading 1713 bytes... + -> "{\n \"id\": \"cus_KMNzQZK4SN7Agn\",\n \"object\": \"customer\",\n \"account_balance\": 0,\n \"address\": null,\n \"balance\": 0,\n \"created\": 1633546290,\n \"currency\": null,\n \"default_source\": \"ba_1JhfDCAWOtgoysogLB5vljcp\",\n \"delinquent\": false,\n \"description\": null,\n \"discount\": null,\n \"email\": null,\n \"invoice_prefix\": \"02D58981\",\n \"invoice_settings\": {\n \"custom_fields\": null,\n \"default_payment_method\": null,\n \"footer\": null\n },\n \"livemode\": false,\n \"metadata\": {\n },\n \"name\": null,\n \"next_invoice_sequence\": 1,\n \"phone\": null,\n \"preferred_locales\": [\n\n ],\n \"shipping\": null,\n \"sources\": {\n \"object\": \"list\",\n \"data\": [\n {\n \"id\": \"ba_1JhfDCAWOtgoysogLB5vljcp\",\n \"object\": \"bank_account\",\n \"account_holder_name\": \"Jim Smith\",\n \"account_holder_type\": \"individual\",\n \"account_type\": null,\n \"bank_name\": \"STRIPE TEST BANK\",\n \"country\": \"US\",\n \"currency\": \"usd\",\n \"customer\": \"cus_KMNzQZK4SN7Agn\",\n \"fingerprint\": \"uCkXlMFxqys7GosR\",\n \"last4\": \"6789\",\n \"metadata\": {\n },\n \"name\": \"Jim Smith\",\n \"routing_number\": \"110000000\",\n \"status\": \"new\"\n }\n ],\n \"has_more\": false,\n \"total_count\": 1,\n \"url\": \"/v1/customers/cus_KMNzQZK4SN7Agn/sources\"\n },\n \"subscriptions\": {\n \"object\": \"list\",\n \"data\": [\n\n ],\n \"has_more\": false,\n \"total_count\": 0,\n \"url\": \"/v1/customers/cus_KMNzQZK4SN7Agn/subscriptions\"\n },\n \"tax_exempt\": \"none\",\n \"tax_ids\": {\n \"object\": \"list\",\n \"data\": [\n\n ],\n \"has_more\": false,\n \"total_count\": 0,\n \"url\": \"/v1/customers/cus_KMNzQZK4SN7Agn/tax_ids\"\n },\n \"tax_info\": null,\n \"tax_info_verification\": null\n}\n" + read 1713 bytes + Conn close + POST_SCRUBBED + end + def successful_new_customer_response <<-RESPONSE { @@ -1837,6 +2127,173 @@ def successful_capture_response RESPONSE end + def successful_capture_response_without_icc_data + <<-RESPONSE + { + "id": "ch_test_emv_charge", + "object": "charge", + "amount": 1000, + "amount_refunded": 0, + "application": "ca_37BT8jOfv0Cu42Vfd", + "application_fee": "fee_test_fee", + "application_fee_amount": 55, + "authorization_code": "123456", + "balance_transaction": { + "id": "txn_1FSQUtFwNjfzuchLeWPB9X8K", + "object": "balance_transaction", + "amount": 1000, + "available_on": 1571184000, + "created": 1570809463, + "currency": "usd", + "description": null, + "exchange_rate": null, + "fee": 55, + "fee_details": [ + { + "amount": 55, + "application": "ca_37BT8jOfv0Cu42Vfd", + "currency": "usd", + "description": "application fee", + "type": "application_fee" + } + ], + "net": 945, + "source": "ch_test_emv_charge", + "sourced_transfers": { + "object": "list", + "data": [ + ], + "has_more": false, + "total_count": 0, + "url": "/v1/transfers?source_transaction=ch_test_emv_charge" + }, + "status": "pending", + "type": "charge" + }, + "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": 1570809458, + "currency": "usd", + "customer": null, + "description": null, + "destination": null, + "dispute": null, + "failure_code": null, + "failure_message": null, + "fraud_details": { + }, + "invoice": null, + "livemode": false, + "metadata": { + "card_read_method": "contact", + "shop_id": "1", + "shop_name": "Shop 1", + "transaction_fee_total_amount": "55", + "transaction_fee_tax_amount": "0", + "payments_charge_id": "86", + "order_transaction_id": "149", + "order_id": "c62.1" + }, + "on_behalf_of": null, + "order": null, + "outcome": { + "network_status": "approved_by_network", + "reason": null, + "risk_level": "normal", + "risk_score": 2, + "seller_message": "Payment complete.", + "type": "authorized" + }, + "paid": true, + "payment_intent": null, + "payment_method": "card_1FSQUo", + "payment_method_details": { + "card": { + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": null + }, + "country": "US", + "ds_transaction_id": null, + "exp_month": 12, + "exp_year": 2022, + "fingerprint": "PgHpMSUia1FIuXM6", + "funding": "unknown", + "installments": null, + "last4": "0119", + "moto": null, + "network": "visa", + "network_transaction_id": "ihQUSGFPfpVpg6J3", + "three_d_secure": null, + "wallet": null + }, + "type": "card" + }, + "receipt_email": null, + "receipt_number": null, + "receipt_url": "https://pay.stripe.com/receipts/acct_1FPxOBFwNjfzuchL/ch_test_emv_charge/rcpt_FyNFASxNs66DXe7zTe9jW9hpYZkQda9", + "refunded": false, + "refunds": { + "object": "list", + "data": [ + ], + "has_more": false, + "total_count": 0, + "url": "/v1/charges/ch_test_emv_charge/refunds" + }, + "review": null, + "shipping": null, + "source": { + "id": "card_1FSQUo", + "object": "card", + "address_city": null, + "address_country": null, + "address_line1": null, + "address_line1_check": null, + "address_line2": null, + "address_state": null, + "address_zip": null, + "address_zip_check": null, + "brand": "Visa", + "country": "US", + "customer": null, + "cvc_check": null, + "dynamic_last4": null, + "emv_auth_data": "8A023030", + "exp_month": 12, + "exp_year": 2022, + "fingerprint": "ihQUSGFPfpVpg6J3", + "funding": "unknown", + "last4": "0119", + "metadata": { + }, + "name": null, + "tokenization_method": null + }, + "source_transfer": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "succeeded", + "transfer_data": null, + "transfer_group": null + } + RESPONSE + end + def successful_capture_response_with_icc_data <<-RESPONSE { @@ -1915,7 +2372,7 @@ def successful_capture_response_with_icc_data RESPONSE end - def successful_purchase_response(refunded=false) + def successful_purchase_response(refunded = false) <<-RESPONSE { "amount": 400, @@ -1939,8 +2396,7 @@ def successful_purchase_response(refunded=false) RESPONSE end - def successful_partially_refunded_response(options = {}) - options = {:livemode=>false}.merge!(options) + def successful_partially_refunded_response <<-RESPONSE { "id": "re_test_refund", @@ -2357,4 +2813,191 @@ def token_params 'used' => false } end + + def threeds_first_sources_created_response + <<-RESPONSE + { + "id": "src_1Dj5lqAWOtgoysogqA4CJX9Y", + "object": "source", + "amount": null, + "card": { + "exp_month": 9, + "exp_year": 2019, + "brand": "Visa", + "country": "US", + "cvc_check": "unchecked", + "fingerprint": "53W491Mwz0OMuEJr", + "funding": "credit", + "last4": "3063", + "three_d_secure": "required", + "name": null, + "address_line1_check": null, + "address_zip_check": null, + "tokenization_method": null, + "dynamic_last4": null + }, + "client_secret": "src_client_secret_EBShsJorDXd6WD521kRIQlbP", + "created": 1545228694, + "currency": null, + "flow": "none", + "livemode": false, + "metadata": { + }, + "owner": { + "address": null, + "email": null, + "name": null, + "phone": null, + "verified_address": null, + "verified_email": null, + "verified_name": null, + "verified_phone": null + }, + "statement_descriptor": null, + "status": "chargeable", + "type": "card", + "usage": "reusable" + } + RESPONSE + end + + def threeds_second_sources_created_response + <<-RESPONSE + { + "id": "src_1Dj5lrAWOtgoysog910mc8oS", + "object": "source", + "amount": 100, + "client_secret": "src_client_secret_EBShU4HfxQAw2bVGMxvRECO1", + "created": 1545228695, + "currency": "usd", + "flow": "redirect", + "livemode": false, + "metadata": { + }, + "owner": { + "address": { + "city": null, + "country": null, + "line1": "", + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": null, + "phone": null, + "verified_address": null, + "verified_email": null, + "verified_name": null, + "verified_phone": null + }, + "redirect": { + "failure_reason": null, + "return_url": "http://www.example.com/callback", + "status": "pending", + "url": "https://hooks.stripe.com/redirect/authenticate/src_1Dj5lrAWOtgoysog910mc8oS?client_secret=src_client_secret_EBShU4HfxQAw2bVGMxvRECO1" + }, + "statement_descriptor": null, + "status": "pending", + "three_d_secure": { + "card": "src_1Dj5lqAWOtgoysogqA4CJX9Y", + "brand": "Visa", + "country": "US", + "cvc_check": "unchecked", + "exp_month": 9, + "exp_year": 2019, + "fingerprint": "53W491Mwz0OMuEJr", + "funding": "credit", + "last4": "3063", + "three_d_secure": "required", + "customer": null, + "authenticated": false, + "name": null, + "address_line1_check": null, + "address_zip_check": null, + "tokenization_method": null, + "dynamic_last4": null + }, + "type": "three_d_secure", + "usage": "single_use" + } + RESPONSE + end + + def non_3ds_sources_create_response + <<-RESPONSE + { + "id": "src_1Dj5yAAWOtgoysogPB6hwOa1", + "object": "source", + "amount": null, + "card": { + "exp_month": 9, + "exp_year": 2019, + "brand": "American Express", + "country": "US", + "cvc_check": "unchecked", + "fingerprint": "DjZpoV89lmOMsJLF", + "funding": "credit", + "last4": "0005", + "three_d_secure": "not_supported", + "name": null, + "address_line1_check": null, + "address_zip_check": null, + "tokenization_method": null, + "dynamic_last4": null + }, + "client_secret": "src_client_secret_EBStgH6cBMsODApAChcj9Kkq", + "created": 1545229458, + "currency": null, + "flow": "none", + "livemode": false, + "metadata": { + }, + "owner": { + "address": null, + "email": null, + "name": null, + "phone": null, + "verified_address": null, + "verified_email": null, + "verified_name": null, + "verified_phone": null + }, + "statement_descriptor": null, + "status": "chargeable", + "type": "card", + "usage": "reusable" + } + RESPONSE + end + + def webhook_event_creation_response + <<-RESPONSE + { + "id": "we_1Dj8GvAWOtgoysogAW1V5FFm", + "object": "webhook_endpoint", + "application": null, + "created": 1545238309, + "enabled_events": [ + "source.chargeable", + "source.failed", + "source.canceled" + ], + "livemode": false, + "secret": "whsec_sJVAv7f1rddt1bNhouoDvxwQbZ8t0Pgn", + "status": "enabled", + "url": "http://www.example.com/callback" + } + RESPONSE + end + + def webhook_event_deletion_response + <<-RESPONSE + { + "id": "we_1Dj8GvAWOtgoysogAW1V5FFm", + "object": "webhook_endpoint", + "deleted": true + } + RESPONSE + end end diff --git a/test/unit/gateways/sum_up_test.rb b/test/unit/gateways/sum_up_test.rb new file mode 100644 index 00000000000..6a34efe96ee --- /dev/null +++ b/test/unit/gateways/sum_up_test.rb @@ -0,0 +1,463 @@ +require 'test_helper' + +class SumUpTest < Test::Unit::TestCase + include CommStub + + 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', + partner_id: 'PartnerId', + order_id: SecureRandom.uuid + } + 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_successful_purchase_with_options + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + json_data = JSON.parse(data) + if checkout_ref = json_data['checkout_reference'] + assert_match(/#{@options[:partner_id]}-#{@options[:order_id]}/, checkout_ref) + end + end.respond_with(successful_create_checkout_response) + end + + def test_successful_purchase_without_partner_id + @options.delete(:partner_id) + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + json_data = JSON.parse(data) + if checkout_ref = json_data['checkout_reference'] + assert_match(/#{@options[:order_id]}/, checkout_ref) + end + end.respond_with(successful_create_checkout_response) + 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/swipe_checkout_test.rb b/test/unit/gateways/swipe_checkout_test.rb index e6c4e0da63a..0536f400685 100644 --- a/test/unit/gateways/swipe_checkout_test.rb +++ b/test/unit/gateways/swipe_checkout_test.rb @@ -19,7 +19,7 @@ def setup end def test_supported_countries - assert @gateway.supported_countries == ['NZ', 'CA'] + assert @gateway.supported_countries == %w[NZ CA] end def test_successful_purchase diff --git a/test/unit/gateways/telr_test.rb b/test/unit/gateways/telr_test.rb index 8e3ac5766bd..e34399b6faf 100644 --- a/test/unit/gateways/telr_test.rb +++ b/test/unit/gateways/telr_test.rb @@ -43,7 +43,7 @@ def test_successful_authorize_and_capture capture = stub_comms do @gateway.capture(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/029894296182/, data) end.respond_with(successful_capture_response) @@ -77,7 +77,7 @@ def test_successful_void void = stub_comms do @gateway.void(response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/029894296182/, data) end.respond_with(successful_void_response) @@ -87,7 +87,7 @@ def test_successful_void def test_failed_void response = stub_comms do @gateway.void('5d53a33d960c46d00f5dc061947d998c') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/5d53a33d960c46d00f5dc061947d998c/, data) end.respond_with(failed_void_response) @@ -104,7 +104,7 @@ def test_successful_refund refund = stub_comms do @gateway.refund(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/029724176180/, data) end.respond_with(successful_refund_response) @@ -145,7 +145,7 @@ def test_successful_reference_purchase ref_purchase = stub_comms do @gateway.purchase(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/029724176180/, data) end.respond_with(successful_reference_purchase_response) diff --git a/test/unit/gateways/tns_test.rb b/test/unit/gateways/tns_test.rb index f97a0e6e242..d7939f10630 100644 --- a/test/unit/gateways/tns_test.rb +++ b/test/unit/gateways/tns_test.rb @@ -48,7 +48,7 @@ def test_authorize_and_capture capture = stub_comms(@gateway, :ssl_request) do @gateway.capture(@amount, response.authorization) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/f3d100a7-18d9-4609-aabc-8a710ad0e210/, data) end.respond_with(successful_capture_response) @@ -65,7 +65,7 @@ def test_refund refund = stub_comms(@gateway, :ssl_request) do @gateway.refund(@amount, response.authorization) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/ce61e06e-8c92-4a0f-a491-6eb473d883dd/, data) end.respond_with(successful_refund_response) @@ -82,7 +82,7 @@ def test_void void = stub_comms(@gateway, :ssl_request) do @gateway.void(response.authorization) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/ce61e06e-8c92-4a0f-a491-6eb473d883dd/, data) end.respond_with(successful_void_response) @@ -91,16 +91,16 @@ def test_void def test_passing_alpha3_country_code stub_comms(@gateway, :ssl_request) do - @gateway.authorize(@amount, @credit_card, :billing_address => {country: 'US'}) - end.check_request do |method, endpoint, data, headers| + @gateway.authorize(@amount, @credit_card, billing_address: { country: 'US' }) + end.check_request do |_method, _endpoint, data, _headers| assert_match(/USA/, data) end.respond_with(successful_authorize_response) end def test_non_existent_country stub_comms(@gateway, :ssl_request) do - @gateway.authorize(@amount, @credit_card, :billing_address => {country: 'Blah'}) - end.check_request do |method, endpoint, data, headers| + @gateway.authorize(@amount, @credit_card, billing_address: { country: 'Blah' }) + end.check_request do |_method, _endpoint, data, _headers| assert_match(/"country":null/, data) end.respond_with(successful_authorize_response) end @@ -108,15 +108,15 @@ def test_non_existent_country def test_passing_cvv stub_comms(@gateway, :ssl_request) do @gateway.authorize(@amount, @credit_card) - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/#{@credit_card.verification_value}/, data) end.respond_with(successful_authorize_response) end def test_passing_billing_address stub_comms(@gateway, :ssl_request) do - @gateway.authorize(@amount, @credit_card, :billing_address => address) - end.check_request do |method, endpoint, data, headers| + @gateway.authorize(@amount, @credit_card, billing_address: address) + end.check_request do |_method, _endpoint, data, _headers| parsed = JSON.parse(data) assert_equal('456 My Street', parsed['billing']['address']['street']) assert_equal('K1C2N6', parsed['billing']['address']['postcodeZip']) @@ -125,8 +125,8 @@ def test_passing_billing_address def test_passing_shipping_name stub_comms(@gateway, :ssl_request) do - @gateway.authorize(@amount, @credit_card, :shipping_address => address) - end.check_request do |method, endpoint, data, headers| + @gateway.authorize(@amount, @credit_card, shipping_address: address) + end.check_request do |_method, _endpoint, data, _headers| parsed = JSON.parse(data) assert_equal('Jim', parsed['shipping']['firstName']) assert_equal('Smith', parsed['shipping']['lastName']) @@ -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', @@ -166,24 +166,8 @@ 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.check_request do |_method, endpoint, _data, _headers| + assert_match(/secure.uat.tnspayments.com/, endpoint) end.respond_with(successful_capture_response) assert_success response diff --git a/test/unit/gateways/trans_first_test.rb b/test/unit/gateways/trans_first_test.rb index 34f04f9a75c..d8a2ca93ed2 100644 --- a/test/unit/gateways/trans_first_test.rb +++ b/test/unit/gateways/trans_first_test.rb @@ -1,17 +1,16 @@ require 'test_helper' class TransFirstTest < Test::Unit::TestCase - def setup @gateway = TransFirstGateway.new( - :login => 'LOGIN', - :password => 'PASSWORD' + login: 'LOGIN', + password: 'PASSWORD' ) @credit_card = credit_card('4242424242424242') @check = check @options = { - :billing_address => address + billing_address: address } @amount = 100 end @@ -68,7 +67,7 @@ def test_successful_refund response = @gateway.refund(@amount, 'TransID') assert_success response assert_equal '207686608|creditcard', response.authorization - assert_equal @amount, response.params['amount'].to_i*100 + assert_equal @amount, response.params['amount'].to_i * 100 end def test_failed_refund @@ -351,7 +350,7 @@ def successful_void_response <AVSCode>N</AVSCode> <CVV2Code /> </BankCardRefundStatus> - XML + XML end def failed_void_response diff --git a/test/unit/gateways/trans_first_transaction_express_test.rb b/test/unit/gateways/trans_first_transaction_express_test.rb index 1bbfdf81f88..38521f93391 100644 --- a/test/unit/gateways/trans_first_transaction_express_test.rb +++ b/test/unit/gateways/trans_first_transaction_express_test.rb @@ -26,6 +26,25 @@ def test_successful_purchase assert response.test? end + def test_strip_hyphens_from_zip + options = { + billing_address: { + name: 'John & Mary Smith', + address1: '1 Main St.', + city: 'Burlington', + state: 'MA', + zip: '01803-3747', + country: 'US' + } + } + + stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/018033747/, data) + end.respond_with(successful_purchase_response) + end + def test_failed_purchase response = stub_comms do @gateway.purchase(@declined_amount, @credit_card) @@ -63,7 +82,7 @@ def test_successful_authorize_and_capture capture = stub_comms do @gateway.capture(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/000015377801/, data) end.respond_with(successful_capture_response) @@ -99,7 +118,7 @@ def test_successful_void void = stub_comms do @gateway.void(response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/000015212561/, data) end.respond_with(successful_void_response) @@ -109,7 +128,7 @@ def test_successful_void def test_failed_void response = stub_comms do @gateway.void('purchase|5d53a33d960c46d00f5dc061947d998c') - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/5d53a33d960c46d00f5dc061947d998c/, data) end.respond_with(failed_void_response) @@ -127,7 +146,7 @@ def test_successful_refund refund = stub_comms do @gateway.refund(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/000015212561/, data) end.respond_with(successful_refund_response) @@ -153,7 +172,7 @@ def test_successful_refund_with_echeck refund = stub_comms do @gateway.refund(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/000028705491/, data) end.respond_with(successful_refund_echeck_response) @@ -238,10 +257,23 @@ def test_empty_response_fails assert_equal nil, response.message end + def test_transaction_code_xml_tag_added_with_correct_prefix + stub_comms do + @gateway.purchase(@amount, @credit_card) + end.check_request do |_endpoint, data, _headers| + doc = Nokogiri::XML(data) + assert_not_empty doc.xpath('//v1:tranCode', 'v1' => 'http://postilion/realtime/merchantframework/xsd/v1/') + end.respond_with(successful_purchase_response) + end + def test_transcript_scrubbing assert_equal scrubbed_transcript, @gateway.scrub(transcript) end + def test_account_number_scrubbing + assert_equal post_scrubbed_account_number, @gateway.scrub(pre_scrubbed_account_number) + end + private def successful_purchase_response @@ -321,62 +353,118 @@ def empty_purchase_response end def transcript - <<-PRE_SCRUBBED -opening connection to ws.cert.transactionexpress.com:443... -opened -starting SSL for ws.cert.transactionexpress.com:443... -SSL established -<- "POST /portal/merchantframework/MerchantWebServices-v1?wsdl HTTP/1.1\r\nContent-Type: text/xml\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: ws.cert.transactionexpress.com\r\nContent-Length: 1186\r\n\r\n" -<- "<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\"><soapenv:Body><v1:SendTranRequest xmlns:v1=\"http://postilion/realtime/merchantframework/xsd/v1/\"><v1:merc><v1:id>7777778764</v1:id><v1:regKey>M84PKPDMD5BY86HN</v1:regKey><v1:inType>1</v1:inType></v1:merc><v1:tranCode>1</v1:tranCode><v1:card><v1:pan>4485896261017708</v1:pan><v1:xprDt>1709</v1:xprDt></v1:card><v1:contact><v1:fullName>Longbob Longsen</v1:fullName><v1:coName>Acme</v1:coName><v1:title>QA Manager</v1:title><v1:phone><v1:type>4</v1:type><v1:nr>3334445555</v1:nr></v1:phone><v1:addrLn1>450 Main</v1:addrLn1><v1:addrLn2>Suite 100</v1:addrLn2><v1:city>Broomfield</v1:city><v1:state>CO</v1:state><v1:zipCode>85284</v1:zipCode><v1:ctry>US</v1:ctry><v1:email>example@example.com</v1:email><v1:ship><v1:fullName>Longbob Longsen</v1:fullName><v1:addrLn1>450 Main</v1:addrLn1><v1:addrLn2>Suite 100</v1:addrLn2><v1:city>Broomfield</v1:city><v1:state>CO</v1:state><v1:zipCode>85284</v1:zipCode><v1:phone>3334445555</v1:phone></v1:ship></v1:contact><v1:reqAmt>100</v1:reqAmt><v1:authReq><v1:ordNr>7a0f975b6e86aff44364360cbc6d0f00</v1:ordNr></v1:authReq></v1:SendTranRequest></soapenv:Body></soapenv:Envelope>" --> "HTTP/1.1 200 OK\r\n" --> "Content-Type: text/xml;charset=utf-8\r\n" --> "Date: Thu, 21 Jan 2016 20:09:44 GMT\r\n" --> "Server: WebServer\r\n" --> "Set-Cookie: NSC_UMT12_DFSU-xt.dfsu.UYQ.dpn=ffffffff0918172545525d5f4f58455e445a4a42378b;expires=Thu, 21-Jan-2016 20:17:43 GMT;path=/;secure;httponly\r\n" --> "Cache-Control: private\r\n" --> "Content-Encoding: gzip\r\n" --> "Transfer-Encoding: chunked\r\n" --> "\r\n" --> "1AA \r\n" -reading 426 bytes... --> "" -read 426 bytes -reading 2 bytes... --> "\r\n" -read 2 bytes --> "0\r\n" --> "\r\n" -Conn close + <<~PRE_SCRUBBED + opening connection to ws.cert.transactionexpress.com:443... + opened + starting SSL for ws.cert.transactionexpress.com:443... + SSL established + <- "POST /portal/merchantframework/MerchantWebServices-v1?wsdl HTTP/1.1\r\nContent-Type: text/xml\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: ws.cert.transactionexpress.com\r\nContent-Length: 1186\r\n\r\n" + <- "<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\"><soapenv:Body><v1:SendTranRequest xmlns:v1=\"http://postilion/realtime/merchantframework/xsd/v1/\"><v1:merc><v1:id>7777778764</v1:id><v1:regKey>M84PKPDMD5BY86HN</v1:regKey><v1:inType>1</v1:inType></v1:merc><v1:tranCode>1</v1:tranCode><v1:card><v1:pan>4485896261017708</v1:pan><v1:xprDt>1709</v1:xprDt></v1:card><v1:contact><v1:fullName>Longbob Longsen</v1:fullName><v1:coName>Acme</v1:coName><v1:title>QA Manager</v1:title><v1:phone><v1:type>4</v1:type><v1:nr>3334445555</v1:nr></v1:phone><v1:addrLn1>450 Main</v1:addrLn1><v1:addrLn2>Suite 100</v1:addrLn2><v1:city>Broomfield</v1:city><v1:state>CO</v1:state><v1:zipCode>85284</v1:zipCode><v1:ctry>US</v1:ctry><v1:email>example@example.com</v1:email><v1:ship><v1:fullName>Longbob Longsen</v1:fullName><v1:addrLn1>450 Main</v1:addrLn1><v1:addrLn2>Suite 100</v1:addrLn2><v1:city>Broomfield</v1:city><v1:state>CO</v1:state><v1:zipCode>85284</v1:zipCode><v1:phone>3334445555</v1:phone></v1:ship></v1:contact><v1:reqAmt>100</v1:reqAmt><v1:authReq><v1:ordNr>7a0f975b6e86aff44364360cbc6d0f00</v1:ordNr></v1:authReq></v1:SendTranRequest></soapenv:Body></soapenv:Envelope>" + -> "HTTP/1.1 200 OK\r\n" + -> "Content-Type: text/xml;charset=utf-8\r\n" + -> "Date: Thu, 21 Jan 2016 20:09:44 GMT\r\n" + -> "Server: WebServer\r\n" + -> "Set-Cookie: NSC_UMT12_DFSU-xt.dfsu.UYQ.dpn=ffffffff0918172545525d5f4f58455e445a4a42378b;expires=Thu, 21-Jan-2016 20:17:43 GMT;path=/;secure;httponly\r\n" + -> "Cache-Control: private\r\n" + -> "Content-Encoding: gzip\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "\r\n" + -> "1AA \r\n" + reading 426 bytes... + -> "" + read 426 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close PRE_SCRUBBED end def scrubbed_transcript - <<-POST_SCRUBBED -opening connection to ws.cert.transactionexpress.com:443... -opened -starting SSL for ws.cert.transactionexpress.com:443... -SSL established -<- "POST /portal/merchantframework/MerchantWebServices-v1?wsdl HTTP/1.1\r\nContent-Type: text/xml\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: ws.cert.transactionexpress.com\r\nContent-Length: 1186\r\n\r\n" -<- "<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\"><soapenv:Body><v1:SendTranRequest xmlns:v1=\"http://postilion/realtime/merchantframework/xsd/v1/\"><v1:merc><v1:id>[FILTERED]</v1:id><v1:regKey>[FILTERED]</v1:regKey><v1:inType>1</v1:inType></v1:merc><v1:tranCode>1</v1:tranCode><v1:card><v1:pan>[FILTERED]</v1:pan><v1:xprDt>1709</v1:xprDt></v1:card><v1:contact><v1:fullName>Longbob Longsen</v1:fullName><v1:coName>Acme</v1:coName><v1:title>QA Manager</v1:title><v1:phone><v1:type>4</v1:type><v1:nr>3334445555</v1:nr></v1:phone><v1:addrLn1>450 Main</v1:addrLn1><v1:addrLn2>Suite 100</v1:addrLn2><v1:city>Broomfield</v1:city><v1:state>CO</v1:state><v1:zipCode>85284</v1:zipCode><v1:ctry>US</v1:ctry><v1:email>example@example.com</v1:email><v1:ship><v1:fullName>Longbob Longsen</v1:fullName><v1:addrLn1>450 Main</v1:addrLn1><v1:addrLn2>Suite 100</v1:addrLn2><v1:city>Broomfield</v1:city><v1:state>CO</v1:state><v1:zipCode>85284</v1:zipCode><v1:phone>3334445555</v1:phone></v1:ship></v1:contact><v1:reqAmt>100</v1:reqAmt><v1:authReq><v1:ordNr>7a0f975b6e86aff44364360cbc6d0f00</v1:ordNr></v1:authReq></v1:SendTranRequest></soapenv:Body></soapenv:Envelope>" --> "HTTP/1.1 200 OK\r\n" --> "Content-Type: text/xml;charset=utf-8\r\n" --> "Date: Thu, 21 Jan 2016 20:09:44 GMT\r\n" --> "Server: WebServer\r\n" --> "Set-Cookie: NSC_UMT12_DFSU-xt.dfsu.UYQ.dpn=ffffffff0918172545525d5f4f58455e445a4a42378b;expires=Thu, 21-Jan-2016 20:17:43 GMT;path=/;secure;httponly\r\n" --> "Cache-Control: private\r\n" --> "Content-Encoding: gzip\r\n" --> "Transfer-Encoding: chunked\r\n" --> "\r\n" --> "1AA \r\n" -reading 426 bytes... --> "" -read 426 bytes -reading 2 bytes... --> "\r\n" -read 2 bytes --> "0\r\n" --> "\r\n" -Conn close + <<~POST_SCRUBBED + opening connection to ws.cert.transactionexpress.com:443... + opened + starting SSL for ws.cert.transactionexpress.com:443... + SSL established + <- "POST /portal/merchantframework/MerchantWebServices-v1?wsdl HTTP/1.1\r\nContent-Type: text/xml\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: ws.cert.transactionexpress.com\r\nContent-Length: 1186\r\n\r\n" + <- "<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\"><soapenv:Body><v1:SendTranRequest xmlns:v1=\"http://postilion/realtime/merchantframework/xsd/v1/\"><v1:merc><v1:id>[FILTERED]</v1:id><v1:regKey>[FILTERED]</v1:regKey><v1:inType>1</v1:inType></v1:merc><v1:tranCode>1</v1:tranCode><v1:card><v1:pan>[FILTERED]</v1:pan><v1:xprDt>1709</v1:xprDt></v1:card><v1:contact><v1:fullName>Longbob Longsen</v1:fullName><v1:coName>Acme</v1:coName><v1:title>QA Manager</v1:title><v1:phone><v1:type>4</v1:type><v1:nr>3334445555</v1:nr></v1:phone><v1:addrLn1>450 Main</v1:addrLn1><v1:addrLn2>Suite 100</v1:addrLn2><v1:city>Broomfield</v1:city><v1:state>CO</v1:state><v1:zipCode>85284</v1:zipCode><v1:ctry>US</v1:ctry><v1:email>example@example.com</v1:email><v1:ship><v1:fullName>Longbob Longsen</v1:fullName><v1:addrLn1>450 Main</v1:addrLn1><v1:addrLn2>Suite 100</v1:addrLn2><v1:city>Broomfield</v1:city><v1:state>CO</v1:state><v1:zipCode>85284</v1:zipCode><v1:phone>3334445555</v1:phone></v1:ship></v1:contact><v1:reqAmt>100</v1:reqAmt><v1:authReq><v1:ordNr>7a0f975b6e86aff44364360cbc6d0f00</v1:ordNr></v1:authReq></v1:SendTranRequest></soapenv:Body></soapenv:Envelope>" + -> "HTTP/1.1 200 OK\r\n" + -> "Content-Type: text/xml;charset=utf-8\r\n" + -> "Date: Thu, 21 Jan 2016 20:09:44 GMT\r\n" + -> "Server: WebServer\r\n" + -> "Set-Cookie: NSC_UMT12_DFSU-xt.dfsu.UYQ.dpn=ffffffff0918172545525d5f4f58455e445a4a42378b;expires=Thu, 21-Jan-2016 20:17:43 GMT;path=/;secure;httponly\r\n" + -> "Cache-Control: private\r\n" + -> "Content-Encoding: gzip\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "\r\n" + -> "1AA \r\n" + reading 426 bytes... + -> "" + read 426 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + POST_SCRUBBED + end + + def pre_scrubbed_account_number + <<~PRE_SCRUBBED + opening connection to ws.cert.transactionexpress.com:443... + opened + starting SSL for ws.cert.transactionexpress.com:443... + SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384 + <- "POST /portal/merchantframework/MerchantWebServices-v1?wsdl HTTP/1.1\r\nContent-Type: text/xml\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: ws.cert.transactionexpress.com\r\nContent-Length: 1553\r\n\r\n" + <- "<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\">\n <soapenv:Body>\n <v1:SendTranRequest xmlns:v1=\"http://postilion/realtime/merchantframework/xsd/v1/\">\n <v1:merc>\n <v1:id>7777778764</v1:id>\n <v1:regKey>M84PKPDMD5BY86HN</v1:regKey>\n <v1:inType>1</v1:inType>\n </v1:merc><v1:tranCode>11</v1:tranCode>\n <v1:achEcheck>\n <v1:bankRtNr>244183602</v1:bankRtNr>\n <v1:acctNr>15378535</v1:acctNr>\n </v1:achEcheck>\n <v1:contact>\n <v1:fullName>Jim Smith</v1:fullName>\n <v1:coName>Acme</v1:coName>\n <v1:title>QA Manager</v1:title>\n <v1:phone>\n <v1:type>4</v1:type>\n <v1:nr>3334445555</v1:nr>\n </v1:phone>\n <v1:addrLn1>450 Main</v1:addrLn1>\n <v1:addrLn2>Suite 100</v1:addrLn2>\n <v1:city>Broomfield</v1:city>\n <v1:state>CO</v1:state>\n <v1:zipCode>85284</v1:zipCode>\n <v1:ctry>US</v1:ctry>\n <v1:email>example@example.com</v1:email>\n <v1:ship>\n <v1:fullName>Jim Smith</v1:fullName>\n <v1:addrLn1>450 Main</v1:addrLn1>\n <v1:addrLn2>Suite 100</v1:addrLn2>\n <v1:city>Broomfield</v1:city>\n <v1:state>CO</v1:state>\n <v1:zipCode>85284</v1:zipCode>\n <v1:phone>3334445555</v1:phone>\n </v1:ship>\n </v1:contact>\n <v1:reqAmt>100</v1:reqAmt>\n <v1:authReq>\n <v1:ordNr>20811a5033205f7dcdc5c7e0c89a0189</v1:ordNr>\n </v1:authReq>\n </v1:SendTranRequest>\n </soapenv:Body>\n</soapenv:Envelope>" + -> "HTTP/1.1 200 OK\r\n" + -> "Cache-Control: private\r\n" + -> "Content-Type: text/xml; charset=utf-8\r\n" + -> "Server: Microsoft-IIS/10.0\r\n" + -> "X-AspNet-Version: 4.0.30319\r\n" + -> "X-Powered-By: ASP.NET\r\n" + -> "Strict-Transport-Security: max-age=31536000; includeSubDomains\r\n" + -> "Date: Mon, 11 Oct 2021 13:53:27 GMT\r\n" + -> "Cteonnt-Length: 782\r\n" + -> "X-Frame-Options: SAMEORIGIN\r\n" + -> "Set-Cookie: NSC_JOyfb3nwcgpzfpmezjperccrokp05cn=ffffffff09180b7045525d5f4f58455e445a4a42378b;expires=Mon, 11-Oct-2021 14:03:27 GMT;path=/;secure;httponly\r\n" + -> "Content-Encoding: gzip\r\n" + -> "Content-Length: 437\r\n" + -> "\r\n" + reading 437 bytes... + read 437 bytes + Conn close + PRE_SCRUBBED + end + + def post_scrubbed_account_number + <<~POST_SCRUBBED + opening connection to ws.cert.transactionexpress.com:443... + opened + starting SSL for ws.cert.transactionexpress.com:443... + SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384 + <- "POST /portal/merchantframework/MerchantWebServices-v1?wsdl HTTP/1.1\r\nContent-Type: text/xml\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: ws.cert.transactionexpress.com\r\nContent-Length: 1553\r\n\r\n" + <- "<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\">\n <soapenv:Body>\n <v1:SendTranRequest xmlns:v1=\"http://postilion/realtime/merchantframework/xsd/v1/\">\n <v1:merc>\n <v1:id>[FILTERED]</v1:id>\n <v1:regKey>[FILTERED]</v1:regKey>\n <v1:inType>1</v1:inType>\n </v1:merc><v1:tranCode>11</v1:tranCode>\n <v1:achEcheck>\n <v1:bankRtNr>244183602</v1:bankRtNr>\n <v1:acctNr>[FILTERED]</v1:acctNr>\n </v1:achEcheck>\n <v1:contact>\n <v1:fullName>Jim Smith</v1:fullName>\n <v1:coName>Acme</v1:coName>\n <v1:title>QA Manager</v1:title>\n <v1:phone>\n <v1:type>4</v1:type>\n <v1:nr>3334445555</v1:nr>\n </v1:phone>\n <v1:addrLn1>450 Main</v1:addrLn1>\n <v1:addrLn2>Suite 100</v1:addrLn2>\n <v1:city>Broomfield</v1:city>\n <v1:state>CO</v1:state>\n <v1:zipCode>85284</v1:zipCode>\n <v1:ctry>US</v1:ctry>\n <v1:email>example@example.com</v1:email>\n <v1:ship>\n <v1:fullName>Jim Smith</v1:fullName>\n <v1:addrLn1>450 Main</v1:addrLn1>\n <v1:addrLn2>Suite 100</v1:addrLn2>\n <v1:city>Broomfield</v1:city>\n <v1:state>CO</v1:state>\n <v1:zipCode>85284</v1:zipCode>\n <v1:phone>3334445555</v1:phone>\n </v1:ship>\n </v1:contact>\n <v1:reqAmt>100</v1:reqAmt>\n <v1:authReq>\n <v1:ordNr>20811a5033205f7dcdc5c7e0c89a0189</v1:ordNr>\n </v1:authReq>\n </v1:SendTranRequest>\n </soapenv:Body>\n</soapenv:Envelope>" + -> "HTTP/1.1 200 OK\r\n" + -> "Cache-Control: private\r\n" + -> "Content-Type: text/xml; charset=utf-8\r\n" + -> "Server: Microsoft-IIS/10.0\r\n" + -> "X-AspNet-Version: 4.0.30319\r\n" + -> "X-Powered-By: ASP.NET\r\n" + -> "Strict-Transport-Security: max-age=31536000; includeSubDomains\r\n" + -> "Date: Mon, 11 Oct 2021 13:53:27 GMT\r\n" + -> "Cteonnt-Length: 782\r\n" + -> "X-Frame-Options: SAMEORIGIN\r\n" + -> "Set-Cookie: NSC_JOyfb3nwcgpzfpmezjperccrokp05cn=ffffffff09180b7045525d5f4f58455e445a4a42378b;expires=Mon, 11-Oct-2021 14:03:27 GMT;path=/;secure;httponly\r\n" + -> "Content-Encoding: gzip\r\n" + -> "Content-Length: 437\r\n" + -> "\r\n" + reading 437 bytes... + read 437 bytes + Conn close POST_SCRUBBED end end diff --git a/test/unit/gateways/transact_pro_test.rb b/test/unit/gateways/transact_pro_test.rb index 6590b82cb2a..642fc6f4ed6 100644 --- a/test/unit/gateways/transact_pro_test.rb +++ b/test/unit/gateways/transact_pro_test.rb @@ -5,7 +5,7 @@ def setup @gateway = TransactProGateway.new( guid: 'login', password: 'password', - terminal: 'terminal', + terminal: 'terminal' ) @credit_card = credit_card @@ -75,7 +75,7 @@ def test_partial_capture @gateway.expects(:ssl_post).never assert_raise(ArgumentError) do - @gateway.capture(@amount-1, '3d25ab044075924479d3836f549b015481d15d74|100') + @gateway.capture(@amount - 1, '3d25ab044075924479d3836f549b015481d15d74|100') end end @@ -100,7 +100,7 @@ def test_successful_refund def test_partial_refund @gateway.expects(:ssl_post).returns(successful_refund_response) - assert refund = @gateway.refund(@amount-1, '3d25ab044075924479d3836f549b015481d15d74|100') + assert refund = @gateway.refund(@amount - 1, '3d25ab044075924479d3836f549b015481d15d74|100') assert_success refund assert_equal 'Refund Success', refund.message assert_equal '3d25ab044075924479d3836f549b015481d15d74', refund.authorization @@ -109,7 +109,7 @@ def test_partial_refund def test_failed_refund @gateway.expects(:ssl_post).returns(failed_refund_response) - assert refund = @gateway.refund(@amount+1, '3d25ab044075924479d3836f549b015481d15d74|100') + assert refund = @gateway.refund(@amount + 1, '3d25ab044075924479d3836f549b015481d15d74|100') assert_failure refund end diff --git a/test/unit/gateways/trexle_test.rb b/test/unit/gateways/trexle_test.rb index 288ca190d21..b1bb3625ec2 100644 --- a/test/unit/gateways/trexle_test.rb +++ b/test/unit/gateways/trexle_test.rb @@ -42,11 +42,11 @@ def test_supported_countries GI GR HK HU ID IE IL IM IN IS IT JO KW LB LI LK LT LU LV MC MT MU MV MX MY NL NO NZ OM PH PL PT QA RO SA SE SG SI SK SM TR TT UM US VA VN ZA) - assert_equal expected_supported_countries, TrexleGateway.supported_countries + assert_equal expected_supported_countries, TrexleGateway.supported_countries end def test_supported_cardtypes - assert_equal [:visa, :master, :american_express], TrexleGateway.supported_cardtypes + assert_equal %i[visa master american_express], TrexleGateway.supported_cardtypes end def test_display_name @@ -123,7 +123,7 @@ def test_successful_update def test_successful_refund token = 'charge_0cfad7ee5ffe75f58222bff214bfa5cc7ad7c367' - @gateway.expects(:ssl_request).with(:post, "https://core.trexle.com/api/v1/charges/#{token}/refunds", {amount: '100'}.to_json, instance_of(Hash)).returns(successful_refund_response) + @gateway.expects(:ssl_request).with(:post, "https://core.trexle.com/api/v1/charges/#{token}/refunds", { amount: '100' }.to_json, instance_of(Hash)).returns(successful_refund_response) assert response = @gateway.refund(100, token) assert_equal 'refund_7f696a86f9cb136520c51ea90c17f687b8df40b0', response.authorization @@ -133,7 +133,7 @@ def test_successful_refund def test_unsuccessful_refund token = 'charge_0cfad7ee5ffe75f58222bff214bfa5cc7ad7c367' - @gateway.expects(:ssl_request).with(:post, "https://core.trexle.com/api/v1/charges/#{token}/refunds", {amount: '100'}.to_json, instance_of(Hash)).returns(failed_refund_response) + @gateway.expects(:ssl_request).with(:post, "https://core.trexle.com/api/v1/charges/#{token}/refunds", { amount: '100' }.to_json, instance_of(Hash)).returns(failed_refund_response) assert response = @gateway.refund(100, token) assert_failure response @@ -286,13 +286,13 @@ def test_headers } @gateway.expects(:ssl_request).with(:post, anything, anything, expected_headers).returns(successful_purchase_response) - assert response = @gateway.purchase(@amount, @credit_card, {}) + assert @gateway.purchase(@amount, @credit_card, {}) expected_headers['X-Partner-Key'] = 'MyPartnerKey' expected_headers['X-Safe-Card'] = '1' @gateway.expects(:ssl_request).with(:post, anything, anything, expected_headers).returns(successful_purchase_response) - assert response = @gateway.purchase(@amount, @credit_card, partner_key: 'MyPartnerKey', safe_card: '1') + assert @gateway.purchase(@amount, @credit_card, partner_key: 'MyPartnerKey', safe_card: '1') end def test_transcript_scrubbing @@ -302,8 +302,8 @@ def test_transcript_scrubbing private def successful_purchase_response - '{ - "response":{ + '{ + "response":{ "token":"charge_0cfad7ee5ffe75f58222bff214bfa5cc7ad7c367", "success":true, "captured":true @@ -312,17 +312,17 @@ def successful_purchase_response end def failed_purchase_response - '{ + '{ "error":"Payment failed", "detail":"An error occurred while processing your card. Try again in a little bit." }' end def successful_store_response - '{ - "response":{ + '{ + "response":{ "token":"token_2cb443cf26b6ecdadd8144d1fac8240710aa41f1", - "card":{ + "card":{ "token":"token_f974687e4e866d6cca534e1cd42236817d315b3a", "primary":true } @@ -331,17 +331,17 @@ def successful_store_response end def failed_store_response - '{ + '{ "error":"an error has occured", "detail":"invalid token" }' end def successful_customer_store_response - '{ - "response":{ + '{ + "response":{ "token":"token_940ade441a23d53e04017f53af6c3a1eae9978ae", - "card":{ + "card":{ "token":"token_9a3f559962cbf6828e2cc38a02023565b0294548", "scheme":"master", "display_number":"XXXX-XXXX-XXXX-4444", @@ -362,15 +362,15 @@ def successful_customer_store_response end def failed_customer_store_response - '{ + '{ "error":"an error has occured", "detail":"invalid token" }' end def successful_refund_response - '{ - "response":{ + '{ + "response":{ "token":"refund_7f696a86f9cb136520c51ea90c17f687b8df40b0", "success":true, "amount":100, @@ -381,15 +381,15 @@ def successful_refund_response end def failed_refund_response - '{ + '{ "error":"Refund failed", "detail":"invalid token" }' end def successful_capture_response - '{ - "response":{ + '{ + "response":{ "token":"charge_6e47a330dca67ec7f696e8b650db22fe69bb8499", "success":true, "captured":true @@ -440,5 +440,4 @@ def scrubbed_transcript } }' end - end diff --git a/test/unit/gateways/trust_commerce_test.rb b/test/unit/gateways/trust_commerce_test.rb index fbba691fece..9d1782e4e51 100644 --- a/test/unit/gateways/trust_commerce_test.rb +++ b/test/unit/gateways/trust_commerce_test.rb @@ -1,16 +1,25 @@ require 'test_helper' class TrustCommerceTest < Test::Unit::TestCase + include CommStub def setup @gateway = TrustCommerceGateway.new( - :login => 'TestMerchant', - :password => 'password' + login: 'TestMerchant', + password: 'password', + aggregator_id: 'abc123' ) # Force SSL post @gateway.stubs(:tclink?).returns(false) @amount = 100 + @check = check @credit_card = credit_card('4111111111111111') + + @options_with_custom_fields = { + custom_fields: { + 'customfield1' => 'test1' + } + } end def test_successful_purchase @@ -18,7 +27,7 @@ def test_successful_purchase assert response = @gateway.purchase(@amount, @credit_card) assert_instance_of Response, response assert_success response - assert_equal '025-0007423614', response.authorization + assert_equal '025-0007423614|sale', response.authorization end def test_unsuccessful_purchase @@ -28,12 +37,94 @@ def test_unsuccessful_purchase assert_failure response end + def test_succesful_purchase_with_check + ActiveMerchant::Billing::TrustCommerceGateway.application_id = 'abc123' + stub_comms do + @gateway.purchase(@amount, @check) + end.check_request do |_endpoint, data, _headers| + assert_match(%r{aggregator1}, data) + assert_match(%r{name=Jim\+Smith}, data) + end.respond_with(successful_purchase_response) + end + + def test_succesful_purchase_with_custom_fields + stub_comms do + @gateway.purchase(@amount, @credit_card, @options_with_custom_fields) + end.check_request do |_endpoint, data, _headers| + assert_match(%r{customfield1=test1}, data) + end.respond_with(successful_purchase_response) + end + + def test_succesful_authorize_with_custom_fields + stub_comms do + @gateway.authorize(@amount, @check, @options_with_custom_fields) + end.check_request do |_endpoint, data, _headers| + assert_match(%r{customfield1=test1}, data) + end.respond_with(successful_authorize_response) + end + + def test_successful_void_from_purchase + stub_comms do + @gateway.void('1235|sale') + end.check_request do |_endpoint, data, _headers| + assert_match(%r{action=void}, data) + end.respond_with(successful_void_response) + end + + def test_successful_void_from_authorize + stub_comms do + @gateway.void('1235|preauth') + end.check_request do |_endpoint, data, _headers| + assert_match(%r{action=reversal}, data) + end.respond_with(successful_void_response) + end + + def test_succesful_capture_with_custom_fields + stub_comms do + @gateway.capture(@amount, 'auth', @options_with_custom_fields) + end.check_request do |_endpoint, data, _headers| + assert_match(%r{customfield1=test1}, data) + end.respond_with(successful_capture_response) + end + + def test_succesful_refund_with_custom_fields + stub_comms do + @gateway.refund(@amount, 'auth|100', @options_with_custom_fields) + end.check_request do |_endpoint, data, _headers| + assert_match(%r{customfield1=test1}, data) + end.respond_with(successful_refund_response) + end + + def test_succesful_void_with_custom_fields + stub_comms do + @gateway.void('1235|sale', @options_with_custom_fields) + end.check_request do |_endpoint, data, _headers| + assert_match(%r{customfield1=test1}, data) + end.respond_with(successful_void_response) + end + + def test_succesful_store_with_custom_fields + stub_comms do + @gateway.store(@credit_card, @options_with_custom_fields) + end.check_request do |_endpoint, data, _headers| + assert_match(%r{customfield1=test1}, data) + end.respond_with(successful_store_response) + end + + def test_succesful_unstore_with_custom_fields + stub_comms do + @gateway.unstore('test', @options_with_custom_fields) + end.check_request do |_endpoint, data, _headers| + assert_match(%r{customfield1=test1}, data) + end.respond_with(successful_unstore_response) + end + def test_amount_style - assert_equal '1034', @gateway.send(:amount, 1034) + assert_equal '1034', @gateway.send(:amount, 1034) - assert_raise(ArgumentError) do - @gateway.send(:amount, '10.34') - end + assert_raise(ArgumentError) do + @gateway.send(:amount, '10.34') + end end def test_avs_result @@ -55,7 +146,7 @@ def test_supported_countries end def test_supported_card_types - assert_equal [:visa, :master, :discover, :american_express, :diners_club, :jcb], TrustCommerceGateway.supported_cardtypes + assert_equal %i[visa master discover american_express diners_club jcb], TrustCommerceGateway.supported_cardtypes end def test_test_flag_should_be_set_when_using_test_login_in_production @@ -72,35 +163,132 @@ def test_transcript_scrubbing assert_equal scrubbed_transcript, @gateway.scrub(transcript) end + 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 + <<~RESPONSE + authcode=123456 + transid=026-0193338367, + status=approved + avs=Y + cvv=M + RESPONSE + end + def successful_purchase_response - <<-RESPONSE -transid=025-0007423614 -status=approved -avs=Y -cvv=P + <<~RESPONSE + transid=025-0007423614 + status=approved + avs=Y + cvv=P + RESPONSE + end + + def successful_capture_response + <<~RESPONSE + transid=026-0193338993 + status=accepted RESPONSE end def unsuccessful_purchase_response - <<-RESPONSE -transid=025-0007423827 -declinetype=cvv -status=decline -cvv=N + <<~RESPONSE + transid=025-0007423827 + declinetype=cvv + status=decline + cvv=N + RESPONSE + end + + def successful_void_response + <<~RESPONSE + transid=025-0007423828 + status=accpeted + RESPONSE + end + + def successful_refund_response + <<~RESPONSE + transid=026-0193345407 + status=accepted + RESPONSE + end + + def successful_store_response + <<~RESPONSE + transid=026-0193346109 + status=approved, + cvv=M, + avs=0 + billingid=Q5T7PT + RESPONSE + end + + def successful_unstore_response + <<~RESPONSE + transid=026-0193346231 + status=rejected + 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 + <<~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 TRANSCRIPT end def scrubbed_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=[FILTERED]&exp=0916&cc=[FILTERED]&name=Longbob+Longsen&media=cc&ip=10.10.10.10&email=cody%40example.com&ticket=%231000.1&amount=100 + <<~TRANSCRIPT + action=sale&demo=y&password=[FILTERED]&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=[FILTERED]&exp=0916&cc=[FILTERED]&name=Longbob+Longsen&media=cc&ip=10.10.10.10&email=cody%40example.com&ticket=%231000.1&amount=100 + TRANSCRIPT + end + + def echeck_transcript + <<~TRANSCRIPT + action=sale&demo=y&password=A3pN3F3Am8du&custid=1249400&customfield1=test1&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.&name=Jim+Smith&account=55544433221&routing=789456124&media=ach&ip=10.10.10.10&email=cody%40example.com&aggregator1=2FCTLKF&aggregators=1&ticket=%231000.1&amount=100 + TRANSCRIPT + end + + def scrubbed_echeck_transcript + <<~TRANSCRIPT + action=sale&demo=y&password=[FILTERED]&custid=1249400&customfield1=test1&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.&name=Jim+Smith&account=[FILTERED]&routing=789456124&media=ach&ip=10.10.10.10&email=cody%40example.com&aggregator1=2FCTLKF&aggregators=1&ticket=%231000.1&amount=100 TRANSCRIPT end end diff --git a/test/unit/gateways/usa_epay_advanced_test.rb b/test/unit/gateways/usa_epay_advanced_test.rb index ad96353bc4e..31c927fffcb 100644 --- a/test/unit/gateways/usa_epay_advanced_test.rb +++ b/test/unit/gateways/usa_epay_advanced_test.rb @@ -1,4 +1,5 @@ # encoding: utf-8 + require 'test_helper' require 'logger' @@ -15,90 +16,88 @@ def setup # UsaEpayAdvancedGateway.wiredump_device.sync = true @gateway = UsaEpayAdvancedGateway.new( - :login => 'X', - :password => 'Y', - :software_id => 'Z' - ) + login: 'X', + password: 'Y', + software_id: 'Z' + ) @credit_card = ActiveMerchant::Billing::CreditCard.new( - :number => '4000100011112224', - :month => 12, - :year => 12, - :brand => 'visa', - :verification_value => '123', - :first_name => 'Fred', - :last_name => 'Flintstone' + number: '4000100011112224', + month: 12, + year: 12, + brand: 'visa', + verification_value: '123', + first_name: 'Fred', + last_name: 'Flintstone' ) @check = ActiveMerchant::Billing::Check.new( - :account_number => '123456789012', - :routing_number => '123456789', - :account_type => 'checking', - :first_name => 'Fred', - :last_name => 'Flintstone' + account_number: '123456789012', + routing_number: '123456789', + account_type: 'checking', + first_name: 'Fred', + last_name: 'Flintstone' ) payment_methods = [ { - :name => 'My Visa', # optional - :sort => 2, # optional - :method => @credit_card + name: 'My Visa', # optional + sort: 2, # optional + method: @credit_card }, { - :name => 'My Checking', - :method => @check + name: 'My Checking', + method: @check } ] payment_method = { - :name => 'My new Visa', # optional - :method => @credit_card + name: 'My new Visa', # optional + method: @credit_card } @customer_options = { - :id => 1, # optional: merchant assigned id, usually db id - :notes => 'Note about customer', # optional - :data => 'Some Data', # optional - :url => 'awesomesite.com', # optional - :payment_methods => payment_methods # optional + id: 1, # optional: merchant assigned id, usually db id + notes: 'Note about customer', # optional + data: 'Some Data', # optional + url: 'awesomesite.com', # optional + payment_methods: # optional } - @payment_options = { - :payment_method => payment_method - } + @payment_options = { payment_method: } @transaction_options = { - :payment_method => @credit_card, - :recurring => { - :schedule => 'monthly', - :amount => 4000 + payment_method: @credit_card, + recurring: { + schedule: 'monthly', + amount: 4000 } } @standard_transaction_options = { - :method_id => 0, - :command => 'Sale', - :amount => 2000 #20.00 + method_id: 0, + command: 'Sale', + amount: 2000 # 20.00 } @get_payment_options = { - :method_id => 0 + method_id: 0 } @delete_customer_options = { - :customer_number => 299461 + customer_number: 299461 } @custom_options = { - :fields => ['Response.StatusCode', 'Response.Status'] + fields: ['Response.StatusCode', 'Response.Status'] } @options = { - :client_ip => '127.0.0.1', - :billing_address => address, + client_ip: '127.0.0.1', + billing_address: address, - :customer_number => 298741, - :reference_number => 9999 + customer_number: 298741, + reference_number: 9999 } end @@ -202,7 +201,7 @@ def test_successful_update_customer def test_successful_quick_update_customer @gateway.expects(:ssl_post).returns(successful_customer_response('quickUpdateCustomer')) - assert response = @gateway.quick_update_customer({customer_number: @options[:customer_number], update_data: @customer_options}) + assert response = @gateway.quick_update_customer({ customer_number: @options[:customer_number], update_data: @customer_options }) assert_instance_of Response, response assert response.test? assert_success response @@ -278,7 +277,7 @@ def test_successful_get_customer_payment_method def test_successful_get_customer_payment_methods @gateway.expects(:ssl_post).returns(successful_get_customer_payment_methods_response) - assert response = @gateway.get_customer_payment_methods(@options.merge!(:customer_number => 298741)) + assert response = @gateway.get_customer_payment_methods(@options.merge!(customer_number: 298741)) assert_instance_of Response, response assert_success response assert response.test? @@ -289,7 +288,7 @@ def test_successful_get_customer_payment_methods end def test_successful_update_customer_payment_method - @options.merge!(@payment_options).merge!(:method_id => 1) + @options.merge!(@payment_options)[:method_id] = 1 @gateway.expects(:ssl_post).returns(successful_update_customer_payment_method_response) assert response = @gateway.update_customer_payment_method(@options) @@ -304,7 +303,7 @@ def test_successful_update_customer_payment_method def test_successful_delete_customer_payment_method @gateway.expects(:ssl_post).returns(successful_delete_customer_payment_method_response) - assert response = @gateway.delete_customer_payment_method(@options.merge!(:customer_number => 298741, :method_id => 15)) + assert response = @gateway.delete_customer_payment_method(@options.merge!(customer_number: 298741, method_id: 15)) assert_instance_of Response, response assert_success response assert_equal 'true', response.message @@ -395,8 +394,8 @@ def test_successful_run_check_sale @options.merge!(@transaction_options) response = stub_comms do - @gateway.run_check_sale(@options.merge(:payment_method => @check)) - end.check_request do |endpoint, data, headers| + @gateway.run_check_sale(@options.merge(payment_method: @check)) + end.check_request do |_endpoint, data, _headers| assert_match(/123456789012/, data) end.respond_with(successful_transaction_response('runCheckSale')) @@ -420,7 +419,7 @@ def test_successful_run_check_credit end # TODO get post_auth response - #def test_successful_post_auth + # def test_successful_post_auth # @options.merge!(:authorization_code => 'bogus') # @gateway.expects(:ssl_post).returns(successful_post_auth_response) @@ -432,7 +431,7 @@ def test_successful_run_check_credit # #assert_equal '47568732', response.authorization # puts response.inspect - #end + # end def test_successful_run_quick_sale @options.merge!(@transaction_options) @@ -494,7 +493,7 @@ def test_successful_refund_transaction end # TODO get override_transaction response - #def test_successful_override_transaction + # def test_successful_override_transaction # @gateway.expects(:ssl_post).returns(successful_override_transaction_response) # assert response = @gateway.override_transaction(@options) @@ -503,7 +502,7 @@ def test_successful_refund_transaction # assert response.test? # puts response.inspect - #end + # end # Transaction Status ================================================ @@ -579,125 +578,125 @@ def test_mismatch_response # Standard Gateway ================================================== def successful_purchase_response - <<-XML -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:runSaleResponse><runSaleReturn xsi:type="ns1:TransactionResponse"><AcsUrl xsi:type="xsd:string"></AcsUrl><AuthAmount xsi:type="xsd:double">0</AuthAmount><AuthCode xsi:type="xsd:string">017523</AuthCode><AvsResult xsi:type="xsd:string">Address: Match &amp; 5 Digit Zip: Match</AvsResult><AvsResultCode xsi:type="xsd:string">YYY</AvsResultCode><BatchNum xsi:type="xsd:integer">1</BatchNum><BatchRefNum xsi:type="xsd:integer">14004</BatchRefNum><CardCodeResult xsi:type="xsd:string">Match</CardCodeResult><CardCodeResultCode xsi:type="xsd:string">M</CardCodeResultCode><CardLevelResult xsi:type="xsd:string">Visa Traditional</CardLevelResult><CardLevelResultCode xsi:type="xsd:string">A</CardLevelResultCode><ConversionRate xsi:type="xsd:double">0</ConversionRate><ConvertedAmount xsi:type="xsd:double">0</ConvertedAmount><ConvertedAmountCurrency xsi:type="xsd:string">840</ConvertedAmountCurrency><CustNum xsi:type="xsd:integer">0</CustNum><Error xsi:type="xsd:string">Approved</Error><ErrorCode xsi:type="xsd:integer">0</ErrorCode><isDuplicate xsi:type="xsd:boolean">false</isDuplicate><Payload xsi:type="xsd:string"></Payload><RefNum xsi:type="xsd:integer">47602591</RefNum><Result xsi:type="xsd:string">Approved</Result><ResultCode xsi:type="xsd:string">A</ResultCode><Status xsi:type="xsd:string">Pending</Status><StatusCode xsi:type="xsd:string">P</StatusCode><VpasResultCode xsi:type="xsd:string"></VpasResultCode></runSaleReturn></ns1:runSaleResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:runSaleResponse><runSaleReturn xsi:type="ns1:TransactionResponse"><AcsUrl xsi:type="xsd:string"></AcsUrl><AuthAmount xsi:type="xsd:double">0</AuthAmount><AuthCode xsi:type="xsd:string">017523</AuthCode><AvsResult xsi:type="xsd:string">Address: Match &amp; 5 Digit Zip: Match</AvsResult><AvsResultCode xsi:type="xsd:string">YYY</AvsResultCode><BatchNum xsi:type="xsd:integer">1</BatchNum><BatchRefNum xsi:type="xsd:integer">14004</BatchRefNum><CardCodeResult xsi:type="xsd:string">Match</CardCodeResult><CardCodeResultCode xsi:type="xsd:string">M</CardCodeResultCode><CardLevelResult xsi:type="xsd:string">Visa Traditional</CardLevelResult><CardLevelResultCode xsi:type="xsd:string">A</CardLevelResultCode><ConversionRate xsi:type="xsd:double">0</ConversionRate><ConvertedAmount xsi:type="xsd:double">0</ConvertedAmount><ConvertedAmountCurrency xsi:type="xsd:string">840</ConvertedAmountCurrency><CustNum xsi:type="xsd:integer">0</CustNum><Error xsi:type="xsd:string">Approved</Error><ErrorCode xsi:type="xsd:integer">0</ErrorCode><isDuplicate xsi:type="xsd:boolean">false</isDuplicate><Payload xsi:type="xsd:string"></Payload><RefNum xsi:type="xsd:integer">47602591</RefNum><Result xsi:type="xsd:string">Approved</Result><ResultCode xsi:type="xsd:string">A</ResultCode><Status xsi:type="xsd:string">Pending</Status><StatusCode xsi:type="xsd:string">P</StatusCode><VpasResultCode xsi:type="xsd:string"></VpasResultCode></runSaleReturn></ns1:runSaleResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end def successful_authorize_response - <<-XML -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:runAuthOnlyResponse><runAuthOnlyReturn xsi:type="ns1:TransactionResponse"><AcsUrl xsi:type="xsd:string"></AcsUrl><AuthAmount xsi:type="xsd:double">0</AuthAmount><AuthCode xsi:type="xsd:string">017524</AuthCode><AvsResult xsi:type="xsd:string">Address: Match &amp; 5 Digit Zip: Match</AvsResult><AvsResultCode xsi:type="xsd:string">YYY</AvsResultCode><BatchNum xsi:type="xsd:integer">1</BatchNum><BatchRefNum xsi:type="xsd:integer">14004</BatchRefNum><CardCodeResult xsi:type="xsd:string">Match</CardCodeResult><CardCodeResultCode xsi:type="xsd:string">M</CardCodeResultCode><CardLevelResult xsi:type="xsd:string">Visa Traditional</CardLevelResult><CardLevelResultCode xsi:type="xsd:string">A</CardLevelResultCode><ConversionRate xsi:type="xsd:double">0</ConversionRate><ConvertedAmount xsi:type="xsd:double">0</ConvertedAmount><ConvertedAmountCurrency xsi:type="xsd:string">840</ConvertedAmountCurrency><CustNum xsi:type="xsd:integer">0</CustNum><Error xsi:type="xsd:string">Approved</Error><ErrorCode xsi:type="xsd:integer">0</ErrorCode><isDuplicate xsi:type="xsd:boolean">false</isDuplicate><Payload xsi:type="xsd:string"></Payload><RefNum xsi:type="xsd:integer">47602592</RefNum><Result xsi:type="xsd:string">Approved</Result><ResultCode xsi:type="xsd:string">A</ResultCode><Status xsi:type="xsd:string">Pending</Status><StatusCode xsi:type="xsd:string">P</StatusCode><VpasResultCode xsi:type="xsd:string"></VpasResultCode></runAuthOnlyReturn></ns1:runAuthOnlyResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:runAuthOnlyResponse><runAuthOnlyReturn xsi:type="ns1:TransactionResponse"><AcsUrl xsi:type="xsd:string"></AcsUrl><AuthAmount xsi:type="xsd:double">0</AuthAmount><AuthCode xsi:type="xsd:string">017524</AuthCode><AvsResult xsi:type="xsd:string">Address: Match &amp; 5 Digit Zip: Match</AvsResult><AvsResultCode xsi:type="xsd:string">YYY</AvsResultCode><BatchNum xsi:type="xsd:integer">1</BatchNum><BatchRefNum xsi:type="xsd:integer">14004</BatchRefNum><CardCodeResult xsi:type="xsd:string">Match</CardCodeResult><CardCodeResultCode xsi:type="xsd:string">M</CardCodeResultCode><CardLevelResult xsi:type="xsd:string">Visa Traditional</CardLevelResult><CardLevelResultCode xsi:type="xsd:string">A</CardLevelResultCode><ConversionRate xsi:type="xsd:double">0</ConversionRate><ConvertedAmount xsi:type="xsd:double">0</ConvertedAmount><ConvertedAmountCurrency xsi:type="xsd:string">840</ConvertedAmountCurrency><CustNum xsi:type="xsd:integer">0</CustNum><Error xsi:type="xsd:string">Approved</Error><ErrorCode xsi:type="xsd:integer">0</ErrorCode><isDuplicate xsi:type="xsd:boolean">false</isDuplicate><Payload xsi:type="xsd:string"></Payload><RefNum xsi:type="xsd:integer">47602592</RefNum><Result xsi:type="xsd:string">Approved</Result><ResultCode xsi:type="xsd:string">A</ResultCode><Status xsi:type="xsd:string">Pending</Status><StatusCode xsi:type="xsd:string">P</StatusCode><VpasResultCode xsi:type="xsd:string"></VpasResultCode></runAuthOnlyReturn></ns1:runAuthOnlyResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end def successful_capture_response - <<-XML -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:captureTransactionResponse><captureTransactionReturn xsi:type="ns1:TransactionResponse"><AcsUrl xsi:type="xsd:string"></AcsUrl><AuthAmount xsi:type="xsd:double">0</AuthAmount><AuthCode xsi:type="xsd:string">017525</AuthCode><AvsResult xsi:type="xsd:string">No AVS response (Typically no AVS data sent or swiped transaction)</AvsResult><AvsResultCode xsi:type="xsd:string"></AvsResultCode><BatchNum xsi:type="xsd:integer">0</BatchNum><BatchRefNum xsi:type="xsd:integer">0</BatchRefNum><CardCodeResult xsi:type="xsd:string">No CVV2/CVC data available for transaction.</CardCodeResult><CardCodeResultCode xsi:type="xsd:string"></CardCodeResultCode><CardLevelResult xsi:type="xsd:string">Unknown Code </CardLevelResult><CardLevelResultCode xsi:type="xsd:string"></CardLevelResultCode><ConversionRate xsi:type="xsd:double">0</ConversionRate><ConvertedAmount xsi:type="xsd:double">0</ConvertedAmount><ConvertedAmountCurrency xsi:type="xsd:string">840</ConvertedAmountCurrency><CustNum xsi:type="xsd:integer">0</CustNum><Error xsi:type="xsd:string">Approved</Error><ErrorCode xsi:type="xsd:integer">0</ErrorCode><isDuplicate xsi:type="xsd:boolean">false</isDuplicate><Payload xsi:type="xsd:string"></Payload><RefNum xsi:type="xsd:integer">47602593</RefNum><Result xsi:type="xsd:string">Approved</Result><ResultCode xsi:type="xsd:string">A</ResultCode><Status xsi:type="xsd:string">Pending</Status><StatusCode xsi:type="xsd:string">P</StatusCode><VpasResultCode xsi:type="xsd:string"></VpasResultCode></captureTransactionReturn></ns1:captureTransactionResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:captureTransactionResponse><captureTransactionReturn xsi:type="ns1:TransactionResponse"><AcsUrl xsi:type="xsd:string"></AcsUrl><AuthAmount xsi:type="xsd:double">0</AuthAmount><AuthCode xsi:type="xsd:string">017525</AuthCode><AvsResult xsi:type="xsd:string">No AVS response (Typically no AVS data sent or swiped transaction)</AvsResult><AvsResultCode xsi:type="xsd:string"></AvsResultCode><BatchNum xsi:type="xsd:integer">0</BatchNum><BatchRefNum xsi:type="xsd:integer">0</BatchRefNum><CardCodeResult xsi:type="xsd:string">No CVV2/CVC data available for transaction.</CardCodeResult><CardCodeResultCode xsi:type="xsd:string"></CardCodeResultCode><CardLevelResult xsi:type="xsd:string">Unknown Code </CardLevelResult><CardLevelResultCode xsi:type="xsd:string"></CardLevelResultCode><ConversionRate xsi:type="xsd:double">0</ConversionRate><ConvertedAmount xsi:type="xsd:double">0</ConvertedAmount><ConvertedAmountCurrency xsi:type="xsd:string">840</ConvertedAmountCurrency><CustNum xsi:type="xsd:integer">0</CustNum><Error xsi:type="xsd:string">Approved</Error><ErrorCode xsi:type="xsd:integer">0</ErrorCode><isDuplicate xsi:type="xsd:boolean">false</isDuplicate><Payload xsi:type="xsd:string"></Payload><RefNum xsi:type="xsd:integer">47602593</RefNum><Result xsi:type="xsd:string">Approved</Result><ResultCode xsi:type="xsd:string">A</ResultCode><Status xsi:type="xsd:string">Pending</Status><StatusCode xsi:type="xsd:string">P</StatusCode><VpasResultCode xsi:type="xsd:string"></VpasResultCode></captureTransactionReturn></ns1:captureTransactionResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end def successful_void_response - <<-XML -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:voidTransactionResponse><voidTransactionReturn xsi:type="xsd:boolean">true</voidTransactionReturn></ns1:voidTransactionResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:voidTransactionResponse><voidTransactionReturn xsi:type="xsd:boolean">true</voidTransactionReturn></ns1:voidTransactionResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end def successful_credit_response - <<-XML -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="urn:usaepay" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:refundTransactionResponse><refundTransactionReturn xsi:type="ns1:TransactionResponse"><AcsUrl xsi:nil="true"/><AuthAmount xsi:type="xsd:double">0</AuthAmount><AuthCode xsi:type="xsd:string">47612622</AuthCode><AvsResult xsi:type="xsd:string">Unmapped AVS response ( )</AvsResult><AvsResultCode xsi:type="xsd:string"></AvsResultCode><BatchNum xsi:nil="true"/><BatchRefNum xsi:nil="true"/><CardCodeResult xsi:type="xsd:string">No CVV2/CVC data available for transaction.</CardCodeResult><CardCodeResultCode xsi:type="xsd:string"></CardCodeResultCode><CardLevelResult xsi:type="xsd:string">Unknown Code </CardLevelResult><CardLevelResultCode xsi:type="xsd:string"></CardLevelResultCode><ConversionRate xsi:type="xsd:double">0</ConversionRate><ConvertedAmount xsi:type="xsd:double">0</ConvertedAmount><ConvertedAmountCurrency xsi:nil="true"/><CustNum xsi:type="xsd:integer">0</CustNum><Error xsi:type="xsd:string"></Error><ErrorCode xsi:nil="true"/><isDuplicate xsi:type="xsd:boolean">false</isDuplicate><Payload xsi:nil="true"/><RefNum xsi:type="xsd:integer">47602599</RefNum><Result xsi:type="xsd:string">Approved</Result><ResultCode xsi:type="xsd:string">A</ResultCode><Status xsi:type="xsd:string">Pending</Status><StatusCode xsi:type="xsd:string">P</StatusCode><VpasResultCode xsi:type="xsd:string"></VpasResultCode></refundTransactionReturn></ns1:refundTransactionResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="urn:usaepay" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:refundTransactionResponse><refundTransactionReturn xsi:type="ns1:TransactionResponse"><AcsUrl xsi:nil="true"/><AuthAmount xsi:type="xsd:double">0</AuthAmount><AuthCode xsi:type="xsd:string">47612622</AuthCode><AvsResult xsi:type="xsd:string">Unmapped AVS response ( )</AvsResult><AvsResultCode xsi:type="xsd:string"></AvsResultCode><BatchNum xsi:nil="true"/><BatchRefNum xsi:nil="true"/><CardCodeResult xsi:type="xsd:string">No CVV2/CVC data available for transaction.</CardCodeResult><CardCodeResultCode xsi:type="xsd:string"></CardCodeResultCode><CardLevelResult xsi:type="xsd:string">Unknown Code </CardLevelResult><CardLevelResultCode xsi:type="xsd:string"></CardLevelResultCode><ConversionRate xsi:type="xsd:double">0</ConversionRate><ConvertedAmount xsi:type="xsd:double">0</ConvertedAmount><ConvertedAmountCurrency xsi:nil="true"/><CustNum xsi:type="xsd:integer">0</CustNum><Error xsi:type="xsd:string"></Error><ErrorCode xsi:nil="true"/><isDuplicate xsi:type="xsd:boolean">false</isDuplicate><Payload xsi:nil="true"/><RefNum xsi:type="xsd:integer">47602599</RefNum><Result xsi:type="xsd:string">Approved</Result><ResultCode xsi:type="xsd:string">A</ResultCode><Status xsi:type="xsd:string">Pending</Status><StatusCode xsi:type="xsd:string">P</StatusCode><VpasResultCode xsi:type="xsd:string"></VpasResultCode></refundTransactionReturn></ns1:refundTransactionResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end # Customer ========================================================== def successful_add_customer_response - <<-XML -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:addCustomerResponse><addCustomerReturn xsi:type="xsd:integer">274141</addCustomerReturn></ns1:addCustomerResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:addCustomerResponse><addCustomerReturn xsi:type="xsd:integer">274141</addCustomerReturn></ns1:addCustomerResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end def invalid_checking_add_customer_response - <<-XML -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><SOAP-ENV:Fault><faultcode>SOAP-ENV:Server</faultcode><faultstring>39: Invalid Checking Account Number.</faultstring></SOAP-ENV:Fault></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><SOAP-ENV:Fault><faultcode>SOAP-ENV:Server</faultcode><faultstring>39: Invalid Checking Account Number.</faultstring></SOAP-ENV:Fault></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end def successful_customer_response(method) - <<-XML -<SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:#{method}Response><#{method}Return xsi:type="xsd:boolean">true</#{method}Return></ns1:#{method}Response></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:#{method}Response><#{method}Return xsi:type="xsd:boolean">true</#{method}Return></ns1:#{method}Response></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end def successful_run_customer_transaction_response - <<-XML -<SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:runCustomerTransactionResponse><runCustomerTransactionReturn xsi:type="ns1:TransactionResponse"><AcsUrl xsi:type="xsd:string"></AcsUrl><AuthAmount xsi:nil="true"/><AuthCode xsi:type="xsd:string">038460</AuthCode><AvsResult xsi:type="xsd:string">Address: Match &amp; 5 Digit Zip: Match</AvsResult><AvsResultCode xsi:type="xsd:string">YYY</AvsResultCode><BatchNum xsi:type="xsd:integer">1</BatchNum><BatchRefNum xsi:type="xsd:integer">14004</BatchRefNum><CardCodeResult xsi:type="xsd:string">Not Processed</CardCodeResult><CardCodeResultCode xsi:type="xsd:string">P</CardCodeResultCode><CardLevelResult xsi:nil="true"/><CardLevelResultCode xsi:nil="true"/><ConversionRate xsi:type="xsd:double">0</ConversionRate><ConvertedAmount xsi:type="xsd:double">0</ConvertedAmount><ConvertedAmountCurrency xsi:nil="true"/><CustNum xsi:type="xsd:integer">0</CustNum><Error xsi:type="xsd:string">Approved</Error><ErrorCode xsi:type="xsd:integer">0</ErrorCode><isDuplicate xsi:type="xsd:boolean">false</isDuplicate><Payload xsi:type="xsd:string"></Payload><RefNum xsi:type="xsd:integer">47555081</RefNum><Result xsi:type="xsd:string">Approved</Result><ResultCode xsi:type="xsd:string">A</ResultCode><Status xsi:type="xsd:string">Pending</Status><StatusCode xsi:type="xsd:string">P</StatusCode><VpasResultCode xsi:nil="true"/></runCustomerTransactionReturn></ns1:runCustomerTransactionResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:runCustomerTransactionResponse><runCustomerTransactionReturn xsi:type="ns1:TransactionResponse"><AcsUrl xsi:type="xsd:string"></AcsUrl><AuthAmount xsi:nil="true"/><AuthCode xsi:type="xsd:string">038460</AuthCode><AvsResult xsi:type="xsd:string">Address: Match &amp; 5 Digit Zip: Match</AvsResult><AvsResultCode xsi:type="xsd:string">YYY</AvsResultCode><BatchNum xsi:type="xsd:integer">1</BatchNum><BatchRefNum xsi:type="xsd:integer">14004</BatchRefNum><CardCodeResult xsi:type="xsd:string">Not Processed</CardCodeResult><CardCodeResultCode xsi:type="xsd:string">P</CardCodeResultCode><CardLevelResult xsi:nil="true"/><CardLevelResultCode xsi:nil="true"/><ConversionRate xsi:type="xsd:double">0</ConversionRate><ConvertedAmount xsi:type="xsd:double">0</ConvertedAmount><ConvertedAmountCurrency xsi:nil="true"/><CustNum xsi:type="xsd:integer">0</CustNum><Error xsi:type="xsd:string">Approved</Error><ErrorCode xsi:type="xsd:integer">0</ErrorCode><isDuplicate xsi:type="xsd:boolean">false</isDuplicate><Payload xsi:type="xsd:string"></Payload><RefNum xsi:type="xsd:integer">47555081</RefNum><Result xsi:type="xsd:string">Approved</Result><ResultCode xsi:type="xsd:string">A</ResultCode><Status xsi:type="xsd:string">Pending</Status><StatusCode xsi:type="xsd:string">P</StatusCode><VpasResultCode xsi:nil="true"/></runCustomerTransactionReturn></ns1:runCustomerTransactionResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end def successful_add_customer_payment_method_response - <<-XML -<SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:addCustomerPaymentMethodResponse><addCustomerPaymentMethodReturn xsi:type="xsd:integer">77</addCustomerPaymentMethodReturn></ns1:addCustomerPaymentMethodResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:addCustomerPaymentMethodResponse><addCustomerPaymentMethodReturn xsi:type="xsd:integer">77</addCustomerPaymentMethodReturn></ns1:addCustomerPaymentMethodResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end def failed_add_customer_payment_method_response - <<-XML -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><SOAP-ENV:Fault><faultcode>SOAP-ENV:Server</faultcode><faultstring>40459: Payment method not added because verification returned a Declined:10127:Card Declined (00)</faultstring></SOAP-ENV:Fault></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><SOAP-ENV:Fault><faultcode>SOAP-ENV:Server</faultcode><faultstring>40459: Payment method not added because verification returned a Declined:10127:Card Declined (00)</faultstring></SOAP-ENV:Fault></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end def successful_get_customer_payment_method_response - <<-XML -<SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:getCustomerPaymentMethodResponse><getCustomerPaymentMethodReturn xsi:type="ns1:PaymentMethod"><MethodType xsi:type="xsd:string">cc</MethodType><MethodID xsi:type="xsd:integer">103</MethodID><MethodName xsi:type="xsd:string">My CC</MethodName><SecondarySort xsi:type="xsd:integer">5</SecondarySort><Created xsi:type="xsd:dateTime">2011-06-09T13:48:57+08:00</Created><Modified xsi:type="xsd:dateTime">2011-06-09T13:48:57+08:00</Modified><AvsStreet xsi:type="xsd:string">456 My Street</AvsStreet><AvsZip xsi:type="xsd:string">K1C2N6</AvsZip><CardExpiration xsi:type="xsd:string">2012-12</CardExpiration><CardNumber xsi:type="xsd:string">XXXXXXXXXXXX2224</CardNumber><CardType xsi:type="xsd:string">V</CardType></getCustomerPaymentMethodReturn></ns1:getCustomerPaymentMethodResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:getCustomerPaymentMethodResponse><getCustomerPaymentMethodReturn xsi:type="ns1:PaymentMethod"><MethodType xsi:type="xsd:string">cc</MethodType><MethodID xsi:type="xsd:integer">103</MethodID><MethodName xsi:type="xsd:string">My CC</MethodName><SecondarySort xsi:type="xsd:integer">5</SecondarySort><Created xsi:type="xsd:dateTime">2011-06-09T13:48:57+08:00</Created><Modified xsi:type="xsd:dateTime">2011-06-09T13:48:57+08:00</Modified><AvsStreet xsi:type="xsd:string">456 My Street</AvsStreet><AvsZip xsi:type="xsd:string">K1C2N6</AvsZip><CardExpiration xsi:type="xsd:string">2012-12</CardExpiration><CardNumber xsi:type="xsd:string">XXXXXXXXXXXX2224</CardNumber><CardType xsi:type="xsd:string">V</CardType></getCustomerPaymentMethodReturn></ns1:getCustomerPaymentMethodResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end def failed_get_customer_payment_method_response - <<-XML -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><SOAP-ENV:Fault><faultcode>SOAP-ENV:Server</faultcode><faultstring>40453: Unable to locate requested payment method.</faultstring></SOAP-ENV:Fault></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><SOAP-ENV:Fault><faultcode>SOAP-ENV:Server</faultcode><faultstring>40453: Unable to locate requested payment method.</faultstring></SOAP-ENV:Fault></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end def successful_get_customer_payment_methods_response - <<-XML -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="urn:usaepay" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:getCustomerPaymentMethodsResponse><getCustomerPaymentMethodsReturn SOAP-ENC:arrayType="ns1:PaymentMethod[2]" xsi:type="ns1:PaymentMethodArray"><item xsi:type="ns1:PaymentMethod"><MethodType xsi:type="xsd:string">cc</MethodType><MethodID xsi:type="xsd:integer">93</MethodID><MethodName xsi:type="xsd:string">My CC</MethodName><SecondarySort xsi:type="xsd:integer">5</SecondarySort><Created xsi:type="xsd:dateTime">2011-06-09T08:10:44+08:00</Created><Modified xsi:type="xsd:dateTime">2011-06-09T08:10:44+08:00</Modified><AvsStreet xsi:type="xsd:string">456 My Street</AvsStreet><AvsZip xsi:type="xsd:string">K1C2N6</AvsZip><CardExpiration xsi:type="xsd:string">2012-12</CardExpiration><CardNumber xsi:type="xsd:string">XXXXXXXXXXXX2224</CardNumber><CardType xsi:type="xsd:string">V</CardType></item><item xsi:type="ns1:PaymentMethod"><MethodType xsi:type="xsd:string">cc</MethodType><MethodID xsi:type="xsd:integer">94</MethodID><MethodName xsi:type="xsd:string">Other CC</MethodName><SecondarySort xsi:type="xsd:integer">12</SecondarySort><Created xsi:type="xsd:dateTime">2011-06-09T08:10:44+08:00</Created><Modified xsi:type="xsd:dateTime">2011-06-09T08:10:44+08:00</Modified><AvsStreet xsi:type="xsd:string">456 My Street</AvsStreet><AvsZip xsi:type="xsd:string">K1C2N6</AvsZip><CardExpiration xsi:type="xsd:string">2012-12</CardExpiration><CardNumber xsi:type="xsd:string">XXXXXXXXXXXX2224</CardNumber><CardType xsi:type="xsd:string">V</CardType></item></getCustomerPaymentMethodsReturn></ns1:getCustomerPaymentMethodsResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="urn:usaepay" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:getCustomerPaymentMethodsResponse><getCustomerPaymentMethodsReturn SOAP-ENC:arrayType="ns1:PaymentMethod[2]" xsi:type="ns1:PaymentMethodArray"><item xsi:type="ns1:PaymentMethod"><MethodType xsi:type="xsd:string">cc</MethodType><MethodID xsi:type="xsd:integer">93</MethodID><MethodName xsi:type="xsd:string">My CC</MethodName><SecondarySort xsi:type="xsd:integer">5</SecondarySort><Created xsi:type="xsd:dateTime">2011-06-09T08:10:44+08:00</Created><Modified xsi:type="xsd:dateTime">2011-06-09T08:10:44+08:00</Modified><AvsStreet xsi:type="xsd:string">456 My Street</AvsStreet><AvsZip xsi:type="xsd:string">K1C2N6</AvsZip><CardExpiration xsi:type="xsd:string">2012-12</CardExpiration><CardNumber xsi:type="xsd:string">XXXXXXXXXXXX2224</CardNumber><CardType xsi:type="xsd:string">V</CardType></item><item xsi:type="ns1:PaymentMethod"><MethodType xsi:type="xsd:string">cc</MethodType><MethodID xsi:type="xsd:integer">94</MethodID><MethodName xsi:type="xsd:string">Other CC</MethodName><SecondarySort xsi:type="xsd:integer">12</SecondarySort><Created xsi:type="xsd:dateTime">2011-06-09T08:10:44+08:00</Created><Modified xsi:type="xsd:dateTime">2011-06-09T08:10:44+08:00</Modified><AvsStreet xsi:type="xsd:string">456 My Street</AvsStreet><AvsZip xsi:type="xsd:string">K1C2N6</AvsZip><CardExpiration xsi:type="xsd:string">2012-12</CardExpiration><CardNumber xsi:type="xsd:string">XXXXXXXXXXXX2224</CardNumber><CardType xsi:type="xsd:string">V</CardType></item></getCustomerPaymentMethodsReturn></ns1:getCustomerPaymentMethodsResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end def successful_single_get_customer_payment_methods_response - <<-XML -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="urn:usaepay" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:getCustomerPaymentMethodsResponse><getCustomerPaymentMethodsReturn SOAP-ENC:arrayType="ns1:PaymentMethod[1]" xsi:type="ns1:PaymentMethodArray"><item xsi:type="ns1:PaymentMethod"><MethodType xsi:type="xsd:string">cc</MethodType><MethodID xsi:type="xsd:integer">15</MethodID><MethodName xsi:type="xsd:string">My Visa</MethodName><SecondarySort xsi:type="xsd:integer">2</SecondarySort><Created xsi:type="xsd:dateTime">2011-06-05T19:44:09+08:00</Created><Modified xsi:type="xsd:dateTime">2011-06-05T19:44:09+08:00</Modified><AvsStreet xsi:type="xsd:string">456 My Street</AvsStreet><AvsZip xsi:type="xsd:string">K1C2N6</AvsZip><CardExpiration xsi:type="xsd:string">2012-09</CardExpiration><CardNumber xsi:type="xsd:string">XXXXXXXXXXXX4242</CardNumber><CardType xsi:type="xsd:string">V</CardType></item></getCustomerPaymentMethodsReturn></ns1:getCustomerPaymentMethodsResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="urn:usaepay" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:getCustomerPaymentMethodsResponse><getCustomerPaymentMethodsReturn SOAP-ENC:arrayType="ns1:PaymentMethod[1]" xsi:type="ns1:PaymentMethodArray"><item xsi:type="ns1:PaymentMethod"><MethodType xsi:type="xsd:string">cc</MethodType><MethodID xsi:type="xsd:integer">15</MethodID><MethodName xsi:type="xsd:string">My Visa</MethodName><SecondarySort xsi:type="xsd:integer">2</SecondarySort><Created xsi:type="xsd:dateTime">2011-06-05T19:44:09+08:00</Created><Modified xsi:type="xsd:dateTime">2011-06-05T19:44:09+08:00</Modified><AvsStreet xsi:type="xsd:string">456 My Street</AvsStreet><AvsZip xsi:type="xsd:string">K1C2N6</AvsZip><CardExpiration xsi:type="xsd:string">2012-09</CardExpiration><CardNumber xsi:type="xsd:string">XXXXXXXXXXXX4242</CardNumber><CardType xsi:type="xsd:string">V</CardType></item></getCustomerPaymentMethodsReturn></ns1:getCustomerPaymentMethodsResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end def successful_update_customer_payment_method_response - <<-XML -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:updateCustomerPaymentMethodResponse><updateCustomerPaymentMethodReturn xsi:type="xsd:boolean">true</updateCustomerPaymentMethodReturn></ns1:updateCustomerPaymentMethodResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:updateCustomerPaymentMethodResponse><updateCustomerPaymentMethodReturn xsi:type="xsd:boolean">true</updateCustomerPaymentMethodReturn></ns1:updateCustomerPaymentMethodResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end def successful_delete_customer_payment_method_response - <<-XML -<SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:deleteCustomerPaymentMethodResponse><deleteCustomerPaymentMethodReturn xsi:type="xsd:boolean">true</deleteCustomerPaymentMethodReturn></ns1:deleteCustomerPaymentMethodResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:deleteCustomerPaymentMethodResponse><deleteCustomerPaymentMethodReturn xsi:type="xsd:boolean">true</deleteCustomerPaymentMethodReturn></ns1:deleteCustomerPaymentMethodResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end def failed_delete_customer_payment_method_response - <<-XML -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><SOAP-ENV:Fault><faultcode>SOAP-ENV:Server</faultcode><faultstring>40453: Unable to locate requested payment method.</faultstring></SOAP-ENV:Fault></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><SOAP-ENV:Fault><faultcode>SOAP-ENV:Server</faultcode><faultstring>40453: Unable to locate requested payment method.</faultstring></SOAP-ENV:Fault></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end def failed_delete_customer_response - <<-XML -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><SOAP-ENV:Fault><faultcode>SOAP-ENV:Server</faultcode><faultstring>40030: Customer Not Found</faultstring></SOAP-ENV:Fault></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><SOAP-ENV:Fault><faultcode>SOAP-ENV:Server</faultcode><faultstring>40030: Customer Not Found</faultstring></SOAP-ENV:Fault></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end @@ -709,20 +708,20 @@ def successful_avs_cvv_transaction_response(method) end def successful_transaction_response(method) - <<-XML -<SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:#{method}Response><#{method}Return xsi:type="ns1:TransactionResponse"><AcsUrl xsi:type="xsd:string"></AcsUrl><AuthAmount xsi:type="xsd:double">0</AuthAmount><AuthCode xsi:type="xsd:string">47578712</AuthCode><AvsResult xsi:type="xsd:string">Unmapped AVS response ( )</AvsResult><AvsResultCode xsi:type="xsd:string"></AvsResultCode><BatchNum xsi:type="xsd:integer">0</BatchNum><BatchRefNum xsi:type="xsd:integer">0</BatchRefNum><CardCodeResult xsi:type="xsd:string">No CVV2/CVC data available for transaction.</CardCodeResult><CardCodeResultCode xsi:type="xsd:string"></CardCodeResultCode><CardLevelResult xsi:type="xsd:string">Unknown Code </CardLevelResult><CardLevelResultCode xsi:type="xsd:string"></CardLevelResultCode><ConversionRate xsi:type="xsd:double">0</ConversionRate><ConvertedAmount xsi:type="xsd:double">0</ConvertedAmount><ConvertedAmountCurrency xsi:type="xsd:string">840</ConvertedAmountCurrency><CustNum xsi:type="xsd:integer">0</CustNum><Error xsi:type="xsd:string"></Error><ErrorCode xsi:type="xsd:integer">0</ErrorCode><isDuplicate xsi:type="xsd:boolean">false</isDuplicate><Payload xsi:type="xsd:string"></Payload><RefNum xsi:type="xsd:integer">47568689</RefNum><Result xsi:type="xsd:string">Approved</Result><ResultCode xsi:type="xsd:string">A</ResultCode><Status xsi:type="xsd:string">Pending</Status><StatusCode xsi:type="xsd:string">P</StatusCode><VpasResultCode xsi:type="xsd:string"></VpasResultCode></#{method}Return></ns1:#{method}Response></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:#{method}Response><#{method}Return xsi:type="ns1:TransactionResponse"><AcsUrl xsi:type="xsd:string"></AcsUrl><AuthAmount xsi:type="xsd:double">0</AuthAmount><AuthCode xsi:type="xsd:string">47578712</AuthCode><AvsResult xsi:type="xsd:string">Unmapped AVS response ( )</AvsResult><AvsResultCode xsi:type="xsd:string"></AvsResultCode><BatchNum xsi:type="xsd:integer">0</BatchNum><BatchRefNum xsi:type="xsd:integer">0</BatchRefNum><CardCodeResult xsi:type="xsd:string">No CVV2/CVC data available for transaction.</CardCodeResult><CardCodeResultCode xsi:type="xsd:string"></CardCodeResultCode><CardLevelResult xsi:type="xsd:string">Unknown Code </CardLevelResult><CardLevelResultCode xsi:type="xsd:string"></CardLevelResultCode><ConversionRate xsi:type="xsd:double">0</ConversionRate><ConvertedAmount xsi:type="xsd:double">0</ConvertedAmount><ConvertedAmountCurrency xsi:type="xsd:string">840</ConvertedAmountCurrency><CustNum xsi:type="xsd:integer">0</CustNum><Error xsi:type="xsd:string"></Error><ErrorCode xsi:type="xsd:integer">0</ErrorCode><isDuplicate xsi:type="xsd:boolean">false</isDuplicate><Payload xsi:type="xsd:string"></Payload><RefNum xsi:type="xsd:integer">47568689</RefNum><Result xsi:type="xsd:string">Approved</Result><ResultCode xsi:type="xsd:string">A</ResultCode><Status xsi:type="xsd:string">Pending</Status><StatusCode xsi:type="xsd:string">P</StatusCode><VpasResultCode xsi:type="xsd:string"></VpasResultCode></#{method}Return></ns1:#{method}Response></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end def failed_run_check_sale_response - <<-XML -<SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:runCheckSaleResponse><runCheckSaleReturn xsi:type="ns1:TransactionResponse"><AcsUrl xsi:type="xsd:string"></AcsUrl><AuthAmount xsi:type="xsd:double">0</AuthAmount><AuthCode xsi:type="xsd:string">000000</AuthCode><AvsResult xsi:type="xsd:string">No AVS response (Typically no AVS data sent or swiped transaction)</AvsResult><AvsResultCode xsi:type="xsd:string"></AvsResultCode><BatchNum xsi:type="xsd:integer">0</BatchNum><BatchRefNum xsi:type="xsd:integer">0</BatchRefNum><CardCodeResult xsi:type="xsd:string">No CVV2/CVC data available for transaction.</CardCodeResult><CardCodeResultCode xsi:type="xsd:string"></CardCodeResultCode><CardLevelResult xsi:type="xsd:string">Unknown Code </CardLevelResult><CardLevelResultCode xsi:type="xsd:string"></CardLevelResultCode><ConversionRate xsi:type="xsd:double">0</ConversionRate><ConvertedAmount xsi:type="xsd:double">0</ConvertedAmount><ConvertedAmountCurrency xsi:type="xsd:string"></ConvertedAmountCurrency><CustNum xsi:type="xsd:integer">0</CustNum><Error xsi:type="xsd:string">Invalid Routing Number.</Error><ErrorCode xsi:type="xsd:integer">38</ErrorCode><isDuplicate xsi:type="xsd:boolean">false</isDuplicate><Payload xsi:type="xsd:string"></Payload><RefNum xsi:type="xsd:integer">0</RefNum><Result xsi:type="xsd:string">Error</Result><ResultCode xsi:type="xsd:string">E</ResultCode><Status xsi:type="xsd:string">Pending</Status><StatusCode xsi:type="xsd:string">P</StatusCode><VpasResultCode xsi:type="xsd:string"></VpasResultCode></runCheckSaleReturn></ns1:runCheckSaleResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:runCheckSaleResponse><runCheckSaleReturn xsi:type="ns1:TransactionResponse"><AcsUrl xsi:type="xsd:string"></AcsUrl><AuthAmount xsi:type="xsd:double">0</AuthAmount><AuthCode xsi:type="xsd:string">000000</AuthCode><AvsResult xsi:type="xsd:string">No AVS response (Typically no AVS data sent or swiped transaction)</AvsResult><AvsResultCode xsi:type="xsd:string"></AvsResultCode><BatchNum xsi:type="xsd:integer">0</BatchNum><BatchRefNum xsi:type="xsd:integer">0</BatchRefNum><CardCodeResult xsi:type="xsd:string">No CVV2/CVC data available for transaction.</CardCodeResult><CardCodeResultCode xsi:type="xsd:string"></CardCodeResultCode><CardLevelResult xsi:type="xsd:string">Unknown Code </CardLevelResult><CardLevelResultCode xsi:type="xsd:string"></CardLevelResultCode><ConversionRate xsi:type="xsd:double">0</ConversionRate><ConvertedAmount xsi:type="xsd:double">0</ConvertedAmount><ConvertedAmountCurrency xsi:type="xsd:string"></ConvertedAmountCurrency><CustNum xsi:type="xsd:integer">0</CustNum><Error xsi:type="xsd:string">Invalid Routing Number.</Error><ErrorCode xsi:type="xsd:integer">38</ErrorCode><isDuplicate xsi:type="xsd:boolean">false</isDuplicate><Payload xsi:type="xsd:string"></Payload><RefNum xsi:type="xsd:integer">0</RefNum><Result xsi:type="xsd:string">Error</Result><ResultCode xsi:type="xsd:string">E</ResultCode><Status xsi:type="xsd:string">Pending</Status><StatusCode xsi:type="xsd:string">P</StatusCode><VpasResultCode xsi:type="xsd:string"></VpasResultCode></runCheckSaleReturn></ns1:runCheckSaleResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end def failed_run_check_credit_response - <<-XML -<SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:runCheckCreditResponse><runCheckCreditReturn xsi:type="ns1:TransactionResponse"><AcsUrl xsi:type="xsd:string"></AcsUrl><AuthAmount xsi:type="xsd:double">0</AuthAmount><AuthCode xsi:type="xsd:string">000000</AuthCode><AvsResult xsi:type="xsd:string">No AVS response (Typically no AVS data sent or swiped transaction)</AvsResult><AvsResultCode xsi:type="xsd:string"></AvsResultCode><BatchNum xsi:type="xsd:integer">0</BatchNum><BatchRefNum xsi:type="xsd:integer">0</BatchRefNum><CardCodeResult xsi:type="xsd:string">No CVV2/CVC data available for transaction.</CardCodeResult><CardCodeResultCode xsi:type="xsd:string"></CardCodeResultCode><CardLevelResult xsi:type="xsd:string">Unknown Code </CardLevelResult><CardLevelResultCode xsi:type="xsd:string"></CardLevelResultCode><ConversionRate xsi:type="xsd:double">0</ConversionRate><ConvertedAmount xsi:type="xsd:double">0</ConvertedAmount><ConvertedAmountCurrency xsi:type="xsd:string"></ConvertedAmountCurrency><CustNum xsi:type="xsd:integer">0</CustNum><Error xsi:type="xsd:string">Invalid Routing Number.</Error><ErrorCode xsi:type="xsd:integer">38</ErrorCode><isDuplicate xsi:type="xsd:boolean">false</isDuplicate><Payload xsi:type="xsd:string"></Payload><RefNum xsi:type="xsd:integer">0</RefNum><Result xsi:type="xsd:string">Error</Result><ResultCode xsi:type="xsd:string">E</ResultCode><Status xsi:type="xsd:string">Pending</Status><StatusCode xsi:type="xsd:string">P</StatusCode><VpasResultCode xsi:type="xsd:string"></VpasResultCode></runCheckCreditReturn></ns1:runCheckCreditResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:runCheckCreditResponse><runCheckCreditReturn xsi:type="ns1:TransactionResponse"><AcsUrl xsi:type="xsd:string"></AcsUrl><AuthAmount xsi:type="xsd:double">0</AuthAmount><AuthCode xsi:type="xsd:string">000000</AuthCode><AvsResult xsi:type="xsd:string">No AVS response (Typically no AVS data sent or swiped transaction)</AvsResult><AvsResultCode xsi:type="xsd:string"></AvsResultCode><BatchNum xsi:type="xsd:integer">0</BatchNum><BatchRefNum xsi:type="xsd:integer">0</BatchRefNum><CardCodeResult xsi:type="xsd:string">No CVV2/CVC data available for transaction.</CardCodeResult><CardCodeResultCode xsi:type="xsd:string"></CardCodeResultCode><CardLevelResult xsi:type="xsd:string">Unknown Code </CardLevelResult><CardLevelResultCode xsi:type="xsd:string"></CardLevelResultCode><ConversionRate xsi:type="xsd:double">0</ConversionRate><ConvertedAmount xsi:type="xsd:double">0</ConvertedAmount><ConvertedAmountCurrency xsi:type="xsd:string"></ConvertedAmountCurrency><CustNum xsi:type="xsd:integer">0</CustNum><Error xsi:type="xsd:string">Invalid Routing Number.</Error><ErrorCode xsi:type="xsd:integer">38</ErrorCode><isDuplicate xsi:type="xsd:boolean">false</isDuplicate><Payload xsi:type="xsd:string"></Payload><RefNum xsi:type="xsd:integer">0</RefNum><Result xsi:type="xsd:string">Error</Result><ResultCode xsi:type="xsd:string">E</ResultCode><Status xsi:type="xsd:string">Pending</Status><StatusCode xsi:type="xsd:string">P</StatusCode><VpasResultCode xsi:type="xsd:string"></VpasResultCode></runCheckCreditReturn></ns1:runCheckCreditResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end @@ -732,15 +731,15 @@ def successful_post_auth_response end def failed_post_auth_response - <<-XML -<SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:postAuthResponse><postAuthReturn xsi:type="ns1:TransactionResponse"><AcsUrl xsi:type="xsd:string"></AcsUrl><AuthAmount xsi:type="xsd:double">0</AuthAmount><AuthCode xsi:type="xsd:string">000000</AuthCode><AvsResult xsi:type="xsd:string">No AVS response (Typically no AVS data sent or swiped transaction)</AvsResult><AvsResultCode xsi:type="xsd:string"></AvsResultCode><BatchNum xsi:type="xsd:integer">0</BatchNum><BatchRefNum xsi:type="xsd:integer">0</BatchRefNum><CardCodeResult xsi:type="xsd:string">No CVV2/CVC data available for transaction.</CardCodeResult><CardCodeResultCode xsi:type="xsd:string"></CardCodeResultCode><CardLevelResult xsi:type="xsd:string">Unknown Code </CardLevelResult><CardLevelResultCode xsi:type="xsd:string"></CardLevelResultCode><ConversionRate xsi:type="xsd:double">0</ConversionRate><ConvertedAmount xsi:type="xsd:double">0</ConvertedAmount><ConvertedAmountCurrency xsi:type="xsd:string"></ConvertedAmountCurrency><CustNum xsi:type="xsd:integer">0</CustNum><Error xsi:type="xsd:string">Valid AuthCode required for PostAuth</Error><ErrorCode xsi:type="xsd:integer">108</ErrorCode><isDuplicate xsi:type="xsd:boolean">false</isDuplicate><Payload xsi:type="xsd:string"></Payload><RefNum xsi:type="xsd:integer">0</RefNum><Result xsi:type="xsd:string">Error</Result><ResultCode xsi:type="xsd:string">E</ResultCode><Status xsi:type="xsd:string">Pending</Status><StatusCode xsi:type="xsd:string">P</StatusCode><VpasResultCode xsi:type="xsd:string"></VpasResultCode></postAuthReturn></ns1:postAuthResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:postAuthResponse><postAuthReturn xsi:type="ns1:TransactionResponse"><AcsUrl xsi:type="xsd:string"></AcsUrl><AuthAmount xsi:type="xsd:double">0</AuthAmount><AuthCode xsi:type="xsd:string">000000</AuthCode><AvsResult xsi:type="xsd:string">No AVS response (Typically no AVS data sent or swiped transaction)</AvsResult><AvsResultCode xsi:type="xsd:string"></AvsResultCode><BatchNum xsi:type="xsd:integer">0</BatchNum><BatchRefNum xsi:type="xsd:integer">0</BatchRefNum><CardCodeResult xsi:type="xsd:string">No CVV2/CVC data available for transaction.</CardCodeResult><CardCodeResultCode xsi:type="xsd:string"></CardCodeResultCode><CardLevelResult xsi:type="xsd:string">Unknown Code </CardLevelResult><CardLevelResultCode xsi:type="xsd:string"></CardLevelResultCode><ConversionRate xsi:type="xsd:double">0</ConversionRate><ConvertedAmount xsi:type="xsd:double">0</ConvertedAmount><ConvertedAmountCurrency xsi:type="xsd:string"></ConvertedAmountCurrency><CustNum xsi:type="xsd:integer">0</CustNum><Error xsi:type="xsd:string">Valid AuthCode required for PostAuth</Error><ErrorCode xsi:type="xsd:integer">108</ErrorCode><isDuplicate xsi:type="xsd:boolean">false</isDuplicate><Payload xsi:type="xsd:string"></Payload><RefNum xsi:type="xsd:integer">0</RefNum><Result xsi:type="xsd:string">Error</Result><ResultCode xsi:type="xsd:string">E</ResultCode><Status xsi:type="xsd:string">Pending</Status><StatusCode xsi:type="xsd:string">P</StatusCode><VpasResultCode xsi:type="xsd:string"></VpasResultCode></postAuthReturn></ns1:postAuthResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end def successful_capture_transaction_response - <<-XML -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:captureTransactionResponse><captureTransactionReturn xsi:type="ns1:TransactionResponse"><AcsUrl xsi:type="xsd:string"></AcsUrl><AuthAmount xsi:type="xsd:double">0</AuthAmount><AuthCode xsi:type="xsd:string">004043</AuthCode><AvsResult xsi:type="xsd:string">No AVS response (Typically no AVS data sent or swiped transaction)</AvsResult><AvsResultCode xsi:type="xsd:string"></AvsResultCode><BatchNum xsi:type="xsd:integer">0</BatchNum><BatchRefNum xsi:type="xsd:integer">0</BatchRefNum><CardCodeResult xsi:type="xsd:string">No CVV2/CVC data available for transaction.</CardCodeResult><CardCodeResultCode xsi:type="xsd:string"></CardCodeResultCode><CardLevelResult xsi:type="xsd:string">Unknown Code </CardLevelResult><CardLevelResultCode xsi:type="xsd:string"></CardLevelResultCode><ConversionRate xsi:type="xsd:double">0</ConversionRate><ConvertedAmount xsi:type="xsd:double">0</ConvertedAmount><ConvertedAmountCurrency xsi:type="xsd:string">840</ConvertedAmountCurrency><CustNum xsi:type="xsd:integer">0</CustNum><Error xsi:type="xsd:string">Approved</Error><ErrorCode xsi:type="xsd:integer">0</ErrorCode><isDuplicate xsi:type="xsd:boolean">false</isDuplicate><Payload xsi:type="xsd:string"></Payload><RefNum xsi:type="xsd:integer">47587252</RefNum><Result xsi:type="xsd:string">Approved</Result><ResultCode xsi:type="xsd:string">A</ResultCode><Status xsi:type="xsd:string">Pending</Status><StatusCode xsi:type="xsd:string">P</StatusCode><VpasResultCode xsi:type="xsd:string"></VpasResultCode></captureTransactionReturn></ns1:captureTransactionResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:captureTransactionResponse><captureTransactionReturn xsi:type="ns1:TransactionResponse"><AcsUrl xsi:type="xsd:string"></AcsUrl><AuthAmount xsi:type="xsd:double">0</AuthAmount><AuthCode xsi:type="xsd:string">004043</AuthCode><AvsResult xsi:type="xsd:string">No AVS response (Typically no AVS data sent or swiped transaction)</AvsResult><AvsResultCode xsi:type="xsd:string"></AvsResultCode><BatchNum xsi:type="xsd:integer">0</BatchNum><BatchRefNum xsi:type="xsd:integer">0</BatchRefNum><CardCodeResult xsi:type="xsd:string">No CVV2/CVC data available for transaction.</CardCodeResult><CardCodeResultCode xsi:type="xsd:string"></CardCodeResultCode><CardLevelResult xsi:type="xsd:string">Unknown Code </CardLevelResult><CardLevelResultCode xsi:type="xsd:string"></CardLevelResultCode><ConversionRate xsi:type="xsd:double">0</ConversionRate><ConvertedAmount xsi:type="xsd:double">0</ConvertedAmount><ConvertedAmountCurrency xsi:type="xsd:string">840</ConvertedAmountCurrency><CustNum xsi:type="xsd:integer">0</CustNum><Error xsi:type="xsd:string">Approved</Error><ErrorCode xsi:type="xsd:integer">0</ErrorCode><isDuplicate xsi:type="xsd:boolean">false</isDuplicate><Payload xsi:type="xsd:string"></Payload><RefNum xsi:type="xsd:integer">47587252</RefNum><Result xsi:type="xsd:string">Approved</Result><ResultCode xsi:type="xsd:string">A</ResultCode><Status xsi:type="xsd:string">Pending</Status><StatusCode xsi:type="xsd:string">P</StatusCode><VpasResultCode xsi:type="xsd:string"></VpasResultCode></captureTransactionReturn></ns1:captureTransactionResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end @@ -750,61 +749,61 @@ def successful_override_transaction_response end def failed_override_transaction_response - <<-XML -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><SOAP-ENV:Fault><faultcode>SOAP-ENV:Server</faultcode><faultstring>105: Override not available for requested transaction.</faultstring></SOAP-ENV:Fault></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><SOAP-ENV:Fault><faultcode>SOAP-ENV:Server</faultcode><faultstring>105: Override not available for requested transaction.</faultstring></SOAP-ENV:Fault></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end def successful_void_transaction_response - <<-XML -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:voidTransactionResponse><voidTransactionReturn xsi:type="xsd:boolean">true</voidTransactionReturn></ns1:voidTransactionResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:voidTransactionResponse><voidTransactionReturn xsi:type="xsd:boolean">true</voidTransactionReturn></ns1:voidTransactionResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end def successful_refund_transaction_response - <<-XML -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="urn:usaepay" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:refundTransactionResponse><refundTransactionReturn xsi:type="ns1:TransactionResponse"><AcsUrl xsi:nil="true"/><AuthAmount xsi:type="xsd:double">0</AuthAmount><AuthCode xsi:type="xsd:string">47597281</AuthCode><AvsResult xsi:type="xsd:string">Unmapped AVS response ( )</AvsResult><AvsResultCode xsi:type="xsd:string"></AvsResultCode><BatchNum xsi:nil="true"/><BatchRefNum xsi:nil="true"/><CardCodeResult xsi:type="xsd:string">No CVV2/CVC data available for transaction.</CardCodeResult><CardCodeResultCode xsi:type="xsd:string"></CardCodeResultCode><CardLevelResult xsi:type="xsd:string">Unknown Code </CardLevelResult><CardLevelResultCode xsi:type="xsd:string"></CardLevelResultCode><ConversionRate xsi:type="xsd:double">0</ConversionRate><ConvertedAmount xsi:type="xsd:double">0</ConvertedAmount><ConvertedAmountCurrency xsi:nil="true"/><CustNum xsi:type="xsd:integer">0</CustNum><Error xsi:type="xsd:string"></Error><ErrorCode xsi:nil="true"/><isDuplicate xsi:type="xsd:boolean">false</isDuplicate><Payload xsi:nil="true"/><RefNum xsi:type="xsd:integer">47587258</RefNum><Result xsi:type="xsd:string">Approved</Result><ResultCode xsi:type="xsd:string">A</ResultCode><Status xsi:type="xsd:string">Pending</Status><StatusCode xsi:type="xsd:string">P</StatusCode><VpasResultCode xsi:type="xsd:string"></VpasResultCode></refundTransactionReturn></ns1:refundTransactionResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="urn:usaepay" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:refundTransactionResponse><refundTransactionReturn xsi:type="ns1:TransactionResponse"><AcsUrl xsi:nil="true"/><AuthAmount xsi:type="xsd:double">0</AuthAmount><AuthCode xsi:type="xsd:string">47597281</AuthCode><AvsResult xsi:type="xsd:string">Unmapped AVS response ( )</AvsResult><AvsResultCode xsi:type="xsd:string"></AvsResultCode><BatchNum xsi:nil="true"/><BatchRefNum xsi:nil="true"/><CardCodeResult xsi:type="xsd:string">No CVV2/CVC data available for transaction.</CardCodeResult><CardCodeResultCode xsi:type="xsd:string"></CardCodeResultCode><CardLevelResult xsi:type="xsd:string">Unknown Code </CardLevelResult><CardLevelResultCode xsi:type="xsd:string"></CardLevelResultCode><ConversionRate xsi:type="xsd:double">0</ConversionRate><ConvertedAmount xsi:type="xsd:double">0</ConvertedAmount><ConvertedAmountCurrency xsi:nil="true"/><CustNum xsi:type="xsd:integer">0</CustNum><Error xsi:type="xsd:string"></Error><ErrorCode xsi:nil="true"/><isDuplicate xsi:type="xsd:boolean">false</isDuplicate><Payload xsi:nil="true"/><RefNum xsi:type="xsd:integer">47587258</RefNum><Result xsi:type="xsd:string">Approved</Result><ResultCode xsi:type="xsd:string">A</ResultCode><Status xsi:type="xsd:string">Pending</Status><StatusCode xsi:type="xsd:string">P</StatusCode><VpasResultCode xsi:type="xsd:string"></VpasResultCode></refundTransactionReturn></ns1:refundTransactionResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end # Transaction Response ============================================== def successful_get_transaction_status_response - <<-XML -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="urn:usaepay" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:getTransactionStatusResponse><getTransactionStatusReturn xsi:type="ns1:TransactionResponse"><AcsUrl xsi:nil="true"/><AuthAmount xsi:type="xsd:double">50</AuthAmount><AuthCode xsi:type="xsd:string">050162</AuthCode><AvsResult xsi:type="xsd:string">Address: Match &amp; 5 Digit Zip: Match</AvsResult><AvsResultCode xsi:type="xsd:string">YYY</AvsResultCode><BatchNum xsi:type="xsd:integer">1</BatchNum><BatchRefNum xsi:type="xsd:integer">14004</BatchRefNum><CardCodeResult xsi:type="xsd:string">Match</CardCodeResult><CardCodeResultCode xsi:type="xsd:string">M</CardCodeResultCode><CardLevelResult xsi:nil="true"/><CardLevelResultCode xsi:nil="true"/><ConversionRate xsi:type="xsd:double">0</ConversionRate><ConvertedAmount xsi:type="xsd:double">0</ConvertedAmount><ConvertedAmountCurrency xsi:type="xsd:string"></ConvertedAmountCurrency><CustNum xsi:type="xsd:integer">0</CustNum><Error xsi:type="xsd:string">Approved</Error><ErrorCode xsi:type="xsd:integer">0</ErrorCode><isDuplicate xsi:type="xsd:boolean">false</isDuplicate><Payload xsi:nil="true"/><RefNum xsi:type="xsd:integer">47569011</RefNum><Result xsi:type="xsd:string">Approved</Result><ResultCode xsi:type="xsd:string">A</ResultCode><Status xsi:type="xsd:string">Pending</Status><StatusCode xsi:type="xsd:string">P</StatusCode><VpasResultCode xsi:nil="true"/></getTransactionStatusReturn></ns1:getTransactionStatusResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="urn:usaepay" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:getTransactionStatusResponse><getTransactionStatusReturn xsi:type="ns1:TransactionResponse"><AcsUrl xsi:nil="true"/><AuthAmount xsi:type="xsd:double">50</AuthAmount><AuthCode xsi:type="xsd:string">050162</AuthCode><AvsResult xsi:type="xsd:string">Address: Match &amp; 5 Digit Zip: Match</AvsResult><AvsResultCode xsi:type="xsd:string">YYY</AvsResultCode><BatchNum xsi:type="xsd:integer">1</BatchNum><BatchRefNum xsi:type="xsd:integer">14004</BatchRefNum><CardCodeResult xsi:type="xsd:string">Match</CardCodeResult><CardCodeResultCode xsi:type="xsd:string">M</CardCodeResultCode><CardLevelResult xsi:nil="true"/><CardLevelResultCode xsi:nil="true"/><ConversionRate xsi:type="xsd:double">0</ConversionRate><ConvertedAmount xsi:type="xsd:double">0</ConvertedAmount><ConvertedAmountCurrency xsi:type="xsd:string"></ConvertedAmountCurrency><CustNum xsi:type="xsd:integer">0</CustNum><Error xsi:type="xsd:string">Approved</Error><ErrorCode xsi:type="xsd:integer">0</ErrorCode><isDuplicate xsi:type="xsd:boolean">false</isDuplicate><Payload xsi:nil="true"/><RefNum xsi:type="xsd:integer">47569011</RefNum><Result xsi:type="xsd:string">Approved</Result><ResultCode xsi:type="xsd:string">A</ResultCode><Status xsi:type="xsd:string">Pending</Status><StatusCode xsi:type="xsd:string">P</StatusCode><VpasResultCode xsi:nil="true"/></getTransactionStatusReturn></ns1:getTransactionStatusResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end def successful_get_transaction_custom_response - <<-XML -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="urn:usaepay" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:getTransactionCustomResponse><getTransactionCustomReturn SOAP-ENC:arrayType="ns1:FieldValue[2]" xsi:type="ns1:FieldValueArray"><item xsi:type="ns1:FieldValue"><Field xsi:type="xsd:string">Response.StatusCode</Field><Value xsi:type="xsd:string">P</Value></item><item xsi:type="ns1:FieldValue"><Field xsi:type="xsd:string">Response.Status</Field><Value xsi:type="xsd:string">Pending</Value></item></getTransactionCustomReturn></ns1:getTransactionCustomResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="urn:usaepay" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:getTransactionCustomResponse><getTransactionCustomReturn SOAP-ENC:arrayType="ns1:FieldValue[2]" xsi:type="ns1:FieldValueArray"><item xsi:type="ns1:FieldValue"><Field xsi:type="xsd:string">Response.StatusCode</Field><Value xsi:type="xsd:string">P</Value></item><item xsi:type="ns1:FieldValue"><Field xsi:type="xsd:string">Response.Status</Field><Value xsi:type="xsd:string">Pending</Value></item></getTransactionCustomReturn></ns1:getTransactionCustomResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end def successful_get_check_trace_response - <<-XML -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:getCheckTraceResponse><getCheckTraceReturn xsi:type="ns1:CheckTrace"><Status xsi:type="xsd:string">Pending</Status><StatusCode xsi:type="xsd:string">P</StatusCode><Effective xsi:type="xsd:string">2011-06-21</Effective><TrackingNum xsi:type="xsd:string">11061908516155</TrackingNum></getCheckTraceReturn></ns1:getCheckTraceResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:getCheckTraceResponse><getCheckTraceReturn xsi:type="ns1:CheckTrace"><Status xsi:type="xsd:string">Pending</Status><StatusCode xsi:type="xsd:string">P</StatusCode><Effective xsi:type="xsd:string">2011-06-21</Effective><TrackingNum xsi:type="xsd:string">11061908516155</TrackingNum></getCheckTraceReturn></ns1:getCheckTraceResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end def successful_get_transaction_response - <<-XML -<?xml version="1.0" encoding="UTF-8"?> -<SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:getTransactionResponse><getTransactionReturn xsi:type="ns1:TransactionObject"><AccountHolder xsi:type="xsd:string"></AccountHolder><BillingAddress xsi:type="ns1:Address"><City xsi:type="xsd:string">Ottawa</City><Company xsi:type="xsd:string">Widgets Inc</Company><Country xsi:type="xsd:string">CA</Country><Email xsi:type="xsd:string"></Email><Fax xsi:type="xsd:string"></Fax><FirstName xsi:type="xsd:string">Jim</FirstName><LastName xsi:type="xsd:string">Smith</LastName><Phone xsi:type="xsd:string">(555)555-5555</Phone><State xsi:type="xsd:string">ON</State><Street xsi:type="xsd:string">456 My Street</Street><Street2 xsi:type="xsd:string">Apt 1</Street2><Zip xsi:type="xsd:string">K1C2N6</Zip></BillingAddress><CheckData xsi:type="ns1:CheckData"><Account xsi:nil="true"/><Routing xsi:nil="true"/></CheckData><CheckTrace xsi:type="ns1:CheckTrace"/><ClientIP xsi:type="xsd:string">127.0.0.1</ClientIP><CreditCardData xsi:type="ns1:CreditCardData"><AvsStreet xsi:type="xsd:string">456 My Street</AvsStreet><AvsZip xsi:type="xsd:string">K1C2N6</AvsZip><CardCode xsi:type="xsd:string">XXX</CardCode><CardExpiration xsi:type="xsd:string">XXXX</CardExpiration><CardNumber xsi:type="xsd:string">XXXXXXXXXXXX2224</CardNumber><CardPresent xsi:type="xsd:boolean">false</CardPresent><CardType xsi:type="xsd:string">V</CardType><InternalCardAuth xsi:type="xsd:boolean">false</InternalCardAuth><MagStripe xsi:type="xsd:string"></MagStripe><MagSupport xsi:type="xsd:string"></MagSupport><Pares xsi:type="xsd:string"></Pares><TermType xsi:type="xsd:string"></TermType></CreditCardData><CustomerID xsi:type="xsd:string"></CustomerID><CustomFields SOAP-ENC:arrayType="ns1:FieldValue[0]" xsi:type="ns1:FieldValueArray"/><DateTime xsi:type="xsd:string">2011-06-11 19:23:37</DateTime><Details xsi:type="ns1:TransactionDetail"><Amount xsi:type="xsd:double">50</Amount><Clerk xsi:type="xsd:string"></Clerk><Currency xsi:type="xsd:string"></Currency><Description xsi:type="xsd:string"></Description><Comments xsi:type="xsd:string"></Comments><Discount xsi:type="xsd:double">0</Discount><Invoice xsi:type="xsd:string"></Invoice><NonTax xsi:type="xsd:boolean">false</NonTax><OrderID xsi:type="xsd:string"></OrderID><PONum xsi:type="xsd:string"></PONum><Shipping xsi:type="xsd:double">0</Shipping><Subtotal xsi:type="xsd:double">0</Subtotal><Table xsi:type="xsd:string"></Table><Tax xsi:type="xsd:double">0</Tax><Terminal xsi:type="xsd:string"></Terminal><Tip xsi:type="xsd:double">0</Tip></Details><LineItems SOAP-ENC:arrayType="ns1:LineItem[0]" xsi:type="ns1:LineItemArray"/><Response xsi:type="ns1:TransactionResponse"><AcsUrl xsi:nil="true"/><AuthAmount xsi:type="xsd:double">50</AuthAmount><AuthCode xsi:type="xsd:string">050129</AuthCode><AvsResult xsi:type="xsd:string">Address: Match &amp; 5 Digit Zip: Match</AvsResult><AvsResultCode xsi:type="xsd:string">YYY</AvsResultCode><BatchNum xsi:type="xsd:integer">1</BatchNum><BatchRefNum xsi:type="xsd:integer">14004</BatchRefNum><CardCodeResult xsi:type="xsd:string">Match</CardCodeResult><CardCodeResultCode xsi:type="xsd:string">M</CardCodeResultCode><CardLevelResult xsi:nil="true"/><CardLevelResultCode xsi:nil="true"/><ConversionRate xsi:type="xsd:double">0</ConversionRate><ConvertedAmount xsi:type="xsd:double">0</ConvertedAmount><ConvertedAmountCurrency xsi:type="xsd:string"></ConvertedAmountCurrency><CustNum xsi:type="xsd:integer">0</CustNum><Error xsi:type="xsd:string">Approved</Error><ErrorCode xsi:type="xsd:integer">0</ErrorCode><isDuplicate xsi:type="xsd:boolean">false</isDuplicate><Payload xsi:nil="true"/><RefNum xsi:type="xsd:integer">47568950</RefNum><Result xsi:type="xsd:string">Approved</Result><ResultCode xsi:type="xsd:string">A</ResultCode><Status xsi:type="xsd:string">Pending</Status><StatusCode xsi:type="xsd:string">P</StatusCode><VpasResultCode xsi:nil="true"/></Response><ServerIP xsi:type="xsd:string">67.168.21.42</ServerIP><ShippingAddress xsi:type="ns1:Address"><City xsi:type="xsd:string">Ottawa</City><Company xsi:type="xsd:string">Widgets Inc</Company><Country xsi:type="xsd:string">CA</Country><Email xsi:type="xsd:string"></Email><Fax xsi:type="xsd:string"></Fax><FirstName xsi:type="xsd:string">Jim</FirstName><LastName xsi:type="xsd:string">Smith</LastName><Phone xsi:type="xsd:string">(555)555-5555</Phone><State xsi:type="xsd:string">ON</State><Street xsi:type="xsd:string">456 My Street</Street><Street2 xsi:type="xsd:string">Apt 1</Street2><Zip xsi:type="xsd:string">K1C2N6</Zip></ShippingAddress><Source xsi:type="xsd:string">test</Source><Status xsi:type="xsd:string">Authorized (Pending Settlement)</Status><TransactionType xsi:type="xsd:string">Sale</TransactionType><User xsi:type="xsd:string">auto</User></getTransactionReturn></ns1:getTransactionResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:getTransactionResponse><getTransactionReturn xsi:type="ns1:TransactionObject"><AccountHolder xsi:type="xsd:string"></AccountHolder><BillingAddress xsi:type="ns1:Address"><City xsi:type="xsd:string">Ottawa</City><Company xsi:type="xsd:string">Widgets Inc</Company><Country xsi:type="xsd:string">CA</Country><Email xsi:type="xsd:string"></Email><Fax xsi:type="xsd:string"></Fax><FirstName xsi:type="xsd:string">Jim</FirstName><LastName xsi:type="xsd:string">Smith</LastName><Phone xsi:type="xsd:string">(555)555-5555</Phone><State xsi:type="xsd:string">ON</State><Street xsi:type="xsd:string">456 My Street</Street><Street2 xsi:type="xsd:string">Apt 1</Street2><Zip xsi:type="xsd:string">K1C2N6</Zip></BillingAddress><CheckData xsi:type="ns1:CheckData"><Account xsi:nil="true"/><Routing xsi:nil="true"/></CheckData><CheckTrace xsi:type="ns1:CheckTrace"/><ClientIP xsi:type="xsd:string">127.0.0.1</ClientIP><CreditCardData xsi:type="ns1:CreditCardData"><AvsStreet xsi:type="xsd:string">456 My Street</AvsStreet><AvsZip xsi:type="xsd:string">K1C2N6</AvsZip><CardCode xsi:type="xsd:string">XXX</CardCode><CardExpiration xsi:type="xsd:string">XXXX</CardExpiration><CardNumber xsi:type="xsd:string">XXXXXXXXXXXX2224</CardNumber><CardPresent xsi:type="xsd:boolean">false</CardPresent><CardType xsi:type="xsd:string">V</CardType><InternalCardAuth xsi:type="xsd:boolean">false</InternalCardAuth><MagStripe xsi:type="xsd:string"></MagStripe><MagSupport xsi:type="xsd:string"></MagSupport><Pares xsi:type="xsd:string"></Pares><TermType xsi:type="xsd:string"></TermType></CreditCardData><CustomerID xsi:type="xsd:string"></CustomerID><CustomFields SOAP-ENC:arrayType="ns1:FieldValue[0]" xsi:type="ns1:FieldValueArray"/><DateTime xsi:type="xsd:string">2011-06-11 19:23:37</DateTime><Details xsi:type="ns1:TransactionDetail"><Amount xsi:type="xsd:double">50</Amount><Clerk xsi:type="xsd:string"></Clerk><Currency xsi:type="xsd:string"></Currency><Description xsi:type="xsd:string"></Description><Comments xsi:type="xsd:string"></Comments><Discount xsi:type="xsd:double">0</Discount><Invoice xsi:type="xsd:string"></Invoice><NonTax xsi:type="xsd:boolean">false</NonTax><OrderID xsi:type="xsd:string"></OrderID><PONum xsi:type="xsd:string"></PONum><Shipping xsi:type="xsd:double">0</Shipping><Subtotal xsi:type="xsd:double">0</Subtotal><Table xsi:type="xsd:string"></Table><Tax xsi:type="xsd:double">0</Tax><Terminal xsi:type="xsd:string"></Terminal><Tip xsi:type="xsd:double">0</Tip></Details><LineItems SOAP-ENC:arrayType="ns1:LineItem[0]" xsi:type="ns1:LineItemArray"/><Response xsi:type="ns1:TransactionResponse"><AcsUrl xsi:nil="true"/><AuthAmount xsi:type="xsd:double">50</AuthAmount><AuthCode xsi:type="xsd:string">050129</AuthCode><AvsResult xsi:type="xsd:string">Address: Match &amp; 5 Digit Zip: Match</AvsResult><AvsResultCode xsi:type="xsd:string">YYY</AvsResultCode><BatchNum xsi:type="xsd:integer">1</BatchNum><BatchRefNum xsi:type="xsd:integer">14004</BatchRefNum><CardCodeResult xsi:type="xsd:string">Match</CardCodeResult><CardCodeResultCode xsi:type="xsd:string">M</CardCodeResultCode><CardLevelResult xsi:nil="true"/><CardLevelResultCode xsi:nil="true"/><ConversionRate xsi:type="xsd:double">0</ConversionRate><ConvertedAmount xsi:type="xsd:double">0</ConvertedAmount><ConvertedAmountCurrency xsi:type="xsd:string"></ConvertedAmountCurrency><CustNum xsi:type="xsd:integer">0</CustNum><Error xsi:type="xsd:string">Approved</Error><ErrorCode xsi:type="xsd:integer">0</ErrorCode><isDuplicate xsi:type="xsd:boolean">false</isDuplicate><Payload xsi:nil="true"/><RefNum xsi:type="xsd:integer">47568950</RefNum><Result xsi:type="xsd:string">Approved</Result><ResultCode xsi:type="xsd:string">A</ResultCode><Status xsi:type="xsd:string">Pending</Status><StatusCode xsi:type="xsd:string">P</StatusCode><VpasResultCode xsi:nil="true"/></Response><ServerIP xsi:type="xsd:string">67.168.21.42</ServerIP><ShippingAddress xsi:type="ns1:Address"><City xsi:type="xsd:string">Ottawa</City><Company xsi:type="xsd:string">Widgets Inc</Company><Country xsi:type="xsd:string">CA</Country><Email xsi:type="xsd:string"></Email><Fax xsi:type="xsd:string"></Fax><FirstName xsi:type="xsd:string">Jim</FirstName><LastName xsi:type="xsd:string">Smith</LastName><Phone xsi:type="xsd:string">(555)555-5555</Phone><State xsi:type="xsd:string">ON</State><Street xsi:type="xsd:string">456 My Street</Street><Street2 xsi:type="xsd:string">Apt 1</Street2><Zip xsi:type="xsd:string">K1C2N6</Zip></ShippingAddress><Source xsi:type="xsd:string">test</Source><Status xsi:type="xsd:string">Authorized (Pending Settlement)</Status><TransactionType xsi:type="xsd:string">Sale</TransactionType><User xsi:type="xsd:string">auto</User></getTransactionReturn></ns1:getTransactionResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end # Account =========================================================== def successful_get_account_details - <<-XML -<SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:getAccountDetailsResponse><getAccountDetailsReturn xsi:type="ns1:AccountDetails"><CardholderAuthentication xsi:type="xsd:string">Disabled</CardholderAuthentication> -<CheckPlatform xsi:type="xsd:string">TestBed</CheckPlatform><CreditCardPlatform xsi:type="xsd:string">Test Bed</CreditCardPlatform><DebitCardSupport xsi:type="xsd:boolean">false</DebitCardSupport><DirectPayPlatform xsi:type="xsd:string">Disabled</DirectPayPlatform><Industry xsi:type="xsd:string">eCommerce</Industry><SupportedCurrencies SOAP-ENC:arrayType="ns1:CurrencyObject[0]" xsi:type="ns1:CurrencyObjectArray"/></getAccountDetailsReturn></ns1:getAccountDetailsResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> + <<~XML + <SOAP-ENV:Envelope 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/"><SOAP-ENV:Body><ns1:getAccountDetailsResponse><getAccountDetailsReturn xsi:type="ns1:AccountDetails"><CardholderAuthentication xsi:type="xsd:string">Disabled</CardholderAuthentication> + <CheckPlatform xsi:type="xsd:string">TestBed</CheckPlatform><CreditCardPlatform xsi:type="xsd:string">Test Bed</CreditCardPlatform><DebitCardSupport xsi:type="xsd:boolean">false</DebitCardSupport><DirectPayPlatform xsi:type="xsd:string">Disabled</DirectPayPlatform><Industry xsi:type="xsd:string">eCommerce</Industry><SupportedCurrencies SOAP-ENC:arrayType="ns1:CurrencyObject[0]" xsi:type="ns1:CurrencyObjectArray"/></getAccountDetailsReturn></ns1:getAccountDetailsResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> XML end diff --git a/test/unit/gateways/usa_epay_test.rb b/test/unit/gateways/usa_epay_test.rb index 4aa91905676..e7647eb15c8 100644 --- a/test/unit/gateways/usa_epay_test.rb +++ b/test/unit/gateways/usa_epay_test.rb @@ -2,29 +2,28 @@ require 'logger' class UsaEpayTest < Test::Unit::TestCase - def test_transaction_gateway_created gateway = UsaEpayGateway.new( - :login => 'X' + login: 'X' ) assert_kind_of UsaEpayTransactionGateway, gateway end def test_advanced_gateway_created_with_software_id gateway = UsaEpayGateway.new( - :login => 'X', - :password => 'Y', - :software_id => 'Z' + login: 'X', + password: 'Y', + software_id: 'Z' ) assert_kind_of UsaEpayAdvancedGateway, gateway end def test_advanced_gateway_created_with_urls gateway = UsaEpayGateway.new( - :login => 'X', - :password => 'Y', - :test_url => 'Z', - :live_url => 'Z' + login: 'X', + password: 'Y', + test_url: 'Z', + live_url: 'Z' ) assert_kind_of UsaEpayAdvancedGateway, gateway end diff --git a/test/unit/gateways/usa_epay_transaction_test.rb b/test/unit/gateways/usa_epay_transaction_test.rb index 7138a6e5b52..297e1a2ef65 100644 --- a/test/unit/gateways/usa_epay_transaction_test.rb +++ b/test/unit/gateways/usa_epay_transaction_test.rb @@ -4,13 +4,13 @@ class UsaEpayTransactionTest < Test::Unit::TestCase include CommStub def setup - @gateway = UsaEpayTransactionGateway.new(:login => 'LOGIN') + @gateway = UsaEpayTransactionGateway.new(login: 'LOGIN') @credit_card = credit_card('4242424242424242') @check = check @options = { - :billing_address => address, - :shipping_address => address + billing_address: address, + shipping_address: address } @amount = 100 end @@ -21,7 +21,7 @@ def test_urls end def test_request_url_live - gateway = UsaEpayTransactionGateway.new(:login => 'LOGIN', :test => false) + gateway = UsaEpayTransactionGateway.new(login: 'LOGIN', test: false) gateway.expects(:ssl_post). with('https://www.usaepay.com/gate', regexp_matches(Regexp.new('^' + Regexp.escape(purchase_request)))). returns(successful_purchase_response) @@ -53,6 +53,20 @@ def test_successful_request_with_echeck assert response.test? end + def test_successful_purchase_with_echeck_and_extra_options + response = stub_comms do + @gateway.purchase(@amount, check(account_type: 'savings'), @options.merge(check_format: 'ARC')) + end.check_request do |_endpoint, data, _headers| + assert_match(/UMcheckformat=ARC/, data) + assert_match(/UMaccounttype=Savings/, data) + end.respond_with(successful_purchase_response_echeck) + + assert_equal 'Success', response.message + assert_equal '133134803', response.authorization + assert_success response + assert response.test? + end + def test_unsuccessful_request @gateway.expects(:ssl_post).returns(unsuccessful_purchase_response) @@ -64,8 +78,8 @@ def test_unsuccessful_request def test_successful_purchase_passing_extra_info response = stub_comms do - @gateway.purchase(@amount, @credit_card, @options.merge(:order_id => '1337', :description => 'socool')) - end.check_request do |endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, @options.merge(invoice: '1337', description: 'socool')) + end.check_request do |_endpoint, data, _headers| assert_match(/UMinvoice=1337/, data) assert_match(/UMdescription=socool/, data) assert_match(/UMtestmode=0/, data) @@ -75,8 +89,8 @@ def test_successful_purchase_passing_extra_info def test_successful_purchase_passing_extra_test_mode response = stub_comms do - @gateway.purchase(@amount, @credit_card, @options.merge(:test_mode => true)) - end.check_request do |endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, @options.merge(test_mode: true)) + end.check_request do |_endpoint, data, _headers| assert_match(/UMtestmode=1/, data) end.respond_with(successful_purchase_response) assert_success response @@ -84,8 +98,8 @@ def test_successful_purchase_passing_extra_test_mode def test_successful_purchase_email_receipt response = stub_comms do - @gateway.purchase(@amount, @credit_card, @options.merge(:email => 'bobby@hill.com', :cust_receipt => 'Yes', :cust_receipt_name => 'socool')) - end.check_request do |endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, @options.merge(email: 'bobby@hill.com', cust_receipt: 'Yes', cust_receipt_name: 'socool')) + end.check_request do |_endpoint, data, _headers| assert_match(/UMcustreceipt=Yes/, data) assert_match(/UMcustreceiptname=socool/, data) assert_match(/UMtestmode=0/, data) @@ -94,14 +108,15 @@ def test_successful_purchase_email_receipt end def test_successful_purchase_split_payment + options = @options.merge( + split_payments: [ + { key: 'abc123', amount: 199, description: 'Second payee' }, + { key: 'def456', amount: 911, description: 'Third payee' }, + ] + ) response = stub_comms do - @gateway.purchase(@amount, @credit_card, @options.merge( - :split_payments => [ - { :key => 'abc123', :amount => 199, :description => 'Second payee' }, - { :key => 'def456', :amount => 911, :description => 'Third payee' }, - ] - )) - end.check_request do |endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| assert_match %r{UM02key=abc123}, data assert_match %r{UM02amount=1.99}, data assert_match %r{UM02description=Second\+payee}, data @@ -116,19 +131,116 @@ def test_successful_purchase_split_payment end def test_successful_purchase_split_payment_with_custom_on_error + options = @options.merge( + split_payments: [ + { key: 'abc123', amount: 199, description: 'Second payee' } + ], + on_error: 'Continue' + ) response = stub_comms do - @gateway.purchase(@amount, @credit_card, @options.merge( - :split_payments => [ - { :key => 'abc123', :amount => 199, :description => 'Second payee' } - ], - :on_error => 'Continue' - )) - end.check_request do |endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| assert_match %r{UMonError=Continue}, data end.respond_with(successful_purchase_response) assert_success response end + def test_successful_purchase_recurring_fields + options = @options.merge( + recurring_fields: { + add_customer: true, + schedule: 'quarterly', + bill_source_key: 'bill source key', + bill_amount: 123, + num_left: 5, + start: '20501212', + recurring_receipt: true + } + ) + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match %r{UMaddcustomer=yes}, data + assert_match %r{UMschedule=quarterly}, data + assert_match %r{UMbillsourcekey=bill\+source\+key}, data + assert_match %r{UMbillamount=1.23}, data + assert_match %r{UMnumleft=5}, data + assert_match %r{UMstart=20501212}, data + assert_match %r{UMrecurringreceipt=yes}, data + end.respond_with(successful_purchase_response) + assert_success response + end + + def test_successful_purchase_custom_fields + options = @options.merge( + custom_fields: { + 1 => 'diablo', + 2 => 'mephisto', + 3 => 'baal' + } + ) + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match %r{UMcustom1=diablo}, data + assert_match %r{UMcustom2=mephisto}, data + assert_match %r{UMcustom3=baal}, data + end.respond_with(successful_purchase_response) + assert_success response + end + + def test_first_index_guard_on_custom_fields + num_options = @options.merge( + custom_fields: { + 0 => 'butcher', + 1 => 'diablo', + 2 => 'mephisto', + 3 => 'baal' + } + ) + assert_raise(ArgumentError) do + @gateway.purchase(@amount, @credit_card, num_options) + end + + str_options = @options.merge( + custom_fields: { + '0' => 'butcher', + '1' => 'diablo', + '2' => 'mephisto', + '3' => 'baal' + } + ) + assert_raise(ArgumentError) do + @gateway.purchase(@amount, @credit_card, str_options) + end + end + + def test_successful_purchase_line_items + options = @options.merge( + line_items: [ + { sku: 'abc123', cost: 119, quantity: 1 }, + { sku: 'def456', cost: 200, quantity: 2, name: 'an item' }, + { cost: 300, qty: 4 } + ] + ) + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match %r{UMline0sku=abc123}, data + assert_match %r{UMline0cost=1.19}, data + assert_match %r{UMline0qty=1}, data + + assert_match %r{UMline1sku=def456}, data + assert_match %r{UMline1cost=2.00}, data + assert_match %r{UMline1qty=2}, data + assert_match %r{UMline1name=an\+item}, data + + assert_match %r{UMline2cost=3.00}, data + assert_match %r{UMline2qty=4}, data + end.respond_with(successful_purchase_response) + assert_success response + end + def test_successful_authorize_request @gateway.expects(:ssl_post).returns(successful_authorize_response) @@ -140,9 +252,10 @@ def test_successful_authorize_request def test_successful_authorize_passing_extra_info response = stub_comms do - @gateway.authorize(@amount, @credit_card, @options.merge(:order_id => '1337', :description => 'socool')) - end.check_request do |endpoint, data, headers| + @gateway.authorize(@amount, @credit_card, @options.merge(invoice: '1337', order_id: 'w00t', description: 'socool')) + end.check_request do |_endpoint, data, _headers| assert_match(/UMinvoice=1337/, data) + assert_match(/UMorderid=w00t/, data) assert_match(/UMdescription=socool/, data) assert_match(/UMtestmode=0/, data) end.respond_with(successful_authorize_response) @@ -151,8 +264,8 @@ def test_successful_authorize_passing_extra_info def test_successful_authorize_passing_extra_test_mode response = stub_comms do - @gateway.authorize(@amount, @credit_card, @options.merge(:test_mode => true)) - end.check_request do |endpoint, data, headers| + @gateway.authorize(@amount, @credit_card, @options.merge(test_mode: true)) + end.check_request do |_endpoint, data, _headers| assert_match(/UMtestmode=1/, data) end.respond_with(successful_authorize_response) assert_success response @@ -167,10 +280,19 @@ def test_successful_capture_request assert response.test? end + def test_successful_store_request + @gateway.expects(:ssl_post).returns(successful_store_response) + + response = @gateway.store(@credit_card, @options) + assert_success response + expected_auth = response.params['card_ref'] + assert_equal expected_auth, response.authorization + end + def test_successful_capture_passing_extra_info response = stub_comms do @gateway.capture(@amount, '65074409', @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/UMamount=1.00/, data) assert_match(/UMtestmode=0/, data) end.respond_with(successful_capture_response) @@ -179,8 +301,8 @@ def test_successful_capture_passing_extra_info def test_successful_capture_passing_extra_test_mode response = stub_comms do - @gateway.capture(@amount, '65074409', @options.merge(:test_mode => true)) - end.check_request do |endpoint, data, headers| + @gateway.capture(@amount, '65074409', @options.merge(test_mode: true)) + end.check_request do |_endpoint, data, _headers| assert_match(/UMtestmode=1/, data) end.respond_with(successful_capture_response) assert_success response @@ -207,7 +329,7 @@ def test_successful_refund_request_with_echeck def test_successful_refund_passing_extra_info response = stub_comms do @gateway.refund(@amount, '65074409', @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/UMamount=1.00/, data) assert_match(/UMtestmode=0/, data) end.respond_with(successful_refund_response) @@ -216,8 +338,8 @@ def test_successful_refund_passing_extra_info def test_successful_refund_passing_extra_test_mode response = stub_comms do - @gateway.refund(@amount, '65074409', @options.merge(:test_mode => true)) - end.check_request do |endpoint, data, headers| + @gateway.refund(@amount, '65074409', @options.merge(test_mode: true)) + end.check_request do |_endpoint, data, _headers| assert_match(/UMtestmode=1/, data) end.respond_with(successful_refund_response) assert_success response @@ -243,8 +365,8 @@ def test_successful_void_request_with_echeck def test_successful_void_passing_extra_info response = stub_comms do - @gateway.void('65074409', @options.merge(:no_release => true)) - end.check_request do |endpoint, data, headers| + @gateway.void('65074409', @options.merge(no_release: true)) + end.check_request do |_endpoint, data, _headers| assert_match(/UMcommand=cc%3Avoid/, data) assert_match(/UMtestmode=0/, data) end.respond_with(successful_void_response) @@ -253,8 +375,8 @@ def test_successful_void_passing_extra_info def test_successful_void_passing_extra_test_mode response = stub_comms do - @gateway.refund(@amount, '65074409', @options.merge(:test_mode => true)) - end.check_request do |endpoint, data, headers| + @gateway.refund(@amount, '65074409', @options.merge(test_mode: true)) + end.check_request do |_endpoint, data, _headers| assert_match(/UMtestmode=1/, data) end.respond_with(successful_void_response) assert_success response @@ -306,8 +428,8 @@ def test_add_address_with_empty_billing_and_shipping_names def test_add_address_with_single_billing_and_shipping_names post = {} options = { - :billing_address => address(:name => 'Smith'), - :shipping_address => address(:name => 'Longsen') + billing_address: address(name: 'Smith'), + shipping_address: address(name: 'Longsen') } @gateway.send(:add_address, post, @credit_card, options) @@ -324,22 +446,22 @@ def test_add_test_mode_without_test_mode_option def test_add_test_mode_with_true_test_mode_option post = {} - @gateway.send(:add_test_mode, post, :test_mode => true) + @gateway.send(:add_test_mode, post, test_mode: true) assert_equal 1, post[:testmode] end def test_add_test_mode_with_false_test_mode_option post = {} - @gateway.send(:add_test_mode, post, :test_mode => false) + @gateway.send(:add_test_mode, post, test_mode: false) assert_equal 0, post[:testmode] end def test_amount_style - assert_equal '10.34', @gateway.send(:amount, 1034) + assert_equal '10.34', @gateway.send(:amount, 1034) - assert_raise(ArgumentError) do - @gateway.send(:amount, '10.34') - end + assert_raise(ArgumentError) do + @gateway.send(:amount, '10.34') + end end def test_supported_countries @@ -347,7 +469,7 @@ def test_supported_countries end def test_supported_card_types - assert_equal [:visa, :master, :american_express], UsaEpayTransactionGateway.supported_cardtypes + assert_equal %i[visa master american_express], UsaEpayTransactionGateway.supported_cardtypes end def test_avs_result @@ -395,11 +517,9 @@ def test_manual_entry_is_properly_indicated_on_purchase @credit_card.manual_entry = true response = stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| - + end.check_request do |_endpoint, data, _headers| assert_match %r{UMcard=4242424242424242}, data assert_match %r{UMcardpresent=true}, data - end.respond_with(successful_purchase_response) assert_success response @@ -456,7 +576,7 @@ def split_names(full_name) end def purchase_request - "UMamount=1.00&UMinvoice=&UMdescription=&UMcard=4242424242424242&UMcvv2=123&UMexpir=09#{@credit_card.year.to_s[-2..-1]}&UMname=Longbob+Longsen&UMbillfname=Jim&UMbilllname=Smith&UMbillcompany=Widgets+Inc&UMbillstreet=456+My+Street&UMbillstreet2=Apt+1&UMbillcity=Ottawa&UMbillstate=ON&UMbillzip=K1C2N6&UMbillcountry=CA&UMbillphone=%28555%29555-5555&UMshipfname=Jim&UMshiplname=Smith&UMshipcompany=Widgets+Inc&UMshipstreet=456+My+Street&UMshipstreet2=Apt+1&UMshipcity=Ottawa&UMshipstate=ON&UMshipzip=K1C2N6&UMshipcountry=CA&UMshipphone=%28555%29555-5555&UMstreet=456+My+Street&UMzip=K1C2N6&UMcommand=cc%3Asale&UMkey=LOGIN&UMsoftware=Active+Merchant&UMtestmode=0" + "UMamount=1.00&UMinvoice=&UMorderid=&UMdescription=&UMcard=4242424242424242&UMcvv2=123&UMexpir=09#{@credit_card.year.to_s[-2..-1]}&UMname=Longbob+Longsen&UMbillfname=Jim&UMbilllname=Smith&UMbillcompany=Widgets+Inc&UMbillstreet=456+My+Street&UMbillstreet2=Apt+1&UMbillcity=Ottawa&UMbillstate=ON&UMbillzip=K1C2N6&UMbillcountry=CA&UMbillphone=%28555%29555-5555&UMshipfname=Jim&UMshiplname=Smith&UMshipcompany=Widgets+Inc&UMshipstreet=456+My+Street&UMshipstreet2=Apt+1&UMshipcity=Ottawa&UMshipstate=ON&UMshipzip=K1C2N6&UMshipcountry=CA&UMshipphone=%28555%29555-5555&UMstreet=456+My+Street&UMzip=K1C2N6&UMcommand=cc%3Asale&UMkey=LOGIN&UMsoftware=Active+Merchant&UMtestmode=0" end def successful_purchase_response @@ -495,148 +615,151 @@ def successful_void_response_echeck 'UMversion=2.9&UMstatus=Approved&UMauthCode=TM80A5&UMrefNum=133134971&UMavsResult=No%20AVS%20response%20%28Typically%20no%20AVS%20data%20sent%20or%20swiped%20transaction%29&UMavsResultCode=&UMcvv2Result=No%20CVV2%2FCVC%20data%20available%20for%20transaction.&UMcvv2ResultCode=&UMresult=A&UMvpasResultCode=&UMerror=&UMerrorcode=00000&UMcustnum=&UMbatch=&UMbatchRefNum=&UMisDuplicate=N&UMconvertedAmount=&UMconvertedAmountCurrency=840&UMconversionRate=&UMcustReceiptResult=No%20Receipt%20Sent&UMprocRefNum=&UMcardLevelResult=&UMauthAmount=&UMfiller=filled' end + def successful_store_response + 'UMversion=2.9&UMstatus=Approved&UMauthCode=&UMrefNum=&UMavsResult=No%20AVS%20response%20%28Typically%20no%20AVS%20data%20sent%20or%20swiped%20transaction%29&UMavsResultCode=&UMcvv2Result=No%20CVV2%2FCVC%20data%20available%20for%20transaction.&UMcvv2ResultCode=&UMresult=A&UMvpasResultCode=&UMerror=&UMerrorcode=00000&UMcustnum=&UMbatch=&UMbatchRefNum=&UMisDuplicate=N&UMconvertedAmount=&UMconvertedAmountCurrency=840&UMconversionRate=&UMcustReceiptResult=No%20Receipt%20Sent&UMprocRefNum=&UMcardLevelResult=&UMauthAmount=&UMcardRef=whx6-fvz2-24l1-39a8&UMcardType=Visa&UMmaskedCardNum=XXXXXXXXXXXX2224&UMfiller=filled' + end def pre_scrubbed - <<-EOS -opening connection to sandbox.usaepay.com:443... -opened -starting SSL for sandbox.usaepay.com:443... -SSL established -<- "POST /gate 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: sandbox.usaepay.com\r\nContent-Length: 774\r\n\r\n" -<- "UMamount=1.00&UMinvoice=&UMdescription=&UMcard=4000100011112224&UMcvv2=123&UMexpir=0919&UMname=Longbob+Longsen&UMbillfname=Jim&UMbilllname=Smith&UMbillcompany=Widgets+Inc&UMbillstreet=456+My+Street&UMbillstreet2=Apt+1&UMbillcity=Ottawa&UMbillstate=NC&UMbillzip=27614&UMbillcountry=CA&UMbillphone=%28555%29555-5555&UMshipfname=Jim&UMshiplname=Smith&UMshipcompany=Widgets+Inc&UMshipstreet=456+My+Street&UMshipstreet2=Apt+1&UMshipcity=Ottawa&UMshipstate=ON&UMshipzip=K1C2N6&UMshipcountry=CA&UMshipphone=%28555%29555-5555&UMstreet=456+My+Street&UMzip=27614&UMcommand=cc%3Asale&UMkey=4EoZ5U2Q55j976W7eplC71i6b7kn4pcV&UMsoftware=Active+Merchant&UMtestmode=0&UMhash=s%2F5268F91058BC9F9FA944693D799F324B2497B7247850A51E53226309FB2540F0%2F7b4c4f6a4e775141cc0e4e10c0388d9adeb47fd1%2Fn" --> "HTTP/1.1 200 OK\r\n" --> "Server: http\r\n" --> "Date: Tue, 13 Feb 2018 18:17:20 GMT\r\n" --> "Content-Type: text/html\r\n" --> "Content-Length: 485\r\n" --> "Connection: close\r\n" --> "P3P: policyref=\"http://www.usaepay.com/w3c/p3p.xml\", CP=\"NON TAIa IVAa IVDa OUR NOR PHY ONL UNI FIN INT DEM\"\r\n" --> "Strict-Transport-Security: max-age=15768000\r\n" --> "\r\n" -reading 485 bytes... --> "UMversion=2.9&UMstatus=Approved&UMauthCode=042366&UMrefNum=132020588&UMavsResult=Address%3A%20Match%20%26%205%20Digit%20Zip%3A%20Match&UMavsResultCode=YYY&UMcvv2Result=Match&UMcvv2ResultCode=M&UMresult=A&UMvpasResultCode=&UMerror=Approved&UMerrorcode=00000&UMcustnum=&UMbatch=120&UMbatchRefNum=848&UMisDuplicate=N&UMconvertedAmount=&UMconvertedAmountCurrency=840&UMconversionRate=&UMcustReceiptResult=No%20Receipt%20Sent&UMprocRefNum=&UMcardLevelResult=A&UMauthAmount=1&UMfiller=filled" -read 485 bytes -Conn close - EOS + <<~REQUEST + opening connection to sandbox.usaepay.com:443... + opened + starting SSL for sandbox.usaepay.com:443... + SSL established + <- "POST /gate 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: sandbox.usaepay.com\r\nContent-Length: 774\r\n\r\n" + <- "UMamount=1.00&UMinvoice=&UMdescription=&UMcard=4000100011112224&UMcvv2=123&UMexpir=0919&UMname=Longbob+Longsen&UMbillfname=Jim&UMbilllname=Smith&UMbillcompany=Widgets+Inc&UMbillstreet=456+My+Street&UMbillstreet2=Apt+1&UMbillcity=Ottawa&UMbillstate=NC&UMbillzip=27614&UMbillcountry=CA&UMbillphone=%28555%29555-5555&UMshipfname=Jim&UMshiplname=Smith&UMshipcompany=Widgets+Inc&UMshipstreet=456+My+Street&UMshipstreet2=Apt+1&UMshipcity=Ottawa&UMshipstate=ON&UMshipzip=K1C2N6&UMshipcountry=CA&UMshipphone=%28555%29555-5555&UMstreet=456+My+Street&UMzip=27614&UMcommand=cc%3Asale&UMkey=4EoZ5U2Q55j976W7eplC71i6b7kn4pcV&UMsoftware=Active+Merchant&UMtestmode=0&UMhash=s%2F5268F91058BC9F9FA944693D799F324B2497B7247850A51E53226309FB2540F0%2F7b4c4f6a4e775141cc0e4e10c0388d9adeb47fd1%2Fn" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: http\r\n" + -> "Date: Tue, 13 Feb 2018 18:17:20 GMT\r\n" + -> "Content-Type: text/html\r\n" + -> "Content-Length: 485\r\n" + -> "Connection: close\r\n" + -> "P3P: policyref=\"http://www.usaepay.com/w3c/p3p.xml\", CP=\"NON TAIa IVAa IVDa OUR NOR PHY ONL UNI FIN INT DEM\"\r\n" + -> "Strict-Transport-Security: max-age=15768000\r\n" + -> "\r\n" + reading 485 bytes... + -> "UMversion=2.9&UMstatus=Approved&UMauthCode=042366&UMrefNum=132020588&UMavsResult=Address%3A%20Match%20%26%205%20Digit%20Zip%3A%20Match&UMavsResultCode=YYY&UMcvv2Result=Match&UMcvv2ResultCode=M&UMresult=A&UMvpasResultCode=&UMerror=Approved&UMerrorcode=00000&UMcustnum=&UMbatch=120&UMbatchRefNum=848&UMisDuplicate=N&UMconvertedAmount=&UMconvertedAmountCurrency=840&UMconversionRate=&UMcustReceiptResult=No%20Receipt%20Sent&UMprocRefNum=&UMcardLevelResult=A&UMauthAmount=1&UMfiller=filled" + read 485 bytes + Conn close + REQUEST end def post_scrubbed - <<-EOS -opening connection to sandbox.usaepay.com:443... -opened -starting SSL for sandbox.usaepay.com:443... -SSL established -<- "POST /gate 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: sandbox.usaepay.com\r\nContent-Length: 774\r\n\r\n" -<- "UMamount=1.00&UMinvoice=&UMdescription=&UMcard=[FILTERED]&UMcvv2=[FILTERED]&UMexpir=0919&UMname=Longbob+Longsen&UMbillfname=Jim&UMbilllname=Smith&UMbillcompany=Widgets+Inc&UMbillstreet=456+My+Street&UMbillstreet2=Apt+1&UMbillcity=Ottawa&UMbillstate=NC&UMbillzip=27614&UMbillcountry=CA&UMbillphone=%28555%29555-5555&UMshipfname=Jim&UMshiplname=Smith&UMshipcompany=Widgets+Inc&UMshipstreet=456+My+Street&UMshipstreet2=Apt+1&UMshipcity=Ottawa&UMshipstate=ON&UMshipzip=K1C2N6&UMshipcountry=CA&UMshipphone=%28555%29555-5555&UMstreet=456+My+Street&UMzip=27614&UMcommand=cc%3Asale&UMkey=[FILTERED]&UMsoftware=Active+Merchant&UMtestmode=0&UMhash=s%2F5268F91058BC9F9FA944693D799F324B2497B7247850A51E53226309FB2540F0%2F7b4c4f6a4e775141cc0e4e10c0388d9adeb47fd1%2Fn" --> "HTTP/1.1 200 OK\r\n" --> "Server: http\r\n" --> "Date: Tue, 13 Feb 2018 18:17:20 GMT\r\n" --> "Content-Type: text/html\r\n" --> "Content-Length: 485\r\n" --> "Connection: close\r\n" --> "P3P: policyref=\"http://www.usaepay.com/w3c/p3p.xml\", CP=\"NON TAIa IVAa IVDa OUR NOR PHY ONL UNI FIN INT DEM\"\r\n" --> "Strict-Transport-Security: max-age=15768000\r\n" --> "\r\n" -reading 485 bytes... --> "UMversion=2.9&UMstatus=Approved&UMauthCode=042366&UMrefNum=132020588&UMavsResult=Address%3A%20Match%20%26%205%20Digit%20Zip%3A%20Match&UMavsResultCode=YYY&UMcvv2Result=Match&UMcvv2ResultCode=M&UMresult=A&UMvpasResultCode=&UMerror=Approved&UMerrorcode=00000&UMcustnum=&UMbatch=120&UMbatchRefNum=848&UMisDuplicate=N&UMconvertedAmount=&UMconvertedAmountCurrency=840&UMconversionRate=&UMcustReceiptResult=No%20Receipt%20Sent&UMprocRefNum=&UMcardLevelResult=A&UMauthAmount=1&UMfiller=filled" -read 485 bytes -Conn close - EOS + <<~REQUEST + opening connection to sandbox.usaepay.com:443... + opened + starting SSL for sandbox.usaepay.com:443... + SSL established + <- "POST /gate 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: sandbox.usaepay.com\r\nContent-Length: 774\r\n\r\n" + <- "UMamount=1.00&UMinvoice=&UMdescription=&UMcard=[FILTERED]&UMcvv2=[FILTERED]&UMexpir=0919&UMname=Longbob+Longsen&UMbillfname=Jim&UMbilllname=Smith&UMbillcompany=Widgets+Inc&UMbillstreet=456+My+Street&UMbillstreet2=Apt+1&UMbillcity=Ottawa&UMbillstate=NC&UMbillzip=27614&UMbillcountry=CA&UMbillphone=%28555%29555-5555&UMshipfname=Jim&UMshiplname=Smith&UMshipcompany=Widgets+Inc&UMshipstreet=456+My+Street&UMshipstreet2=Apt+1&UMshipcity=Ottawa&UMshipstate=ON&UMshipzip=K1C2N6&UMshipcountry=CA&UMshipphone=%28555%29555-5555&UMstreet=456+My+Street&UMzip=27614&UMcommand=cc%3Asale&UMkey=[FILTERED]&UMsoftware=Active+Merchant&UMtestmode=0&UMhash=s%2F5268F91058BC9F9FA944693D799F324B2497B7247850A51E53226309FB2540F0%2F7b4c4f6a4e775141cc0e4e10c0388d9adeb47fd1%2Fn" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: http\r\n" + -> "Date: Tue, 13 Feb 2018 18:17:20 GMT\r\n" + -> "Content-Type: text/html\r\n" + -> "Content-Length: 485\r\n" + -> "Connection: close\r\n" + -> "P3P: policyref=\"http://www.usaepay.com/w3c/p3p.xml\", CP=\"NON TAIa IVAa IVDa OUR NOR PHY ONL UNI FIN INT DEM\"\r\n" + -> "Strict-Transport-Security: max-age=15768000\r\n" + -> "\r\n" + reading 485 bytes... + -> "UMversion=2.9&UMstatus=Approved&UMauthCode=042366&UMrefNum=132020588&UMavsResult=Address%3A%20Match%20%26%205%20Digit%20Zip%3A%20Match&UMavsResultCode=YYY&UMcvv2Result=Match&UMcvv2ResultCode=M&UMresult=A&UMvpasResultCode=&UMerror=Approved&UMerrorcode=00000&UMcustnum=&UMbatch=120&UMbatchRefNum=848&UMisDuplicate=N&UMconvertedAmount=&UMconvertedAmountCurrency=840&UMconversionRate=&UMcustReceiptResult=No%20Receipt%20Sent&UMprocRefNum=&UMcardLevelResult=A&UMauthAmount=1&UMfiller=filled" + read 485 bytes + Conn close + REQUEST end def pre_scrubbed_track_data - <<-EOS -opening connection to sandbox.usaepay.com:443... -opened -starting SSL for sandbox.usaepay.com:443... -SSL established -<- "POST /gate 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: sandbox.usaepay.com\r\nContent-Length: 382\r\n\r\n" -<- "UMamount=1.00&UMinvoice=&UMdescription=&UMmagstripe=%25B4000100011112224%5ELONGSEN%2FL.+%5E19091200000000000000%2A%2A123%2A%2A%2A%2A%2A%2A%3F&UMcardpresent=true&UMcommand=cc%3Asale&UMkey=4EoZ5U2Q55j976W7eplC71i6b7kn4pcV&UMsoftware=Active+Merchant&UMtestmode=0&UMhash=s%2FE27734F076643B23131E5432C1E225EFF982A73D350179EFC2F191CA499B59A4%2F13391bd14ab6e61058cc9a1b78f259a4c26aa8e1%2Fn" --> "HTTP/1.1 200 OK\r\n" --> "Server: http\r\n" --> "Date: Tue, 13 Feb 2018 18:13:11 GMT\r\n" --> "Content-Type: text/html\r\n" --> "Content-Length: 485\r\n" --> "Connection: close\r\n" --> "P3P: policyref=\"http://www.usaepay.com/w3c/p3p.xml\", CP=\"NON TAIa IVAa IVDa OUR NOR PHY ONL UNI FIN INT DEM\"\r\n" --> "Strict-Transport-Security: max-age=15768000\r\n" --> "\r\n" -reading 485 bytes... --> "UMversion=2.9&UMstatus=Approved&UMauthCode=042087&UMrefNum=132020522&UMavsResult=Address%3A%20Match%20%26%205%20Digit%20Zip%3A%20Match&UMavsResultCode=YYY&UMcvv2Result=Match&UMcvv2ResultCode=M&UMresult=A&UMvpasResultCode=&UMerror=Approved&UMerrorcode=00000&UMcustnum=&UMbatch=120&UMbatchRefNum=848&UMisDuplicate=N&UMconvertedAmount=&UMconvertedAmountCurrency=840&UMconversionRate=&UMcustReceiptResult=No%20Receipt%20Sent&UMprocRefNum=&UMcardLevelResult=A&UMauthAmount=1&UMfiller=filled" -read 485 bytes -Conn close - EOS + <<~REQUEST + opening connection to sandbox.usaepay.com:443... + opened + starting SSL for sandbox.usaepay.com:443... + SSL established + <- "POST /gate 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: sandbox.usaepay.com\r\nContent-Length: 382\r\n\r\n" + <- "UMamount=1.00&UMinvoice=&UMdescription=&UMmagstripe=%25B4000100011112224%5ELONGSEN%2FL.+%5E19091200000000000000%2A%2A123%2A%2A%2A%2A%2A%2A%3F&UMcardpresent=true&UMcommand=cc%3Asale&UMkey=4EoZ5U2Q55j976W7eplC71i6b7kn4pcV&UMsoftware=Active+Merchant&UMtestmode=0&UMhash=s%2FE27734F076643B23131E5432C1E225EFF982A73D350179EFC2F191CA499B59A4%2F13391bd14ab6e61058cc9a1b78f259a4c26aa8e1%2Fn" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: http\r\n" + -> "Date: Tue, 13 Feb 2018 18:13:11 GMT\r\n" + -> "Content-Type: text/html\r\n" + -> "Content-Length: 485\r\n" + -> "Connection: close\r\n" + -> "P3P: policyref=\"http://www.usaepay.com/w3c/p3p.xml\", CP=\"NON TAIa IVAa IVDa OUR NOR PHY ONL UNI FIN INT DEM\"\r\n" + -> "Strict-Transport-Security: max-age=15768000\r\n" + -> "\r\n" + reading 485 bytes... + -> "UMversion=2.9&UMstatus=Approved&UMauthCode=042087&UMrefNum=132020522&UMavsResult=Address%3A%20Match%20%26%205%20Digit%20Zip%3A%20Match&UMavsResultCode=YYY&UMcvv2Result=Match&UMcvv2ResultCode=M&UMresult=A&UMvpasResultCode=&UMerror=Approved&UMerrorcode=00000&UMcustnum=&UMbatch=120&UMbatchRefNum=848&UMisDuplicate=N&UMconvertedAmount=&UMconvertedAmountCurrency=840&UMconversionRate=&UMcustReceiptResult=No%20Receipt%20Sent&UMprocRefNum=&UMcardLevelResult=A&UMauthAmount=1&UMfiller=filled" + read 485 bytes + Conn close + REQUEST end def post_scrubbed_track_data - <<-EOS -opening connection to sandbox.usaepay.com:443... -opened -starting SSL for sandbox.usaepay.com:443... -SSL established -<- "POST /gate 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: sandbox.usaepay.com\r\nContent-Length: 382\r\n\r\n" -<- "UMamount=1.00&UMinvoice=&UMdescription=&UMmagstripe=[FILTERED]&UMcardpresent=true&UMcommand=cc%3Asale&UMkey=[FILTERED]&UMsoftware=Active+Merchant&UMtestmode=0&UMhash=s%2FE27734F076643B23131E5432C1E225EFF982A73D350179EFC2F191CA499B59A4%2F13391bd14ab6e61058cc9a1b78f259a4c26aa8e1%2Fn" --> "HTTP/1.1 200 OK\r\n" --> "Server: http\r\n" --> "Date: Tue, 13 Feb 2018 18:13:11 GMT\r\n" --> "Content-Type: text/html\r\n" --> "Content-Length: 485\r\n" --> "Connection: close\r\n" --> "P3P: policyref=\"http://www.usaepay.com/w3c/p3p.xml\", CP=\"NON TAIa IVAa IVDa OUR NOR PHY ONL UNI FIN INT DEM\"\r\n" --> "Strict-Transport-Security: max-age=15768000\r\n" --> "\r\n" -reading 485 bytes... --> "UMversion=2.9&UMstatus=Approved&UMauthCode=042087&UMrefNum=132020522&UMavsResult=Address%3A%20Match%20%26%205%20Digit%20Zip%3A%20Match&UMavsResultCode=YYY&UMcvv2Result=Match&UMcvv2ResultCode=M&UMresult=A&UMvpasResultCode=&UMerror=Approved&UMerrorcode=00000&UMcustnum=&UMbatch=120&UMbatchRefNum=848&UMisDuplicate=N&UMconvertedAmount=&UMconvertedAmountCurrency=840&UMconversionRate=&UMcustReceiptResult=No%20Receipt%20Sent&UMprocRefNum=&UMcardLevelResult=A&UMauthAmount=1&UMfiller=filled" -read 485 bytes -Conn close - EOS + <<~REQUEST + opening connection to sandbox.usaepay.com:443... + opened + starting SSL for sandbox.usaepay.com:443... + SSL established + <- "POST /gate 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: sandbox.usaepay.com\r\nContent-Length: 382\r\n\r\n" + <- "UMamount=1.00&UMinvoice=&UMdescription=&UMmagstripe=[FILTERED]&UMcardpresent=true&UMcommand=cc%3Asale&UMkey=[FILTERED]&UMsoftware=Active+Merchant&UMtestmode=0&UMhash=s%2FE27734F076643B23131E5432C1E225EFF982A73D350179EFC2F191CA499B59A4%2F13391bd14ab6e61058cc9a1b78f259a4c26aa8e1%2Fn" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: http\r\n" + -> "Date: Tue, 13 Feb 2018 18:13:11 GMT\r\n" + -> "Content-Type: text/html\r\n" + -> "Content-Length: 485\r\n" + -> "Connection: close\r\n" + -> "P3P: policyref=\"http://www.usaepay.com/w3c/p3p.xml\", CP=\"NON TAIa IVAa IVDa OUR NOR PHY ONL UNI FIN INT DEM\"\r\n" + -> "Strict-Transport-Security: max-age=15768000\r\n" + -> "\r\n" + reading 485 bytes... + -> "UMversion=2.9&UMstatus=Approved&UMauthCode=042087&UMrefNum=132020522&UMavsResult=Address%3A%20Match%20%26%205%20Digit%20Zip%3A%20Match&UMavsResultCode=YYY&UMcvv2Result=Match&UMcvv2ResultCode=M&UMresult=A&UMvpasResultCode=&UMerror=Approved&UMerrorcode=00000&UMcustnum=&UMbatch=120&UMbatchRefNum=848&UMisDuplicate=N&UMconvertedAmount=&UMconvertedAmountCurrency=840&UMconversionRate=&UMcustReceiptResult=No%20Receipt%20Sent&UMprocRefNum=&UMcardLevelResult=A&UMauthAmount=1&UMfiller=filled" + read 485 bytes + Conn close + REQUEST end def pre_scrubbed_echeck - <<-EOS -opening connection to sandbox.usaepay.com:443... -opened -starting SSL for sandbox.usaepay.com:443... -SSL established -<- "POST /gate 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: sandbox.usaepay.com\r\nContent-Length: 762\r\n\r\n" -<- "UMamount=1.00&UMinvoice=&UMdescription=&UMaccount=15378535&UMrouting=244183602&UMname=Jim+Smith&UMbillfname=Jim&UMbilllname=Smith&UMbillcompany=Widgets+Inc&UMbillstreet=456+My+Street&UMbillstreet2=Apt+1&UMbillcity=Ottawa&UMbillstate=NC&UMbillzip=27614&UMbillcountry=CA&UMbillphone=%28555%29555-5555&UMshipfname=Jim&UMshiplname=Smith&UMshipcompany=Widgets+Inc&UMshipstreet=456+My+Street&UMshipstreet2=Apt+1&UMshipcity=Ottawa&UMshipstate=ON&UMshipzip=K1C2N6&UMshipcountry=CA&UMshipphone=%28555%29555-5555&UMstreet=456+My+Street&UMzip=27614&UMcommand=check%3Asale&UMkey=4EoZ5U2Q55j976W7eplC71i6b7kn4pcV&UMsoftware=Active+Merchant&UMtestmode=0&UMhash=s%2F7F71E7DCB851901EA1D4E2CA1C60D2A7E8BAB99FA10F6220E821BD8B8331114B%2F85f1a7ab01b725c4eed80a12c78ef65d3fa367e6%2Fn" --> "HTTP/1.1 200 OK\r\n" --> "Server: http\r\n" --> "Date: Fri, 16 Mar 2018 20:54:49 GMT\r\n" --> "Content-Type: text/html\r\n" --> "Content-Length: 572\r\n" --> "Connection: close\r\n" --> "P3P: policyref=\"http://www.usaepay.com/w3c/p3p.xml\", CP=\"NON TAIa IVAa IVDa OUR NOR PHY ONL UNI FIN INT DEM\"\r\n" --> "Strict-Transport-Security: max-age=15768000\r\n" --> "\r\n" -reading 572 bytes... --> "UMversion=2.9&UMstatus=Approved&UMauthCode=TMEAAF&UMrefNum=133135121&UMavsResult=No%20AVS%20response%20%28Typically%20no%20AVS%20data%20sent%20or%20swiped%20transaction%29&UMavsResultCode=&UMcvv2Result=No%20CVV2%2FCVC%20data%20available%20for%20transaction.&UMcvv2ResultCode=&UMresult=A&UMvpasResultCode=&UMerror=&UMerrorcode=00000&UMcustnum=&UMbatch=180316&UMbatchRefNum=&UMisDuplicate=N&UMconvertedAmount=&UMconvertedAmountCurrency=840&UMconversionRate=&UMcustReceiptResult=No%20Receipt%20Sent&UMprocRefNum=18031621233689&UMcardLevelResult=&UMauthAmount=&UMfiller=filled" -read 572 bytes -Conn close - EOS + <<~REQUEST + opening connection to sandbox.usaepay.com:443... + opened + starting SSL for sandbox.usaepay.com:443... + SSL established + <- "POST /gate 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: sandbox.usaepay.com\r\nContent-Length: 762\r\n\r\n" + <- "UMamount=1.00&UMinvoice=&UMdescription=&UMaccount=15378535&UMrouting=244183602&UMname=Jim+Smith&UMbillfname=Jim&UMbilllname=Smith&UMbillcompany=Widgets+Inc&UMbillstreet=456+My+Street&UMbillstreet2=Apt+1&UMbillcity=Ottawa&UMbillstate=NC&UMbillzip=27614&UMbillcountry=CA&UMbillphone=%28555%29555-5555&UMshipfname=Jim&UMshiplname=Smith&UMshipcompany=Widgets+Inc&UMshipstreet=456+My+Street&UMshipstreet2=Apt+1&UMshipcity=Ottawa&UMshipstate=ON&UMshipzip=K1C2N6&UMshipcountry=CA&UMshipphone=%28555%29555-5555&UMstreet=456+My+Street&UMzip=27614&UMcommand=check%3Asale&UMkey=4EoZ5U2Q55j976W7eplC71i6b7kn4pcV&UMsoftware=Active+Merchant&UMtestmode=0&UMhash=s%2F7F71E7DCB851901EA1D4E2CA1C60D2A7E8BAB99FA10F6220E821BD8B8331114B%2F85f1a7ab01b725c4eed80a12c78ef65d3fa367e6%2Fn" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: http\r\n" + -> "Date: Fri, 16 Mar 2018 20:54:49 GMT\r\n" + -> "Content-Type: text/html\r\n" + -> "Content-Length: 572\r\n" + -> "Connection: close\r\n" + -> "P3P: policyref=\"http://www.usaepay.com/w3c/p3p.xml\", CP=\"NON TAIa IVAa IVDa OUR NOR PHY ONL UNI FIN INT DEM\"\r\n" + -> "Strict-Transport-Security: max-age=15768000\r\n" + -> "\r\n" + reading 572 bytes... + -> "UMversion=2.9&UMstatus=Approved&UMauthCode=TMEAAF&UMrefNum=133135121&UMavsResult=No%20AVS%20response%20%28Typically%20no%20AVS%20data%20sent%20or%20swiped%20transaction%29&UMavsResultCode=&UMcvv2Result=No%20CVV2%2FCVC%20data%20available%20for%20transaction.&UMcvv2ResultCode=&UMresult=A&UMvpasResultCode=&UMerror=&UMerrorcode=00000&UMcustnum=&UMbatch=180316&UMbatchRefNum=&UMisDuplicate=N&UMconvertedAmount=&UMconvertedAmountCurrency=840&UMconversionRate=&UMcustReceiptResult=No%20Receipt%20Sent&UMprocRefNum=18031621233689&UMcardLevelResult=&UMauthAmount=&UMfiller=filled" + read 572 bytes + Conn close + REQUEST end def post_scrubbed_echeck - <<-EOS -opening connection to sandbox.usaepay.com:443... -opened -starting SSL for sandbox.usaepay.com:443... -SSL established -<- "POST /gate 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: sandbox.usaepay.com\r\nContent-Length: 762\r\n\r\n" -<- "UMamount=1.00&UMinvoice=&UMdescription=&UMaccount=[FILTERED]&UMrouting=244183602&UMname=Jim+Smith&UMbillfname=Jim&UMbilllname=Smith&UMbillcompany=Widgets+Inc&UMbillstreet=456+My+Street&UMbillstreet2=Apt+1&UMbillcity=Ottawa&UMbillstate=NC&UMbillzip=27614&UMbillcountry=CA&UMbillphone=%28555%29555-5555&UMshipfname=Jim&UMshiplname=Smith&UMshipcompany=Widgets+Inc&UMshipstreet=456+My+Street&UMshipstreet2=Apt+1&UMshipcity=Ottawa&UMshipstate=ON&UMshipzip=K1C2N6&UMshipcountry=CA&UMshipphone=%28555%29555-5555&UMstreet=456+My+Street&UMzip=27614&UMcommand=check%3Asale&UMkey=[FILTERED]&UMsoftware=Active+Merchant&UMtestmode=0&UMhash=s%2F7F71E7DCB851901EA1D4E2CA1C60D2A7E8BAB99FA10F6220E821BD8B8331114B%2F85f1a7ab01b725c4eed80a12c78ef65d3fa367e6%2Fn" --> "HTTP/1.1 200 OK\r\n" --> "Server: http\r\n" --> "Date: Fri, 16 Mar 2018 20:54:49 GMT\r\n" --> "Content-Type: text/html\r\n" --> "Content-Length: 572\r\n" --> "Connection: close\r\n" --> "P3P: policyref=\"http://www.usaepay.com/w3c/p3p.xml\", CP=\"NON TAIa IVAa IVDa OUR NOR PHY ONL UNI FIN INT DEM\"\r\n" --> "Strict-Transport-Security: max-age=15768000\r\n" --> "\r\n" -reading 572 bytes... --> "UMversion=2.9&UMstatus=Approved&UMauthCode=TMEAAF&UMrefNum=133135121&UMavsResult=No%20AVS%20response%20%28Typically%20no%20AVS%20data%20sent%20or%20swiped%20transaction%29&UMavsResultCode=&UMcvv2Result=No%20CVV2%2FCVC%20data%20available%20for%20transaction.&UMcvv2ResultCode=&UMresult=A&UMvpasResultCode=&UMerror=&UMerrorcode=00000&UMcustnum=&UMbatch=180316&UMbatchRefNum=&UMisDuplicate=N&UMconvertedAmount=&UMconvertedAmountCurrency=840&UMconversionRate=&UMcustReceiptResult=No%20Receipt%20Sent&UMprocRefNum=18031621233689&UMcardLevelResult=&UMauthAmount=&UMfiller=filled" -read 572 bytes -Conn close - EOS + <<~REQUEST + opening connection to sandbox.usaepay.com:443... + opened + starting SSL for sandbox.usaepay.com:443... + SSL established + <- "POST /gate 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: sandbox.usaepay.com\r\nContent-Length: 762\r\n\r\n" + <- "UMamount=1.00&UMinvoice=&UMdescription=&UMaccount=[FILTERED]&UMrouting=244183602&UMname=Jim+Smith&UMbillfname=Jim&UMbilllname=Smith&UMbillcompany=Widgets+Inc&UMbillstreet=456+My+Street&UMbillstreet2=Apt+1&UMbillcity=Ottawa&UMbillstate=NC&UMbillzip=27614&UMbillcountry=CA&UMbillphone=%28555%29555-5555&UMshipfname=Jim&UMshiplname=Smith&UMshipcompany=Widgets+Inc&UMshipstreet=456+My+Street&UMshipstreet2=Apt+1&UMshipcity=Ottawa&UMshipstate=ON&UMshipzip=K1C2N6&UMshipcountry=CA&UMshipphone=%28555%29555-5555&UMstreet=456+My+Street&UMzip=27614&UMcommand=check%3Asale&UMkey=[FILTERED]&UMsoftware=Active+Merchant&UMtestmode=0&UMhash=s%2F7F71E7DCB851901EA1D4E2CA1C60D2A7E8BAB99FA10F6220E821BD8B8331114B%2F85f1a7ab01b725c4eed80a12c78ef65d3fa367e6%2Fn" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: http\r\n" + -> "Date: Fri, 16 Mar 2018 20:54:49 GMT\r\n" + -> "Content-Type: text/html\r\n" + -> "Content-Length: 572\r\n" + -> "Connection: close\r\n" + -> "P3P: policyref=\"http://www.usaepay.com/w3c/p3p.xml\", CP=\"NON TAIa IVAa IVDa OUR NOR PHY ONL UNI FIN INT DEM\"\r\n" + -> "Strict-Transport-Security: max-age=15768000\r\n" + -> "\r\n" + reading 572 bytes... + -> "UMversion=2.9&UMstatus=Approved&UMauthCode=TMEAAF&UMrefNum=133135121&UMavsResult=No%20AVS%20response%20%28Typically%20no%20AVS%20data%20sent%20or%20swiped%20transaction%29&UMavsResultCode=&UMcvv2Result=No%20CVV2%2FCVC%20data%20available%20for%20transaction.&UMcvv2ResultCode=&UMresult=A&UMvpasResultCode=&UMerror=&UMerrorcode=00000&UMcustnum=&UMbatch=180316&UMbatchRefNum=&UMisDuplicate=N&UMconvertedAmount=&UMconvertedAmountCurrency=840&UMconversionRate=&UMcustReceiptResult=No%20Receipt%20Sent&UMprocRefNum=18031621233689&UMcardLevelResult=&UMauthAmount=&UMfiller=filled" + read 572 bytes + Conn close + REQUEST end end diff --git a/test/unit/gateways/vanco_test.rb b/test/unit/gateways/vanco_test.rb index 449cd696448..7fe1eafa753 100644 --- a/test/unit/gateways/vanco_test.rb +++ b/test/unit/gateways/vanco_test.rb @@ -3,6 +3,8 @@ class VancoTest < Test::Unit::TestCase include CommStub + SECONDS_PER_DAY = 3600 * 24 + def setup @gateway = VancoGateway.new(user_id: 'login', password: 'password', client_id: 'client_id') @credit_card = credit_card @@ -29,10 +31,8 @@ def test_successful_purchase def test_successful_purchase_with_fund_id response = stub_comms do @gateway.purchase(@amount, @credit_card, @options.merge(fund_id: 'MyEggcellentFund')) - end.check_request do |endpoint, data, headers| - if data =~ /<RequestType>EFTAdd/ - assert_match(%r(<FundID>MyEggcellentFund<\/FundID>), data) - end + end.check_request do |_endpoint, data, _headers| + assert_match(%r(<FundID>MyEggcellentFund<\/FundID>), data) if data =~ /<RequestType>EFTAdd/ end.respond_with(successful_login_response, successful_purchase_with_fund_id_response) assert_success response @@ -43,12 +43,58 @@ def test_successful_purchase_with_fund_id def test_successful_purchase_with_ip_address response = stub_comms do @gateway.purchase(@amount, @credit_card, @options.merge(ip: '192.168.0.1')) - end.check_request do |endpoint, data, headers| - if data =~ /<RequestType>EFTAdd/ - assert_match(%r(<CustomerIPAddress>192), data) - end + end.check_request do |_endpoint, data, _headers| + assert_match(%r(<CustomerIPAddress>192), data) if data =~ /<RequestType>EFTAdd/ + end.respond_with(successful_login_response, successful_purchase_response) + assert_success response + end + + def test_successful_purchase_with_existing_session_id + response = stub_comms do + previous_login_response = @gateway.send(:login) + @gateway.purchase( + @amount, + @credit_card, + @options.merge( + session_id: + { + id: '5d4177ec3c356ec5f7abe0e17e814250', + created_at: Time.parse(previous_login_response.params['auth_requesttime']) + } + ) + ) + end.check_request do |_endpoint, data, _headers| + assert_match(%r(<SessionID>5d4177ec3c356ec5f7abe0e17e814250<\/SessionID>), data) if data =~ /<RequestType>EFTAddCompleteTransaction/ + end.respond_with(login_request_response, successful_purchase_with_existing_session_id_response) + + assert_success response + assert_equal '14949117|15756594|16136938', response.authorization + assert_equal 'Success', response.message + assert response.test? + end + + def test_successful_purchase_with_expired_session_id + two_days_from_now = Time.now - (2 * SECONDS_PER_DAY) + response = stub_comms do + @gateway.purchase( + @amount, + @credit_card, + @options.merge( + session_id: + { + id: 'f34177ec3c356ec5f7abe0e17e814250', + created_at: two_days_from_now + } + ) + ) + end.check_request do |_endpoint, data, _headers| + assert_match(%r(<SessionID>5d8b104c9d8265db46bdf35ae9685472f4789dc8<\/SessionID>), data) if data =~ /<RequestType>EFTAddCompleteTransaction/ end.respond_with(successful_login_response, successful_purchase_response) + assert_success response + assert_equal '14949117|15756594|16136938', response.authorization + assert_equal 'Success', response.message + assert response.test? end def test_failed_purchase @@ -143,6 +189,12 @@ def successful_purchase_response ) end + def successful_purchase_with_existing_session_id_response + %( + <?xml version="1.0" encoding="UTF-8" ?><VancoWS><Auth><RequestID>ad4cbab9740e909423a02e622689d6</RequestID><RequestTime>2015-05-01 16:08:07 -0400</RequestTime><RequestType>EFTAddCompleteTransaction</RequestType><Signature></Signature><SessionID>5d4177ec3c356ec5f7abe0e17e814250</SessionID><Version>2</Version></Auth><Response><StartDate>2015-05-01</StartDate><CustomerRef>14949117</CustomerRef><PaymentMethodRef>15756594</PaymentMethodRef><TransactionRef>16136938</TransactionRef><TransactionFee>3.20</TransactionFee></Response></VancoWS> + ) + end + def successful_purchase_with_fund_id_response %( <?xml version=\"1.0\" encoding=\"UTF-8\" ?><VancoWS><Auth><RequestID>8cf42301416298c9d6d71a39b27a0d</RequestID><RequestTime>2015-05-05 15:45:57 -0400</RequestTime><RequestType>EFTAddCompleteTransaction</RequestType><Signature></Signature><SessionID>b2ec96e366f38a5c1ecd3f5343475526beaba4f9</SessionID><Version>2</Version></Auth><Response><StartDate>2015-05-05</StartDate><CustomerRef>14949117</CustomerRef><PaymentMethodRef>15756594</PaymentMethodRef><TransactionRef>16137331</TransactionRef><TransactionFee>3.20</TransactionFee></Response></VancoWS>\n\n @@ -185,4 +237,9 @@ def failed_refund_response ) end + def login_request_response + %( + <?xml version=\"1.0\" encoding=\"UTF-8\" ?> <VancoWS> <Auth> <RequestID>38de95050240d49f8897578825c717</RequestID> <RequestTime>#{Time.now}</RequestTime> <RequestType>Login</RequestType> <Version>2</Version> </Auth> <Response> <SessionID>9edc7951048106b821f5e304e9ccdcc0700f533a</SessionID> </Response> </VancoWS> + ) + end end 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 '<MotoECICode>6</MotoECICode>', 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 '<MotoECICode>9</MotoECICode>', 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 '<WalletType>2</WalletType>', 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 '<WalletType>1</WalletType>', 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 '<CardPresentCode>2</CardPresentCode>', 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 "<LodgingAgreementNumber>#{lodging_fields[:agreement_number]}</LodgingAgreementNumber>", data + assert_match "<LodgingCheckInDate>#{lodging_fields[:check_in_date]}</LodgingCheckInDate>", data + assert_match "<LodgingCheckOutDate>#{lodging_fields[:check_out_date]}</LodgingCheckOutDate>", data + assert_match "<LodgingRoomAmount>#{lodging_fields[:room_amount]}</LodgingRoomAmount>", data + assert_match "<LodgingRoomTax>#{lodging_fields[:room_tax]}</LodgingRoomTax>", data + assert_match "<LodgingNoShowIndicator>#{lodging_fields[:no_show_indicator]}</LodgingNoShowIndicator>", data + assert_match "<LodgingDuration>#{lodging_fields[:duration]}</LodgingDuration>", data + assert_match "<LodgingCustomerName>#{lodging_fields[:customer_name]}</LodgingCustomerName>", data + assert_match "<LodgingClientCode>#{lodging_fields[:client_code]}</LodgingClientCode>", data + assert_match "<LodgingExtraChargesDetail>#{lodging_fields[:extra_charges_detail]}</LodgingExtraChargesDetail>", data + assert_match "<LodgingExtraChargesAmounts>#{lodging_fields[:extra_charges_amounts]}</LodgingExtraChargesAmounts>", data + assert_match '<LodgingPrestigiousPropertyCode>1</LodgingPrestigiousPropertyCode>', data + assert_match '<LodgingSpecialProgramCode>3</LodgingSpecialProgramCode>', data + assert_match '<LodgingChargeType>1</LodgingChargeType>', 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 '<LodgingPrestigiousPropertyCode>1</LodgingPrestigiousPropertyCode>', data + assert_match '<LodgingSpecialProgramCode>3</LodgingSpecialProgramCode>', data + assert_match '<LodgingChargeType>1</LodgingChargeType>', 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 '<TerminalID>02</TerminalID>', data + assert_match '<TerminalType>0</TerminalType>', data + assert_match '<CardPresentCode>1</CardPresentCode>', data + assert_match '<CardholderPresentCode>0</CardholderPresentCode>', data + assert_match '<CardInputCode>4</CardInputCode>', data + assert_match '<CVVPresenceCode>3</CVVPresenceCode>', data + assert_match '<TerminalCapabilityCode>3</TerminalCapabilityCode>', data + assert_match '<TerminalEnvironmentCode>2</TerminalEnvironmentCode>', data + assert_match '<MotoECICode>7</MotoECICode>', 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 '<TerminalID>02</TerminalID>', data + assert_match '<TerminalType>0</TerminalType>', data + assert_match '<CardPresentCode>0</CardPresentCode>', data + assert_match '<CardholderPresentCode>0</CardholderPresentCode>', data + assert_match '<CardInputCode>4</CardInputCode>', data + assert_match '<CVVPresenceCode>3</CVVPresenceCode>', data + assert_match '<TerminalCapabilityCode>3</TerminalCapabilityCode>', data + assert_match '<TerminalEnvironmentCode>2</TerminalEnvironmentCode>', data + assert_match '<MotoECICode>7</MotoECICode>', 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 '<PaymentType>0</PaymentType>', 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 '<SubmissionType>0</SubmissionType>', 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 '<DuplicateCheckDisableFlag>1</DuplicateCheckDisableFlag>', 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 '<DuplicateCheckDisableFlag>1</DuplicateCheckDisableFlag>', 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 '<DuplicateCheckDisableFlag>False</DuplicateCheckDisableFlag>', 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 '<DuplicateCheckDisableFlag>False</DuplicateCheckDisableFlag>', 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 '<DuplicateCheckDisableFlag>False</DuplicateCheckDisableFlag>', 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(<DuplicateCheckDisableFlag>False</DuplicateCheckDisableFlag>), 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 '<DuplicateCheckDisableFlag>1</DuplicateCheckDisableFlag>', 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 '<DuplicateCheckDisableFlag>1</DuplicateCheckDisableFlag>', 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 '<DuplicateCheckDisableFlag>False</DuplicateCheckDisableFlag>', 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 '<DuplicateCheckDisableFlag>False</DuplicateCheckDisableFlag>', 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 '<DuplicateCheckDisableFlag>False</DuplicateCheckDisableFlag>', 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(<DuplicateCheckDisableFlag>False</DuplicateCheckDisableFlag>), 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 '<TerminalID>02</TerminalID>', 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 '<BillingEmail>test@example.com</BillingEmail>', 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 '<CreditCardCredit', data + assert_match '<TicketNumber>1</TicketNumber', data + assert_match '<MarketCode>4</MarketCode>', data + assert_match '<MerchantSuppliedTransactionID>123</MerchantSuppliedTransactionID>', 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 + <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<CreditCardSale xmlns=\"https://transaction.elementexpress.com\">\n <credentials>\n <AccountID>1013963</AccountID>\n <AccountToken>683EED8A1A357EB91575A168E74482A74836FD72B1AD11B41B29B473CA9D65B9FE067701</AccountToken>\n <AcceptorID>3928907</AcceptorID>\n </credentials>\n <application>\n <ApplicationID>5211</ApplicationID>\n <ApplicationName>Spreedly</ApplicationName>\n <ApplicationVersion>1</ApplicationVersion>\n </application>\n <card>\n <CardNumber>4000100011112224</CardNumber>\n <ExpirationMonth>09</ExpirationMonth>\n <ExpirationYear>16</ExpirationYear>\n <CardholderName>Longbob Longsen</CardholderName>\n <CVV>123</CVV>\n </card>\n <transaction>\n <TransactionAmount>1.00</TransactionAmount>\n <MarketCode>Default</MarketCode>\n </transaction>\n <terminal>\n <TerminalID>01</TerminalID>\n <CardPresentCode>UseDefault</CardPresentCode>\n <CardholderPresentCode>UseDefault</CardholderPresentCode>\n <CardInputCode>UseDefault</CardInputCode>\n <CVVPresenceCode>UseDefault</CVVPresenceCode>\n <TerminalCapabilityCode>UseDefault</TerminalCapabilityCode>\n <TerminalEnvironmentCode>UseDefault</TerminalEnvironmentCode>\n <MotoECICode>UseDefault</MotoECICode>\n </terminal>\n <address>\n <BillingAddress1>456 My Street</BillingAddress1>\n <BillingAddress2>Apt 1</BillingAddress2>\n <BillingCity>Ottawa</BillingCity>\n <BillingState>ON</BillingState>\n <BillingZipcode>K1C2N6</BillingZipcode>\n </address>\n </CreditCardSale> + XML + end + + def post_scrubbed + <<~XML + <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<CreditCardSale xmlns=\"https://transaction.elementexpress.com\">\n <credentials>\n <AccountID>1013963</AccountID>\n <AccountToken>[FILTERED]</AccountToken>\n <AcceptorID>3928907</AcceptorID>\n </credentials>\n <application>\n <ApplicationID>5211</ApplicationID>\n <ApplicationName>Spreedly</ApplicationName>\n <ApplicationVersion>1</ApplicationVersion>\n </application>\n <card>\n <CardNumber>[FILTERED]</CardNumber>\n <ExpirationMonth>09</ExpirationMonth>\n <ExpirationYear>16</ExpirationYear>\n <CardholderName>Longbob Longsen</CardholderName>\n <CVV>[FILTERED]</CVV>\n </card>\n <transaction>\n <TransactionAmount>1.00</TransactionAmount>\n <MarketCode>Default</MarketCode>\n </transaction>\n <terminal>\n <TerminalID>01</TerminalID>\n <CardPresentCode>UseDefault</CardPresentCode>\n <CardholderPresentCode>UseDefault</CardholderPresentCode>\n <CardInputCode>UseDefault</CardInputCode>\n <CVVPresenceCode>UseDefault</CVVPresenceCode>\n <TerminalCapabilityCode>UseDefault</TerminalCapabilityCode>\n <TerminalEnvironmentCode>UseDefault</TerminalEnvironmentCode>\n <MotoECICode>UseDefault</MotoECICode>\n </terminal>\n <address>\n <BillingAddress1>456 My Street</BillingAddress1>\n <BillingAddress2>Apt 1</BillingAddress2>\n <BillingCity>Ottawa</BillingCity>\n <BillingState>ON</BillingState>\n <BillingZipcode>K1C2N6</BillingZipcode>\n </address>\n </CreditCardSale> + XML + end + + def error_response + <<~XML + <Response xmlns='https://transaction.elementexpress.com'><Response><ExpressResponseCode>103</ExpressResponseCode><ExpressResponseMessage>TargetNamespace required</ExpressResponseMessage></Response></Response> + XML + end + + def successful_purchase_response + <<~XML + <?xml version="1.0" encoding="utf-8"?><CreditCardSaleResponse xmlns="https://transaction.elementexpress.com"><response><ExpressResponseCode>0</ExpressResponseCode><ExpressResponseMessage>Approved</ExpressResponseMessage><ExpressTransactionDate>20151201</ExpressTransactionDate><ExpressTransactionTime>104518</ExpressTransactionTime><ExpressTransactionTimezone>UTC-05:00</ExpressTransactionTimezone><HostResponseCode>000</HostResponseCode><HostResponseMessage>AP</HostResponseMessage><Credentials /><Batch><BatchCloseType>Regular</BatchCloseType><BatchQueryType>Totals</BatchQueryType><HostBatchID>1</HostBatchID><HostItemID>96</HostItemID><HostBatchAmount>2962.00</HostBatchAmount><BatchGroupingCode>FullBatch</BatchGroupingCode><BatchIndexCode>Current</BatchIndexCode></Batch><Card><EncryptedFormat>Default</EncryptedFormat><AVSResponseCode>N</AVSResponseCode><CVVResponseCode>M</CVVResponseCode><CardLogo>Visa</CardLogo></Card><Transaction><TransactionID>2005831886</TransactionID><ApprovalNumber>000045</ApprovalNumber><ReversalType>System</ReversalType><MarketCode>Default</MarketCode><AcquirerData>aVb001234567810425c0425d5e00</AcquirerData><BillPaymentFlag>False</BillPaymentFlag><DuplicateCheckDisableFlag>False</DuplicateCheckDisableFlag><DuplicateOverrideFlag>False</DuplicateOverrideFlag><RecurringFlag>False</RecurringFlag><ProcessorName>NULL_PROCESSOR_TEST</ProcessorName><TransactionStatus>Approved</TransactionStatus><TransactionStatusCode>1</TransactionStatusCode><PartialApprovedFlag>False</PartialApprovedFlag><ApprovedAmount>1.00</ApprovedAmount><EMVEncryptionFormat>Default</EMVEncryptionFormat><ReversalReason>Unknown</ReversalReason></Transaction><PaymentAccount><PaymentAccountType>CreditCard</PaymentAccountType><PASSUpdaterBatchStatus>Null</PASSUpdaterBatchStatus><PASSUpdaterOption>Null</PASSUpdaterOption></PaymentAccount><Address><BillingAddress1>456 My Street</BillingAddress1><BillingZipcode>K1C2N6</BillingZipcode></Address><ScheduledTask><RunFrequency>OneTimeFuture</RunFrequency><RunUntilCancelFlag>False</RunUntilCancelFlag><ScheduledTaskStatus>Active</ScheduledTaskStatus></ScheduledTask><DemandDepositAccount><DDAAccountType>Checking</DDAAccountType><CheckType>Personal</CheckType></DemandDepositAccount><TransactionSetup><TransactionSetupMethod>Null</TransactionSetupMethod><Device>Null</Device><Embedded>False</Embedded><CVVRequired>False</CVVRequired><AutoReturn>False</AutoReturn><DeviceInputCode>NotUsed</DeviceInputCode></TransactionSetup><Terminal><TerminalType>Unknown</TerminalType><CardPresentCode>UseDefault</CardPresentCode><CardholderPresentCode>UseDefault</CardholderPresentCode><CardInputCode>UseDefault</CardInputCode><CVVPresenceCode>UseDefault</CVVPresenceCode><TerminalCapabilityCode>UseDefault</TerminalCapabilityCode><TerminalEnvironmentCode>UseDefault</TerminalEnvironmentCode><MotoECICode>UseDefault</MotoECICode><CVVResponseType>Regular</CVVResponseType><ConsentCode>NotUsed</ConsentCode><TerminalEncryptionFormat>Default</TerminalEncryptionFormat></Terminal><AutoRental><AutoRentalVehicleClassCode>Unused</AutoRentalVehicleClassCode><AutoRentalDistanceUnit>Unused</AutoRentalDistanceUnit><AutoRentalAuditAdjustmentCode>NoAdjustments</AutoRentalAuditAdjustmentCode></AutoRental><Healthcare><HealthcareFlag>False</HealthcareFlag><HealthcareFirstAccountType>NotSpecified</HealthcareFirstAccountType><HealthcareFirstAmountType>LedgerBalance</HealthcareFirstAmountType><HealthcareFirstAmountSign>Positive</HealthcareFirstAmountSign><HealthcareSecondAccountType>NotSpecified</HealthcareSecondAccountType><HealthcareSecondAmountType>LedgerBalance</HealthcareSecondAmountType><HealthcareSecondAmountSign>Positive</HealthcareSecondAmountSign><HealthcareThirdAccountType>NotSpecified</HealthcareThirdAccountType><HealthcareThirdAmountType>LedgerBalance</HealthcareThirdAmountType><HealthcareThirdAmountSign>Positive</HealthcareThirdAmountSign><HealthcareFourthAccountType>NotSpecified</HealthcareFourthAccountType><HealthcareFourthAmountType>LedgerBalance</HealthcareFourthAmountType><HealthcareFourthAmountSign>Positive</HealthcareFourthAmountSign></Healthcare><Lodging><LodgingPrestigiousPropertyCode>NonParticipant</LodgingPrestigiousPropertyCode><LodgingSpecialProgramCode>Default</LodgingSpecialProgramCode><LodgingChargeType>Default</LodgingChargeType></Lodging><BIN /><EnhancedBIN /><Token><TokenProvider>Null</TokenProvider></Token></response></CreditCardSaleResponse> + XML + end + + def successful_purchase_with_echeck_response + <<~XML + <?xml version="1.0" encoding="utf-8"?><CheckSaleResponse xmlns="https://transaction.elementexpress.com"><response><ExpressResponseCode>0</ExpressResponseCode><ExpressResponseMessage>Success</ExpressResponseMessage><ExpressTransactionDate>20151202</ExpressTransactionDate><ExpressTransactionTime>090320</ExpressTransactionTime><ExpressTransactionTimezone>UTC-05:00</ExpressTransactionTimezone><HostResponseCode>0</HostResponseCode><HostResponseMessage>Transaction Processed</HostResponseMessage><Credentials /><Batch><BatchCloseType>Regular</BatchCloseType><BatchQueryType>Totals</BatchQueryType><BatchGroupingCode>FullBatch</BatchGroupingCode><BatchIndexCode>Current</BatchIndexCode></Batch><Card><EncryptedFormat>Default</EncryptedFormat></Card><Transaction><TransactionID>2005838412</TransactionID><ReferenceNumber>347520966b3df3e93051b5dc85c355a54e3012c2</ReferenceNumber><ReversalType>System</ReversalType><MarketCode>Default</MarketCode><BillPaymentFlag>False</BillPaymentFlag><DuplicateCheckDisableFlag>False</DuplicateCheckDisableFlag><DuplicateOverrideFlag>False</DuplicateOverrideFlag><RecurringFlag>False</RecurringFlag><ProcessorName>NULL_PROCESSOR_TEST</ProcessorName><TransactionStatus>Pending</TransactionStatus><TransactionStatusCode>10</TransactionStatusCode><PartialApprovedFlag>False</PartialApprovedFlag><EMVEncryptionFormat>Default</EMVEncryptionFormat><ReversalReason>Unknown</ReversalReason></Transaction><PaymentAccount><PaymentAccountType>CreditCard</PaymentAccountType><PASSUpdaterBatchStatus>Null</PASSUpdaterBatchStatus><PASSUpdaterOption>Null</PASSUpdaterOption></PaymentAccount><Address><BillingAddress1>456 My Street</BillingAddress1><BillingZipcode>K1C2N6</BillingZipcode></Address><ScheduledTask><RunFrequency>OneTimeFuture</RunFrequency><RunUntilCancelFlag>False</RunUntilCancelFlag><ScheduledTaskStatus>Active</ScheduledTaskStatus></ScheduledTask><DemandDepositAccount><DDAAccountType>Checking</DDAAccountType><CheckType>Personal</CheckType></DemandDepositAccount><TransactionSetup><TransactionSetupMethod>Null</TransactionSetupMethod><Device>Null</Device><Embedded>False</Embedded><CVVRequired>False</CVVRequired><AutoReturn>False</AutoReturn><DeviceInputCode>NotUsed</DeviceInputCode></TransactionSetup><Terminal><TerminalType>Unknown</TerminalType><CardPresentCode>UseDefault</CardPresentCode><CardholderPresentCode>UseDefault</CardholderPresentCode><CardInputCode>UseDefault</CardInputCode><CVVPresenceCode>UseDefault</CVVPresenceCode><TerminalCapabilityCode>UseDefault</TerminalCapabilityCode><TerminalEnvironmentCode>UseDefault</TerminalEnvironmentCode><MotoECICode>UseDefault</MotoECICode><CVVResponseType>Regular</CVVResponseType><ConsentCode>NotUsed</ConsentCode><TerminalEncryptionFormat>Default</TerminalEncryptionFormat></Terminal><AutoRental><AutoRentalVehicleClassCode>Unused</AutoRentalVehicleClassCode><AutoRentalDistanceUnit>Unused</AutoRentalDistanceUnit><AutoRentalAuditAdjustmentCode>NoAdjustments</AutoRentalAuditAdjustmentCode></AutoRental><Healthcare><HealthcareFlag>False</HealthcareFlag><HealthcareFirstAccountType>NotSpecified</HealthcareFirstAccountType><HealthcareFirstAmountType>LedgerBalance</HealthcareFirstAmountType><HealthcareFirstAmountSign>Positive</HealthcareFirstAmountSign><HealthcareSecondAccountType>NotSpecified</HealthcareSecondAccountType><HealthcareSecondAmountType>LedgerBalance</HealthcareSecondAmountType><HealthcareSecondAmountSign>Positive</HealthcareSecondAmountSign><HealthcareThirdAccountType>NotSpecified</HealthcareThirdAccountType><HealthcareThirdAmountType>LedgerBalance</HealthcareThirdAmountType><HealthcareThirdAmountSign>Positive</HealthcareThirdAmountSign><HealthcareFourthAccountType>NotSpecified</HealthcareFourthAccountType><HealthcareFourthAmountType>LedgerBalance</HealthcareFourthAmountType><HealthcareFourthAmountSign>Positive</HealthcareFourthAmountSign></Healthcare><Lodging><LodgingPrestigiousPropertyCode>NonParticipant</LodgingPrestigiousPropertyCode><LodgingSpecialProgramCode>Default</LodgingSpecialProgramCode><LodgingChargeType>Default</LodgingChargeType></Lodging><BIN /><EnhancedBIN /><Token><TokenProvider>Null</TokenProvider></Token></response></CheckSaleResponse> + XML + end + + def successful_purchase_with_payment_account_token_response + <<~XML + <?xml version="1.0" encoding="utf-8"?><CreditCardSaleResponse xmlns="https://transaction.elementexpress.com"><response><ExpressResponseCode>0</ExpressResponseCode><ExpressResponseMessage>Approved</ExpressResponseMessage><ExpressTransactionDate>20151202</ExpressTransactionDate><ExpressTransactionTime>090144</ExpressTransactionTime><ExpressTransactionTimezone>UTC-05:00</ExpressTransactionTimezone><HostResponseCode>000</HostResponseCode><HostResponseMessage>AP</HostResponseMessage><Credentials /><Batch><BatchCloseType>Regular</BatchCloseType><BatchQueryType>Totals</BatchQueryType><HostBatchID>1</HostBatchID><HostItemID>155</HostItemID><HostBatchAmount>2995.00</HostBatchAmount><BatchGroupingCode>FullBatch</BatchGroupingCode><BatchIndexCode>Current</BatchIndexCode></Batch><Card><EncryptedFormat>Default</EncryptedFormat><AVSResponseCode>N</AVSResponseCode><CardLogo>Visa</CardLogo></Card><Transaction><TransactionID>2005838405</TransactionID><ApprovalNumber>000001</ApprovalNumber><ReferenceNumber>c0d498aa3c2c07169d13a989a7af91af5bc4e6a0</ReferenceNumber><ReversalType>System</ReversalType><MarketCode>Default</MarketCode><AcquirerData>aVb001234567810425c0425d5e00</AcquirerData><BillPaymentFlag>False</BillPaymentFlag><DuplicateCheckDisableFlag>False</DuplicateCheckDisableFlag><DuplicateOverrideFlag>False</DuplicateOverrideFlag><RecurringFlag>False</RecurringFlag><ProcessorName>NULL_PROCESSOR_TEST</ProcessorName><TransactionStatus>Approved</TransactionStatus><TransactionStatusCode>1</TransactionStatusCode><PartialApprovedFlag>False</PartialApprovedFlag><ApprovedAmount>1.00</ApprovedAmount><EMVEncryptionFormat>Default</EMVEncryptionFormat><ReversalReason>Unknown</ReversalReason></Transaction><PaymentAccount><PaymentAccountID>C875D86C-5913-487D-822E-76B27E2C2A4E</PaymentAccountID><PaymentAccountType>CreditCard</PaymentAccountType><PaymentAccountReferenceNumber>147b0b90f74faac13afb618fdabee3a4e75bf03b</PaymentAccountReferenceNumber><PASSUpdaterBatchStatus>Null</PASSUpdaterBatchStatus><PASSUpdaterOption>Null</PASSUpdaterOption></PaymentAccount><Address><BillingAddress1>456 My Street</BillingAddress1><BillingZipcode>K1C2N6</BillingZipcode></Address><ScheduledTask><RunFrequency>OneTimeFuture</RunFrequency><RunUntilCancelFlag>False</RunUntilCancelFlag><ScheduledTaskStatus>Active</ScheduledTaskStatus></ScheduledTask><DemandDepositAccount><DDAAccountType>Checking</DDAAccountType><CheckType>Personal</CheckType></DemandDepositAccount><TransactionSetup><TransactionSetupMethod>Null</TransactionSetupMethod><Device>Null</Device><Embedded>False</Embedded><CVVRequired>False</CVVRequired><AutoReturn>False</AutoReturn><DeviceInputCode>NotUsed</DeviceInputCode></TransactionSetup><Terminal><TerminalType>Unknown</TerminalType><CardPresentCode>UseDefault</CardPresentCode><CardholderPresentCode>UseDefault</CardholderPresentCode><CardInputCode>UseDefault</CardInputCode><CVVPresenceCode>UseDefault</CVVPresenceCode><TerminalCapabilityCode>UseDefault</TerminalCapabilityCode><TerminalEnvironmentCode>UseDefault</TerminalEnvironmentCode><MotoECICode>UseDefault</MotoECICode><CVVResponseType>Regular</CVVResponseType><ConsentCode>NotUsed</ConsentCode><TerminalEncryptionFormat>Default</TerminalEncryptionFormat></Terminal><AutoRental><AutoRentalVehicleClassCode>Unused</AutoRentalVehicleClassCode><AutoRentalDistanceUnit>Unused</AutoRentalDistanceUnit><AutoRentalAuditAdjustmentCode>NoAdjustments</AutoRentalAuditAdjustmentCode></AutoRental><Healthcare><HealthcareFlag>False</HealthcareFlag><HealthcareFirstAccountType>NotSpecified</HealthcareFirstAccountType><HealthcareFirstAmountType>LedgerBalance</HealthcareFirstAmountType><HealthcareFirstAmountSign>Positive</HealthcareFirstAmountSign><HealthcareSecondAccountType>NotSpecified</HealthcareSecondAccountType><HealthcareSecondAmountType>LedgerBalance</HealthcareSecondAmountType><HealthcareSecondAmountSign>Positive</HealthcareSecondAmountSign><HealthcareThirdAccountType>NotSpecified</HealthcareThirdAccountType><HealthcareThirdAmountType>LedgerBalance</HealthcareThirdAmountType><HealthcareThirdAmountSign>Positive</HealthcareThirdAmountSign><HealthcareFourthAccountType>NotSpecified</HealthcareFourthAccountType><HealthcareFourthAmountType>LedgerBalance</HealthcareFourthAmountType><HealthcareFourthAmountSign>Positive</HealthcareFourthAmountSign></Healthcare><Lodging><LodgingPrestigiousPropertyCode>NonParticipant</LodgingPrestigiousPropertyCode><LodgingSpecialProgramCode>Default</LodgingSpecialProgramCode><LodgingChargeType>Default</LodgingChargeType></Lodging><BIN /><EnhancedBIN /><Token><TokenProvider>Null</TokenProvider></Token></response></CreditCardSaleResponse> + XML + end + + def successful_credit_response + <<~XML + <?xml version=\"1.0\" encoding=\"utf-8\"?><CreditCardCreditResponse xmlns=\"https://transaction.elementexpress.com\"><response><ExpressResponseCode>0</ExpressResponseCode><ExpressResponseMessage>Approved</ExpressResponseMessage><ExpressTransactionDate>20211122</ExpressTransactionDate><ExpressTransactionTime>174635</ExpressTransactionTime><ExpressTransactionTimezone>UTC-06:00</ExpressTransactionTimezone><HostResponseCode>000</HostResponseCode><HostResponseMessage>AP</HostResponseMessage><Credentials /><Batch><BatchCloseType>Regular</BatchCloseType><BatchQueryType>Totals</BatchQueryType><HostBatchID>1</HostBatchID><HostItemID>102</HostItemID><HostBatchAmount>103.00</HostBatchAmount><BatchGroupingCode>FullBatch</BatchGroupingCode><BatchIndexCode>Current</BatchIndexCode></Batch><Card><CardLogo>Visa</CardLogo><BIN>400010</BIN></Card><Transaction><TransactionID>122816253</TransactionID><ApprovalNumber>000046</ApprovalNumber><ReferenceNumber>1</ReferenceNumber><ReversalType>System</ReversalType><MarketCode>Default</MarketCode><BillPaymentFlag>False</BillPaymentFlag><DuplicateCheckDisableFlag>False</DuplicateCheckDisableFlag><DuplicateOverrideFlag>False</DuplicateOverrideFlag><RecurringFlag>False</RecurringFlag><ProcessorName>NULL_PROCESSOR_TEST</ProcessorName><TransactionStatus>Approved</TransactionStatus><TransactionStatusCode>1</TransactionStatusCode><PartialApprovedFlag>False</PartialApprovedFlag><EMVEncryptionFormat>Default</EMVEncryptionFormat><ReversalReason>Unknown</ReversalReason><PaymentType>NotUsed</PaymentType><SubmissionType>NotUsed</SubmissionType></Transaction><PaymentAccount><PaymentAccountType>CreditCard</PaymentAccountType><PASSUpdaterBatchStatus>Null</PASSUpdaterBatchStatus><PASSUpdaterOption>Null</PASSUpdaterOption></PaymentAccount><Address /><ScheduledTask><RunFrequency>OneTimeFuture</RunFrequency><RunUntilCancelFlag>False</RunUntilCancelFlag><ScheduledTaskStatus>Active</ScheduledTaskStatus></ScheduledTask><DemandDepositAccount><CheckType>Personal</CheckType></DemandDepositAccount><TransactionSetup><TransactionSetupMethod>Null</TransactionSetupMethod><Device>Null</Device><Embedded>False</Embedded><CVVRequired>False</CVVRequired><AutoReturn>False</AutoReturn><DeviceInputCode>NotUsed</DeviceInputCode></TransactionSetup><Terminal><TerminalType>Unknown</TerminalType><CardPresentCode>UseDefault</CardPresentCode><CardholderPresentCode>UseDefault</CardholderPresentCode><CardInputCode>UseDefault</CardInputCode><CVVPresenceCode>UseDefault</CVVPresenceCode><TerminalCapabilityCode>UseDefault</TerminalCapabilityCode><TerminalEnvironmentCode>UseDefault</TerminalEnvironmentCode><MotoECICode>UseDefault</MotoECICode><CVVResponseType>Regular</CVVResponseType><ConsentCode>NotUsed</ConsentCode><TerminalEncryptionFormat>Default</TerminalEncryptionFormat></Terminal><AutoRental><AutoRentalVehicleClassCode>Unused</AutoRentalVehicleClassCode><AutoRentalDistanceUnit>Unused</AutoRentalDistanceUnit><AutoRentalAuditAdjustmentCode>NoAdjustments</AutoRentalAuditAdjustmentCode></AutoRental><Healthcare><HealthcareFlag>False</HealthcareFlag><HealthcareFirstAccountType>NotSpecified</HealthcareFirstAccountType><HealthcareFirstAmountType>LedgerBalance</HealthcareFirstAmountType><HealthcareFirstAmountSign>Positive</HealthcareFirstAmountSign><HealthcareSecondAccountType>NotSpecified</HealthcareSecondAccountType><HealthcareSecondAmountType>LedgerBalance</HealthcareSecondAmountType><HealthcareSecondAmountSign>Positive</HealthcareSecondAmountSign><HealthcareThirdAccountType>NotSpecified</HealthcareThirdAccountType><HealthcareThirdAmountType>LedgerBalance</HealthcareThirdAmountType><HealthcareThirdAmountSign>Positive</HealthcareThirdAmountSign><HealthcareFourthAccountType>NotSpecified</HealthcareFourthAccountType><HealthcareFourthAmountType>LedgerBalance</HealthcareFourthAmountType><HealthcareFourthAmountSign>Positive</HealthcareFourthAmountSign></Healthcare><Lodging><LodgingPrestigiousPropertyCode>NonParticipant</LodgingPrestigiousPropertyCode><LodgingSpecialProgramCode>Default</LodgingSpecialProgramCode><LodgingChargeType>Default</LodgingChargeType></Lodging><BIN /><EnhancedBIN /><Token /></response></CreditCardCreditResponse> + XML + end + + def failed_purchase_with_echeck_response + <<~XML + <?xml version="1.0" encoding="utf-8"?><CreditCardSaleResponse xmlns="https://transaction.elementexpress.com"><response><ExpressResponseCode>101</ExpressResponseCode><ExpressResponseMessage>CardNumber Required</ExpressResponseMessage><ExpressTransactionDate>20151202</ExpressTransactionDate><ExpressTransactionTime>090342</ExpressTransactionTime><ExpressTransactionTimezone>UTC-05:00</ExpressTransactionTimezone><Credentials /><Batch><BatchCloseType>Regular</BatchCloseType><BatchQueryType>Totals</BatchQueryType><HostBatchID>1</HostBatchID><BatchGroupingCode>FullBatch</BatchGroupingCode><BatchIndexCode>Current</BatchIndexCode></Batch><Card><EncryptedFormat>Default</EncryptedFormat></Card><Transaction><ReferenceNumber>8fe3b762a2a4344d938c32be31f36e354fb28ee3</ReferenceNumber><ReversalType>System</ReversalType><MarketCode>Default</MarketCode><BillPaymentFlag>False</BillPaymentFlag><DuplicateCheckDisableFlag>False</DuplicateCheckDisableFlag><DuplicateOverrideFlag>False</DuplicateOverrideFlag><RecurringFlag>False</RecurringFlag><ProcessorName>NULL_PROCESSOR_TEST</ProcessorName><PartialApprovedFlag>False</PartialApprovedFlag><EMVEncryptionFormat>Default</EMVEncryptionFormat><ReversalReason>Unknown</ReversalReason></Transaction><PaymentAccount><PaymentAccountType>CreditCard</PaymentAccountType><PASSUpdaterBatchStatus>Null</PASSUpdaterBatchStatus><PASSUpdaterOption>Null</PASSUpdaterOption></PaymentAccount><Address /><ScheduledTask><RunFrequency>OneTimeFuture</RunFrequency><RunUntilCancelFlag>False</RunUntilCancelFlag><ScheduledTaskStatus>Active</ScheduledTaskStatus></ScheduledTask><DemandDepositAccount><DDAAccountType>Checking</DDAAccountType><CheckType>Personal</CheckType></DemandDepositAccount><TransactionSetup><TransactionSetupMethod>Null</TransactionSetupMethod><Device>Null</Device><Embedded>False</Embedded><CVVRequired>False</CVVRequired><AutoReturn>False</AutoReturn><DeviceInputCode>NotUsed</DeviceInputCode></TransactionSetup><Terminal><TerminalType>Unknown</TerminalType><CardPresentCode>UseDefault</CardPresentCode><CardholderPresentCode>UseDefault</CardholderPresentCode><CardInputCode>UseDefault</CardInputCode><CVVPresenceCode>UseDefault</CVVPresenceCode><TerminalCapabilityCode>UseDefault</TerminalCapabilityCode><TerminalEnvironmentCode>UseDefault</TerminalEnvironmentCode><MotoECICode>UseDefault</MotoECICode><CVVResponseType>Regular</CVVResponseType><ConsentCode>NotUsed</ConsentCode><TerminalEncryptionFormat>Default</TerminalEncryptionFormat></Terminal><AutoRental><AutoRentalVehicleClassCode>Unused</AutoRentalVehicleClassCode><AutoRentalDistanceUnit>Unused</AutoRentalDistanceUnit><AutoRentalAuditAdjustmentCode>NoAdjustments</AutoRentalAuditAdjustmentCode></AutoRental><Healthcare><HealthcareFlag>False</HealthcareFlag><HealthcareFirstAccountType>NotSpecified</HealthcareFirstAccountType><HealthcareFirstAmountType>LedgerBalance</HealthcareFirstAmountType><HealthcareFirstAmountSign>Positive</HealthcareFirstAmountSign><HealthcareSecondAccountType>NotSpecified</HealthcareSecondAccountType><HealthcareSecondAmountType>LedgerBalance</HealthcareSecondAmountType><HealthcareSecondAmountSign>Positive</HealthcareSecondAmountSign><HealthcareThirdAccountType>NotSpecified</HealthcareThirdAccountType><HealthcareThirdAmountType>LedgerBalance</HealthcareThirdAmountType><HealthcareThirdAmountSign>Positive</HealthcareThirdAmountSign><HealthcareFourthAccountType>NotSpecified</HealthcareFourthAccountType><HealthcareFourthAmountType>LedgerBalance</HealthcareFourthAmountType><HealthcareFourthAmountSign>Positive</HealthcareFourthAmountSign></Healthcare><Lodging><LodgingPrestigiousPropertyCode>NonParticipant</LodgingPrestigiousPropertyCode><LodgingSpecialProgramCode>Default</LodgingSpecialProgramCode><LodgingChargeType>Default</LodgingChargeType></Lodging><BIN /><EnhancedBIN /><Token><TokenProvider>Null</TokenProvider></Token></response></CreditCardSaleResponse> + XML + end + + def failed_purchase_with_payment_account_token_response + <<~XML + <?xml version="1.0" encoding="utf-8"?><CreditCardSaleResponse xmlns="https://transaction.elementexpress.com"><response><ExpressResponseCode>103</ExpressResponseCode><ExpressResponseMessage>PAYMENT ACCOUNT NOT FOUND</ExpressResponseMessage><ExpressTransactionDate>20151202</ExpressTransactionDate><ExpressTransactionTime>090245</ExpressTransactionTime><ExpressTransactionTimezone>UTC-05:00</ExpressTransactionTimezone><Credentials /><Batch><BatchCloseType>Regular</BatchCloseType><BatchQueryType>Totals</BatchQueryType><HostBatchID>1</HostBatchID><BatchGroupingCode>FullBatch</BatchGroupingCode><BatchIndexCode>Current</BatchIndexCode></Batch><Card><EncryptedFormat>Default</EncryptedFormat></Card><Transaction><ReferenceNumber>564bd4943761a37bdbb3f201faa56faa091781b5</ReferenceNumber><ReversalType>System</ReversalType><MarketCode>Default</MarketCode><BillPaymentFlag>False</BillPaymentFlag><DuplicateCheckDisableFlag>False</DuplicateCheckDisableFlag><DuplicateOverrideFlag>False</DuplicateOverrideFlag><RecurringFlag>False</RecurringFlag><ProcessorName>NULL_PROCESSOR_TEST</ProcessorName><PartialApprovedFlag>False</PartialApprovedFlag><EMVEncryptionFormat>Default</EMVEncryptionFormat><ReversalReason>Unknown</ReversalReason></Transaction><PaymentAccount><PaymentAccountID>asdf</PaymentAccountID><PaymentAccountType>CreditCard</PaymentAccountType><PASSUpdaterBatchStatus>Null</PASSUpdaterBatchStatus><PASSUpdaterOption>Null</PASSUpdaterOption></PaymentAccount><Address /><ScheduledTask><RunFrequency>OneTimeFuture</RunFrequency><RunUntilCancelFlag>False</RunUntilCancelFlag><ScheduledTaskStatus>Active</ScheduledTaskStatus></ScheduledTask><DemandDepositAccount><DDAAccountType>Checking</DDAAccountType><CheckType>Personal</CheckType></DemandDepositAccount><TransactionSetup><TransactionSetupMethod>Null</TransactionSetupMethod><Device>Null</Device><Embedded>False</Embedded><CVVRequired>False</CVVRequired><AutoReturn>False</AutoReturn><DeviceInputCode>NotUsed</DeviceInputCode></TransactionSetup><Terminal><TerminalType>Unknown</TerminalType><CardPresentCode>UseDefault</CardPresentCode><CardholderPresentCode>UseDefault</CardholderPresentCode><CardInputCode>UseDefault</CardInputCode><CVVPresenceCode>UseDefault</CVVPresenceCode><TerminalCapabilityCode>UseDefault</TerminalCapabilityCode><TerminalEnvironmentCode>UseDefault</TerminalEnvironmentCode><MotoECICode>UseDefault</MotoECICode><CVVResponseType>Regular</CVVResponseType><ConsentCode>NotUsed</ConsentCode><TerminalEncryptionFormat>Default</TerminalEncryptionFormat></Terminal><AutoRental><AutoRentalVehicleClassCode>Unused</AutoRentalVehicleClassCode><AutoRentalDistanceUnit>Unused</AutoRentalDistanceUnit><AutoRentalAuditAdjustmentCode>NoAdjustments</AutoRentalAuditAdjustmentCode></AutoRental><Healthcare><HealthcareFlag>False</HealthcareFlag><HealthcareFirstAccountType>NotSpecified</HealthcareFirstAccountType><HealthcareFirstAmountType>LedgerBalance</HealthcareFirstAmountType><HealthcareFirstAmountSign>Positive</HealthcareFirstAmountSign><HealthcareSecondAccountType>NotSpecified</HealthcareSecondAccountType><HealthcareSecondAmountType>LedgerBalance</HealthcareSecondAmountType><HealthcareSecondAmountSign>Positive</HealthcareSecondAmountSign><HealthcareThirdAccountType>NotSpecified</HealthcareThirdAccountType><HealthcareThirdAmountType>LedgerBalance</HealthcareThirdAmountType><HealthcareThirdAmountSign>Positive</HealthcareThirdAmountSign><HealthcareFourthAccountType>NotSpecified</HealthcareFourthAccountType><HealthcareFourthAmountType>LedgerBalance</HealthcareFourthAmountType><HealthcareFourthAmountSign>Positive</HealthcareFourthAmountSign></Healthcare><Lodging><LodgingPrestigiousPropertyCode>NonParticipant</LodgingPrestigiousPropertyCode><LodgingSpecialProgramCode>Default</LodgingSpecialProgramCode><LodgingChargeType>Default</LodgingChargeType></Lodging><BIN /><EnhancedBIN /><Token><TokenProvider>Null</TokenProvider></Token></response></CreditCardSaleResponse> + XML + end + + def failed_purchase_response + <<~XML + <?xml version="1.0" encoding="utf-8"?><CreditCardSaleResponse xmlns="https://transaction.elementexpress.com"><response><ExpressResponseCode>20</ExpressResponseCode><ExpressResponseMessage>Declined</ExpressResponseMessage><ExpressTransactionDate>20151201</ExpressTransactionDate><ExpressTransactionTime>104817</ExpressTransactionTime><ExpressTransactionTimezone>UTC-05:00</ExpressTransactionTimezone><HostResponseCode>007</HostResponseCode><HostResponseMessage>DECLINED</HostResponseMessage><Credentials /><Batch><BatchCloseType>Regular</BatchCloseType><BatchQueryType>Totals</BatchQueryType><HostBatchID>1</HostBatchID><BatchGroupingCode>FullBatch</BatchGroupingCode><BatchIndexCode>Current</BatchIndexCode></Batch><Card><EncryptedFormat>Default</EncryptedFormat><AVSResponseCode>N</AVSResponseCode><CVVResponseCode>M</CVVResponseCode><CardLogo>Visa</CardLogo></Card><Transaction><TransactionID>2005831909</TransactionID><ReversalType>System</ReversalType><MarketCode>Default</MarketCode><AcquirerData>aVb001234567810425c0425d5e00</AcquirerData><BillPaymentFlag>False</BillPaymentFlag><DuplicateCheckDisableFlag>False</DuplicateCheckDisableFlag><DuplicateOverrideFlag>False</DuplicateOverrideFlag><RecurringFlag>False</RecurringFlag><ProcessorName>NULL_PROCESSOR_TEST</ProcessorName><TransactionStatus>Declined</TransactionStatus><TransactionStatusCode>2</TransactionStatusCode><PartialApprovedFlag>False</PartialApprovedFlag><EMVEncryptionFormat>Default</EMVEncryptionFormat><ReversalReason>Unknown</ReversalReason></Transaction><PaymentAccount><PaymentAccountType>CreditCard</PaymentAccountType><PASSUpdaterBatchStatus>Null</PASSUpdaterBatchStatus><PASSUpdaterOption>Null</PASSUpdaterOption></PaymentAccount><Address><BillingAddress1>456 My Street</BillingAddress1><BillingZipcode>K1C2N6</BillingZipcode></Address><ScheduledTask><RunFrequency>OneTimeFuture</RunFrequency><RunUntilCancelFlag>False</RunUntilCancelFlag><ScheduledTaskStatus>Active</ScheduledTaskStatus></ScheduledTask><DemandDepositAccount><DDAAccountType>Checking</DDAAccountType><CheckType>Personal</CheckType></DemandDepositAccount><TransactionSetup><TransactionSetupMethod>Null</TransactionSetupMethod><Device>Null</Device><Embedded>False</Embedded><CVVRequired>False</CVVRequired><AutoReturn>False</AutoReturn><DeviceInputCode>NotUsed</DeviceInputCode></TransactionSetup><Terminal><TerminalType>Unknown</TerminalType><CardPresentCode>UseDefault</CardPresentCode><CardholderPresentCode>UseDefault</CardholderPresentCode><CardInputCode>UseDefault</CardInputCode><CVVPresenceCode>UseDefault</CVVPresenceCode><TerminalCapabilityCode>UseDefault</TerminalCapabilityCode><TerminalEnvironmentCode>UseDefault</TerminalEnvironmentCode><MotoECICode>UseDefault</MotoECICode><CVVResponseType>Regular</CVVResponseType><ConsentCode>NotUsed</ConsentCode><TerminalEncryptionFormat>Default</TerminalEncryptionFormat></Terminal><AutoRental><AutoRentalVehicleClassCode>Unused</AutoRentalVehicleClassCode><AutoRentalDistanceUnit>Unused</AutoRentalDistanceUnit><AutoRentalAuditAdjustmentCode>NoAdjustments</AutoRentalAuditAdjustmentCode></AutoRental><Healthcare><HealthcareFlag>False</HealthcareFlag><HealthcareFirstAccountType>NotSpecified</HealthcareFirstAccountType><HealthcareFirstAmountType>LedgerBalance</HealthcareFirstAmountType><HealthcareFirstAmountSign>Positive</HealthcareFirstAmountSign><HealthcareSecondAccountType>NotSpecified</HealthcareSecondAccountType><HealthcareSecondAmountType>LedgerBalance</HealthcareSecondAmountType><HealthcareSecondAmountSign>Positive</HealthcareSecondAmountSign><HealthcareThirdAccountType>NotSpecified</HealthcareThirdAccountType><HealthcareThirdAmountType>LedgerBalance</HealthcareThirdAmountType><HealthcareThirdAmountSign>Positive</HealthcareThirdAmountSign><HealthcareFourthAccountType>NotSpecified</HealthcareFourthAccountType><HealthcareFourthAmountType>LedgerBalance</HealthcareFourthAmountType><HealthcareFourthAmountSign>Positive</HealthcareFourthAmountSign></Healthcare><Lodging><LodgingPrestigiousPropertyCode>NonParticipant</LodgingPrestigiousPropertyCode><LodgingSpecialProgramCode>Default</LodgingSpecialProgramCode><LodgingChargeType>Default</LodgingChargeType></Lodging><BIN /><EnhancedBIN /><Token><TokenProvider>Null</TokenProvider></Token></response></CreditCardSaleResponse> + XML + end + + def successful_authorize_response + <<~XML + <?xml version="1.0" encoding="utf-8"?><CreditCardAuthorizationResponse xmlns="https://transaction.elementexpress.com"><response><ExpressResponseCode>0</ExpressResponseCode><ExpressResponseMessage>Approved</ExpressResponseMessage><ExpressTransactionDate>20151201</ExpressTransactionDate><ExpressTransactionTime>120220</ExpressTransactionTime><ExpressTransactionTimezone>UTC-05:00</ExpressTransactionTimezone><HostResponseCode>000</HostResponseCode><HostResponseMessage>AP</HostResponseMessage><Credentials /><Batch><BatchCloseType>Regular</BatchCloseType><BatchQueryType>Totals</BatchQueryType><HostBatchID>1</HostBatchID><BatchGroupingCode>FullBatch</BatchGroupingCode><BatchIndexCode>Current</BatchIndexCode></Batch><Card><EncryptedFormat>Default</EncryptedFormat><AVSResponseCode>N</AVSResponseCode><CVVResponseCode>M</CVVResponseCode><CardLogo>Visa</CardLogo></Card><Transaction><TransactionID>2005832533</TransactionID><ApprovalNumber>000002</ApprovalNumber><ReversalType>System</ReversalType><MarketCode>Default</MarketCode><AcquirerData>aVb001234567810425c0425d5e00</AcquirerData><BillPaymentFlag>False</BillPaymentFlag><DuplicateCheckDisableFlag>False</DuplicateCheckDisableFlag><DuplicateOverrideFlag>False</DuplicateOverrideFlag><RecurringFlag>False</RecurringFlag><ProcessorName>NULL_PROCESSOR_TEST</ProcessorName><TransactionStatus>Authorized</TransactionStatus><TransactionStatusCode>5</TransactionStatusCode><PartialApprovedFlag>False</PartialApprovedFlag><ApprovedAmount>1.00</ApprovedAmount><EMVEncryptionFormat>Default</EMVEncryptionFormat><ReversalReason>Unknown</ReversalReason></Transaction><PaymentAccount><PaymentAccountType>CreditCard</PaymentAccountType><PASSUpdaterBatchStatus>Null</PASSUpdaterBatchStatus><PASSUpdaterOption>Null</PASSUpdaterOption></PaymentAccount><Address><BillingAddress1>456 My Street</BillingAddress1><BillingZipcode>K1C2N6</BillingZipcode></Address><ScheduledTask><RunFrequency>OneTimeFuture</RunFrequency><RunUntilCancelFlag>False</RunUntilCancelFlag><ScheduledTaskStatus>Active</ScheduledTaskStatus></ScheduledTask><DemandDepositAccount><DDAAccountType>Checking</DDAAccountType><CheckType>Personal</CheckType></DemandDepositAccount><TransactionSetup><TransactionSetupMethod>Null</TransactionSetupMethod><Device>Null</Device><Embedded>False</Embedded><CVVRequired>False</CVVRequired><AutoReturn>False</AutoReturn><DeviceInputCode>NotUsed</DeviceInputCode></TransactionSetup><Terminal><TerminalType>Unknown</TerminalType><CardPresentCode>UseDefault</CardPresentCode><CardholderPresentCode>UseDefault</CardholderPresentCode><CardInputCode>UseDefault</CardInputCode><CVVPresenceCode>UseDefault</CVVPresenceCode><TerminalCapabilityCode>UseDefault</TerminalCapabilityCode><TerminalEnvironmentCode>UseDefault</TerminalEnvironmentCode><MotoECICode>UseDefault</MotoECICode><CVVResponseType>Regular</CVVResponseType><ConsentCode>NotUsed</ConsentCode><TerminalEncryptionFormat>Default</TerminalEncryptionFormat></Terminal><AutoRental><AutoRentalVehicleClassCode>Unused</AutoRentalVehicleClassCode><AutoRentalDistanceUnit>Unused</AutoRentalDistanceUnit><AutoRentalAuditAdjustmentCode>NoAdjustments</AutoRentalAuditAdjustmentCode></AutoRental><Healthcare><HealthcareFlag>False</HealthcareFlag><HealthcareFirstAccountType>NotSpecified</HealthcareFirstAccountType><HealthcareFirstAmountType>LedgerBalance</HealthcareFirstAmountType><HealthcareFirstAmountSign>Positive</HealthcareFirstAmountSign><HealthcareSecondAccountType>NotSpecified</HealthcareSecondAccountType><HealthcareSecondAmountType>LedgerBalance</HealthcareSecondAmountType><HealthcareSecondAmountSign>Positive</HealthcareSecondAmountSign><HealthcareThirdAccountType>NotSpecified</HealthcareThirdAccountType><HealthcareThirdAmountType>LedgerBalance</HealthcareThirdAmountType><HealthcareThirdAmountSign>Positive</HealthcareThirdAmountSign><HealthcareFourthAccountType>NotSpecified</HealthcareFourthAccountType><HealthcareFourthAmountType>LedgerBalance</HealthcareFourthAmountType><HealthcareFourthAmountSign>Positive</HealthcareFourthAmountSign></Healthcare><Lodging><LodgingPrestigiousPropertyCode>NonParticipant</LodgingPrestigiousPropertyCode><LodgingSpecialProgramCode>Default</LodgingSpecialProgramCode><LodgingChargeType>Default</LodgingChargeType></Lodging><BIN /><EnhancedBIN /><Token><TokenProvider>Null</TokenProvider></Token></response></CreditCardAuthorizationResponse> + XML + end + + def failed_authorize_response + <<~XML + <?xml version="1.0" encoding="utf-8"?><CreditCardAuthorizationResponse xmlns="https://transaction.elementexpress.com"><response><ExpressResponseCode>20</ExpressResponseCode><ExpressResponseMessage>Declined</ExpressResponseMessage><ExpressTransactionDate>20151201</ExpressTransactionDate><ExpressTransactionTime>120315</ExpressTransactionTime><ExpressTransactionTimezone>UTC-05:00</ExpressTransactionTimezone><HostResponseCode>007</HostResponseCode><HostResponseMessage>DECLINED</HostResponseMessage><Credentials /><Batch><BatchCloseType>Regular</BatchCloseType><BatchQueryType>Totals</BatchQueryType><HostBatchID>1</HostBatchID><BatchGroupingCode>FullBatch</BatchGroupingCode><BatchIndexCode>Current</BatchIndexCode></Batch><Card><EncryptedFormat>Default</EncryptedFormat><AVSResponseCode>N</AVSResponseCode><CVVResponseCode>M</CVVResponseCode><CardLogo>Visa</CardLogo></Card><Transaction><TransactionID>2005832537</TransactionID><ReversalType>System</ReversalType><MarketCode>Default</MarketCode><AcquirerData>aVb001234567810425c0425d5e00</AcquirerData><BillPaymentFlag>False</BillPaymentFlag><DuplicateCheckDisableFlag>False</DuplicateCheckDisableFlag><DuplicateOverrideFlag>False</DuplicateOverrideFlag><RecurringFlag>False</RecurringFlag><ProcessorName>NULL_PROCESSOR_TEST</ProcessorName><TransactionStatus>Declined</TransactionStatus><TransactionStatusCode>2</TransactionStatusCode><PartialApprovedFlag>False</PartialApprovedFlag><EMVEncryptionFormat>Default</EMVEncryptionFormat><ReversalReason>Unknown</ReversalReason></Transaction><PaymentAccount><PaymentAccountType>CreditCard</PaymentAccountType><PASSUpdaterBatchStatus>Null</PASSUpdaterBatchStatus><PASSUpdaterOption>Null</PASSUpdaterOption></PaymentAccount><Address><BillingAddress1>456 My Street</BillingAddress1><BillingZipcode>K1C2N6</BillingZipcode></Address><ScheduledTask><RunFrequency>OneTimeFuture</RunFrequency><RunUntilCancelFlag>False</RunUntilCancelFlag><ScheduledTaskStatus>Active</ScheduledTaskStatus></ScheduledTask><DemandDepositAccount><DDAAccountType>Checking</DDAAccountType><CheckType>Personal</CheckType></DemandDepositAccount><TransactionSetup><TransactionSetupMethod>Null</TransactionSetupMethod><Device>Null</Device><Embedded>False</Embedded><CVVRequired>False</CVVRequired><AutoReturn>False</AutoReturn><DeviceInputCode>NotUsed</DeviceInputCode></TransactionSetup><Terminal><TerminalType>Unknown</TerminalType><CardPresentCode>UseDefault</CardPresentCode><CardholderPresentCode>UseDefault</CardholderPresentCode><CardInputCode>UseDefault</CardInputCode><CVVPresenceCode>UseDefault</CVVPresenceCode><TerminalCapabilityCode>UseDefault</TerminalCapabilityCode><TerminalEnvironmentCode>UseDefault</TerminalEnvironmentCode><MotoECICode>UseDefault</MotoECICode><CVVResponseType>Regular</CVVResponseType><ConsentCode>NotUsed</ConsentCode><TerminalEncryptionFormat>Default</TerminalEncryptionFormat></Terminal><AutoRental><AutoRentalVehicleClassCode>Unused</AutoRentalVehicleClassCode><AutoRentalDistanceUnit>Unused</AutoRentalDistanceUnit><AutoRentalAuditAdjustmentCode>NoAdjustments</AutoRentalAuditAdjustmentCode></AutoRental><Healthcare><HealthcareFlag>False</HealthcareFlag><HealthcareFirstAccountType>NotSpecified</HealthcareFirstAccountType><HealthcareFirstAmountType>LedgerBalance</HealthcareFirstAmountType><HealthcareFirstAmountSign>Positive</HealthcareFirstAmountSign><HealthcareSecondAccountType>NotSpecified</HealthcareSecondAccountType><HealthcareSecondAmountType>LedgerBalance</HealthcareSecondAmountType><HealthcareSecondAmountSign>Positive</HealthcareSecondAmountSign><HealthcareThirdAccountType>NotSpecified</HealthcareThirdAccountType><HealthcareThirdAmountType>LedgerBalance</HealthcareThirdAmountType><HealthcareThirdAmountSign>Positive</HealthcareThirdAmountSign><HealthcareFourthAccountType>NotSpecified</HealthcareFourthAccountType><HealthcareFourthAmountType>LedgerBalance</HealthcareFourthAmountType><HealthcareFourthAmountSign>Positive</HealthcareFourthAmountSign></Healthcare><Lodging><LodgingPrestigiousPropertyCode>NonParticipant</LodgingPrestigiousPropertyCode><LodgingSpecialProgramCode>Default</LodgingSpecialProgramCode><LodgingChargeType>Default</LodgingChargeType></Lodging><BIN /><EnhancedBIN /><Token><TokenProvider>Null</TokenProvider></Token></response></CreditCardAuthorizationResponse> + XML + end + + def successful_capture_response + <<~XML + <?xml version="1.0" encoding="utf-8"?><CreditCardAuthorizationCompletionResponse xmlns="https://transaction.elementexpress.com"><response><ExpressResponseCode>0</ExpressResponseCode><ExpressResponseMessage>Success</ExpressResponseMessage><ExpressTransactionDate>20151201</ExpressTransactionDate><ExpressTransactionTime>120222</ExpressTransactionTime><ExpressTransactionTimezone>UTC-05:00</ExpressTransactionTimezone><HostResponseCode>000</HostResponseCode><HostResponseMessage>AP</HostResponseMessage><Credentials /><Batch><BatchCloseType>Regular</BatchCloseType><BatchQueryType>Totals</BatchQueryType><HostBatchID>1</HostBatchID><HostItemID>97</HostItemID><HostBatchAmount>2963.00</HostBatchAmount><BatchGroupingCode>FullBatch</BatchGroupingCode><BatchIndexCode>Current</BatchIndexCode></Batch><Card><EncryptedFormat>Default</EncryptedFormat><CardLogo>Visa</CardLogo></Card><Transaction><TransactionID>2005832535</TransactionID><ApprovalNumber>000002</ApprovalNumber><ReversalType>System</ReversalType><MarketCode>Default</MarketCode><AcquirerData>aVb001234567810425c0425d5e00</AcquirerData><BillPaymentFlag>False</BillPaymentFlag><DuplicateCheckDisableFlag>False</DuplicateCheckDisableFlag><DuplicateOverrideFlag>False</DuplicateOverrideFlag><RecurringFlag>False</RecurringFlag><ProcessorName>NULL_PROCESSOR_TEST</ProcessorName><TransactionStatus>Approved</TransactionStatus><TransactionStatusCode>1</TransactionStatusCode><PartialApprovedFlag>False</PartialApprovedFlag><EMVEncryptionFormat>Default</EMVEncryptionFormat><ReversalReason>Unknown</ReversalReason></Transaction><PaymentAccount><PaymentAccountType>CreditCard</PaymentAccountType><PASSUpdaterBatchStatus>Null</PASSUpdaterBatchStatus><PASSUpdaterOption>Null</PASSUpdaterOption></PaymentAccount><Address /><ScheduledTask><RunFrequency>OneTimeFuture</RunFrequency><RunUntilCancelFlag>False</RunUntilCancelFlag><ScheduledTaskStatus>Active</ScheduledTaskStatus></ScheduledTask><DemandDepositAccount><DDAAccountType>Checking</DDAAccountType><CheckType>Personal</CheckType></DemandDepositAccount><TransactionSetup><TransactionSetupMethod>Null</TransactionSetupMethod><Device>Null</Device><Embedded>False</Embedded><CVVRequired>False</CVVRequired><AutoReturn>False</AutoReturn><DeviceInputCode>NotUsed</DeviceInputCode></TransactionSetup><Terminal><TerminalType>Unknown</TerminalType><CardPresentCode>UseDefault</CardPresentCode><CardholderPresentCode>UseDefault</CardholderPresentCode><CardInputCode>UseDefault</CardInputCode><CVVPresenceCode>UseDefault</CVVPresenceCode><TerminalCapabilityCode>UseDefault</TerminalCapabilityCode><TerminalEnvironmentCode>UseDefault</TerminalEnvironmentCode><MotoECICode>UseDefault</MotoECICode><CVVResponseType>Regular</CVVResponseType><ConsentCode>NotUsed</ConsentCode><TerminalEncryptionFormat>Default</TerminalEncryptionFormat></Terminal><AutoRental><AutoRentalVehicleClassCode>Unused</AutoRentalVehicleClassCode><AutoRentalDistanceUnit>Unused</AutoRentalDistanceUnit><AutoRentalAuditAdjustmentCode>NoAdjustments</AutoRentalAuditAdjustmentCode></AutoRental><Healthcare><HealthcareFlag>False</HealthcareFlag><HealthcareFirstAccountType>NotSpecified</HealthcareFirstAccountType><HealthcareFirstAmountType>LedgerBalance</HealthcareFirstAmountType><HealthcareFirstAmountSign>Positive</HealthcareFirstAmountSign><HealthcareSecondAccountType>NotSpecified</HealthcareSecondAccountType><HealthcareSecondAmountType>LedgerBalance</HealthcareSecondAmountType><HealthcareSecondAmountSign>Positive</HealthcareSecondAmountSign><HealthcareThirdAccountType>NotSpecified</HealthcareThirdAccountType><HealthcareThirdAmountType>LedgerBalance</HealthcareThirdAmountType><HealthcareThirdAmountSign>Positive</HealthcareThirdAmountSign><HealthcareFourthAccountType>NotSpecified</HealthcareFourthAccountType><HealthcareFourthAmountType>LedgerBalance</HealthcareFourthAmountType><HealthcareFourthAmountSign>Positive</HealthcareFourthAmountSign></Healthcare><Lodging><LodgingPrestigiousPropertyCode>NonParticipant</LodgingPrestigiousPropertyCode><LodgingSpecialProgramCode>Default</LodgingSpecialProgramCode><LodgingChargeType>Default</LodgingChargeType></Lodging><BIN /><EnhancedBIN /><Token><TokenProvider>Null</TokenProvider></Token></response></CreditCardAuthorizationCompletionResponse> + XML + end + + def failed_capture_response + <<~XML + <?xml version="1.0" encoding="utf-8"?><CreditCardAuthorizationCompletionResponse xmlns="https://transaction.elementexpress.com"><response><ExpressResponseCode>101</ExpressResponseCode><ExpressResponseMessage>TransactionID required</ExpressResponseMessage><ExpressTransactionTimezone>UTC-05:00</ExpressTransactionTimezone><Credentials /><Batch><BatchCloseType>Regular</BatchCloseType><BatchQueryType>Totals</BatchQueryType><BatchGroupingCode>FullBatch</BatchGroupingCode><BatchIndexCode>Current</BatchIndexCode></Batch><Card><EncryptedFormat>Default</EncryptedFormat></Card><Transaction><ReversalType>System</ReversalType><MarketCode>Default</MarketCode><BillPaymentFlag>False</BillPaymentFlag><DuplicateCheckDisableFlag>False</DuplicateCheckDisableFlag><DuplicateOverrideFlag>False</DuplicateOverrideFlag><RecurringFlag>False</RecurringFlag><PartialApprovedFlag>False</PartialApprovedFlag><EMVEncryptionFormat>Default</EMVEncryptionFormat><ReversalReason>Unknown</ReversalReason></Transaction><PaymentAccount><PaymentAccountType>CreditCard</PaymentAccountType><PASSUpdaterBatchStatus>Null</PASSUpdaterBatchStatus><PASSUpdaterOption>Null</PASSUpdaterOption></PaymentAccount><Address /><ScheduledTask><RunFrequency>OneTimeFuture</RunFrequency><RunUntilCancelFlag>False</RunUntilCancelFlag><ScheduledTaskStatus>Active</ScheduledTaskStatus></ScheduledTask><DemandDepositAccount><DDAAccountType>Checking</DDAAccountType><CheckType>Personal</CheckType></DemandDepositAccount><TransactionSetup><TransactionSetupMethod>Null</TransactionSetupMethod><Device>Null</Device><Embedded>False</Embedded><CVVRequired>False</CVVRequired><AutoReturn>False</AutoReturn><DeviceInputCode>NotUsed</DeviceInputCode></TransactionSetup><Terminal><TerminalType>Unknown</TerminalType><CardPresentCode>UseDefault</CardPresentCode><CardholderPresentCode>UseDefault</CardholderPresentCode><CardInputCode>UseDefault</CardInputCode><CVVPresenceCode>UseDefault</CVVPresenceCode><TerminalCapabilityCode>UseDefault</TerminalCapabilityCode><TerminalEnvironmentCode>UseDefault</TerminalEnvironmentCode><MotoECICode>UseDefault</MotoECICode><CVVResponseType>Regular</CVVResponseType><ConsentCode>NotUsed</ConsentCode><TerminalEncryptionFormat>Default</TerminalEncryptionFormat></Terminal><AutoRental><AutoRentalVehicleClassCode>Unused</AutoRentalVehicleClassCode><AutoRentalDistanceUnit>Unused</AutoRentalDistanceUnit><AutoRentalAuditAdjustmentCode>NoAdjustments</AutoRentalAuditAdjustmentCode></AutoRental><Healthcare><HealthcareFlag>False</HealthcareFlag><HealthcareFirstAccountType>NotSpecified</HealthcareFirstAccountType><HealthcareFirstAmountType>LedgerBalance</HealthcareFirstAmountType><HealthcareFirstAmountSign>Positive</HealthcareFirstAmountSign><HealthcareSecondAccountType>NotSpecified</HealthcareSecondAccountType><HealthcareSecondAmountType>LedgerBalance</HealthcareSecondAmountType><HealthcareSecondAmountSign>Positive</HealthcareSecondAmountSign><HealthcareThirdAccountType>NotSpecified</HealthcareThirdAccountType><HealthcareThirdAmountType>LedgerBalance</HealthcareThirdAmountType><HealthcareThirdAmountSign>Positive</HealthcareThirdAmountSign><HealthcareFourthAccountType>NotSpecified</HealthcareFourthAccountType><HealthcareFourthAmountType>LedgerBalance</HealthcareFourthAmountType><HealthcareFourthAmountSign>Positive</HealthcareFourthAmountSign></Healthcare><Lodging><LodgingPrestigiousPropertyCode>NonParticipant</LodgingPrestigiousPropertyCode><LodgingSpecialProgramCode>Default</LodgingSpecialProgramCode><LodgingChargeType>Default</LodgingChargeType></Lodging><BIN /><EnhancedBIN /><Token><TokenProvider>Null</TokenProvider></Token></response></CreditCardAuthorizationCompletionResponse> + XML + end + + def successful_refund_response + <<~XML + <?xml version="1.0" encoding="utf-8"?><CreditCardReturnResponse xmlns="https://transaction.elementexpress.com"><response><ExpressResponseCode>0</ExpressResponseCode><ExpressResponseMessage>Approved</ExpressResponseMessage><ExpressTransactionDate>20151201</ExpressTransactionDate><ExpressTransactionTime>120437</ExpressTransactionTime><ExpressTransactionTimezone>UTC-05:00</ExpressTransactionTimezone><HostResponseCode>000</HostResponseCode><HostResponseMessage>AP</HostResponseMessage><Credentials /><Batch><BatchCloseType>Regular</BatchCloseType><BatchQueryType>Totals</BatchQueryType><HostBatchID>1</HostBatchID><HostItemID>99</HostItemID><HostBatchAmount>2963.00</HostBatchAmount><BatchGroupingCode>FullBatch</BatchGroupingCode><BatchIndexCode>Current</BatchIndexCode></Batch><Card><EncryptedFormat>Default</EncryptedFormat><CardLogo>Visa</CardLogo></Card><Transaction><TransactionID>2005832540</TransactionID><ApprovalNumber>000004</ApprovalNumber><ReversalType>System</ReversalType><MarketCode>Default</MarketCode><BillPaymentFlag>False</BillPaymentFlag><DuplicateCheckDisableFlag>False</DuplicateCheckDisableFlag><DuplicateOverrideFlag>False</DuplicateOverrideFlag><RecurringFlag>False</RecurringFlag><ProcessorName>NULL_PROCESSOR_TEST</ProcessorName><TransactionStatus>Approved</TransactionStatus><TransactionStatusCode>1</TransactionStatusCode><PartialApprovedFlag>False</PartialApprovedFlag><EMVEncryptionFormat>Default</EMVEncryptionFormat><ReversalReason>Unknown</ReversalReason></Transaction><PaymentAccount><PaymentAccountType>CreditCard</PaymentAccountType><PASSUpdaterBatchStatus>Null</PASSUpdaterBatchStatus><PASSUpdaterOption>Null</PASSUpdaterOption></PaymentAccount><Address><BillingAddress1>456 My Street</BillingAddress1><BillingZipcode>K1C2N6</BillingZipcode></Address><ScheduledTask><RunFrequency>OneTimeFuture</RunFrequency><RunUntilCancelFlag>False</RunUntilCancelFlag><ScheduledTaskStatus>Active</ScheduledTaskStatus></ScheduledTask><DemandDepositAccount><DDAAccountType>Checking</DDAAccountType><CheckType>Personal</CheckType></DemandDepositAccount><TransactionSetup><TransactionSetupMethod>Null</TransactionSetupMethod><Device>Null</Device><Embedded>False</Embedded><CVVRequired>False</CVVRequired><AutoReturn>False</AutoReturn><DeviceInputCode>NotUsed</DeviceInputCode></TransactionSetup><Terminal><TerminalType>Unknown</TerminalType><CardPresentCode>UseDefault</CardPresentCode><CardholderPresentCode>UseDefault</CardholderPresentCode><CardInputCode>UseDefault</CardInputCode><CVVPresenceCode>UseDefault</CVVPresenceCode><TerminalCapabilityCode>UseDefault</TerminalCapabilityCode><TerminalEnvironmentCode>UseDefault</TerminalEnvironmentCode><MotoECICode>UseDefault</MotoECICode><CVVResponseType>Regular</CVVResponseType><ConsentCode>NotUsed</ConsentCode><TerminalEncryptionFormat>Default</TerminalEncryptionFormat></Terminal><AutoRental><AutoRentalVehicleClassCode>Unused</AutoRentalVehicleClassCode><AutoRentalDistanceUnit>Unused</AutoRentalDistanceUnit><AutoRentalAuditAdjustmentCode>NoAdjustments</AutoRentalAuditAdjustmentCode></AutoRental><Healthcare><HealthcareFlag>False</HealthcareFlag><HealthcareFirstAccountType>NotSpecified</HealthcareFirstAccountType><HealthcareFirstAmountType>LedgerBalance</HealthcareFirstAmountType><HealthcareFirstAmountSign>Positive</HealthcareFirstAmountSign><HealthcareSecondAccountType>NotSpecified</HealthcareSecondAccountType><HealthcareSecondAmountType>LedgerBalance</HealthcareSecondAmountType><HealthcareSecondAmountSign>Positive</HealthcareSecondAmountSign><HealthcareThirdAccountType>NotSpecified</HealthcareThirdAccountType><HealthcareThirdAmountType>LedgerBalance</HealthcareThirdAmountType><HealthcareThirdAmountSign>Positive</HealthcareThirdAmountSign><HealthcareFourthAccountType>NotSpecified</HealthcareFourthAccountType><HealthcareFourthAmountType>LedgerBalance</HealthcareFourthAmountType><HealthcareFourthAmountSign>Positive</HealthcareFourthAmountSign></Healthcare><Lodging><LodgingPrestigiousPropertyCode>NonParticipant</LodgingPrestigiousPropertyCode><LodgingSpecialProgramCode>Default</LodgingSpecialProgramCode><LodgingChargeType>Default</LodgingChargeType></Lodging><BIN /><EnhancedBIN /><Token><TokenProvider>Null</TokenProvider></Token></response></CreditCardReturnResponse> + XML + end + + def failed_refund_response + <<~XML + <?xml version="1.0" encoding="utf-8"?><CreditCardReturnResponse xmlns="https://transaction.elementexpress.com"><response><ExpressResponseCode>101</ExpressResponseCode><ExpressResponseMessage>TransactionID required</ExpressResponseMessage><ExpressTransactionTimezone>UTC-05:00</ExpressTransactionTimezone><Credentials /><Batch><BatchCloseType>Regular</BatchCloseType><BatchQueryType>Totals</BatchQueryType><BatchGroupingCode>FullBatch</BatchGroupingCode><BatchIndexCode>Current</BatchIndexCode></Batch><Card><EncryptedFormat>Default</EncryptedFormat></Card><Transaction><ReversalType>System</ReversalType><MarketCode>Default</MarketCode><BillPaymentFlag>False</BillPaymentFlag><DuplicateCheckDisableFlag>False</DuplicateCheckDisableFlag><DuplicateOverrideFlag>False</DuplicateOverrideFlag><RecurringFlag>False</RecurringFlag><PartialApprovedFlag>False</PartialApprovedFlag><EMVEncryptionFormat>Default</EMVEncryptionFormat><ReversalReason>Unknown</ReversalReason></Transaction><PaymentAccount><PaymentAccountType>CreditCard</PaymentAccountType><PASSUpdaterBatchStatus>Null</PASSUpdaterBatchStatus><PASSUpdaterOption>Null</PASSUpdaterOption></PaymentAccount><Address /><ScheduledTask><RunFrequency>OneTimeFuture</RunFrequency><RunUntilCancelFlag>False</RunUntilCancelFlag><ScheduledTaskStatus>Active</ScheduledTaskStatus></ScheduledTask><DemandDepositAccount><DDAAccountType>Checking</DDAAccountType><CheckType>Personal</CheckType></DemandDepositAccount><TransactionSetup><TransactionSetupMethod>Null</TransactionSetupMethod><Device>Null</Device><Embedded>False</Embedded><CVVRequired>False</CVVRequired><AutoReturn>False</AutoReturn><DeviceInputCode>NotUsed</DeviceInputCode></TransactionSetup><Terminal><TerminalType>Unknown</TerminalType><CardPresentCode>UseDefault</CardPresentCode><CardholderPresentCode>UseDefault</CardholderPresentCode><CardInputCode>UseDefault</CardInputCode><CVVPresenceCode>UseDefault</CVVPresenceCode><TerminalCapabilityCode>UseDefault</TerminalCapabilityCode><TerminalEnvironmentCode>UseDefault</TerminalEnvironmentCode><MotoECICode>UseDefault</MotoECICode><CVVResponseType>Regular</CVVResponseType><ConsentCode>NotUsed</ConsentCode><TerminalEncryptionFormat>Default</TerminalEncryptionFormat></Terminal><AutoRental><AutoRentalVehicleClassCode>Unused</AutoRentalVehicleClassCode><AutoRentalDistanceUnit>Unused</AutoRentalDistanceUnit><AutoRentalAuditAdjustmentCode>NoAdjustments</AutoRentalAuditAdjustmentCode></AutoRental><Healthcare><HealthcareFlag>False</HealthcareFlag><HealthcareFirstAccountType>NotSpecified</HealthcareFirstAccountType><HealthcareFirstAmountType>LedgerBalance</HealthcareFirstAmountType><HealthcareFirstAmountSign>Positive</HealthcareFirstAmountSign><HealthcareSecondAccountType>NotSpecified</HealthcareSecondAccountType><HealthcareSecondAmountType>LedgerBalance</HealthcareSecondAmountType><HealthcareSecondAmountSign>Positive</HealthcareSecondAmountSign><HealthcareThirdAccountType>NotSpecified</HealthcareThirdAccountType><HealthcareThirdAmountType>LedgerBalance</HealthcareThirdAmountType><HealthcareThirdAmountSign>Positive</HealthcareThirdAmountSign><HealthcareFourthAccountType>NotSpecified</HealthcareFourthAccountType><HealthcareFourthAmountType>LedgerBalance</HealthcareFourthAmountType><HealthcareFourthAmountSign>Positive</HealthcareFourthAmountSign></Healthcare><Lodging><LodgingPrestigiousPropertyCode>NonParticipant</LodgingPrestigiousPropertyCode><LodgingSpecialProgramCode>Default</LodgingSpecialProgramCode><LodgingChargeType>Default</LodgingChargeType></Lodging><BIN /><EnhancedBIN /><Token><TokenProvider>Null</TokenProvider></Token></response></CreditCardReturnResponse> + XML + end + + def successful_void_response + <<~XML + <?xml version="1.0" encoding="utf-8"?><CreditCardReversalResponse xmlns="https://transaction.elementexpress.com"><response><ExpressResponseCode>0</ExpressResponseCode><ExpressResponseMessage>Success</ExpressResponseMessage><ExpressTransactionDate>20151201</ExpressTransactionDate><ExpressTransactionTime>120516</ExpressTransactionTime><ExpressTransactionTimezone>UTC-05:00</ExpressTransactionTimezone><HostResponseCode>006</HostResponseCode><HostResponseMessage>REVERSED</HostResponseMessage><Credentials /><Batch><BatchCloseType>Regular</BatchCloseType><BatchQueryType>Totals</BatchQueryType><BatchGroupingCode>FullBatch</BatchGroupingCode><BatchIndexCode>Current</BatchIndexCode></Batch><Card><EncryptedFormat>Default</EncryptedFormat><CardLogo>Visa</CardLogo></Card><Transaction><TransactionID>2005832551</TransactionID><ApprovalNumber>000005</ApprovalNumber><ReversalType>System</ReversalType><MarketCode>Default</MarketCode><AcquirerData>aVb001234567810425c0425d5e00</AcquirerData><BillPaymentFlag>False</BillPaymentFlag><DuplicateCheckDisableFlag>False</DuplicateCheckDisableFlag><DuplicateOverrideFlag>False</DuplicateOverrideFlag><RecurringFlag>False</RecurringFlag><ProcessorName>NULL_PROCESSOR_TEST</ProcessorName><TransactionStatus>Success</TransactionStatus><TransactionStatusCode>8</TransactionStatusCode><PartialApprovedFlag>False</PartialApprovedFlag><EMVEncryptionFormat>Default</EMVEncryptionFormat><ReversalReason>Unknown</ReversalReason></Transaction><PaymentAccount><PaymentAccountType>CreditCard</PaymentAccountType><PASSUpdaterBatchStatus>Null</PASSUpdaterBatchStatus><PASSUpdaterOption>Null</PASSUpdaterOption></PaymentAccount><Address /><ScheduledTask><RunFrequency>OneTimeFuture</RunFrequency><RunUntilCancelFlag>False</RunUntilCancelFlag><ScheduledTaskStatus>Active</ScheduledTaskStatus></ScheduledTask><DemandDepositAccount><DDAAccountType>Checking</DDAAccountType><CheckType>Personal</CheckType></DemandDepositAccount><TransactionSetup><TransactionSetupMethod>Null</TransactionSetupMethod><Device>Null</Device><Embedded>False</Embedded><CVVRequired>False</CVVRequired><AutoReturn>False</AutoReturn><DeviceInputCode>NotUsed</DeviceInputCode></TransactionSetup><Terminal><TerminalType>Unknown</TerminalType><CardPresentCode>UseDefault</CardPresentCode><CardholderPresentCode>UseDefault</CardholderPresentCode><CardInputCode>UseDefault</CardInputCode><CVVPresenceCode>UseDefault</CVVPresenceCode><TerminalCapabilityCode>UseDefault</TerminalCapabilityCode><TerminalEnvironmentCode>UseDefault</TerminalEnvironmentCode><MotoECICode>UseDefault</MotoECICode><CVVResponseType>Regular</CVVResponseType><ConsentCode>NotUsed</ConsentCode><TerminalEncryptionFormat>Default</TerminalEncryptionFormat></Terminal><AutoRental><AutoRentalVehicleClassCode>Unused</AutoRentalVehicleClassCode><AutoRentalDistanceUnit>Unused</AutoRentalDistanceUnit><AutoRentalAuditAdjustmentCode>NoAdjustments</AutoRentalAuditAdjustmentCode></AutoRental><Healthcare><HealthcareFlag>False</HealthcareFlag><HealthcareFirstAccountType>NotSpecified</HealthcareFirstAccountType><HealthcareFirstAmountType>LedgerBalance</HealthcareFirstAmountType><HealthcareFirstAmountSign>Positive</HealthcareFirstAmountSign><HealthcareSecondAccountType>NotSpecified</HealthcareSecondAccountType><HealthcareSecondAmountType>LedgerBalance</HealthcareSecondAmountType><HealthcareSecondAmountSign>Positive</HealthcareSecondAmountSign><HealthcareThirdAccountType>NotSpecified</HealthcareThirdAccountType><HealthcareThirdAmountType>LedgerBalance</HealthcareThirdAmountType><HealthcareThirdAmountSign>Positive</HealthcareThirdAmountSign><HealthcareFourthAccountType>NotSpecified</HealthcareFourthAccountType><HealthcareFourthAmountType>LedgerBalance</HealthcareFourthAmountType><HealthcareFourthAmountSign>Positive</HealthcareFourthAmountSign></Healthcare><Lodging><LodgingPrestigiousPropertyCode>NonParticipant</LodgingPrestigiousPropertyCode><LodgingSpecialProgramCode>Default</LodgingSpecialProgramCode><LodgingChargeType>Default</LodgingChargeType></Lodging><BIN /><EnhancedBIN /><Token><TokenProvider>Null</TokenProvider></Token></response></CreditCardReversalResponse> + XML + end + + def failed_void_response + <<~XML + <?xml version="1.0" encoding="utf-8"?><CreditCardReversalResponse xmlns="https://transaction.elementexpress.com"><response><ExpressResponseCode>101</ExpressResponseCode><ExpressResponseMessage>TransactionAmount required</ExpressResponseMessage><ExpressTransactionTimezone>UTC-05:00</ExpressTransactionTimezone><Credentials /><Batch><BatchCloseType>Regular</BatchCloseType><BatchQueryType>Totals</BatchQueryType><BatchGroupingCode>FullBatch</BatchGroupingCode><BatchIndexCode>Current</BatchIndexCode></Batch><Card><EncryptedFormat>Default</EncryptedFormat></Card><Transaction><ReversalType>System</ReversalType><MarketCode>Default</MarketCode><BillPaymentFlag>False</BillPaymentFlag><DuplicateCheckDisableFlag>False</DuplicateCheckDisableFlag><DuplicateOverrideFlag>False</DuplicateOverrideFlag><RecurringFlag>False</RecurringFlag><PartialApprovedFlag>False</PartialApprovedFlag><EMVEncryptionFormat>Default</EMVEncryptionFormat><ReversalReason>Unknown</ReversalReason></Transaction><PaymentAccount><PaymentAccountType>CreditCard</PaymentAccountType><PASSUpdaterBatchStatus>Null</PASSUpdaterBatchStatus><PASSUpdaterOption>Null</PASSUpdaterOption></PaymentAccount><Address /><ScheduledTask><RunFrequency>OneTimeFuture</RunFrequency><RunUntilCancelFlag>False</RunUntilCancelFlag><ScheduledTaskStatus>Active</ScheduledTaskStatus></ScheduledTask><DemandDepositAccount><DDAAccountType>Checking</DDAAccountType><CheckType>Personal</CheckType></DemandDepositAccount><TransactionSetup><TransactionSetupMethod>Null</TransactionSetupMethod><Device>Null</Device><Embedded>False</Embedded><CVVRequired>False</CVVRequired><AutoReturn>False</AutoReturn><DeviceInputCode>NotUsed</DeviceInputCode></TransactionSetup><Terminal><TerminalType>Unknown</TerminalType><CardPresentCode>UseDefault</CardPresentCode><CardholderPresentCode>UseDefault</CardholderPresentCode><CardInputCode>UseDefault</CardInputCode><CVVPresenceCode>UseDefault</CVVPresenceCode><TerminalCapabilityCode>UseDefault</TerminalCapabilityCode><TerminalEnvironmentCode>UseDefault</TerminalEnvironmentCode><MotoECICode>UseDefault</MotoECICode><CVVResponseType>Regular</CVVResponseType><ConsentCode>NotUsed</ConsentCode><TerminalEncryptionFormat>Default</TerminalEncryptionFormat></Terminal><AutoRental><AutoRentalVehicleClassCode>Unused</AutoRentalVehicleClassCode><AutoRentalDistanceUnit>Unused</AutoRentalDistanceUnit><AutoRentalAuditAdjustmentCode>NoAdjustments</AutoRentalAuditAdjustmentCode></AutoRental><Healthcare><HealthcareFlag>False</HealthcareFlag><HealthcareFirstAccountType>NotSpecified</HealthcareFirstAccountType><HealthcareFirstAmountType>LedgerBalance</HealthcareFirstAmountType><HealthcareFirstAmountSign>Positive</HealthcareFirstAmountSign><HealthcareSecondAccountType>NotSpecified</HealthcareSecondAccountType><HealthcareSecondAmountType>LedgerBalance</HealthcareSecondAmountType><HealthcareSecondAmountSign>Positive</HealthcareSecondAmountSign><HealthcareThirdAccountType>NotSpecified</HealthcareThirdAccountType><HealthcareThirdAmountType>LedgerBalance</HealthcareThirdAmountType><HealthcareThirdAmountSign>Positive</HealthcareThirdAmountSign><HealthcareFourthAccountType>NotSpecified</HealthcareFourthAccountType><HealthcareFourthAmountType>LedgerBalance</HealthcareFourthAmountType><HealthcareFourthAmountSign>Positive</HealthcareFourthAmountSign></Healthcare><Lodging><LodgingPrestigiousPropertyCode>NonParticipant</LodgingPrestigiousPropertyCode><LodgingSpecialProgramCode>Default</LodgingSpecialProgramCode><LodgingChargeType>Default</LodgingChargeType></Lodging><BIN /><EnhancedBIN /><Token><TokenProvider>Null</TokenProvider></Token></response></CreditCardReversalResponse> + XML + end + + def successful_verify_response + <<~XML + <?xml version="1.0" encoding="utf-8"?><CreditCardAVSOnlyResponse xmlns="https://transaction.elementexpress.com"><response><ExpressResponseCode>0</ExpressResponseCode><ExpressResponseMessage>Success</ExpressResponseMessage><ExpressTransactionDate>20200505</ExpressTransactionDate><ExpressTransactionTime>094556</ExpressTransactionTime><ExpressTransactionTimezone>UTC-05:00</ExpressTransactionTimezone><HostResponseCode>000</HostResponseCode><HostResponseMessage>AP</HostResponseMessage><Credentials /><Batch><BatchCloseType>Regular</BatchCloseType><BatchQueryType>Totals</BatchQueryType><BatchGroupingCode>FullBatch</BatchGroupingCode><BatchIndexCode>Current</BatchIndexCode></Batch><Card><AVSResponseCode>N</AVSResponseCode><CardLogo>Visa</CardLogo><BIN>400010</BIN></Card><Transaction><TransactionID>48138154</TransactionID><ReferenceNumber>1</ReferenceNumber><ReversalType>System</ReversalType><MarketCode>Default</MarketCode><BillPaymentFlag>False</BillPaymentFlag><DuplicateCheckDisableFlag>False</DuplicateCheckDisableFlag><DuplicateOverrideFlag>False</DuplicateOverrideFlag><RecurringFlag>False</RecurringFlag><ProcessorName>NULL_PROCESSOR_TEST</ProcessorName><TransactionStatus>Success</TransactionStatus><TransactionStatusCode>8</TransactionStatusCode><PartialApprovedFlag>False</PartialApprovedFlag><EMVEncryptionFormat>Default</EMVEncryptionFormat><ReversalReason>Unknown</ReversalReason><PaymentType>NotUsed</PaymentType><SubmissionType>NotUsed</SubmissionType></Transaction><PaymentAccount><PaymentAccountType>CreditCard</PaymentAccountType><PASSUpdaterBatchStatus>Null</PASSUpdaterBatchStatus><PASSUpdaterOption>Null</PASSUpdaterOption></PaymentAccount><Address><BillingAddress1>456 My Street</BillingAddress1><BillingZipcode>K1C2N6</BillingZipcode></Address><ScheduledTask><RunFrequency>OneTimeFuture</RunFrequency><RunUntilCancelFlag>False</RunUntilCancelFlag><ScheduledTaskStatus>Active</ScheduledTaskStatus></ScheduledTask><DemandDepositAccount><CheckType>Personal</CheckType></DemandDepositAccount><TransactionSetup><TransactionSetupMethod>Null</TransactionSetupMethod><Device>Null</Device><Embedded>False</Embedded><CVVRequired>False</CVVRequired><AutoReturn>False</AutoReturn><DeviceInputCode>NotUsed</DeviceInputCode></TransactionSetup><Terminal><TerminalType>Unknown</TerminalType><CardPresentCode>UseDefault</CardPresentCode><CardholderPresentCode>UseDefault</CardholderPresentCode><CardInputCode>UseDefault</CardInputCode><CVVPresenceCode>UseDefault</CVVPresenceCode><TerminalCapabilityCode>UseDefault</TerminalCapabilityCode><TerminalEnvironmentCode>UseDefault</TerminalEnvironmentCode><MotoECICode>UseDefault</MotoECICode><CVVResponseType>Regular</CVVResponseType><ConsentCode>NotUsed</ConsentCode><TerminalEncryptionFormat>Default</TerminalEncryptionFormat></Terminal><AutoRental><AutoRentalVehicleClassCode>Unused</AutoRentalVehicleClassCode><AutoRentalDistanceUnit>Unused</AutoRentalDistanceUnit><AutoRentalAuditAdjustmentCode>NoAdjustments</AutoRentalAuditAdjustmentCode></AutoRental><Healthcare><HealthcareFlag>False</HealthcareFlag><HealthcareFirstAccountType>NotSpecified</HealthcareFirstAccountType><HealthcareFirstAmountType>LedgerBalance</HealthcareFirstAmountType><HealthcareFirstAmountSign>Positive</HealthcareFirstAmountSign><HealthcareSecondAccountType>NotSpecified</HealthcareSecondAccountType><HealthcareSecondAmountType>LedgerBalance</HealthcareSecondAmountType><HealthcareSecondAmountSign>Positive</HealthcareSecondAmountSign><HealthcareThirdAccountType>NotSpecified</HealthcareThirdAccountType><HealthcareThirdAmountType>LedgerBalance</HealthcareThirdAmountType><HealthcareThirdAmountSign>Positive</HealthcareThirdAmountSign><HealthcareFourthAccountType>NotSpecified</HealthcareFourthAccountType><HealthcareFourthAmountType>LedgerBalance</HealthcareFourthAmountType><HealthcareFourthAmountSign>Positive</HealthcareFourthAmountSign></Healthcare><Lodging><LodgingPrestigiousPropertyCode>NonParticipant</LodgingPrestigiousPropertyCode><LodgingSpecialProgramCode>Default</LodgingSpecialProgramCode><LodgingChargeType>Default</LodgingChargeType></Lodging><BIN /><EnhancedBIN /><Token /></response></CreditCardAVSOnlyResponse> + XML + end +end diff --git a/test/unit/gateways/verifi_test.rb b/test/unit/gateways/verifi_test.rb index 028c623a996..f32e8ee6a74 100644 --- a/test/unit/gateways/verifi_test.rb +++ b/test/unit/gateways/verifi_test.rb @@ -5,16 +5,16 @@ class VerifiTest < Test::Unit::TestCase def setup @gateway = VerifiGateway.new( - :login => 'l', - :password => 'p' + login: 'l', + password: 'p' ) @credit_card = credit_card('4111111111111111') @options = { - :order_id => '37', - :email => 'paul@example.com', - :billing_address => address + order_id: '37', + email: 'paul@example.com', + billing_address: address } @amount = 100 @@ -61,15 +61,14 @@ def test_amount_style assert_equal '10.34', @gateway.send(:amount, 1034) assert_raise(ArgumentError) do - @gateway.send(:amount, '10.34') + @gateway.send(:amount, '10.34') end end def test_add_description result = {} - @gateway.send(:add_invoice_data, result, :description => 'My Purchase is great') + @gateway.send(:add_invoice_data, result, description: 'My Purchase is great') assert_equal 'My Purchase is great', result[:orderdescription] - end def test_purchase_meets_minimum_requirements @@ -83,7 +82,6 @@ def test_purchase_meets_minimum_requirements minimum_requirements.each do |key| assert_not_nil(data =~ /#{key}=/) end - end def test_avs_result diff --git a/test/unit/gateways/versa_pay_test.rb b/test/unit/gateways/versa_pay_test.rb new file mode 100644 index 00000000000..686eb40ecce --- /dev/null +++ b/test/unit/gateways/versa_pay_test.rb @@ -0,0 +1,991 @@ +require 'test_helper' + +class VersaPayTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = VersaPayGateway.new(fixtures(:versa_pay)) + @credit_card = credit_card + @amount = 100 + @options = { + email: 'test@gmail.com', + billing_address: address.merge(name: 'Cure Tester') + } + @test_url = @gateway.test_url + end + + def test_required_client_id_and_client_secret + error = assert_raises(ArgumentError) { VersaPayGateway.new } + assert_equal 'Missing required parameter: api_token', error.message + end + + def test_supported_card_types + assert_equal VersaPayGateway.supported_cardtypes, %i[visa master american_express discover] + end + + def test_supported_countries + assert_equal VersaPayGateway.supported_countries, ['US'] + end + + def test_request_headers_building + gateway = VersaPayGateway.new(api_token: 'abc123', api_key: 'def456') + headers = gateway.send :request_headers + + assert_equal 'application/json', headers['Content-Type'] + assert_equal 'Basic YWJjMTIzOmRlZjQ1Ng==', headers['Authorization'] + end + + def test_build_order_request_url + action = 'auth' + assert_match %r{^#{Regexp.escape(@test_url)}/api/gateway/v1/orders/auth$}, @gateway.send(:url, action) + end + + def test_build_store_unstore_request_url + action = 'store' + assert_match %r{^#{Regexp.escape(@test_url)}/api/gateway/v1/wallets$}, @gateway.send(:url, action) + + action = 'unstore' + assert_match %r{^#{Regexp.escape(@test_url)}/api/gateway/v1/wallets/WT/methods/FT$}, @gateway.send(:url, action, { fund_token: 'FT', wallet_token: 'WT' }) + # Test URL for 'wallets' action with missing tokens + assert_match %r{^#{Regexp.escape(@test_url)}/api/gateway/v1/wallets//methods/$}, @gateway.send(:url, action) + end + + def test_authorization_from + response = { 'transaction' => 'token_from_transaction', 'wallet_token' => 'token_from_wallet', 'fund_token' => 'token_from_fund' } + auth = @gateway.send(:authorization_from, response) + assert_equal auth, 'token_from_transaction|token_from_wallet|token_from_fund' + end + + def test_authorization_from_diggin + response = { 'transaction' => 'token_from_transaction', 'wallets' => [{ 'token' => 'token_from_wallets', 'credit_cards' => [{ 'token' => 'token_from_credit_cards' }] }] } + auth = @gateway.send(:authorization_from, response) + assert_equal auth, 'token_from_transaction|token_from_wallets|token_from_credit_cards' + end + + def test_authorization_from_with_no_info_returned + response = {} + auth = @gateway.send(:authorization_from, response) + assert_equal auth, '' + end + + def test_authorization_from_with_one_filed_returned + response = { 'transaction' => 'token_from_transaction' } + auth = @gateway.send(:authorization_from, response) + assert_equal auth, 'token_from_transaction' + end + + def test_authorization_from_with_two_fields_returned + response = { 'wallet_token' => 'token_from_wallet', 'fund_token' => 'token_from_fund' } + auth = @gateway.send(:authorization_from, response) + assert_equal auth, 'token_from_wallet|token_from_fund' + end + + def test_error_code_from_errors + # a HTTP 412 response structure + error = @gateway.send(:error_code_from, { 'success' => false, 'errors' => ['fund_address_unspecified'], 'response_code' => 999 }, 'sale') + assert_equal error, nil + end + + def test_error_code_from_gateway_error_code + error = @gateway.send(:error_code_from, declined_errors, 'sale') + assert_equal error, '567.005' + end + + def test_message_error_from_wallet + message = @gateway.send(:message_from, wallet_error, 'store') + assert_equal message, "error: Validation failed: CVV can't be blank, CVV should be a number, CVV too short (minimum is 3 characters), CVV should be 3 digits" + end + + def test_message_error_from_loggin + message = @gateway.send(:message_from, log_in_error, 'authorize') + assert_equal message, 'error: Please log in or create an account to continue.' + end + + def test_message_from_successful_purchase + message = @gateway.send(:message_from, @gateway.send(:parse, successful_purchase_response), 'sale') + assert_equal message, 'Succeeded' + end + + def test_message_from_failed_transaction_response + message = @gateway.send(:message_from, declined_errors, 'sale') + assert_equal message, 'gateway_error_message: DECLINED | gateway_response_errors: [gateway - DECLINED]' + end + + def test_message_from_failed_transaction_response_412 + message = @gateway.send(:message_from, { 'success' => false, 'errors' => ['fund_address_unspecified'], 'response_code' => 999 }, 'sale') + assert_equal message, 'errors: fund_address_unspecified' + end + + def test_successful_authorize + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_method, endpoint, data, _headers| + assert_match 'auth', endpoint + auth_purchase_credit_assertions(data) + end.respond_with(successful_authorize_response) + end + + def test_successful_purchase + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_method, endpoint, data, _headers| + assert_match 'sale', endpoint + auth_purchase_credit_assertions(data) + end.respond_with(successful_purchase_response) + end + + def test_successful_capture + authorization = 'some_authorize' + stub_comms(@gateway, :ssl_request) do + @gateway.capture(@amount, authorization, @options) + end.check_request do |_method, endpoint, data, _headers| + assert_match 'capture', endpoint + data = JSON.parse(data) + assert_equal @amount, data['amount_cents'] + assert_equal authorization, data['transaction'] + end.respond_with(successful_purchase_response) + end + + def test_parse_valid_json + body = '{"key1": "value1", "key2": "value2"}' + expected = { 'key1' => 'value1', 'key2' => 'value2' }.with_indifferent_access + assert_equal expected, @gateway.send(:parse, body) + end + + def test_parse_invalid_json + body = '{"key1": "value1", "key2": "value2"' + + assert_equal({ 'errors' => '{"key1": "value1", "key2": "value2"', + 'status' => 'Unable to parse JSON response', + 'message' => "859: unexpected token at '{\"key1\": \"value1\", \"key2\": \"value2\"'" }, + @gateway.send(:parse, body)) + end + + def test_dig_avs_code_first_level + first_transaction = { 'avs_response' => 'A', 'cvv_response' => 'M' } + assert_equal 'A', @gateway.send(:dig_avs_code, first_transaction) + assert_equal 'M', @gateway.send(:dig_cvv_code, first_transaction) + end + + def test_dig_avs_code_gateway_response_level + first_transaction = { 'gateway_response' => { 'avs_response' => 'B', 'cvv_response' => 'N' } } + assert_equal 'B', @gateway.send(:dig_avs_code, first_transaction) + assert_equal 'N', @gateway.send(:dig_cvv_code, first_transaction) + end + + def test_dig_avs_code_nested_array + first_transaction = { + 'gateway_response' => { + 'gateway_response' => { + 'response' => { + 'content' => { + 'create' => [ + { 'transaction' => { 'avsresponse' => 'C', 'cvvresponse' => 'P' } }, + { 'transaction' => { 'avsresponse' => 'D', 'cvvresponse' => 'Q' } } + ] + } + } + } + } + } + assert_equal 'C', @gateway.send(:dig_avs_code, first_transaction) + assert_equal 'P', @gateway.send(:dig_cvv_code, first_transaction) + end + + def test_dig_avs_code_not_found + first_transaction = { 'some_other_key' => 'value' } + assert_nil @gateway.send(:dig_avs_code, first_transaction) + assert_nil @gateway.send(:dig_cvv_code, first_transaction) + end + + def test_dig_avs_code_nil_transaction + first_transaction = nil + assert_nil @gateway.send(:dig_avs_code, first_transaction) + assert_nil @gateway.send(:dig_cvv_code, first_transaction) + end + + def test_successful_void + stub_comms(@gateway, :ssl_request) do + @gateway.void('transaction_ID') + end.check_request do |_method, endpoint, data, _headers| + assert_match 'void', endpoint + data = JSON.parse(data) + assert_equal 'transaction_ID', data['transaction'] + end.respond_with('{"success": true}') + end + + def test_successful_refund + stub_comms(@gateway, :ssl_request) do + @gateway.refund(@amount, 'transaction_ID') + end.check_request do |_method, endpoint, data, _headers| + assert_match 'refund', endpoint + data = JSON.parse(data) + assert_equal @amount, data['amount_cents'] + assert_equal 'transaction_ID', data['transaction'] + end.respond_with('{"success": true}') + end + + def test_successful_credit + stub_comms(@gateway, :ssl_request) do + @gateway.credit(@amount, @credit_card, @options) + end.check_request do |_method, endpoint, data, _headers| + assert_match 'credit', endpoint + auth_purchase_credit_assertions(data) + end.respond_with('{"success": true}') + end + + def test_successful_store + stub_comms(@gateway, :ssl_request) do + @gateway.store(@credit_card, @options) + end.check_request do |_method, endpoint, data, _headers| + assert_match 'wallets', endpoint + parsed_data = JSON.parse(data) + credit_card_assertions(parsed_data) + assert_include parsed_data, 'contact' + assert_equal parsed_data['contact']['email'], @options[:email] + credit_card_address_assertions(parsed_data) + end.respond_with('{"success": true}') + end + + def test_successful_purchase_third_party_token + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, 'wallet_token|third_party_token', @options) + end.check_request do |_method, endpoint, data, _headers| + assert_match 'sale', endpoint + parsed_data = JSON.parse(data) + assert_equal parsed_data['fund_token'], 'third_party_token' + billing_address_assertions(parsed_data) + invoice_assertions(parsed_data) + end.respond_with(successful_purchase_response) + end + + def test_successful_unstore + stub_comms(@gateway, :ssl_request) do + @gateway.unstore('wallet_token|fund_token') + end.check_request do |_method, endpoint, data, _headers| + assert_match 'wallets/wallet_token/methods/fund_token', endpoint + assert_equal({}, JSON.parse(data)) + end.respond_with('{"success": true}') + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + private + + def auth_purchase_credit_assertions(data) + parsed_data = JSON.parse(data) + invoice_assertions(parsed_data) + billing_address_assertions(parsed_data) + credit_card_assertions(parsed_data) + credit_card_address_assertions(parsed_data) + end + + def credit_card_assertions(data) + assert_equal @credit_card.name, data['credit_card']['name'] + assert_equal "0#{credit_card.month}", data['credit_card']['expiry_month'] + assert_equal @credit_card.year, data['credit_card']['expiry_year'] + assert_equal @credit_card.number, data['credit_card']['card_number'] + assert_equal @credit_card.verification_value, data['credit_card']['cvv'] + end + + def credit_card_address_assertions(data) + billing_address = @options[:billing_address] + + address = data['credit_card']['address'] + assert_equal billing_address[:address1], address['address_1'] + assert_equal billing_address[:city], address['city'] + assert_equal 'ON', address['province'] + assert_equal billing_address[:zip], address['postal_code'] + assert_equal 'CAN', address['country'] + end + + def billing_address_assertions(data) + billing_address = @options[:billing_address] + assert_equal @options[:email], data['contact']['email'] + assert_equal billing_address[:company], data['order']['billing_name'] + assert_equal billing_address[:address1], data['order']['billing_address'] + assert_equal billing_address[:city], data['order']['billing_city'] + assert_equal 'CAN', data['order']['billing_country'] + assert_equal @options[:email], data['order']['billing_email'] + assert_equal billing_address[:phone], data['order']['billing_telephone'] + end + + def invoice_assertions(data) + assert_equal @amount.to_s, data['order']['amount_cents'] + assert_equal 'USD', data['order']['currency'] + end + + def successful_authorize_response + '{ + "success": true, + "transaction": "3JAKFW7LPS3E", + "authorization": "rnvlqd0o6uh7", + "gateway_token": "1693864", + "order": "ABCDF", + "wallet": "7BMJIY82GASM", + "credit_card": "CC8EE82PIWHW", + "transactions": [ + { + "token": "3JAKFW7LPS3E", + "amount_in_cents": 500, + "message": null, + "link_url": null, + "type": "transaction", + "transaction_type": "request_money", + "email": "spreedlyuat@gmail.com", + "state": "completed", + "transaction_reference": null, + "unique_reference": null, + "from_account": "John Smith", + "to_account": "SpreedlyUAT", + "process_on": null, + "created_by_user": "ZGpuG4yfzg9uJKSGjuqw", + "auto_withdraw": false, + "auto_withdrawal_token": null, + "created_at": "2024-10-01T11:17:54-04:00", + "step_types": [ + "TransactionSteps::CardAuthorizeStep" + ], + "action": "authorize", + "payment_method": "credit_card", + "wallet": "7BMJIY82GASM", + "credit_card": "CC8EE82PIWHW", + "settlement_token": "MA48RC9CU55R", + "currency": "usd", + "approved_amount_cents": 500, + "fee_amount_cents": 0, + "fee_exempt": false, + "gateway_response": { + "token": "rnvlqd0o6uh7", + "gateway_token": "1693864", + "credit_card_bin": "489528", + "credit_card_masked_number": "XXXXXXXXXXXX0006", + "authorization_response": null, + "authorization_code": "630753", + "avs_response": "D", + "approved_amount_cents": 500, + "gateway_response": { + "response": { + "authentication": { + "responsestatus": "success", + "sessionid": null + }, + "content": { + "update": [ + { + "customer": { + "refname": "customer", + "responsestatus": "success", + "id": "10448" + } + }, + { + "contact": { + "refname": "contact", + "responsestatus": "failure", + "errors": { + "error": { + "number": "102.021", + "description": "name is invalid" + } + } + } + }, + { + "contact": { + "refname": "shippingcontact", + "responsestatus": "failure", + "errors": { + "error": { + "number": "102.021", + "description": "name is invalid" + } + } + } + } + ], + "create": [ + { + "contact": { + "refname": "contact", + "responsestatus": "success", + "id": "1804100" + } + }, + { + "contact": { + "refname": "shippingcontact", + "responsestatus": "success", + "id": "1804101" + } + }, + { + "salesdocument": { + "refname": "invoice", + "responsestatus": "success", + "id": "1653055" + } + }, + { + "transaction": { + "refname": "33636d0a-63b1-4f71-a493-46ab68dc0fc1", + "responsestatus": "success", + "authorizationresponse": "APPROVAL", + "authorizationcode": "630753", + "avsresponse": "D", + "hash": "######0006", + "cardtype.name": "Visa", + "accountholder": "John Smith", + "amount": "5.00", + "account.id": "2013", + "token": "7b5a6d65-2408-4ce7-bfd9-f27d0e4d50f4", + "id": "1693864" + } + } + ] + } + } + }, + "credit_card": { + "token": "7b5a6d65-2408-4ce7-bfd9-f27d0e4d50f4" + }, + "gateway_status": "success", + "error_code": null, + "message": "" + }, + "gateway_token": "1693864", + "gateway_authorization_code": "630753", + "gateway_error_scope": "tpro4", + "gateway_error_message": "", + "authorization_response": "100", + "avs_response": "D", + "credit_card_bin": "489528", + "credit_card_masked_number": "XXXXXXXXXXXX0006", + "credit_card_brand": "visa", + "credit_card_expiry": "092025" + } + ], + "response_code": 100 + }' + end + + def successful_purchase_response + '{ + "success": true, + "transaction": "9FWZJY6PYSLC", + "authorization": "d2qzud1t4jfy", + "gateway_token": "1693860", + "order": "ABCDF", + "wallet": "4IV2MFVWC5MZ", + "credit_card": "CC5NDN53P6B1", + "transactions": [ + { + "token": "9FWZJY6PYSLC", + "amount_in_cents": 500, + "message": null, + "link_url": null, + "type": "transaction", + "transaction_type": "request_money", + "email": "spreedlyuat@gmail.com", + "state": "completed", + "transaction_reference": null, + "unique_reference": null, + "from_account": "John Smith", + "to_account": "SpreedlyUAT", + "process_on": null, + "created_by_user": "ZGpuG4yfzg9uJKSGjuqw", + "auto_withdraw": false, + "auto_withdrawal_token": null, + "created_at": "2024-10-01T11:15:29-04:00", + "step_types": [ + "TransactionSteps::CardSaleStep" + ], + "action": "sale", + "payment_method": "credit_card", + "wallet": "4IV2MFVWC5MZ", + "credit_card": "CC5NDN53P6B1", + "settlement_token": "MA48RC9CU55R", + "currency": "usd", + "approved_amount_cents": 500, + "fee_amount_cents": 0, + "fee_exempt": false, + "gateway_response": { + "token": "d2qzud1t4jfy", + "gateway_token": "1693860", + "credit_card_bin": "489528", + "credit_card_masked_number": "XXXXXXXXXXXX0006", + "authorization_response": null, + "authorization_code": "883400", + "avs_response": "D", + "approved_amount_cents": 500, + "gateway_response": { + "response": { + "authentication": { + "responsestatus": "success", + "sessionid": null + }, + "content": { + "update": [ + { + "customer": { + "refname": "customer", + "responsestatus": "success", + "id": "10448" + } + }, + { + "contact": { + "refname": "contact", + "responsestatus": "failure", + "errors": { + "error": { + "number": "102.021", + "description": "name is invalid" + } + } + } + }, + { + "contact": { + "refname": "shippingcontact", + "responsestatus": "failure", + "errors": { + "error": { + "number": "102.021", + "description": "name is invalid" + } + } + } + } + ], + "create": [ + { + "contact": { + "refname": "contact", + "responsestatus": "success", + "id": "1804092" + } + }, + { + "contact": { + "refname": "shippingcontact", + "responsestatus": "success", + "id": "1804093" + } + }, + { + "salesdocument": { + "refname": "invoice", + "responsestatus": "success", + "id": "1653051" + } + }, + { + "transaction": { + "refname": "8c8d7108-e116-4b1b-beab-9866d0505d3e", + "responsestatus": "success", + "authorizationresponse": "APPROVAL", + "authorizationcode": "883400", + "avsresponse": "D", + "hash": "######0006", + "cardtype.name": "Visa", + "accountholder": "John Smith", + "amount": "5.00", + "account.id": "2013", + "token": "42aae6ae-a70e-4659-83c7-b09019d5f687", + "id": "1693860" + } + } + ] + } + } + }, + "credit_card": { + "token": "42aae6ae-a70e-4659-83c7-b09019d5f687" + }, + "gateway_status": "success", + "error_code": null, + "message": "" + }, + "gateway_token": "1693860", + "gateway_authorization_code": "883400", + "gateway_error_scope": "tpro4", + "gateway_error_message": "", + "authorization_response": "100", + "avs_response": "D", + "credit_card_bin": "489528", + "credit_card_masked_number": "XXXXXXXXXXXX0006", + "credit_card_brand": "visa", + "credit_card_expiry": "092025" + } + ], + "response_code": 100 + }' + end + + def successful_verify_response + { + success: true, + transaction: '5WWQLJ95M4UJ', + order: 'ABCDF', + wallet: '7WRP6YFJGNND', + credit_card: 'CC6AEGBDGIVA', + transactions: [ + { + token: '5WWQLJ95M4UJ', + amount_in_cents: 0, + message: null, + link_url: null, + type: 'transaction', + transaction_type: 'request_money', + email: 'spreedlyuat@gmail.com', + state: 'completed', + transaction_reference: null, + unique_reference: null, + from_account: 'John Smith', + to_account: 'SpreedlyUAT', + process_on: null, + created_by_user: 'ZGpuG4yfzg9uJKSGjuqw', + auto_withdraw: false, + auto_withdrawal_token: null, + created_at: '2024-10-02T11:20:59-04:00', + step_types: [ + 'TransactionSteps::CardVerifyStep' + ], + action: 'verify', + payment_method: 'credit_card', + wallet: '7WRP6YFJGNND', + credit_card: 'CC6AEGBDGIVA', + settlement_token: 'MA48RC9CU55R', + currency: 'usd', + approved_amount_cents: 0, + fee_amount_cents: 0, + fee_exempt: false, + gateway_response: { + token: 'qa3qhyo96hci', + gateway_token: '1695157', + credit_card_bin: '489528', + credit_card_masked_number: 'XXXXXXXXXXXX0006', + authorization_response: null, + authorization_code: '789749', + avs_response: 'D', + cvv_response: 'P', + gateway_response: { + response: { + authentication: { + responsestatus: 'success', + sessionid: null + }, + content: { + update: [ + { + customer: { + refname: 'customer', + responsestatus: 'success', + id: '10441' + } + }, + { + contact: { + refname: 'contact', + responsestatus: 'failure', + errors: { + error: { + number: '102.021', + description: 'name is invalid' + } + } + } + }, + { + contact: { + refname: 'shippingcontact', + responsestatus: 'failure', + errors: { + error: { + number: '102.021', + description: 'name is invalid' + } + } + } + } + ], + create: [ + { + contact: { + refname: 'contact', + responsestatus: 'success', + id: '1806296' + } + }, + { + contact: { + refname: 'shippingcontact', + responsestatus: 'success', + id: '1806297' + } + }, + { + salesdocument: { + refname: 'invoice', + responsestatus: 'success', + id: '1654169' + } + }, + { + transaction: { + refname: 'c1ff4389-16e3-40c5-99b1-46a006528a35', + responsestatus: 'success', + authorizationresponse: 'APPROVAL', + authorizationcode: '789749', + cvvresponse: 'P', + avsresponse: 'D', + hash: '######0006', + 'cardtype.name': 'Visa', + accountholder: 'John Smith', + amount: '0.00', + 'account.id': '2013', + token: '9bbb5a74-2df1-489a-8fdd-595fab2dd8b6', + id: '1695157' + } + } + ] + } + } + }, + credit_card: { + token: '9bbb5a74-2df1-489a-8fdd-595fab2dd8b6' + }, + approved_amount_cents: 0, + gateway_status: 'success', + error_code: null, + message: '' + }, + gateway_token: '1695157', + gateway_authorization_code: '789749', + gateway_error_scope: 'tpro4', + gateway_error_message: '', + authorization_response: '100', + avs_response: 'D', + cvv_response: 'P', + credit_card_bin: '489528', + credit_card_masked_number: 'XXXXXXXXXXXX0006', + credit_card_brand: 'visa', + credit_card_expiry: '092025' + } + ], + response_code: 100 + } + end + + def successful_capture_response + '{ + "success": true, + "transaction": "24CBTZLZWRBL", + "authorization": "hh3mv9rf6rq2", + "gateway_token": "1695201", + "order": "ABCDF", + "wallet": "8UTAGL9Q9A3Y", + "credit_card": "CC4GDVXJD8KW", + "transactions": [ + { + "token": "24CBTZLZWRBL", + "amount_in_cents": 500, + "message": null, + "link_url": null, + "type": "transaction", + "transaction_type": "request_money", + "email": "spreedlyuat@gmail.com", + "state": "completed", + "transaction_reference": null, + "unique_reference": null, + "from_account": "Longbob Longsen", + "to_account": "SpreedlyUAT", + "process_on": null, + "created_by_user": "ZGpuG4yfzg9uJKSGjuqw", + "auto_withdraw": false, + "auto_withdrawal_token": null, + "created_at": "2024-10-02T11:53:52-04:00", + "step_types": [ + "TransactionSteps::CardCaptureStep" + ], + "action": "capture", + "payment_method": "credit_card", + "wallet": "8UTAGL9Q9A3Y", + "credit_card": "CC4GDVXJD8KW", + "settlement_token": "MA48RC9CU55R", + "currency": "usd", + "approved_amount_cents": 500, + "fee_amount_cents": 0, + "fee_exempt": false, + "gateway_response": { + "token": "hh3mv9rf6rq2", + "gateway_token": "1695201", + "authorization_response": null, + "authorization_code": "085997", + "avs_response": "D", + "approved_amount_cents": 500, + "gateway_response": { + "response": { + "authentication": { + "responsestatus": "success", + "sessionid": null + }, + "content": { + "create": { + "transaction": { + "refname": "90a02da5-813e-40cd-a1ae-ba7ac1ef6b62", + "responsestatus": "success", + "authorizationresponse": "APPROVAL", + "authorizationcode": "085997", + "avsresponse": "D", + "hash": "######0006", + "cardtype.name": "Visa", + "accountholder": "Longbob Longsen", + "amount": "5.00", + "account.id": "2013", + "id": "1695201" + } + } + } + } + }, + "gateway_status": "success", + "error_code": null, + "message": "" + }, + "gateway_token": "1695201", + "gateway_authorization_code": "085997", + "gateway_error_scope": "tpro4", + "gateway_error_message": "", + "authorization_response": "100", + "avs_response": "D", + "credit_card_brand": "visa", + "credit_card_expiry": "122025" + } + ], + "response_code": 100 + }' + end + + def declined_errors + { 'success' => false, + 'transactions' => + [{ 'state' => 'declined', + 'step_types' => ['TransactionSteps::CardVerifyStep'], + 'action' => 'verify', + 'fee_exempt' => false, + 'gateway_response' => + { 'errors' => { 'gateway' => ['DECLINED'] }, + 'gateway_response' => { 'response' => { 'authentication' => { 'responsestatus' => 'success', 'sessionid' => nil } } }, + 'gateway_status' => 'failure', + 'authorization_response' => '567.005: DECLINED', + 'error_code' => '567.005', + 'message' => 'DECLINED' }, + 'gateway_token' => '1695306', + 'gateway_authorization_response' => '567.005: DECLINED', + 'gateway_error_code' => '567.005', + 'gateway_error_message' => 'DECLINED', + 'authorization_response' => '200', + 'credit_card_bin' => '426428', + 'credit_card_masked_number' => 'XXXXXXXXXXXX4500', + 'credit_card_brand' => 'visa', + 'credit_card_expiry' => '092025' }], + 'response_code' => 999 } + end + + def log_in_error + { 'error' => 'Please log in or create an account to continue.' } + end + + def wallet_error + { 'wallet_token' => nil, + 'fund_token' => nil, + 'wallets' => + { 'error' => + "Validation failed: CVV can't be blank, CVV should be a number, CVV too short (minimum is 3 characters), CVV should be 3 digits" } } + end + + def pre_scrubbed + <<~PRE_SCRUBBED + opening connection to uat.versapay.com:443... + opened + starting SSL for uat.versapay.com:443... + SSL established, protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384 + <- "POST /api/gateway/v1/orders/sale HTTP/1.1\r\nContent-Type: application/json\r\nAuthorization: Basic SOMETHING=\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: uat.versapay.com\r\nContent-Length: 721\r\n\r\n" + <- "{\"amount_cents\":500,\"contact\":{\"email\":\"john.smith@test.com\"},\"order\":{\"identifier\":\"ABCDF\",\"number\":\"25e0293a-d1d2-4ba9-ad5e-b322a4fb2e8c\",\"date\":\"2024-10-02\",\"draft\":false,\"amount_cents\":\"500\",\"currency\":\"USD\",\"billing_name\":\"Widgets Inc\",\"billing_address\":\"456 My Street\",\"billing_address2\":\"Apt 1\",\"billing_city\":\"Ottawa\",\"billing_country\":\"CAN\",\"billing_email\":\"john.smith@test.com\",\"billing_telephone\":\"(555)555-5555\",\"billing_postalcode\":\"K1C2N6\",\"billing_state_province\":\"ON\"},\"credit_card\":{\"name\":\"Longbob Longsen\",\"expiry_month\":\"12\",\"expiry_year\":2025,\"card_number\":\"4895281000000006\",\"cvv\":\"123\",\"address\":{\"address_1\":\"456 My Street\",\"city\":\"Ottawa\",\"province\":\"ON\",\"postal_code\":\"K1C2N6\",\"country\":\"CAN\"}}}" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Wed, 02 Oct 2024 21:55:50 GMT\r\n" + -> "Content-Type: application/json; charset=utf-8\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "set-cookie: AWSALBTG=SOMETHING; Expires=Wed, 09 Oct 2024 21:55:48 GMT; Path=/\r\n" + -> "set-cookie: AWSALBTGCORS=SOMETHING; Expires=Wed, 09 Oct 2024 21:55:48 GMT; Path=/; SameSite=None; Secure\r\n" + -> "x-xss-protection: 1; mode=block\r\n" + -> "x-content-type-options: nosniff\r\n" + -> "x-download-options: noopen\r\n" + -> "x-permitted-cross-domain-policies: none\r\n" + -> "referrer-policy: strict-origin-when-cross-origin\r\n" + -> "etag: W/\"86b028f98d26b815749e0550ac722270\"\r\n" + -> "Cache-Control: max-age=0, private, must-revalidate\r\n" + -> "x-request-id: 6af9640c-78e6-49c8-9d0b-c0d8920dc8e6\r\n" + -> "x-runtime: 2.352706\r\n" + -> "strict-transport-security: max-age=63072000; includeSubDomains\r\n" + -> "access-control-allow-headers: X-Requested-With\r\n" + -> "access-control-allow-methods: GET, HEAD, OPTIONS\r\n" + -> "access-control-allow-origin: https://testquote.teacherslife.com http://testquote.teacherslife.com\r\n" + -> "p3p: CP=\"This_site_does_not_have_a_p3p_policy\"\r\n" + -> "CF-Cache-Status: DYNAMIC\r\n" + -> "Server: cloudflare\r\n" + -> "CF-RAY: 8cc7f053e9186787-ATL\r\n" + -> "Content-Encoding: gzip\r\n" + -> "\r\n" + -> "3f3\r\n" + reading 1011 bytes... + Conn close + PRE_SCRUBBED + end + + def post_scrubbed + <<~POST_SCRUBBED + opening connection to uat.versapay.com:443... + opened + starting SSL for uat.versapay.com:443... + SSL established, protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384 + <- "POST /api/gateway/v1/orders/sale HTTP/1.1\r\nContent-Type: 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\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: uat.versapay.com\r\nContent-Length: 721\r\n\r\n" + <- "{\"amount_cents\":500,\"contact\":{\"email\":\"john.smith@test.com\"},\"order\":{\"identifier\":\"ABCDF\",\"number\":\"25e0293a-d1d2-4ba9-ad5e-b322a4fb2e8c\",\"date\":\"2024-10-02\",\"draft\":false,\"amount_cents\":\"500\",\"currency\":\"USD\",\"billing_name\":\"Widgets Inc\",\"billing_address\":\"456 My Street\",\"billing_address2\":\"Apt 1\",\"billing_city\":\"Ottawa\",\"billing_country\":\"CAN\",\"billing_email\":\"john.smith@test.com\",\"billing_telephone\":\"(555)555-5555\",\"billing_postalcode\":\"K1C2N6\",\"billing_state_province\":\"ON\"},\"credit_card\":{\"name\":\"Longbob Longsen\",\"expiry_month\":\"12\",\"expiry_year\":2025,\"card_number\":\"[FILTERED]",\"cvv\":\"[FILTERED]",\"address\":{\"address_1\":\"456 My Street\",\"city\":\"Ottawa\",\"province\":\"ON\",\"postal_code\":\"K1C2N6\",\"country\":\"CAN\"}}}" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Wed, 02 Oct 2024 21:55:50 GMT\r\n" + -> "Content-Type: application/json; charset=utf-8\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "set-cookie: AWSALBTG=SOMETHING; Expires=Wed, 09 Oct 2024 21:55:48 GMT; Path=/\r\n" + -> "set-cookie: AWSALBTGCORS=SOMETHING; Expires=Wed, 09 Oct 2024 21:55:48 GMT; Path=/; SameSite=None; Secure\r\n" + -> "x-xss-protection: 1; mode=block\r\n" + -> "x-content-type-options: nosniff\r\n" + -> "x-download-options: noopen\r\n" + -> "x-permitted-cross-domain-policies: none\r\n" + -> "referrer-policy: strict-origin-when-cross-origin\r\n" + -> "etag: W/\"86b028f98d26b815749e0550ac722270\"\r\n" + -> "Cache-Control: max-age=0, private, must-revalidate\r\n" + -> "x-request-id: 6af9640c-78e6-49c8-9d0b-c0d8920dc8e6\r\n" + -> "x-runtime: 2.352706\r\n" + -> "strict-transport-security: max-age=63072000; includeSubDomains\r\n" + -> "access-control-allow-headers: X-Requested-With\r\n" + -> "access-control-allow-methods: GET, HEAD, OPTIONS\r\n" + -> "access-control-allow-origin: https://testquote.teacherslife.com http://testquote.teacherslife.com\r\n" + -> "p3p: CP=\"This_site_does_not_have_a_p3p_policy\"\r\n" + -> "CF-Cache-Status: DYNAMIC\r\n" + -> "Server: cloudflare\r\n" + -> "CF-RAY: 8cc7f053e9186787-ATL\r\n" + -> "Content-Encoding: gzip\r\n" + -> "\r\n" + -> "3f3\r\n" + reading 1011 bytes... + Conn close + POST_SCRUBBED + end +end diff --git a/test/unit/gateways/viaklix_test.rb b/test/unit/gateways/viaklix_test.rb index ea24cf3f3e2..1c92a986145 100644 --- a/test/unit/gateways/viaklix_test.rb +++ b/test/unit/gateways/viaklix_test.rb @@ -1,26 +1,25 @@ require 'test_helper' class ViaklixTest < Test::Unit::TestCase - def setup @gateway = ViaklixGateway.new( - :login => 'LOGIN', - :password => 'PIN' + login: 'LOGIN', + password: 'PIN' ) - - @credit_card = credit_card + + @credit_card = credit_card @options = { - :order_id => '37', - :email => 'paul@domain.com', - :description => 'Test Transaction', - :billing_address => address + order_id: '37', + email: 'paul@domain.com', + description: 'Test Transaction', + billing_address: address } @amount = 100 end - - def test_purchase_success + + def test_purchase_success @gateway.expects(:ssl_post).returns(successful_purchase_response) - + assert response = @gateway.purchase(@amount, @credit_card, @options) assert_instance_of Response, response assert_success response @@ -29,50 +28,50 @@ def test_purchase_success def test_purchase_error @gateway.expects(:ssl_post).returns(unsuccessful_purchase_response) - + assert response = @gateway.purchase(@amount, @credit_card, @options) assert_instance_of Response, response assert_failure response end - + def test_invalid_login @gateway.expects(:ssl_post).returns(invalid_login_response) - + assert response = @gateway.purchase(@amount, @credit_card, @options) - + assert_equal '7000', response.params['result'] assert_equal 'The viaKLIX ID and/or User ID supplied in the authorization request is invalid.', response.params['result_message'] assert_failure response end - + def test_avs_result @gateway.expects(:ssl_post).returns(successful_purchase_response) - + response = @gateway.purchase(@amount, @credit_card) assert_equal 'Y', response.avs_result['code'] end - + def test_cvv_result @gateway.expects(:ssl_post).returns(successful_purchase_response) - + response = @gateway.purchase(@amount, @credit_card) assert_equal 'M', response.cvv_result['code'] end - + private - + def successful_purchase_response "ssl_result=0\r\nssl_company=;\r\nssl_city=Herndon\r\nssl_avs_zip=90201\r\nssl_address2=\r\nssl_ship_to_last_name=Jacobs\r\nssl_ship_to_city=Herndon\r\nssl_approval_code=05737D\r\nssl_avs_response=Y\r\nssl_salestax=\r\nssl_ship_to_phone=\r\ncustomer_code=jacobsr1@cox.net\r\nship_to_country=US\r\ncountry=US\r\nssl_txn_id=7E2419F7-2354-4766-BF5C-19C75A1F379A\r\nssl_transaction_type=SALE\r\nssl_invoice_number=#1158.1\r\nssl_amount=243.95\r\nssl_card_number=43*******6820\r\nssl_description=\r\nssl_phone=703-404-9270\r\nssl_ship_to_avs_address=\r\nssl_first_name=Cody\r\nssl_avs_address=12213 Jonathons Glen Way\r\nssl_result_message=APPROVED\r\nssl_exp_date=1109\r\nssl_last_name=Fauser\r\nssl_ship_to_first_name=Robert\r\nssl_ship_to_address2=\r\nssl_ship_to_state=VA\r\nssl_ship_to_avs_zip=\r\nssl_cvv2_response=M\r\nssl_state=VA\r\nssl_email=cody@example.com\r\nssl_ship_to_company=\r\n" end - + def unsuccessful_purchase_response "ssl_result=1\r\nssl_result_message=This transaction request has not been approved. You may elect to use another form of payment to complete this transaction or contact customer service for additional options." end - + def invalid_login_response - <<-RESPONSE -ssl_result=7000\r -ssl_result_message=The viaKLIX ID and/or User ID supplied in the authorization request is invalid.\r + <<~RESPONSE + ssl_result=7000\r + ssl_result_message=The viaKLIX ID and/or User ID supplied in the authorization request is invalid.\r RESPONSE end -end \ No newline at end of file +end diff --git a/test/unit/gateways/visanet_peru_test.rb b/test/unit/gateways/visanet_peru_test.rb index 55d77cb4427..fdbfc995b4a 100644 --- a/test/unit/gateways/visanet_peru_test.rb +++ b/test/unit/gateways/visanet_peru_test.rb @@ -1,6 +1,9 @@ require 'test_helper' +require 'timecop' class VisanetPeruTest < Test::Unit::TestCase + include CommStub + def setup @gateway = VisanetPeruGateway.new(fixtures(:visanet_peru)) @@ -16,20 +19,21 @@ def setup end def test_successful_purchase - @gateway.expects(:ssl_request).returns(successful_authorize_response) - @gateway.expects(:ssl_request).returns(successful_capture_response) - + @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 @options[:order_id], response.params['externalTransactionId'] + assert_equal 'de9dc65c094fb4f1defddc562731af81', response.params['externalTransactionId'] assert response.test? end def test_failed_purchase - @gateway.expects(:ssl_request).returns(failed_authorize_response_bad_card) + @gateway.expects(:ssl_request).with(:post, any_parameters).returns(failed_authorize_response_bad_card) response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response @@ -37,6 +41,41 @@ def test_failed_purchase assert_equal 'Operacion Denegada.', response.message end + def test_nonconsecutive_purchase_numbers + 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 @gateway.expects(:ssl_request).returns(successful_authorize_response) response = @gateway.authorize(@amount, @credit_card, @options) @@ -60,25 +99,25 @@ def test_failed_authorize response = @gateway.authorize(@amount, @credit_card, @options) assert_failure response assert_equal 400, response.error_code - assert_equal 'REJECT', response.message + assert_equal 'REJECT | Operacion denegada', response.message end def test_successful_capture - @gateway.expects(:ssl_request).returns(successful_authorize_response) - @gateway.expects(:ssl_request).returns(successful_capture_response) + @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.authorize(@amount, @credit_card, @options) - capture = @gateway.capture(response.authorization, @options) + capture = @gateway.capture(@amount, response.authorization, @options) assert_success capture assert_equal 'OK', capture.message assert_match %r(^[0-9]{9}|$), capture.authorization - assert_equal @options[:order_id], capture.params['externalTransactionId'] + assert_equal 'de9dc65c094fb4f1defddc562731af81', capture.params['externalTransactionId'] assert capture.test? end def test_failed_capture @gateway.expects(:ssl_request).returns(failed_capture_response) invalid_purchase_number = '900000044' - response = @gateway.capture(invalid_purchase_number) + response = @gateway.capture(@amount, invalid_purchase_number) assert_failure response assert_equal '[ "NUMORDEN 900000044 no se encuentra registrado", "No se realizo el deposito" ]', response.message assert_equal 400, response.error_code @@ -90,20 +129,41 @@ def test_successful_refund response = @gateway.purchase(@amount, @credit_card, @options) assert_success response - @gateway.expects(:ssl_request).returns(successful_refund_response) + @gateway.expects(:ssl_request).with(:put, any_parameters).returns(successful_refund_response) refund = @gateway.refund(@amount, response.authorization) assert_success refund assert_equal 'OK', refund.message end def test_failed_refund - @gateway.expects(:ssl_request).returns(failed_refund_response) + @gateway.expects(:ssl_request).with(:put, any_parameters).returns(failed_refund_response) response = @gateway.refund(@amount, '122333444') assert_failure response assert_match(/No se realizo la anulacion del deposito/, response.message) assert_equal 400, response.error_code end + def test_failed_full_refund_when_unsettled + @gateway.expects(:ssl_request).with(:put, any_parameters).returns(failed_refund_response) + @gateway.expects(:ssl_request).with(:post, any_parameters).returns(failed_refund_with_action_code_response) + response = @gateway.refund(@amount, '122333444|444333221', force_full_refund_if_unsettled: true) + assert_failure response + assert_equal("Operacion Denegada. | [ 'NUMORDEN 122333444 no se encuentra registrado', 'No se realizo la anulacion del deposito' ]", response.message) + assert_equal 400, response.error_code + end + + def test_failed_full_refund_when_unsettled_additional_message_concatenation + @gateway.expects(:ssl_request).with(:put, any_parameters).returns(failed_refund_with_message_and_action_code_response) + @gateway.expects(:ssl_request).with(:post, any_parameters).returns(failed_refund_with_message_and_action_code_response_2) + first_msg = 'No se realizo la anulacion del deposito' + first_dsc = 'Operacion Denegada.' + second_msg = 'Mal funcionamiento de la inteligencia artificial' + second_dsc = 'Lo siento Dave, me temo que no puedo hacer eso.' + + response = @gateway.refund(@amount, '122333444|444333221', force_full_refund_if_unsettled: true) + assert_equal("#{second_msg} | #{second_dsc} | #{first_msg} | #{first_dsc}", response.message) + end + def test_successful_void @gateway.expects(:ssl_request).returns(successful_authorize_response) response = @gateway.authorize(@amount, @credit_card, @options) @@ -299,7 +359,7 @@ def failed_authorize_response_bad_email end def successful_capture_response - '{"errorCode":0,"errorMessage":"OK","transactionUUID":"8517cf68-4820-4224-959b-01c8117385e0","externalTransactionId":"de9dc65c094fb4f1defddc562731af81","transactionDateTime":1519937673906,"transactionDuration":0,"merchantId":"543025501","userTokenId":null,"aliasName":null,"data":{"FECHAYHORA_TX":null,"DSC_ECI":null,"DSC_COD_ACCION":null,"NOM_EMISOR":null,"ESTADO":"Depositado","RESPUESTA":"1","ID_UNICO":null,"NUMORDEN":null,"CODACCION":null,"ETICKET":null,"IMP_AUTORIZADO":null,"DECISIONCS":null,"COD_AUTORIZA":null,"CODTIENDA":"543025501","PAN":null,"ORI_TARJETA":null}}' + '{"errorCode":0,"errorMessage":"OK","transactionUUID":"8517cf68-4820-4224-959b-01c8117385e0","externalTransactionId":"de9dc65c094fb4f1defddc562731af81","transactionDateTime":1519937673906,"transactionDuration":0,"merchantId":"543025501","userTokenId":null,"aliasName":null,"data":{"FECHAYHORA_TX":null,"DSC_ECI":null,"DSC_COD_ACCION":null,"NOM_EMISOR":null,"ESTADO":"Depositado","RESPUESTA":"1","ID_UNICO":null,"NUMORDEN":null,"CODACCION":null,"ETICKET":null,"IMP_AUTORIZADO":null,"DECISIONCS":null,"COD_AUTORIZA":null,"CODTIENDA":"543025501","PAN":null,"ORI_TARJETA":null}}' end def failed_capture_response @@ -446,4 +506,55 @@ def failed_refund_response } RESPONSE end + + def failed_refund_with_action_code_response + <<-RESPONSE + { + "errorCode": 400, + "errorMessage": "[ ]", + "data": { + "ESTADO": "", + "RESPUESTA": "2", + "DSC_COD_ACCION": "Operacion Denegada." + }, + "transactionLog": { + + } + } + RESPONSE + end + + def failed_refund_with_message_and_action_code_response + <<-RESPONSE + { + "errorCode": 400, + "errorMessage": "No se realizo la anulacion del deposito", + "data": { + "ESTADO": "", + "RESPUESTA": "2", + "DSC_COD_ACCION": "Operacion Denegada." + }, + "transactionLog": { + + } + } + RESPONSE + end + + def failed_refund_with_message_and_action_code_response_2 + <<-RESPONSE + { + "errorCode": 400, + "errorMessage": "Mal funcionamiento de la inteligencia artificial", + "data": { + "ESTADO": "", + "RESPUESTA": "2", + "DSC_COD_ACCION": "Lo siento Dave, me temo que no puedo hacer eso." + }, + "transactionLog": { + + } + } + RESPONSE + end end diff --git a/test/unit/gateways/vpos_test.rb b/test/unit/gateways/vpos_test.rb new file mode 100644 index 00000000000..6bf4cfd66d7 --- /dev/null +++ b/test/unit/gateways/vpos_test.rb @@ -0,0 +1,227 @@ +require 'test_helper' + +module ActiveMerchant # :nodoc: + module Billing # :nodoc: + class VposGateway + def one_time_public_key + OpenSSL::PKey::RSA.new(2048) + end + end + end +end + +class VposTest < Test::Unit::TestCase + def setup + @gateway = VposGateway.new(public_key: 'some_key', private_key: 'some_other_key', encryption_key: OpenSSL::PKey::RSA.new(512)) + @credit_card = credit_card + @amount = 10000 + + @options = { + commerce: '123', + commerce_branch: '45' + } + 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 '701175#233024225526089', 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 + assert_equal '57', response.error_code + end + + def test_successful_credit + @gateway.expects(:ssl_post).returns(successful_credit_response) + + response = @gateway.credit(@amount, @credit_card, @options) + assert_success response + + assert_equal '707860#948868617271843', response.authorization + assert response.test? + end + + def test_failed_credit + @gateway.expects(:ssl_post).returns(failed_credit_response) + + response = @gateway.credit(@amount, @credit_card, @options) + assert_failure response + assert_equal 'RefundsServiceError:TIPO DE TRANSACCION NO PERMITIDA PARA TARJETAS EXTRANJERAS', response.message + end + + def test_successful_void + @gateway.expects(:ssl_post).returns(successful_void_response) + + response = @gateway.void('123#456') + assert_success response + assert_equal 'RollbackSuccessful:Transacción Aprobada', response.message + end + + def test_failed_void + @gateway.expects(:ssl_post).returns(failed_void_response) + + response = @gateway.void(nil) + assert_failure response + assert_equal 'AlreadyRollbackedError:The payment has already been rollbacked.', response.message + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + private + + def pre_scrubbed + <<~TRANSCRIPT + opening connection to vpos.infonet.com.py:8888... + opened + starting SSL for vpos.infonet.com.py:8888... + SSL established + <- "POST /vpos/api/0.3/application/encryption-key HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: vpos.infonet.com.py:8888\r\nContent-Length: 106\r\n\r\n" + <- "{\"public_key\":\"joszzJzNMkn6SKn0a5P9GcMw1HqZjC1u\",\"operation\":{\"token\":\"683137179e606c700805e7773751b705\"}}" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: nginx/1.18.0\r\n" + -> "Date: Tue, 06 Apr 2021 01:56:12 GMT\r\n" + -> "Content-Type: application/json;charset=utf-8\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "Vary: Accept-Encoding\r\n" + -> "Vary: Accept-Encoding\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "Content-Encoding: gzip\r\n" + -> "\r\n" + -> "1a6\r\n" + reading 422 bytes... + -> "\x1F\x8B\b\x00\x00\x00\x00\x00\x00\x03e\x91K\x93\xA20\x00\x84\xFF\xCA\x14\xD7\xDD-QWe\xF6\x16H\x80\x00Fq\xE4!E\xD5\x96\x13\x94wT\x12\x84\xB8\xB5\xFF}u\xAE\xDB\xA7\xAE\xFE.\xDD\xD5\x7F\x14.\x8E\xA2\xE7\xCA/\x85\xF7\x94\x9E8W\xBE+'F;y\x15\xE5\x85\xFD\xAEO\xF2\x89~\xBC\xA4#\v\x93\xB7m\xA0{\xD8xs\xD1\xE1+L\xD9\x1Ac\x1DW\x80\xE8y}+\xEA\xD2z\x1FT\x1D\xF8\xC8\x04`c\x00_\x03/n\xE4\xEE\xD3#p\x93\xC0@\x0E0>\xAF\xD1\x81Q\xF3>\xEF!N\x99\xEF\xCC\xBD\x82\xC7\x17\x1Fy\xB3f\x9C\xAD\xC3\xE8\f\x9Dlb\xD9\xB1D\xC70\xB43>\x8Do\xCB\x0E\x0FZ\xC1\xEF\xE3\xC2\xCC\xB22\xDA\x98\xCD6\xA2\xF7,&E\x9B2\x14s\xAF<4\x1F\x89\xD7\r\x8E1U\x17\xC2\xDA\x17A\xEE\xC8\xED#\xB0\xF1\xA2\xCF\xF5\x1C\xCC\x06]\xEC\xF2\xE1!+\xCA\xC7\x99]H\xEA\xD5$\x88o\xFD\x1E\xE8)\xCB\xC7\xB2\xDA8\xB2\x8E\xC6D}\x9C\xE2\x05\fa?\xC9\xD5\xB3OiI|UD\xA1\xF9y\xF5\x7F\x0E\xE2\x83\x11\xDET\x8E;q\xC2\xB6Y\xD5\xF1\x9Dm\xC3GC\x9E\x1D\x8E\x9Ef\x10\x86\xA5\xBA\x81\xAA\xD7;\xA5m\xA06\x0F`\xAD\xAE\x9A\x8B\x95\x8834\xE5\xE84\xFB\xD5\xD0\xEF\xE2\x15JB\xAER\xB3n\xC66\xC4\xA2\x1A\x10O\xD9\xB7\xED5\x98$r\x9C/\xF4\xDD\xB5\x8B\x96\xD2>\xC4\x19;,\xB9\x06\x89h]w\xDA\xE5;\xB8\nGK\x9C\x93~\xDDV\x13\x88\x9DNK\x102\x1F\xCDL\xA3n\xCA\xC8\x80!\xF0_{\xBE\xCEA\x04\xFE\xFF\x97\xF2\xF7\x1Fe\xDC\xA8\x0E\xF4\x01\x00\x00" + read 422 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + opening connection to vpos.infonet.com.py:8888... + opened + starting SSL for vpos.infonet.com.py:8888... + SSL established + <- "POST /vpos/api/0.3/pci/encrypted HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: vpos.infonet.com.py:8888\r\nContent-Length: 850\r\n\r\n" + <- "{\"public_key\":\"joszzJzNMkn6SKn0a5P9GcMw1HqZjC1u\",\"operation\":{\"token\":\"e0ca140e32edb506b20f47260b97b1ad\",\"commerce\":232,\"commerce_branch\":77,\"shop_process_id\":552384714818596,\"number_of_payments\":1,\"recursive\":false,\"amount\":\"1000.00\",\"currency\":\"PYG\",\"card_encrypted_data\":\"eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.bWLbgRHAl7GmGveBFEoi64bX472TQao5lCausaMSB2LsES8StjWxPbAZpfrFZDcWksnD2WfDbajSX11WsJJApohjp5fawPP30QcDjmSG-I9WXVnW_Qm-mcrejc82Km8A76-pr9aZd_od81QfQCYwOzpA6V_fz1zY_s8oWBBoudBThDQ__fhazJS5UXM8qMWtooUEmsiiGNDlv-0QTvWAQ-ShhZSDeMRQW6E6p8Jo-1rAlaPEpY2a9yUwT1Emq8eqWz6Fb3w6LA2fUCA1-aXwzfm1vs-LQ2ISgEugMU19gYqhl6qKLNXOJs0KkJCCuKutlHC9zbDPoKU8oO0cDSOfNg.6xi5G9fBauLK2c6p.1pF9qw6fMJyfbNU8y0Hi_x4WNH8GZASuZS6tNpfhnJjhUmdHHcEBV-WGF5FoKw.r4cVO2MlpKe229paSt2D1Q\",\"card_month_expiration\":\"08\",\"card_year_expiration\":\"21\",\"additional_data\":null}}" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: nginx/1.18.0\r\n" + -> "Date: Tue, 06 Apr 2021 01:56:21 GMT\r\n" + -> "Content-Type: application/json;charset=utf-8\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "Vary: Accept-Encoding\r\n" + -> "Vary: Accept-Encoding\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "Content-Encoding: gzip\r\n" + -> "\r\n" + -> "147\r\n" + reading 327 bytes... + -> "\x1F\x8B\b\x00\x00\x00\x00\x00\x00\x03U\x91\xCBn\xC3 \x10E\x7F%b\x1DY\x80\x9F\xF1\xCE\xABn\xBA\xA8\x9Av\xD1\x15\xC20VPb\xB0xTI\xA3\xFC{\aWU\xDD\x1D\xC3p\xE7\x9E\xB9\xDCI\x882\xA6@z\x12\x92R\x10\x02\xD9\x13\xE5\xECd\xFC,\xA3q\x96\xF4w\x12\xDD\x19\xF0@\xDAV\xB7S5\xF2\t\xA4\xEE\xBAj*\x99\xAC&\xD6\x8CeS\xD2j\x1C\x19J\xC3\xC9-b\xF1.O\x12F\x93\xBE\xAEy\xD9U-\xAB:\xD6\xD5\x87fO<\x84\xC5\xD9\x008\xEF\x88\x82\xDFRh\x88\xD2\\2\xC8\xCB*\x97\xDA\xED\x8E\x88\x10&\xA9\xA2\xF3F\xCE`#\xA0B\xA6x\xC2\xFAk\xC5\x136\xCD#\xF8\fG9\xAD\e\xECG\xA3\xCE\x10\xFF\x1A\x9C\xB1\xF6\xD0\xF0\x86\xF1\xAD\x9Dr:#P\xFA\x9F!(o\x96\x9F\xBD\xC9\x9B\x976H\xA5\xD0f'q\xA7Qj\x89\xAF\xE1\x1A\xC1j\xD0b\x83\xBE\x91\xD9t\xB9`\x0E\xA0\x927\xF1&\x8C\x9D\xDC&J%\xBD\x16\xC1%\xAF\xB2\xFB3\x8ES)D7\x83\x17f\xC1\eF\xBB\x82\xD7e\xC1yS\xF02'\xBA*\x94K6\xFA[\x0Egx\x1D\x9E\xDE\x87\x0F\xEC|\x82\x0F\xEB\x0F\x11Z\x94y\r\x13\xCE\xE8\xA7\xE1Jz\xFAx<\xBE\x01eg ^\xDC\x01\x00\x00" + read 327 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + TRANSCRIPT + end + + def post_scrubbed + transcript = <<~TRANSCRIPT + opening connection to vpos.infonet.com.py:8888... + opened + starting SSL for vpos.infonet.com.py:8888... + SSL established + <- "POST /vpos/api/0.3/application/encryption-key HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: vpos.infonet.com.py:8888\r\nContent-Length: 106\r\n\r\n" + <- "{\"public_key\":\"joszzJzNMkn6SKn0a5P9GcMw1HqZjC1u\",\"operation\":{\"token\":\"683137179e606c700805e7773751b705\"}}" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: nginx/1.18.0\r\n" + -> "Date: Tue, 06 Apr 2021 01:56:12 GMT\r\n" + -> "Content-Type: application/json;charset=utf-8\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "Vary: Accept-Encoding\r\n" + -> "Vary: Accept-Encoding\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "Content-Encoding: gzip\r\n" + -> "\r\n" + -> "1a6\r\n" + reading 422 bytes... + -> "\x1F\x8B\b\x00\x00\x00\x00\x00\x00\x03e\x91K\x93\xA20\x00\x84\xFF\xCA\x14\xD7\xDD-QWe\xF6\x16H\x80\x00Fq\xE4!E\xD5\x96\x13\x94wT\x12\x84\xB8\xB5\xFF}u\xAE\xDB\xA7\xAE\xFE.\xDD\xD5\x7F\x14.\x8E\xA2\xE7\xCA/\x85\xF7\x94\x9E8W\xBE+'F;y\x15\xE5\x85\xFD\xAEO\xF2\x89~\xBC\xA4#\v\x93\xB7m\xA0{\xD8xs\xD1\xE1+L\xD9\x1Ac\x1DW\x80\xE8y}+\xEA\xD2z\x1FT\x1D\xF8\xC8\x04`c\x00_\x03/n\xE4\xEE\xD3#p\x93\xC0@\x0E0>\xAF\xD1\x81Q\xF3>\xEF!N\x99\xEF\xCC\xBD\x82\xC7\x17\x1Fy\xB3f\x9C\xAD\xC3\xE8\f\x9Dlb\xD9\xB1D\xC70\xB43>\x8Do\xCB\x0E\x0FZ\xC1\xEF\xE3\xC2\xCC\xB22\xDA\x98\xCD6\xA2\xF7,&E\x9B2\x14s\xAF<4\x1F\x89\xD7\r\x8E1U\x17\xC2\xDA\x17A\xEE\xC8\xED#\xB0\xF1\xA2\xCF\xF5\x1C\xCC\x06]\xEC\xF2\xE1!+\xCA\xC7\x99]H\xEA\xD5$\x88o\xFD\x1E\xE8)\xCB\xC7\xB2\xDA8\xB2\x8E\xC6D}\x9C\xE2\x05\fa?\xC9\xD5\xB3OiI|UD\xA1\xF9y\xF5\x7F\x0E\xE2\x83\x11\xDET\x8E;q\xC2\xB6Y\xD5\xF1\x9Dm\xC3GC\x9E\x1D\x8E\x9Ef\x10\x86\xA5\xBA\x81\xAA\xD7;\xA5m\xA06\x0F`\xAD\xAE\x9A\x8B\x95\x8834\xE5\xE84\xFB\xD5\xD0\xEF\xE2\x15JB\xAER\xB3n\xC66\xC4\xA2\x1A\x10O\xD9\xB7\xED5\x98$r\x9C/\xF4\xDD\xB5\x8B\x96\xD2>\xC4\x19;,\xB9\x06\x89h]w\xDA\xE5;\xB8\nGK\x9C\x93~\xDDV\x13\x88\x9DNK\x102\x1F\xCDL\xA3n\xCA\xC8\x80!\xF0_{\xBE\xCEA\x04\xFE\xFF\x97\xF2\xF7\x1Fe\xDC\xA8\x0E\xF4\x01\x00\x00" + read 422 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + opening connection to vpos.infonet.com.py:8888... + opened + starting SSL for vpos.infonet.com.py:8888... + SSL established + <- "POST /vpos/api/0.3/pci/encrypted HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: vpos.infonet.com.py:8888\r\nContent-Length: 850\r\n\r\n" + <- "{\"public_key\":\"joszzJzNMkn6SKn0a5P9GcMw1HqZjC1u\",\"operation\":{\"token\":\"e0ca140e32edb506b20f47260b97b1ad\",\"commerce\":232,\"commerce_branch\":77,\"shop_process_id\":552384714818596,\"number_of_payments\":1,\"recursive\":false,\"amount\":\"1000.00\",\"currency\":\"PYG\",\"card_encrypted_data\":\"eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.bWLbgRHAl7GmGveBFEoi64bX472TQao5lCausaMSB2LsES8StjWxPbAZpfrFZDcWksnD2WfDbajSX11WsJJApohjp5fawPP30QcDjmSG-I9WXVnW_Qm-mcrejc82Km8A76-pr9aZd_od81QfQCYwOzpA6V_fz1zY_s8oWBBoudBThDQ__fhazJS5UXM8qMWtooUEmsiiGNDlv-0QTvWAQ-ShhZSDeMRQW6E6p8Jo-1rAlaPEpY2a9yUwT1Emq8eqWz6Fb3w6LA2fUCA1-aXwzfm1vs-LQ2ISgEugMU19gYqhl6qKLNXOJs0KkJCCuKutlHC9zbDPoKU8oO0cDSOfNg.6xi5G9fBauLK2c6p.1pF9qw6fMJyfbNU8y0Hi_x4WNH8GZASuZS6tNpfhnJjhUmdHHcEBV-WGF5FoKw.r4cVO2MlpKe229paSt2D1Q\",\"card_month_expiration\":\"08\",\"card_year_expiration\":\"21\",\"additional_data\":null}}" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: nginx/1.18.0\r\n" + -> "Date: Tue, 06 Apr 2021 01:56:21 GMT\r\n" + -> "Content-Type: application/json;charset=utf-8\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "Vary: Accept-Encoding\r\n" + -> "Vary: Accept-Encoding\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "Content-Encoding: gzip\r\n" + -> "\r\n" + -> "147\r\n" + reading 327 bytes... + -> "\x1F\x8B\b\x00\x00\x00\x00\x00\x00\x03U\x91\xCBn\xC3 \x10E\x7F%b\x1DY\x80\x9F\xF1\xCE\xABn\xBA\xA8\x9Av\xD1\x15\xC20VPb\xB0xTI\xA3\xFC{\aWU\xDD\x1D\xC3p\xE7\x9E\xB9\xDCI\x882\xA6@z\x12\x92R\x10\x02\xD9\x13\xE5\xECd\xFC,\xA3q\x96\xF4w\x12\xDD\x19\xF0@\xDAV\xB7S5\xF2\t\xA4\xEE\xBAj*\x99\xAC&\xD6\x8CeS\xD2j\x1C\x19J\xC3\xC9-b\xF1.O\x12F\x93\xBE\xAEy\xD9U-\xAB:\xD6\xD5\x87fO<\x84\xC5\xD9\x008\xEF\x88\x82\xDFRh\x88\xD2\\2\xC8\xCB*\x97\xDA\xED\x8E\x88\x10&\xA9\xA2\xF3F\xCE`#\xA0B\xA6x\xC2\xFAk\xC5\x136\xCD#\xF8\fG9\xAD\e\xECG\xA3\xCE\x10\xFF\x1A\x9C\xB1\xF6\xD0\xF0\x86\xF1\xAD\x9Dr:#P\xFA\x9F!(o\x96\x9F\xBD\xC9\x9B\x976H\xA5\xD0f'q\xA7Qj\x89\xAF\xE1\x1A\xC1j\xD0b\x83\xBE\x91\xD9t\xB9`\x0E\xA0\x927\xF1&\x8C\x9D\xDC&J%\xBD\x16\xC1%\xAF\xB2\xFB3\x8ES)D7\x83\x17f\xC1\eF\xBB\x82\xD7e\xC1yS\xF02'\xBA*\x94K6\xFA[\x0Egx\x1D\x9E\xDE\x87\x0F\xEC|\x82\x0F\xEB\x0F\x11Z\x94y\r\x13\xCE\xE8\xA7\xE1Jz\xFAx<\xBE\x01eg ^\xDC\x01\x00\x00" + read 327 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + TRANSCRIPT + @gateway.remove_invalid_utf_8_byte_sequences(transcript) + end + + def successful_purchase_response + %({"status":"success","confirmation":{"token":"2e2f60bd985018defc145a2f5dc0060e","shop_process_id":233024225526089,"response":"S","response_details":"Procesado Satisfactoriamente","authorization_number":"701175","ticket_number":"2117959993","response_code":"00","response_description":"Transaccion aprobada","extended_response_description":null,"security_information":{"card_source":"L","customer_ip":"108.253.226.231","card_country":"PARAGUAY","version":"0.3","risk_index":0}}} + ) + end + + def failed_purchase_response + %({"status":"success","confirmation":{"token":"d08dd5bd604f4c4ba1049195b9e015e2","shop_process_id":845868143743681,"response":"N","response_details":"Procesado Satisfactoriamente","authorization_number":null,"ticket_number":"2117962608","response_code":"57","response_description":"Transaccion denegada","extended_response_description":"IMPORTE DE LA TRN INFERIOR AL M\u00bfNIMO PERMITIDO","security_information":{"card_source":"I","customer_ip":"108.253.226.231","card_country":"UNITED STATES","version":"0.3","risk_index":0}}}) + end + + def successful_credit_response + %({"status":"success","refund":{"status":4,"request_token":"74845bf692d3ff78ce5d5c7d0d8ecdfa","shop_process_id":948868617271843,"origin_shop_process_id":null,"amount":"1000.0","currency":"PYG","commerce":232,"commerce_branch":77,"ticket_number":2117984322,"authorization_code":"707860","response_code":"00","response_description":"Transaccion aprobada","extended_response":null}}) + end + + def failed_credit_response + %({"status":"error","messages":[{"level":"error","key":"RefundsServiceError","dsc":"TIPO DE TRANSACCION NO PERMITIDA PARA TARJETAS EXTRANJERAS"}]}) + end + + def successful_void_response + %({"status":"success","messages":[{"dsc":"Transacción Aprobada","key":"RollbackSuccessful","level":"info"}]}) + end + + def failed_void_response + %({"status":"error","messages":[{"level":"error","key":"AlreadyRollbackedError","dsc":"The payment has already been rollbacked."}]}) + end +end diff --git a/test/unit/gateways/webpay_test.rb b/test/unit/gateways/webpay_test.rb index d0163560e37..66176804c87 100644 --- a/test/unit/gateways/webpay_test.rb +++ b/test/unit/gateways/webpay_test.rb @@ -4,15 +4,15 @@ 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 @refund_amount = 20000 @options = { - :billing_address => address(), - :description => 'Test Purchase' + billing_address: address(), + description: 'Test Purchase' } end @@ -60,7 +60,7 @@ def test_appropriate_purchase_amount def test_successful_purchase_with_token response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, 'cus_xxx|card_xxx') - end.check_request do |method, endpoint, data, headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/customer=cus_xxx/, data) assert_match(/card=card_xxx/, data) end.respond_with(successful_purchase_response) @@ -94,7 +94,7 @@ def test_successful_refund end def test_successful_request_always_uses_live_mode_to_determine_test_request - @gateway.expects(:ssl_request).returns(successful_partially_refunded_response(:livemode => true)) + @gateway.expects(:ssl_request).returns(successful_partially_refunded_response(livemode: true)) assert response = @gateway.refund(@refund_amount, 'ch_test_charge') assert_success response @@ -121,24 +121,24 @@ def test_invalid_raw_response def test_add_customer post = {} - @gateway.send(:add_customer, post, 'card_token', {:customer => 'test_customer'}) + @gateway.send(:add_customer, post, 'card_token', { customer: 'test_customer' }) assert_equal 'test_customer', post[:customer] end def test_doesnt_add_customer_if_card post = {} - @gateway.send(:add_customer, post, @credit_card, {:customer => 'test_customer'}) + @gateway.send(:add_customer, post, @credit_card, { customer: 'test_customer' }) assert !post[:customer] end def test_add_customer_data post = {} - @gateway.send(:add_customer_data, post, {:description => 'a test customer'}) + @gateway.send(:add_customer_data, post, { description: 'a test customer' }) assert_equal 'a test customer', post[:description] end def test_add_address - post = {:card => {}} + post = { card: {} } @gateway.send(:add_address, post, @options) assert_equal @options[:billing_address][:zip], post[:card][:address_zip] assert_equal @options[:billing_address][:state], post[:card][:address_state] @@ -158,270 +158,270 @@ def test_gateway_without_credentials end def test_metadata_header - @gateway.expects(:ssl_request).once.with {|method, url, post, headers| - headers && headers['X-Webpay-Client-User-Metadata'] == {:ip => '1.1.1.1'}.to_json + @gateway.expects(:ssl_request).once.with { |_method, _url, _post, headers| + headers && headers['X-Webpay-Client-User-Metadata'] == { ip: '1.1.1.1' }.to_json }.returns(successful_purchase_response) - @gateway.purchase(@amount, @credit_card, @options.merge(:ip => '1.1.1.1')) + @gateway.purchase(@amount, @credit_card, @options.merge(ip: '1.1.1.1')) end private def successful_authorization_response - <<-RESPONSE -{ - "id": "ch_test_charge", - "object": "charge", - "livemode": false, - "currency": "jpy", - "description": "ActiveMerchant Test Purchase", - "amount": 40000, - "amount_refunded": 0, - "customer": null, - "recursion": null, - "created": 1309131571, - "paid": false, - "refunded": false, - "failure_message": null, - "card": { - "object": "card", - "exp_year": #{Time.now.year + 1}, - "exp_month": 11, - "fingerprint": "215b5b2fe460809b8bb90bae6eeac0e0e0987bd7", - "name": "LONGBOB LONGSEN", - "country": "JP", - "type": "Visa", - "cvc_check": "pass", - "last4": "4242" - }, - "captured": false, - "expire_time": 1309736371, - "fees": [ - - ] -} + <<~RESPONSE + { + "id": "ch_test_charge", + "object": "charge", + "livemode": false, + "currency": "jpy", + "description": "ActiveMerchant Test Purchase", + "amount": 40000, + "amount_refunded": 0, + "customer": null, + "recursion": null, + "created": 1309131571, + "paid": false, + "refunded": false, + "failure_message": null, + "card": { + "object": "card", + "exp_year": #{Time.now.year + 1}, + "exp_month": 11, + "fingerprint": "215b5b2fe460809b8bb90bae6eeac0e0e0987bd7", + "name": "LONGBOB LONGSEN", + "country": "JP", + "type": "Visa", + "cvc_check": "pass", + "last4": "4242" + }, + "captured": false, + "expire_time": 1309736371, + "fees": [ + + ] + } RESPONSE end def successful_capture_response - <<-RESPONSE -{ - "id": "ch_test_charge", - "object": "charge", - "livemode": false, - "currency": "jpy", - "description": "ActiveMerchant Test Purchase", - "amount": 40000, - "amount_refunded": 0, - "customer": null, - "recursion": null, - "created": 1309131571, - "paid": true, - "refunded": false, - "failure_message": null, - "card": { - "object": "card", - "exp_year": #{Time.now.year + 1}, - "exp_month": 11, - "fingerprint": "215b5b2fe460809b8bb90bae6eeac0e0e0987bd7", - "name": "LONGBOB LONGSEN", - "country": "JP", - "type": "Visa", - "cvc_check": "pass", - "last4": "4242" - }, - "captured": true, - "expire_time": 1309736371, - "fees": [ - { - "object": "fee", - "transaction_type": "payment", - "transaction_fee": 0, - "rate": 3.25, - "amount": 1300, - "created": 1408585142 - } - ] -} + <<~RESPONSE + { + "id": "ch_test_charge", + "object": "charge", + "livemode": false, + "currency": "jpy", + "description": "ActiveMerchant Test Purchase", + "amount": 40000, + "amount_refunded": 0, + "customer": null, + "recursion": null, + "created": 1309131571, + "paid": true, + "refunded": false, + "failure_message": null, + "card": { + "object": "card", + "exp_year": #{Time.now.year + 1}, + "exp_month": 11, + "fingerprint": "215b5b2fe460809b8bb90bae6eeac0e0e0987bd7", + "name": "LONGBOB LONGSEN", + "country": "JP", + "type": "Visa", + "cvc_check": "pass", + "last4": "4242" + }, + "captured": true, + "expire_time": 1309736371, + "fees": [ + { + "object": "fee", + "transaction_type": "payment", + "transaction_fee": 0, + "rate": 3.25, + "amount": 1300, + "created": 1408585142 + } + ] + } RESPONSE end # Place raw successful response from gateway here - def successful_purchase_response(refunded=false) - <<-RESPONSE -{ - "id": "ch_test_charge", - "object": "charge", - "livemode": false, - "currency": "jpy", - "description": "ActiveMerchant Test Purchase", - "amount": 400, - "amount_refunded": 0, - "customer": null, - "recursion": null, - "created": 1408585273, - "paid": true, - "refunded": false, - "failure_message": null, - "card": { - "object": "card", - "exp_year": #{Time.now.year + 1}, - "exp_month": 11, - "fingerprint": "215b5b2fe460809b8bb90bae6eeac0e0e0987bd7", - "name": "LONGBOB LONGSEN", - "country": "JP", - "type": "Visa", - "cvc_check": "pass", - "last4": "4242" - }, - "captured": true, - "expire_time": null, - "fees": [ - { - "object": "fee", - "transaction_type": "payment", - "transaction_fee": 0, - "rate": 3.25, - "amount": 1300, - "created": 1408585273 - } - ] -} + def successful_purchase_response(refunded = false) + <<~RESPONSE + { + "id": "ch_test_charge", + "object": "charge", + "livemode": false, + "currency": "jpy", + "description": "ActiveMerchant Test Purchase", + "amount": 400, + "amount_refunded": 0, + "customer": null, + "recursion": null, + "created": 1408585273, + "paid": true, + "refunded": false, + "failure_message": null, + "card": { + "object": "card", + "exp_year": #{Time.now.year + 1}, + "exp_month": 11, + "fingerprint": "215b5b2fe460809b8bb90bae6eeac0e0e0987bd7", + "name": "LONGBOB LONGSEN", + "country": "JP", + "type": "Visa", + "cvc_check": "pass", + "last4": "4242" + }, + "captured": true, + "expire_time": null, + "fees": [ + { + "object": "fee", + "transaction_type": "payment", + "transaction_fee": 0, + "rate": 3.25, + "amount": 1300, + "created": 1408585273 + } + ] + } RESPONSE end def successful_refunded_response - <<-RESPONSE -{ - "id": "ch_test_charge", - "object": "charge", - "livemode": false, - "currency": "jpy", - "description": "ActiveMerchant Test Purchase", - "amount": 400, - "amount_refunded": 400, - "customer": null, - "recursion": null, - "created": 1408585273, - "paid": true, - "refunded": true, - "failure_message": null, - "card": { - "object": "card", - "exp_year": #{Time.now.year + 1}, - "exp_month": 11, - "fingerprint": "215b5b2fe460809b8bb90bae6eeac0e0e0987bd7", - "name": "KEI KUBO", - "country": "JP", - "type": "Visa", - "cvc_check": "pass", - "last4": "4242" - }, - "captured": true, - "expire_time": null, - "fees": [ - { - "object": "fee", - "transaction_type": "payment", - "transaction_fee": 0, - "rate": 3.25, - "amount": 1300, - "created": 1408585273 - }, - { - "object": "fee", - "transaction_type": "refund", - "transaction_fee": 0, - "rate": 3.25, - "amount": -1300, - "created": 1408585461 - } - ] -} + <<~RESPONSE + { + "id": "ch_test_charge", + "object": "charge", + "livemode": false, + "currency": "jpy", + "description": "ActiveMerchant Test Purchase", + "amount": 400, + "amount_refunded": 400, + "customer": null, + "recursion": null, + "created": 1408585273, + "paid": true, + "refunded": true, + "failure_message": null, + "card": { + "object": "card", + "exp_year": #{Time.now.year + 1}, + "exp_month": 11, + "fingerprint": "215b5b2fe460809b8bb90bae6eeac0e0e0987bd7", + "name": "KEI KUBO", + "country": "JP", + "type": "Visa", + "cvc_check": "pass", + "last4": "4242" + }, + "captured": true, + "expire_time": null, + "fees": [ + { + "object": "fee", + "transaction_type": "payment", + "transaction_fee": 0, + "rate": 3.25, + "amount": 1300, + "created": 1408585273 + }, + { + "object": "fee", + "transaction_type": "refund", + "transaction_fee": 0, + "rate": 3.25, + "amount": -1300, + "created": 1408585461 + } + ] + } RESPONSE end def successful_partially_refunded_response(options = {}) - options = {:livemode=>false}.merge!(options) - <<-RESPONSE -{ - "id": "ch_test_charge", - "object": "charge", - "livemode": #{options[:livemode]}, - "currency": "jpy", - "description": "ActiveMerchant Test Purchase", - "amount": 400, - "amount_refunded": 200, - "customer": null, - "recursion": null, - "created": 1408584994, - "paid": true, - "refunded": false, - "failure_message": null, - "card": { - "object": "card", - "exp_year": #{Time.now.year + 1}, - "exp_month": 11, - "fingerprint": "215b5b2fe460809b8bb90bae6eeac0e0e0987bd7", - "name": "KEI KUBO", - "country": "JP", - "type": "Visa", - "cvc_check": "pass", - "last4": "4242" - }, - "captured": true, - "expire_time": 1409189794, - "fees": [ - { - "object": "fee", - "transaction_type": "payment", - "transaction_fee": 0, - "rate": 3.25, - "amount": 1300, - "created": 1408585142 - }, - { - "object": "fee", - "transaction_type": "refund", - "transaction_fee": 0, - "rate": 3.25, - "amount": -1300, - "created": 1408585699 - }, - { - "object": "fee", - "transaction_type": "payment", - "transaction_fee": 0, - "rate": 3.25, - "amount": 650, - "created": 1408585699 - } - ] -} + options = { livemode: false }.merge!(options) + <<~RESPONSE + { + "id": "ch_test_charge", + "object": "charge", + "livemode": #{options[:livemode]}, + "currency": "jpy", + "description": "ActiveMerchant Test Purchase", + "amount": 400, + "amount_refunded": 200, + "customer": null, + "recursion": null, + "created": 1408584994, + "paid": true, + "refunded": false, + "failure_message": null, + "card": { + "object": "card", + "exp_year": #{Time.now.year + 1}, + "exp_month": 11, + "fingerprint": "215b5b2fe460809b8bb90bae6eeac0e0e0987bd7", + "name": "KEI KUBO", + "country": "JP", + "type": "Visa", + "cvc_check": "pass", + "last4": "4242" + }, + "captured": true, + "expire_time": 1409189794, + "fees": [ + { + "object": "fee", + "transaction_type": "payment", + "transaction_fee": 0, + "rate": 3.25, + "amount": 1300, + "created": 1408585142 + }, + { + "object": "fee", + "transaction_type": "refund", + "transaction_fee": 0, + "rate": 3.25, + "amount": -1300, + "created": 1408585699 + }, + { + "object": "fee", + "transaction_type": "payment", + "transaction_fee": 0, + "rate": 3.25, + "amount": 650, + "created": 1408585699 + } + ] + } RESPONSE end # Place raw failed response from gateway here def failed_purchase_response - <<-RESPONSE -{ - "error": { - "message": "The card number is invalid. Make sure the number entered matches your credit card.", - "caused_by": "buyer", - "param": "number", - "type": "card_error", - "code": "incorrect_number" - } -} + <<~RESPONSE + { + "error": { + "message": "The card number is invalid. Make sure the number entered matches your credit card.", + "caused_by": "buyer", + "param": "number", + "type": "card_error", + "code": "incorrect_number" + } + } RESPONSE end # Place raw invalid JSON from gateway here def invalid_json_response - <<-RESPONSE - { - foo : bar - } + <<~RESPONSE + { + foo : bar + } RESPONSE end end diff --git a/test/unit/gateways/wepay_test.rb b/test/unit/gateways/wepay_test.rb index 9f749e27916..30c012b590a 100644 --- a/test/unit/gateways/wepay_test.rb +++ b/test/unit/gateways/wepay_test.rb @@ -160,7 +160,7 @@ def test_invalid_json_response def test_no_version_by_default stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, _data, headers| assert_no_match(/Api-Version/, headers.to_s) end.respond_with(successful_authorize_response) end @@ -168,7 +168,7 @@ def test_no_version_by_default def test_version_override stub_comms do @gateway.purchase(@amount, @credit_card, @options.merge(version: '2017-05-31')) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, _data, headers| assert_match(/"Api-Version\"=>\"2017-05-31\"/, headers.to_s) end.respond_with(successful_authorize_response) end @@ -425,5 +425,4 @@ def failed_capture_response def invalid_json_response %({"checkout_id"=1852898602,"state":"captured") end - end diff --git a/test/unit/gateways/wirecard_test.rb b/test/unit/gateways/wirecard_test.rb index b5ee72ebbc7..fcbd76c83d6 100644 --- a/test/unit/gateways/wirecard_test.rb +++ b/test/unit/gateways/wirecard_test.rb @@ -1,4 +1,5 @@ # encoding: UTF-8 + require 'test_helper' class WirecardTest < Test::Unit::TestCase @@ -31,7 +32,7 @@ def setup city: 'Ottawa', zip: 'K12 P2A', country: 'CA', - state: nil, + state: nil } @address_avs = { @@ -39,7 +40,7 @@ def setup city: 'London', zip: 'W8 2TE', country: 'GB', - state: 'London', + state: 'London' } end @@ -191,7 +192,7 @@ def test_description_trucated_to_32_chars_in_authorize stub_comms do @gateway.authorize(@amount, @credit_card, options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/<FunctionID>32chars-------------------------<\/FunctionID>/, data) end.respond_with(successful_authorization_response) end @@ -201,7 +202,7 @@ def test_description_trucated_to_32_chars_in_purchase stub_comms do @gateway.purchase(@amount, @credit_card, options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/<FunctionID>32chars-------------------------<\/FunctionID>/, data) end.respond_with(successful_purchase_response) end @@ -211,7 +212,7 @@ def test_description_is_ascii_encoded_since_wirecard_does_not_like_utf_8 stub_comms do @gateway.purchase(@amount, @credit_card, options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/<FunctionID>\?D\?nde est\? la estaci\?n\?<\/FunctionID>/, data) end.respond_with(successful_purchase_response) end @@ -235,7 +236,7 @@ def test_commerce_type_option stub_comms do @gateway.purchase(@amount, @credit_card, options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/<CommerceType>MOTO<\/CommerceType>/, data) end.respond_with(successful_purchase_response) end @@ -243,7 +244,7 @@ def test_commerce_type_option def test_store_sets_recurring_transaction_type_to_initial stub_comms do @gateway.store(@credit_card) - end.check_request do |endpoint, body, headers| + end.check_request do |_endpoint, body, _headers| assert_xml_element_text(body, '//RECURRING_TRANSACTION/Type', 'Initial') end.respond_with(successful_authorization_response) end @@ -251,15 +252,15 @@ def test_store_sets_recurring_transaction_type_to_initial def test_store_sets_amount_to_100_by_default stub_comms do @gateway.store(@credit_card) - end.check_request do |endpoint, body, headers| + end.check_request do |_endpoint, body, _headers| assert_xml_element_text(body, '//CC_TRANSACTION/Amount', '100') end.respond_with(successful_authorization_response) end def test_store_sets_amount_to_amount_from_options stub_comms do - @gateway.store(@credit_card, :amount => 120) - end.check_request do |endpoint, body, headers| + @gateway.store(@credit_card, amount: 120) + end.check_request do |_endpoint, body, _headers| assert_xml_element_text(body, '//CC_TRANSACTION/Amount', '120') end.respond_with(successful_authorization_response) end @@ -267,7 +268,7 @@ def test_store_sets_amount_to_amount_from_options def test_authorization_using_reference_sets_proper_elements stub_comms do @gateway.authorize(@amount, '45678', @options) - end.check_request do |endpoint, body, headers| + end.check_request do |_endpoint, body, _headers| assert_xml_element_text(body, '//GuWID', '45678') assert_no_match(/<CREDIT_CARD_DATA>/, body) end.respond_with(successful_authorization_response) @@ -276,7 +277,7 @@ def test_authorization_using_reference_sets_proper_elements def test_purchase_using_reference_sets_proper_elements stub_comms do @gateway.purchase(@amount, '87654', @options) - end.check_request do |endpoint, body, headers| + end.check_request do |_endpoint, body, _headers| assert_xml_element_text(body, '//GuWID', '87654') assert_no_match(/<CREDIT_CARD_DATA>/, body) end.respond_with(successful_authorization_response) @@ -284,16 +285,16 @@ def test_purchase_using_reference_sets_proper_elements def test_authorization_with_recurring_transaction_type_initial stub_comms do - @gateway.authorize(@amount, @credit_card, @options.merge(:recurring => 'Initial')) - end.check_request do |endpoint, body, headers| + @gateway.authorize(@amount, @credit_card, @options.merge(recurring: 'Initial')) + end.check_request do |_endpoint, body, _headers| assert_xml_element_text(body, '//RECURRING_TRANSACTION/Type', 'Initial') end.respond_with(successful_authorization_response) end def test_purchase_using_with_recurring_transaction_type_initial stub_comms do - @gateway.purchase(@amount, @credit_card, @options.merge(:recurring => 'Initial')) - end.check_request do |endpoint, body, headers| + @gateway.purchase(@amount, @credit_card, @options.merge(recurring: 'Initial')) + end.check_request do |_endpoint, body, _headers| assert_xml_element_text(body, '//RECURRING_TRANSACTION/Type', 'Initial') end.respond_with(successful_authorization_response) end @@ -328,141 +329,142 @@ def assert_xml_element_text(xml, xpath, expected_text) # Authorization success def successful_authorization_response - <<-XML - <?xml version="1.0" encoding="UTF-8"?> - <WIRECARD_BXML xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xsi:noNamespaceSchemaLocation="wirecard.xsd"> - <W_RESPONSE> - <W_JOB> - <JobID>test dummy data</JobID> - <FNC_CC_PREAUTHORIZATION> - <FunctionID>Wirecard remote test purchase</FunctionID> - <CC_TRANSACTION> - <TransactionID>1</TransactionID> - <PROCESSING_STATUS> - <GuWID>C822580121385121429927</GuWID> - <AuthorizationCode>709678</AuthorizationCode> - <Info>THIS IS A DEMO TRANSACTION USING CREDIT CARD NUMBER 420000****0000. NO REAL MONEY WILL BE TRANSFERED.</Info> - <StatusType>INFO</StatusType> - <FunctionResult>ACK</FunctionResult> - <TimeStamp>2008-06-19 06:53:33</TimeStamp> - </PROCESSING_STATUS> - </CC_TRANSACTION> - </FNC_CC_PREAUTHORIZATION> - </W_JOB> - </W_RESPONSE> -</WIRECARD_BXML> + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <WIRECARD_BXML xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xsi:noNamespaceSchemaLocation="wirecard.xsd"> + <W_RESPONSE> + <W_JOB> + <JobID>test dummy data</JobID> + <FNC_CC_PREAUTHORIZATION> + <FunctionID>Wirecard remote test purchase</FunctionID> + <CC_TRANSACTION> + <TransactionID>1</TransactionID> + <PROCESSING_STATUS> + <GuWID>C822580121385121429927</GuWID> + <AuthorizationCode>709678</AuthorizationCode> + <Info>THIS IS A DEMO TRANSACTION USING CREDIT CARD NUMBER 420000****0000. NO REAL MONEY WILL BE TRANSFERED.</Info> + <StatusType>INFO</StatusType> + <FunctionResult>ACK</FunctionResult> + <TimeStamp>2008-06-19 06:53:33</TimeStamp> + </PROCESSING_STATUS> + </CC_TRANSACTION> + </FNC_CC_PREAUTHORIZATION> + </W_JOB> + </W_RESPONSE> + </WIRECARD_BXML> XML end # Authorization failure # TODO: replace with real xml string here (current way seems to complicated) def wrong_creditcard_authorization_response - error = <<-XML - <ERROR> - <Type>DATA_ERROR</Type> - <Number>24997</Number> - <Message>Credit card number not allowed in demo mode.</Message> - <Advice>Only demo card number '4200000000000000' is allowed for VISA in demo mode.</Advice> - </ERROR> - XML + error = <<~XML + <ERROR> + <Type>DATA_ERROR</Type> + <Number>24997</Number> + <Message>Credit card number not allowed in demo mode.</Message> + <Advice>Only demo card number '4200000000000000' is allowed for VISA in demo mode.</Advice> + </ERROR> + XML result_node = '</FunctionResult>' auth = 'AuthorizationCode' - successful_authorization_response.gsub('ACK', 'NOK') \ - .gsub(result_node, result_node + error) \ - .gsub(/<#{auth}>\w+<\/#{auth}>/, "<#{auth}><\/#{auth}>") \ - .gsub(/<Info>.+<\/Info>/, '') + successful_authorization_response. + gsub('ACK', 'NOK'). + gsub(result_node, result_node + error). + gsub(/<#{auth}>\w+<\/#{auth}>/, "<#{auth}><\/#{auth}>"). + gsub(/<Info>.+<\/Info>/, '') end # Capture success def successful_capture_response - <<-XML - <?xml version="1.0" encoding="UTF-8"?> - <WIRECARD_BXML xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xsi:noNamespaceSchemaLocation="wirecard.xsd"> - <W_RESPONSE> - <W_JOB> - <JobID>test dummy data</JobID> - <FNC_CC_CAPTURE> - <FunctionID>Wirecard remote test purchase</FunctionID> - <CC_TRANSACTION> - <TransactionID>1</TransactionID> - <PROCESSING_STATUS> - <GuWID>C833707121385268439116</GuWID> - <AuthorizationCode>915025</AuthorizationCode> - <Info>THIS IS A DEMO TRANSACTION USING CREDIT CARD NUMBER 420000****0000. NO REAL MONEY WILL BE TRANSFERED.</Info> - <StatusType>INFO</StatusType> - <FunctionResult>ACK</FunctionResult> - <TimeStamp>2008-06-19 07:18:04</TimeStamp> - </PROCESSING_STATUS> - </CC_TRANSACTION> - </FNC_CC_CAPTURE> - </W_JOB> - </W_RESPONSE> - </WIRECARD_BXML> + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <WIRECARD_BXML xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xsi:noNamespaceSchemaLocation="wirecard.xsd"> + <W_RESPONSE> + <W_JOB> + <JobID>test dummy data</JobID> + <FNC_CC_CAPTURE> + <FunctionID>Wirecard remote test purchase</FunctionID> + <CC_TRANSACTION> + <TransactionID>1</TransactionID> + <PROCESSING_STATUS> + <GuWID>C833707121385268439116</GuWID> + <AuthorizationCode>915025</AuthorizationCode> + <Info>THIS IS A DEMO TRANSACTION USING CREDIT CARD NUMBER 420000****0000. NO REAL MONEY WILL BE TRANSFERED.</Info> + <StatusType>INFO</StatusType> + <FunctionResult>ACK</FunctionResult> + <TimeStamp>2008-06-19 07:18:04</TimeStamp> + </PROCESSING_STATUS> + </CC_TRANSACTION> + </FNC_CC_CAPTURE> + </W_JOB> + </W_RESPONSE> + </WIRECARD_BXML> XML end # Capture failure def unauthorized_capture_response - <<-XML - <?xml version="1.0" encoding="UTF-8"?> - <WIRECARD_BXML xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xsi:noNamespaceSchemaLocation="wirecard.xsd"> - <W_RESPONSE> - <W_JOB> - <JobID>test dummy data</JobID> - <FNC_CC_CAPTURE> - <FunctionID>Test dummy FunctionID</FunctionID> - <CC_TRANSACTION> - <TransactionID>a2783d471ccc98825b8c498f1a62ce8f</TransactionID> - <PROCESSING_STATUS> - <GuWID>C833707121385268439116</GuWID> - <AuthorizationCode></AuthorizationCode> - <StatusType>INFO</StatusType> - <FunctionResult>NOK</FunctionResult> - <ERROR> - <Type>DATA_ERROR</Type> - <Number>20080</Number> - <Message>Could not find referenced transaction for GuWID 1234567890123456789012.</Message> - </ERROR> - <TimeStamp>2008-06-19 08:09:20</TimeStamp> - </PROCESSING_STATUS> - </CC_TRANSACTION> - </FNC_CC_CAPTURE> - </W_JOB> - </W_RESPONSE> - </WIRECARD_BXML> + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <WIRECARD_BXML xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xsi:noNamespaceSchemaLocation="wirecard.xsd"> + <W_RESPONSE> + <W_JOB> + <JobID>test dummy data</JobID> + <FNC_CC_CAPTURE> + <FunctionID>Test dummy FunctionID</FunctionID> + <CC_TRANSACTION> + <TransactionID>a2783d471ccc98825b8c498f1a62ce8f</TransactionID> + <PROCESSING_STATUS> + <GuWID>C833707121385268439116</GuWID> + <AuthorizationCode></AuthorizationCode> + <StatusType>INFO</StatusType> + <FunctionResult>NOK</FunctionResult> + <ERROR> + <Type>DATA_ERROR</Type> + <Number>20080</Number> + <Message>Could not find referenced transaction for GuWID 1234567890123456789012.</Message> + </ERROR> + <TimeStamp>2008-06-19 08:09:20</TimeStamp> + </PROCESSING_STATUS> + </CC_TRANSACTION> + </FNC_CC_CAPTURE> + </W_JOB> + </W_RESPONSE> + </WIRECARD_BXML> XML end # Purchase success def successful_purchase_response - <<-XML - <?xml version="1.0" encoding="UTF-8"?> - <WIRECARD_BXML xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xsi:noNamespaceSchemaLocation="wirecard.xsd"> - <W_RESPONSE> - <W_JOB> - <JobID>test dummy data</JobID> - <FNC_CC_PURCHASE> - <FunctionID>Wirecard remote test purchase</FunctionID> - <CC_TRANSACTION> - <TransactionID>1</TransactionID> - <PROCESSING_STATUS> - <GuWID>C865402121385575982910</GuWID> - <AuthorizationCode>531750</AuthorizationCode> - <Info>THIS IS A DEMO TRANSACTION USING CREDIT CARD NUMBER 420000****0000. NO REAL MONEY WILL BE TRANSFERED.</Info> - <StatusType>INFO</StatusType> - <FunctionResult>ACK</FunctionResult> - <TimeStamp>2008-06-19 08:09:19</TimeStamp> - </PROCESSING_STATUS> - </CC_TRANSACTION> - </FNC_CC_PURCHASE> - </W_JOB> - </W_RESPONSE> - </WIRECARD_BXML> + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <WIRECARD_BXML xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xsi:noNamespaceSchemaLocation="wirecard.xsd"> + <W_RESPONSE> + <W_JOB> + <JobID>test dummy data</JobID> + <FNC_CC_PURCHASE> + <FunctionID>Wirecard remote test purchase</FunctionID> + <CC_TRANSACTION> + <TransactionID>1</TransactionID> + <PROCESSING_STATUS> + <GuWID>C865402121385575982910</GuWID> + <AuthorizationCode>531750</AuthorizationCode> + <Info>THIS IS A DEMO TRANSACTION USING CREDIT CARD NUMBER 420000****0000. NO REAL MONEY WILL BE TRANSFERED.</Info> + <StatusType>INFO</StatusType> + <FunctionResult>ACK</FunctionResult> + <TimeStamp>2008-06-19 08:09:19</TimeStamp> + </PROCESSING_STATUS> + </CC_TRANSACTION> + </FNC_CC_PURCHASE> + </W_JOB> + </W_RESPONSE> + </WIRECARD_BXML> XML end def successful_refund_response - <<-XML + <<~XML <?xml version="1.0" encoding="UTF-8"?> <WIRECARD_BXML xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xsi:noNamespaceSchemaLocation="wirecard.xsd"> <W_RESPONSE> @@ -489,7 +491,7 @@ def successful_refund_response end def failed_refund_response - <<-XML + <<~XML <?xml version="1.0" encoding="UTF-8"?> <WIRECARD_BXML xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xsi:noNamespaceSchemaLocation="wirecard.xsd"> <W_RESPONSE> @@ -520,7 +522,7 @@ def failed_refund_response end def successful_void_response - <<-XML + <<~XML <?xml version="1.0" encoding="UTF-8"?> <WIRECARD_BXML xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xsi:noNamespaceSchemaLocation="wirecard.xsd"> <W_RESPONSE> @@ -547,7 +549,7 @@ def successful_void_response end def failed_void_response - <<-XML + <<~XML <?xml version="1.0" encoding="UTF-8"?> <WIRECARD_BXML xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xsi:noNamespaceSchemaLocation="wirecard.xsd"> <W_RESPONSE> @@ -577,205 +579,204 @@ def failed_void_response XML end - # Purchase failure def wrong_creditcard_purchase_response - <<-XML - <?xml version="1.0" encoding="UTF-8"?> - <WIRECARD_BXML xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xsi:noNamespaceSchemaLocation="wirecard.xsd"> - <W_RESPONSE> - <W_JOB> - <JobID>test dummy data</JobID> - <FNC_CC_PURCHASE> - <FunctionID>Wirecard remote test purchase</FunctionID> - <CC_TRANSACTION> - <TransactionID>1</TransactionID> - <PROCESSING_STATUS> - <GuWID>C824697121385153203112</GuWID> - <AuthorizationCode></AuthorizationCode> - <StatusType>INFO</StatusType> - <FunctionResult>NOK</FunctionResult> - <ERROR> - <Type>DATA_ERROR</Type> <Number>24997</Number> - <Message>Credit card number not allowed in demo mode.</Message> - <Advice>Only demo card number '4200000000000000' is allowed for VISA in demo mode.</Advice> - </ERROR> - <TimeStamp>2008-06-19 06:58:51</TimeStamp> - </PROCESSING_STATUS> - </CC_TRANSACTION> - </FNC_CC_PURCHASE> - </W_JOB> - </W_RESPONSE> - </WIRECARD_BXML> + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <WIRECARD_BXML xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xsi:noNamespaceSchemaLocation="wirecard.xsd"> + <W_RESPONSE> + <W_JOB> + <JobID>test dummy data</JobID> + <FNC_CC_PURCHASE> + <FunctionID>Wirecard remote test purchase</FunctionID> + <CC_TRANSACTION> + <TransactionID>1</TransactionID> + <PROCESSING_STATUS> + <GuWID>C824697121385153203112</GuWID> + <AuthorizationCode></AuthorizationCode> + <StatusType>INFO</StatusType> + <FunctionResult>NOK</FunctionResult> + <ERROR> + <Type>DATA_ERROR</Type> <Number>24997</Number> + <Message>Credit card number not allowed in demo mode.</Message> + <Advice>Only demo card number '4200000000000000' is allowed for VISA in demo mode.</Advice> + </ERROR> + <TimeStamp>2008-06-19 06:58:51</TimeStamp> + </PROCESSING_STATUS> + </CC_TRANSACTION> + </FNC_CC_PURCHASE> + </W_JOB> + </W_RESPONSE> + </WIRECARD_BXML> XML end # AVS failure def failed_avs_response - <<-XML - <?xml version="1.0" encoding="UTF-8"?> - <WIRECARD_BXML xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xsi:noNamespaceSchemaLocation="wirecard.xsd"> - <W_RESPONSE> - <W_JOB> - <JobID></JobID> - <FNC_CC_PURCHASE> - <FunctionID></FunctionID> - <CC_TRANSACTION> - <TransactionID>E0BCBF30B82D0131000000000000E4CF</TransactionID> - <PROCESSING_STATUS> - <GuWID>C997753139988691610455</GuWID> - <AuthorizationCode>732129</AuthorizationCode> - <Info>THIS IS A DEMO TRANSACTION USING CREDIT CARD NUMBER 420000****0000. NO REAL MONEY WILL BE TRANSFERED.</Info> - <StatusType>INFO</StatusType> - <FunctionResult>PENDING</FunctionResult> - <AVS> - <ResultCode>U</ResultCode> - <Message>AVS Unavailable.</Message> - <AuthorizationEntity>5</AuthorizationEntity> - <AuthorizationEntityMessage>Response provided by issuer processor.</AuthorizationEntityMessage> - <ProviderResultCode>A</ProviderResultCode> - <ProviderResultMessage>Address information is unavailable, or the Issuer does not support AVS. Acquirer has representment rights.</ProviderResultMessage> - </AVS> - <TimeStamp>2014-05-12 11:28:36</TimeStamp> - </PROCESSING_STATUS> - </CC_TRANSACTION> - </FNC_CC_PURCHASE> - </W_JOB> - </W_RESPONSE> - </WIRECARD_BXML> + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <WIRECARD_BXML xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xsi:noNamespaceSchemaLocation="wirecard.xsd"> + <W_RESPONSE> + <W_JOB> + <JobID></JobID> + <FNC_CC_PURCHASE> + <FunctionID></FunctionID> + <CC_TRANSACTION> + <TransactionID>E0BCBF30B82D0131000000000000E4CF</TransactionID> + <PROCESSING_STATUS> + <GuWID>C997753139988691610455</GuWID> + <AuthorizationCode>732129</AuthorizationCode> + <Info>THIS IS A DEMO TRANSACTION USING CREDIT CARD NUMBER 420000****0000. NO REAL MONEY WILL BE TRANSFERED.</Info> + <StatusType>INFO</StatusType> + <FunctionResult>PENDING</FunctionResult> + <AVS> + <ResultCode>U</ResultCode> + <Message>AVS Unavailable.</Message> + <AuthorizationEntity>5</AuthorizationEntity> + <AuthorizationEntityMessage>Response provided by issuer processor.</AuthorizationEntityMessage> + <ProviderResultCode>A</ProviderResultCode> + <ProviderResultMessage>Address information is unavailable, or the Issuer does not support AVS. Acquirer has representment rights.</ProviderResultMessage> + </AVS> + <TimeStamp>2014-05-12 11:28:36</TimeStamp> + </PROCESSING_STATUS> + </CC_TRANSACTION> + </FNC_CC_PURCHASE> + </W_JOB> + </W_RESPONSE> + </WIRECARD_BXML> XML end def system_error_response - <<-XML - <?xml version="1.0" encoding="UTF-8"?> - <WIRECARD_BXML xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xsi:noNamespaceSchemaLocation="wirecard.xsd"> - <W_RESPONSE> - <W_JOB> - <JobID></JobID> - <FNC_CC_PURCHASE> - <FunctionID></FunctionID> - <CC_TRANSACTION> - <TransactionID>3A368E50D50B01310000000000009153</TransactionID> - <PROCESSING_STATUS> - <GuWID>C967464140265180577024</GuWID> - <AuthorizationCode></AuthorizationCode> - <Info>THIS IS A DEMO TRANSACTION USING CREDIT CARD NUMBER 420000****0000. NO REAL MONEY WILL BE TRANSFERED.</Info> - <StatusType>INFO</StatusType> - <FunctionResult>NOK</FunctionResult> - <ERROR> - <Type>SYSTEM_ERROR</Type> - <Number>20205</Number> - <Message></Message> - </ERROR> - <TimeStamp>2014-06-13 11:30:05</TimeStamp> - </PROCESSING_STATUS> - </CC_TRANSACTION> - </FNC_CC_PURCHASE> - </W_JOB> - </W_RESPONSE> - </WIRECARD_BXML> + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <WIRECARD_BXML xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xsi:noNamespaceSchemaLocation="wirecard.xsd"> + <W_RESPONSE> + <W_JOB> + <JobID></JobID> + <FNC_CC_PURCHASE> + <FunctionID></FunctionID> + <CC_TRANSACTION> + <TransactionID>3A368E50D50B01310000000000009153</TransactionID> + <PROCESSING_STATUS> + <GuWID>C967464140265180577024</GuWID> + <AuthorizationCode></AuthorizationCode> + <Info>THIS IS A DEMO TRANSACTION USING CREDIT CARD NUMBER 420000****0000. NO REAL MONEY WILL BE TRANSFERED.</Info> + <StatusType>INFO</StatusType> + <FunctionResult>NOK</FunctionResult> + <ERROR> + <Type>SYSTEM_ERROR</Type> + <Number>20205</Number> + <Message></Message> + </ERROR> + <TimeStamp>2014-06-13 11:30:05</TimeStamp> + </PROCESSING_STATUS> + </CC_TRANSACTION> + </FNC_CC_PURCHASE> + </W_JOB> + </W_RESPONSE> + </WIRECARD_BXML> XML end def system_error_response_without_job - <<-XML - <?xml version="1.0" encoding="UTF-8"?> - <WIRECARD_BXML xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xsi:noNamespaceSchemaLocation="wirecard.xsd"> - <W_RESPONSE> - <ERROR> - <Type>SYSTEM_ERROR</Type> - <Number>10003</Number> - <Message>Job Refused</Message> - </ERROR> - </W_RESPONSE> - </WIRECARD_BXML> + <<~XML + <?xml version="1.0" encoding="UTF-8"?> + <WIRECARD_BXML xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xsi:noNamespaceSchemaLocation="wirecard.xsd"> + <W_RESPONSE> + <ERROR> + <Type>SYSTEM_ERROR</Type> + <Number>10003</Number> + <Message>Job Refused</Message> + </ERROR> + </W_RESPONSE> + </WIRECARD_BXML> XML end def transcript - <<-XML - <WIRECARD_BXML> - <W_REQUEST> - <W_JOB> - <JobID></JobID> - <BusinessCaseSignature>00000031629CAFD5</BusinessCaseSignature> - <FNC_CC_PURCHASE> - <FunctionID>Wirecard remote test purchase</FunctionID> - <CC_TRANSACTION> - <TransactionID>1</TransactionID> - <Amount>100</Amount> - <Currency>EUR</Currency> - <CountryCode>CA</CountryCode> - <RECURRING_TRANSACTION> - <Type>Single</Type> - </RECURRING_TRANSACTION> - <CREDIT_CARD_DATA> - <CreditCardNumber>4200000000000000</CreditCardNumber> - <CVC2>123</CVC2> - <ExpirationYear>2016</ExpirationYear> - <ExpirationMonth>09</ExpirationMonth> - <CardHolderName>Longbob Longsen</CardHolderName> - </CREDIT_CARD_DATA> - <CORPTRUSTCENTER_DATA> - <ADDRESS> - <Address1>456 My Street</Address1> - <Address2>Apt 1</Address2> - <City>Ottawa</City> - <ZipCode>K1C2N6</ZipCode> - <State>ON</State> - <Country>CA</Country> - <Email>soleone@example.com</Email> - </ADDRESS> - </CORPTRUSTCENTER_DATA> - </CC_TRANSACTION> - </FNC_CC_PURCHASE> - </W_JOB> - </W_REQUEST> - </WIRECARD_BXML> + <<~XML + <WIRECARD_BXML> + <W_REQUEST> + <W_JOB> + <JobID></JobID> + <BusinessCaseSignature>00000031629CAFD5</BusinessCaseSignature> + <FNC_CC_PURCHASE> + <FunctionID>Wirecard remote test purchase</FunctionID> + <CC_TRANSACTION> + <TransactionID>1</TransactionID> + <Amount>100</Amount> + <Currency>EUR</Currency> + <CountryCode>CA</CountryCode> + <RECURRING_TRANSACTION> + <Type>Single</Type> + </RECURRING_TRANSACTION> + <CREDIT_CARD_DATA> + <CreditCardNumber>4200000000000000</CreditCardNumber> + <CVC2>123</CVC2> + <ExpirationYear>2016</ExpirationYear> + <ExpirationMonth>09</ExpirationMonth> + <CardHolderName>Longbob Longsen</CardHolderName> + </CREDIT_CARD_DATA> + <CORPTRUSTCENTER_DATA> + <ADDRESS> + <Address1>456 My Street</Address1> + <Address2>Apt 1</Address2> + <City>Ottawa</City> + <ZipCode>K1C2N6</ZipCode> + <State>ON</State> + <Country>CA</Country> + <Email>soleone@example.com</Email> + </ADDRESS> + </CORPTRUSTCENTER_DATA> + </CC_TRANSACTION> + </FNC_CC_PURCHASE> + </W_JOB> + </W_REQUEST> + </WIRECARD_BXML> XML end def scrubbed_transcript - <<-XML - <WIRECARD_BXML> - <W_REQUEST> - <W_JOB> - <JobID></JobID> - <BusinessCaseSignature>00000031629CAFD5</BusinessCaseSignature> - <FNC_CC_PURCHASE> - <FunctionID>Wirecard remote test purchase</FunctionID> - <CC_TRANSACTION> - <TransactionID>1</TransactionID> - <Amount>100</Amount> - <Currency>EUR</Currency> - <CountryCode>CA</CountryCode> - <RECURRING_TRANSACTION> - <Type>Single</Type> - </RECURRING_TRANSACTION> - <CREDIT_CARD_DATA> - <CreditCardNumber>[FILTERED]</CreditCardNumber> - <CVC2>[FILTERED]</CVC2> - <ExpirationYear>2016</ExpirationYear> - <ExpirationMonth>09</ExpirationMonth> - <CardHolderName>Longbob Longsen</CardHolderName> - </CREDIT_CARD_DATA> - <CORPTRUSTCENTER_DATA> - <ADDRESS> - <Address1>456 My Street</Address1> - <Address2>Apt 1</Address2> - <City>Ottawa</City> - <ZipCode>K1C2N6</ZipCode> - <State>ON</State> - <Country>CA</Country> - <Email>soleone@example.com</Email> - </ADDRESS> - </CORPTRUSTCENTER_DATA> - </CC_TRANSACTION> - </FNC_CC_PURCHASE> - </W_JOB> - </W_REQUEST> - </WIRECARD_BXML> + <<~XML + <WIRECARD_BXML> + <W_REQUEST> + <W_JOB> + <JobID></JobID> + <BusinessCaseSignature>00000031629CAFD5</BusinessCaseSignature> + <FNC_CC_PURCHASE> + <FunctionID>Wirecard remote test purchase</FunctionID> + <CC_TRANSACTION> + <TransactionID>1</TransactionID> + <Amount>100</Amount> + <Currency>EUR</Currency> + <CountryCode>CA</CountryCode> + <RECURRING_TRANSACTION> + <Type>Single</Type> + </RECURRING_TRANSACTION> + <CREDIT_CARD_DATA> + <CreditCardNumber>[FILTERED]</CreditCardNumber> + <CVC2>[FILTERED]</CVC2> + <ExpirationYear>2016</ExpirationYear> + <ExpirationMonth>09</ExpirationMonth> + <CardHolderName>Longbob Longsen</CardHolderName> + </CREDIT_CARD_DATA> + <CORPTRUSTCENTER_DATA> + <ADDRESS> + <Address1>456 My Street</Address1> + <Address2>Apt 1</Address2> + <City>Ottawa</City> + <ZipCode>K1C2N6</ZipCode> + <State>ON</State> + <Country>CA</Country> + <Email>soleone@example.com</Email> + </ADDRESS> + </CORPTRUSTCENTER_DATA> + </CC_TRANSACTION> + </FNC_CC_PURCHASE> + </W_JOB> + </W_REQUEST> + </WIRECARD_BXML> XML end end diff --git a/test/unit/gateways/wompi_test.rb b/test/unit/gateways/wompi_test.rb new file mode 100644 index 00000000000..883b69acb20 --- /dev/null +++ b/test/unit/gateways/wompi_test.rb @@ -0,0 +1,237 @@ +require 'test_helper' + +class WompiTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = WompiGateway.new(test_public_key: 'pub_test_1234', test_private_key: 'priv_test_5678') + @prod_gateway = WompiGateway.new(prod_public_key: 'pub_prod_1234', prod_private_key: 'priv_prod_5678') + @ambidextrous_gateway = WompiGateway.new(prod_public_key: 'pub_prod_1234', prod_private_key: 'priv_prod_5678', test_public_key: 'pub_test_1234', test_private_key: 'priv_test_5678') + @credit_card = credit_card + @amount = 150000 + + @options = { + order_id: '1', + billing_address: address, + description: 'Store Purchase' + } + end + + def test_ambidextrous_gateway_behaves_accordingly + response = stub_comms(@ambidextrous_gateway) do + @ambidextrous_gateway.purchase(@amount, @credit_card) + end.check_request do |_endpoint, _data, headers| + assert_match(/Bearer priv_test_5678/, headers['Authorization']) + end.respond_with(successful_purchase_response) + + assert_success response + + assert_equal '113879-1635300853-71494', response.authorization + assert response.test? + end + + def test_gateway_without_creds_raises_useful_error + assert_raise ArgumentError, 'Gateway requires both test_private_key and test_public_key, or both prod_private_key and prod_public_key' do + WompiGateway.new() + end + 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 '113879-1635300853-71494', 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 + assert_equal 'La transacción fue rechazada (Sandbox)', response.message + 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 19930, response.authorization + assert response.test? + end + + def test_successful_capture + @gateway.expects(:ssl_post).returns(successful_capture_response) + + response = @gateway.capture(@amount, '113879-1638483506-80282', @options) + assert_success response + + assert_equal '113879-1638483506-80282', response.authorization + end + + def test_failed_capture + @gateway.expects(:ssl_post).returns(failed_capture_response) + + response = @gateway.capture(@amount, '') + assert_failure response + + assert_equal 'La transacción fue rechazada (Sandbox)', response.message + end + + # def test_successful_refund + # @gateway.expects(:ssl_post).returns(successful_refund_response) + + # response = @gateway.refund(@amount, @credit_card, @options) + # assert_success response + + # assert_equal '113879-1635301011-28454', response.authorization + # assert response.test? + # end + + # def test_failed_refund + # @gateway.expects(:ssl_post).returns(failed_refund_response) + + # response = @gateway.refund(@amount, @credit_card, @options) + # assert_failure response + # message = JSON.parse(response.message) + # assert_equal 'transaction_id Debe ser completado', message['transaction_id'].first + # end + + def test_successful_refund_to_void + response = stub_comms(@gateway) do + @gateway.refund(@amount, @credit_card, @options) + end.check_request do |endpoint, data, _headers| + assert_match 'void_sync', endpoint + assert_match @amount.to_s, data + end.respond_with(successful_void_response) + assert_success response + + assert_equal '113879-1635301067-17128', response.authorization + assert response.test? + end + + def test_successful_partial_refund_to_void + response = stub_comms(@gateway) do + @gateway.refund(@amount - 50000, @credit_card, @options) + end.check_request do |endpoint, data, _headers| + assert_match 'void_sync', endpoint + assert_match (@amount - 50000).to_s, data + end.respond_with(successful_void_response) + assert_success response + + assert_equal '113879-1635301067-17128', response.authorization + assert response.test? + end + + def test_successful_void + response = stub_comms(@gateway) do + @gateway.void(@amount, @options) + end.check_request do |endpoint, data, _headers| + assert_match 'void_sync', endpoint + assert_match '{}', data + end.respond_with(successful_void_response) + assert_success response + + assert_equal '113879-1635301067-17128', response.authorization + assert response.test? + end + + def test_failed_void + @gateway.expects(:ssl_post).returns(failed_void_response) + + response = @gateway.void(@amount, @options) + assert_failure response + 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 + end + + private + + def pre_scrubbed + <<~TRANSCRIPT + "opening connection to sandbox.wompi.co:443...\nopened\nstarting SSL for sandbox.wompi.co:443...\nSSL established\n<- \"POST /v1/transactions_sync HTTP/1.1\\r\\nContent-Type: application/x-www-form-urlencoded\\r\\nAuthorization: Bearer prv_test_apOk1L1TV4qPqrZkfsPsJz5PBABQSI7F\\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: sandbox.wompi.co\\r\\nContent-Length: 282\\r\\n\\r\\n\"\n<- \"{\\\"reference\\\":\\\"rk6PBsDIxaBH\\\",\\\"public_key\\\":\\\"pub_test_RGehkiIXU3opWWryDE6jByz4W9kq6Hdk\\\",\\\"amount_in_cents\\\":150000,\\\"currency\\\":\\\"COP\\\",\\\"payment_method\\\":{\\\"type\\\":\\\"CARD\\\",\\\"number\\\":\\\"4242424242424242\\\",\\\"exp_month\\\":\\\"09\\\",\\\"exp_year\\\":\\\"22\\\",\\\"installments\\\":2,\\\"cvc\\\":\\\"123\\\",\\\"card_holder\\\":\\\"Longbob Longsen\\\"}}\"\n-> \"HTTP/1.1 200 OK\\r\\n\"\n-> \"Content-Type: application/json\\r\\n\"\n-> \"Content-Length: 621\\r\\n\"\n-> \"Connection: close\\r\\n\"\n-> \"Date: Wed, 27 Oct 2021 03:48:17 GMT\\r\\n\"\n-> \"x-amzn-RequestId: 9a64471b-b1ab-4835-9ef0-70b3d6a838fc\\r\\n\"\n-> \"x-amz-apigw-id: H2TOPERIoAMF0kA=\\r\\n\"\n-> \"X-Amzn-Trace-Id: Root=1-6178cbf4-2ba684d62e9bd4bd04017a4b;Sampled=0\\r\\n\"\n-> \"X-Cache: Miss from cloudfront\\r\\n\"\n-> \"Via: 1.1 ee9de9e6182ae0c8e8f119177e905245.cloudfront.net (CloudFront)\\r\\n\"\n-> \"X-Amz-Cf-Pop: DEN50-C2\\r\\n\"\n-> \"X-Amz-Cf-Id: QJH1Iy_rtMcjnWs4FI44anx5cX6RNZbk6JnHd6wvxqlDZnKl5j4W5g==\\r\\n\"\n-> \"\\r\\n\"\nreading 621 bytes...\n-> \"{\\\"data\\\":{\\\"id\\\":\\\"113879-1635306496-65846\\\",\\\"created_at\\\":\\\"2021-10-27T03:48:17.706Z\\\",\\\"amount_in_cents\\\":150000,\\\"reference\\\":\\\"rk6PBsDIxaBH\\\",\\\"currency\\\":\\\"COP\\\",\\\"payment_method_type\\\":\\\"CARD\\\",\\\"payment_method\\\":{\\\"type\\\":\\\"CARD\\\",\\\"extra\\\":{\\\"name\\\":\\\"VISA-4242\\\",\\\"brand\\\":\\\"VISA\\\",\\\"last_four\\\":\\\"4242\\\",\\\"external_identifier\\\":\\\"JdGjsAGDPQ\\\"},\\\"installments\\\":2},\\\"redirect_url\\\":null,\\\"status\\\":\\\"APPROVED\\\",\\\"status_message\\\":null,\\\"merchant\\\":{\\\"name\\\":\\\"Spreedly MV\\\",\\\"legal_name\\\":\\\"Longbob Longsen\\\",\\\"contact_name\\\":\\\"Longbob Longsen\\\",\\\"phone_number\\\":\\\"+573017654567\\\",\\\"logo_url\\\":null,\\\"legal_id_type\\\":\\\"CC\\\",\\\"email\\\":\\\"longbob@example.com\\\",\\\"legal_id\\\":\\\"14671275\\\"},\\\"taxes\\\":[]}}\"\nread 621 bytes\nConn close\n" + TRANSCRIPT + end + + def post_scrubbed + <<~SCRUBBED + "opening connection to sandbox.wompi.co:443...\nopened\nstarting SSL for sandbox.wompi.co:443...\nSSL established\n<- \"POST /v1/transactions_sync HTTP/1.1\\r\\nContent-Type: application/x-www-form-urlencoded\\r\\nAuthorization: Bearer [REDACTED]\\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: sandbox.wompi.co\\r\\nContent-Length: 282\\r\\n\\r\\n\"\n<- \"{\\\"reference\\\":\\\"rk6PBsDIxaBH\\\",\\\"public_key\\\":\\\"pub_test_RGehkiIXU3opWWryDE6jByz4W9kq6Hdk\\\",\\\"amount_in_cents\\\":150000,\\\"currency\\\":\\\"COP\\\",\\\"payment_method\\\":{\\\"type\\\":\\\"CARD\\\",\\\"number\\\":\\\"[REDACTED]\\\",\\\"exp_month\\\":\\\"09\\\",\\\"exp_year\\\":\\\"22\\\",\\\"installments\\\":2,\\\"cvc\\\":\\\"[REDACTED]\\\",\\\"card_holder\\\":\\\"Longbob Longsen\\\"}}\"\n-> \"HTTP/1.1 200 OK\\r\\n\"\n-> \"Content-Type: application/json\\r\\n\"\n-> \"Content-Length: 621\\r\\n\"\n-> \"Connection: close\\r\\n\"\n-> \"Date: Wed, 27 Oct 2021 03:48:17 GMT\\r\\n\"\n-> \"x-amzn-RequestId: 9a64471b-b1ab-4835-9ef0-70b3d6a838fc\\r\\n\"\n-> \"x-amz-apigw-id: H2TOPERIoAMF0kA=\\r\\n\"\n-> \"X-Amzn-Trace-Id: Root=1-6178cbf4-2ba684d62e9bd4bd04017a4b;Sampled=0\\r\\n\"\n-> \"X-Cache: Miss from cloudfront\\r\\n\"\n-> \"Via: 1.1 ee9de9e6182ae0c8e8f119177e905245.cloudfront.net (CloudFront)\\r\\n\"\n-> \"X-Amz-Cf-Pop: DEN50-C2\\r\\n\"\n-> \"X-Amz-Cf-Id: QJH1Iy_rtMcjnWs4FI44anx5cX6RNZbk6JnHd6wvxqlDZnKl5j4W5g==\\r\\n\"\n-> \"\\r\\n\"\nreading 621 bytes...\n-> \"{\\\"data\\\":{\\\"id\\\":\\\"113879-1635306496-65846\\\",\\\"created_at\\\":\\\"2021-10-27T03:48:17.706Z\\\",\\\"amount_in_cents\\\":150000,\\\"reference\\\":\\\"rk6PBsDIxaBH\\\",\\\"currency\\\":\\\"COP\\\",\\\"payment_method_type\\\":\\\"CARD\\\",\\\"payment_method\\\":{\\\"type\\\":\\\"CARD\\\",\\\"extra\\\":{\\\"name\\\":\\\"VISA-4242\\\",\\\"brand\\\":\\\"VISA\\\",\\\"last_four\\\":\\\"4242\\\",\\\"external_identifier\\\":\\\"JdGjsAGDPQ\\\"},\\\"installments\\\":2},\\\"redirect_url\\\":null,\\\"status\\\":\\\"APPROVED\\\",\\\"status_message\\\":null,\\\"merchant\\\":{\\\"name\\\":\\\"Spreedly MV\\\",\\\"legal_name\\\":\\\"Longbob Longsen\\\",\\\"contact_name\\\":\\\"Longbob Longsen\\\",\\\"phone_number\\\":\\\"[REDACTED]\\\",\\\"logo_url\\\":null,\\\"legal_id_type\\\":\\\"CC\\\",\\\"email\\\":\\\"[REDACTED]\\\",\\\"legal_id\\\":\\\"[REDACTED]\\\"},\\\"taxes\\\":[]}}\"\nread 621 bytes\nConn close\n" + SCRUBBED + end + + def successful_purchase_response + %( + {"data":{"id":"113879-1635300853-71494","created_at":"2021-10-27T02:14:16.181Z","amount_in_cents":150000,"reference":"b4DxpcrtsvRs","currency":"COP","payment_method_type":"CARD","payment_method":{"type":"CARD","extra":{"name":"VISA-4242","brand":"VISA","last_four":"4242","external_identifier":"fOYBYDRGuP"},"installments":2},"redirect_url":null,"status":"APPROVED","status_message":null,"merchant":{"name":"Spreedly MV","legal_name":"Longbob Longsen","contact_name":"Longbob Longsen","phone_number":"+573017654567","logo_url":null,"legal_id_type":"CC","email":"longbob@example.com","legal_id":"14671275"},"taxes":[]}} + ) + end + + def failed_purchase_response + %( + {"data":{"id":"113879-1635300920-47863","created_at":"2021-10-27T02:15:21.455Z","amount_in_cents":150000,"reference":"sljAsra9maeh","currency":"COP","payment_method_type":"CARD","payment_method":{"type":"CARD","extra":{"name":"VISA-1111","brand":"VISA","last_four":"1111","external_identifier":"liEZAwoiCD"},"installments":2},"redirect_url":null,"status":"DECLINED","status_message":"La transacción fue rechazada (Sandbox)","merchant":{"name":"Spreedly MV","legal_name":"Longbob Longsen","contact_name":"Longbob Longsen","phone_number":"+573017654567","logo_url":null,"legal_id_type":"CC","email":"longbob@example.com","legal_id":"14671275"},"taxes":[]}} + ) + end + + def successful_authorize_response + %( + {"data":{"id":19930,"public_data":{"type":"CARD","financial_operation":"PREAUTHORIZATION","amount_in_cents":1500,"number_of_installments":1,"currency":"COP"},"token":"tok_test_13879_29dbd1E75A7dc06e42bE08dbad959771","type":"CARD","status":"AVAILABLE","customer_email":null}} + ) + end + + def successful_capture_response + %( + {"data":{"id":"113879-1638483506-80282","created_at":"2021-12-02T22:18:27.877Z","amount_in_cents":160000,"reference":"larenciadediana3","currency":"COP","payment_method_type":"CARD","payment_method":{"type":"CARD","extra":{"name":"VISA-4242","brand":"VISA","last_four":"4242","external_identifier":"N4Dup17YZn"}},"redirect_url":null,"status":"APPROVED","status_message":null,"merchant":{"name":"Spreedly MV","legal_name":"Miguel Valencia","contact_name":"Miguel Valencia","phone_number":"+573117654567","logo_url":null,"legal_id_type":"CC","email":"mvalencia@spreedly.com","legal_id":"14671275"},"taxes":[]}} + ) + end + + def failed_capture_response + %( + {"data":{"id":"113879-1638802203-50693","created_at":"2021-12-06T14:50:04.497Z","amount_in_cents":160000,"reference":"larencia987diana37","currency":"COP","payment_method_type":"CARD","payment_method":{"type":"CARD","extra":{"name":"VISA-1111","brand":"VISA","last_four":"1111","external_identifier":"1cAREQ60RX"}},"redirect_url":null,"status":"DECLINED","status_message":"La transacción fue rechazada (Sandbox)","merchant":{"name":"Spreedly MV","legal_name":"Miguel Valencia","contact_name":"Miguel Valencia","phone_number":"+573117654567","logo_url":null,"legal_id_type":"CC","email":"mvalencia@spreedly.com","legal_id":"14671275"},"taxes":[]}} + ) + end + + def successful_refund_response + %( + {"data":{"id":61,"created_at":"2021-10-27T02:16:55.333Z","transaction_id":"113879-1635301011-28454","status":"APPROVED","amount_in_cents":150000}} + ) + end + + def failed_refund_response + %( + {"error":{"type":"INPUT_VALIDATION_ERROR","messages":{"transaction_id":["transaction_id Debe ser completado"]}}} + ) + end + + def successful_void_response + %( + {"data":{"status":"APPROVED","status_message":null,"transaction":{"id":"113879-1635301067-17128","created_at":"2021-10-27T02:17:48.368Z","finalized_at":"2021-10-27T02:17:48.000Z","amount_in_cents":150000,"reference":"89BFPG90NHAY","customer_email":null,"currency":"COP","payment_method_type":"CARD","payment_method":{"type":"CARD","extra":{"bin":"424242","name":"VISA-4242","brand":"VISA","exp_year":"22","exp_month":"09","last_four":"4242","card_holder":"Longbob Longsen","external_identifier":"hRd7HK6Euo"},"installments":2},"status":"APPROVED","status_message":null,"billing_data":null,"shipping_address":null,"redirect_url":null,"payment_source_id":null,"payment_link_id":null,"customer_data":null,"bill_id":null,"taxes":[]}},"meta":{"trace_id":"03951732b922897303397336c99e2523"}} + ) + end + + def failed_void_response + %( + {"error":{"type":"NOT_FOUND_ERROR","reason":"La entidad solicitada no existe"},"meta":{"trace_id":"f9a18f00e69e61c14bf0abe507d8d110"}} + ) + end +end diff --git a/test/unit/gateways/world_net_test.rb b/test/unit/gateways/world_net_test.rb index d668b615cf6..c32728f69b9 100644 --- a/test/unit/gateways/world_net_test.rb +++ b/test/unit/gateways/world_net_test.rb @@ -191,70 +191,70 @@ def post_scrubbed end def successful_purchase_response - %q(<?xml version="1.0" encoding="UTF-8"?> -<PAYMENTRESPONSE><UNIQUEREF>GZG6IG6VXI</UNIQUEREF><RESPONSECODE>A</RESPONSECODE><RESPONSETEXT>APPROVAL</RESPONSETEXT><APPROVALCODE>475318</APPROVALCODE><DATETIME>2015-09-14T21:22:12</DATETIME><AVSRESPONSE>X</AVSRESPONSE><CVVRESPONSE>M</CVVRESPONSE><HASH>f8642d613c56628371a579443ce8d895</HASH></PAYMENTRESPONSE>) + '<?xml version="1.0" encoding="UTF-8"?> +<PAYMENTRESPONSE><UNIQUEREF>GZG6IG6VXI</UNIQUEREF><RESPONSECODE>A</RESPONSECODE><RESPONSETEXT>APPROVAL</RESPONSETEXT><APPROVALCODE>475318</APPROVALCODE><DATETIME>2015-09-14T21:22:12</DATETIME><AVSRESPONSE>X</AVSRESPONSE><CVVRESPONSE>M</CVVRESPONSE><HASH>f8642d613c56628371a579443ce8d895</HASH></PAYMENTRESPONSE>' end def failed_purchase_response - %q(<?xml version="1.0" encoding="UTF-8"?> -<PAYMENTRESPONSE><UNIQUEREF>JQU1810S4E</UNIQUEREF><RESPONSECODE>D</RESPONSECODE><RESPONSETEXT>DECLINED</RESPONSETEXT><APPROVALCODE></APPROVALCODE><DATETIME>2015-09-14T21:40:07</DATETIME><AVSRESPONSE></AVSRESPONSE><CVVRESPONSE></CVVRESPONSE><HASH>c0ba33a10a6388b12c8fad79a107f2b5</HASH></PAYMENTRESPONSE>) + '<?xml version="1.0" encoding="UTF-8"?> +<PAYMENTRESPONSE><UNIQUEREF>JQU1810S4E</UNIQUEREF><RESPONSECODE>D</RESPONSECODE><RESPONSETEXT>DECLINED</RESPONSETEXT><APPROVALCODE></APPROVALCODE><DATETIME>2015-09-14T21:40:07</DATETIME><AVSRESPONSE></AVSRESPONSE><CVVRESPONSE></CVVRESPONSE><HASH>c0ba33a10a6388b12c8fad79a107f2b5</HASH></PAYMENTRESPONSE>' end def successful_authorize_response - %q(<?xml version="1.0" encoding="UTF-8"?> -<PREAUTHRESPONSE><UNIQUEREF>BF4CNN6WXP</UNIQUEREF><RESPONSECODE>A</RESPONSECODE><RESPONSETEXT>APPROVAL</RESPONSETEXT><APPROVALCODE>450848</APPROVALCODE><DATETIME>2015-09-14T21:53:10</DATETIME><AVSRESPONSE></AVSRESPONSE><CVVRESPONSE></CVVRESPONSE><HASH>e80c52476af1dd969f3bf89ed02fe16f</HASH></PREAUTHRESPONSE>) + '<?xml version="1.0" encoding="UTF-8"?> +<PREAUTHRESPONSE><UNIQUEREF>BF4CNN6WXP</UNIQUEREF><RESPONSECODE>A</RESPONSECODE><RESPONSETEXT>APPROVAL</RESPONSETEXT><APPROVALCODE>450848</APPROVALCODE><DATETIME>2015-09-14T21:53:10</DATETIME><AVSRESPONSE></AVSRESPONSE><CVVRESPONSE></CVVRESPONSE><HASH>e80c52476af1dd969f3bf89ed02fe16f</HASH></PREAUTHRESPONSE>' end def failed_authorize_response - %q(<?xml version="1.0" encoding="UTF-8"?> -<PREAUTHRESPONSE><UNIQUEREF>IP0PUDDXG5</UNIQUEREF><RESPONSECODE>D</RESPONSECODE><RESPONSETEXT>DECLINED</RESPONSETEXT><APPROVALCODE></APPROVALCODE><DATETIME>2015-09-15T14:21:37</DATETIME><AVSRESPONSE></AVSRESPONSE><CVVRESPONSE></CVVRESPONSE><HASH>05dfa85163ee8d8afa8711019f64acb3</HASH></PREAUTHRESPONSE>) + '<?xml version="1.0" encoding="UTF-8"?> +<PREAUTHRESPONSE><UNIQUEREF>IP0PUDDXG5</UNIQUEREF><RESPONSECODE>D</RESPONSECODE><RESPONSETEXT>DECLINED</RESPONSETEXT><APPROVALCODE></APPROVALCODE><DATETIME>2015-09-15T14:21:37</DATETIME><AVSRESPONSE></AVSRESPONSE><CVVRESPONSE></CVVRESPONSE><HASH>05dfa85163ee8d8afa8711019f64acb3</HASH></PREAUTHRESPONSE>' end def successful_capture_response - %q(<?xml version="1.0" encoding="UTF-8"?> -<PREAUTHCOMPLETIONRESPONSE><UNIQUEREF>BF4CNN6WXP</UNIQUEREF><RESPONSECODE>A</RESPONSECODE><RESPONSETEXT>APPROVAL</RESPONSETEXT><APPROVALCODE>450848</APPROVALCODE><DATETIME>2015-09-14T21:53:10</DATETIME><AVSRESPONSE></AVSRESPONSE><CVVRESPONSE></CVVRESPONSE><HASH>e80c52476af1dd969f3bf89ed02fe16f</HASH></PREAUTHCOMPLETIONRESPONSE>) + '<?xml version="1.0" encoding="UTF-8"?> +<PREAUTHCOMPLETIONRESPONSE><UNIQUEREF>BF4CNN6WXP</UNIQUEREF><RESPONSECODE>A</RESPONSECODE><RESPONSETEXT>APPROVAL</RESPONSETEXT><APPROVALCODE>450848</APPROVALCODE><DATETIME>2015-09-14T21:53:10</DATETIME><AVSRESPONSE></AVSRESPONSE><CVVRESPONSE></CVVRESPONSE><HASH>e80c52476af1dd969f3bf89ed02fe16f</HASH></PREAUTHCOMPLETIONRESPONSE>' end def failed_capture_response - %q(<?xml version="1.0" encoding="UTF-8"?> -<ERROR><ERRORSTRING>cvc-minLength-valid: Value &apos;&apos; with length = &apos;0&apos; is not facet-valid with respect to minLength &apos;10&apos; for type &apos;UID&apos;.</ERRORSTRING></ERROR>) + '<?xml version="1.0" encoding="UTF-8"?> +<ERROR><ERRORSTRING>cvc-minLength-valid: Value &apos;&apos; with length = &apos;0&apos; is not facet-valid with respect to minLength &apos;10&apos; for type &apos;UID&apos;.</ERRORSTRING></ERROR>' end def successful_refund_response - %q(<?xml version="1.0" encoding="UTF-8"?> -<REFUNDRESPONSE><RESPONSECODE>A</RESPONSECODE><RESPONSETEXT>SUCCESS</RESPONSETEXT><UNIQUEREF>GIOH10II2J</UNIQUEREF><DATETIME>15-09-2015:14:44:17:999</DATETIME><HASH>aebd69e9db6e4b0db7ecbae79a2970a0</HASH></REFUNDRESPONSE>) + '<?xml version="1.0" encoding="UTF-8"?> +<REFUNDRESPONSE><RESPONSECODE>A</RESPONSECODE><RESPONSETEXT>SUCCESS</RESPONSETEXT><UNIQUEREF>GIOH10II2J</UNIQUEREF><DATETIME>15-09-2015:14:44:17:999</DATETIME><HASH>aebd69e9db6e4b0db7ecbae79a2970a0</HASH></REFUNDRESPONSE>' end def failed_refund_response - %q(<?xml version="1.0" encoding="UTF-8"?> -<ERROR><ERRORSTRING>cvc-minLength-valid: Value &apos;&apos; with length = &apos;0&apos; is not facet-valid with respect to minLength &apos;10&apos; for type &apos;UID&apos;.</ERRORSTRING></ERROR>) + '<?xml version="1.0" encoding="UTF-8"?> +<ERROR><ERRORSTRING>cvc-minLength-valid: Value &apos;&apos; with length = &apos;0&apos; is not facet-valid with respect to minLength &apos;10&apos; for type &apos;UID&apos;.</ERRORSTRING></ERROR>' end def successful_void_response end def successful_store_response - %q(<?xml version="1.0" encoding="UTF-8"?> -<SECURECARDREGISTRATIONRESPONSE><MERCHANTREF>146304412401</MERCHANTREF><CARDREFERENCE>2967530956419033</CARDREFERENCE><DATETIME>12-05-2016:10:08:46:269</DATETIME><HASH>b2e497d14014ad9f4770edbf7716435e</HASH></SECURECARDREGISTRATIONRESPONSE>) + '<?xml version="1.0" encoding="UTF-8"?> +<SECURECARDREGISTRATIONRESPONSE><MERCHANTREF>146304412401</MERCHANTREF><CARDREFERENCE>2967530956419033</CARDREFERENCE><DATETIME>12-05-2016:10:08:46:269</DATETIME><HASH>b2e497d14014ad9f4770edbf7716435e</HASH></SECURECARDREGISTRATIONRESPONSE>' end def failed_store_response - %q(<?xml version="1.0" encoding="UTF-8"?> -<ERROR><ERRORCODE>E11</ERRORCODE><ERRORSTRING>INVALID CARDEXPIRY</ERRORSTRING></ERROR>) + '<?xml version="1.0" encoding="UTF-8"?> +<ERROR><ERRORCODE>E11</ERRORCODE><ERRORSTRING>INVALID CARDEXPIRY</ERRORSTRING></ERROR>' end def successful_unstore_response - %q(<?xml version="1.0" encoding="UTF-8"?> -<SECURECARDREMOVALRESPONSE><MERCHANTREF>146304412401</MERCHANTREF><DATETIME>12-05-2016:10:08:48:399</DATETIME><HASH>7f755e185be8066a535699755f709646</HASH></SECURECARDREMOVALRESPONSE>) + '<?xml version="1.0" encoding="UTF-8"?> +<SECURECARDREMOVALRESPONSE><MERCHANTREF>146304412401</MERCHANTREF><DATETIME>12-05-2016:10:08:48:399</DATETIME><HASH>7f755e185be8066a535699755f709646</HASH></SECURECARDREMOVALRESPONSE>' end def failed_unstore_response - %q(<?xml version="1.0" encoding="UTF-8"?> -<ERROR><ERRORCODE>E04</ERRORCODE><ERRORSTRING>INVALID REFERENCE DETAILS</ERRORSTRING></ERROR>) + '<?xml version="1.0" encoding="UTF-8"?> +<ERROR><ERRORCODE>E04</ERRORCODE><ERRORSTRING>INVALID REFERENCE DETAILS</ERRORSTRING></ERROR>' end def failed_void_response - %q(<?xml version="1.0" encoding="UTF-8"?> -<ERROR><ERRORSTRING>cvc-elt.1: Cannot find the declaration of element &apos;VOID&apos;.</ERRORSTRING></ERROR>) + '<?xml version="1.0" encoding="UTF-8"?> +<ERROR><ERRORSTRING>cvc-elt.1: Cannot find the declaration of element &apos;VOID&apos;.</ERRORSTRING></ERROR>' end end diff --git a/test/unit/gateways/worldpay_online_payments_test.rb b/test/unit/gateways/worldpay_online_payments_test.rb index e97d3b457e0..124f8b3e179 100644 --- a/test/unit/gateways/worldpay_online_payments_test.rb +++ b/test/unit/gateways/worldpay_online_payments_test.rb @@ -43,7 +43,7 @@ def test_successful_authorize_and_capture assert_success authorize @gateway.expects(:ssl_request).returns(successful_capture_response) - assert capture = @gateway.capture(@amount-1, authorize.authorization) + assert capture = @gateway.capture(@amount - 1, authorize.authorization) assert_success capture end @@ -71,7 +71,7 @@ def test_partial_capture assert_success authorize @gateway.expects(:ssl_request).returns(successful_capture_response) - assert capture = @gateway.capture(@amount-1, authorize.authorization) + assert capture = @gateway.capture(@amount - 1, authorize.authorization) assert_success capture end @@ -198,9 +198,11 @@ def test_invalid_login def successful_token_response %({"token": "TEST_RU_8fcc4f2f-8c0d-483d-a0a3-eaad7356623e","paymentMethod": {"type": "ObfuscatedCard","name": "Longbob Longsen","expiryMonth": 10,"expiryYear": 2016,"cardType": "VISA","maskedCardNumber": "**** **** **** 1111"},"reusable": true}) end + def successful_authorize_response %({"orderCode": "a46502d0-80ba-425b-a6db-2c57e9de91da","token": "TEST_RU_8fcc4f2f-8c0d-483d-a0a3-eaad7356623e","orderDescription": "Test Purchase","amount": 0,"currencyCode": "GBP","authorizeOnly": true,"paymentStatus": "AUTHORIZED","paymentResponse": {"type": "ObfuscatedCard","name": "Longbob Longsen","expiryMonth": 10,"expiryYear": 2016,"cardType": "VISA_CREDIT","maskedCardNumber": "**** **** **** 1111"},"environment": "TEST","authorizedAmount": 1000}) end + def failed_authorize_response %({"httpStatusCode":400,"customCode":"BAD_REQUEST","message":"CVC can't be null/empty","description":"Some of request parameters are invalid, please check your request. For more information please refer to Json schema.","errorHelpUrl":null,"originalRequest":"{'reusable':false,'paymentMethod':{'type':'Card','name':'Example Name','expiryMonth':'**','expiryYear':'****','cardNumber':'**** **** **** 1111','cvc':''},'clientKey':'T_C_845d39f4-f33c-430c-8fca-ad89bf1e5810'}"} ) end diff --git a/test/unit/gateways/worldpay_test.rb b/test/unit/gateways/worldpay_test.rb index 2fb4297ffcb..d0a9bc9412a 100644 --- a/test/unit/gateways/worldpay_test.rb +++ b/test/unit/gateways/worldpay_test.rb @@ -4,40 +4,446 @@ class WorldpayTest < Test::Unit::TestCase include CommStub def setup - @gateway = WorldpayGateway.new( - :login => 'testlogin', - :password => 'testpassword' + @gateway = WorldpayGateway.new( + login: 'testlogin', + password: 'testpassword' ) @amount = 100 @credit_card = credit_card('4242424242424242') - @options = {:order_id => 1} + @token = '|99411111780163871111|shopper|59424549c291397379f30c5c082dbed8' + @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: 'visa', + eci: 5, + source: :network_token, + 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' + ) + @sodexo_voucher = credit_card('6060704495764400', brand: 'sodexo') + @options = { order_id: 1 } + @store_options = { + customer: '59424549c291397379f30c5c082dbed8', + email: 'wow@example.com' + } + @sub_merchant_options = { + sub_merchant_data: { + pf_id: '12345678901', + sub_name: 'Example Shop', + sub_id: '1234567' + } + } + + @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 + ) + + @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_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', + tax_amount: '20', + ship_from_postal_code: '43245', + destination_postal_code: '54545', + destination_country_code: 'CO', + order_date: { + day_of_month: Date.today.day, + month: Date.today.month, + year: Date.today.year + } + } + } + + @level_three_data = { + level_3_data: { + customer_reference: 'CUST00000102', + card_acceptor_tax_id: 'VAT1999285', + 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: '1500', + unit_of_measure: 'each', + 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' + }] + } + } + + @aft_options = { + account_funding_transaction: true, + aft_type: 'A', + aft_payment_purpose: '01', + aft_sender_account_type: '02', + aft_sender_account_reference: '4111111111111112', + aft_sender_full_name: { + first: 'First', + middle: 'Middle', + last: 'Sender' + }, + aft_sender_funding_address: { + address1: '123 Sender St', + address2: 'Apt 1', + postal_code: '12345', + city: 'Senderville', + state: 'NC', + country_code: 'US' + }, + aft_recipient_account_type: '03', + aft_recipient_account_reference: '4111111111111111', + aft_recipient_full_name: { + first: 'First', + middle: 'Middle', + last: 'Recipient' + }, + aft_recipient_funding_address: { + address1: '123 Recipient St', + address2: 'Apt 1', + postal_code: '12345', + city: 'Recipientville', + state: 'NC', + country_code: 'US' + }, + aft_recipient_funding_data: { + telephone_number: '123456789', + birth_date: { + day_of_month: '01', + month: '01', + year: '1980' + } + } + } + + @aft_less_options = { + account_funding_transaction: true, + aft_type: 'A', + aft_payment_purpose: '01', + aft_sender_account_type: '02', + aft_sender_account_reference: '4111111111111112', + aft_sender_full_name: { + first: 'First', + last: 'Sender' + }, + aft_sender_funding_address: { + address1: '123 Sender St', + postal_code: '12345', + city: 'Senderville', + state: 'NC', + country_code: 'US' + }, + aft_recipient_account_type: '03', + aft_recipient_account_reference: '4111111111111111', + aft_recipient_full_name: { + first: 'First', + last: 'Recipient' + }, + aft_recipient_funding_address: { + address1: '123 Recipient St', + postal_code: '12345', + city: 'Recipientville', + state: 'NC', + country_code: 'US' + }, + aft_recipient_funding_data: { + telephone_number: '123456789', + birth_date: { + day_of_month: '01', + month: '01', + year: '1980' + } + } + } + end + + def test_supported_card_types + assert_equal WorldpayGateway.supported_cardtypes, %i[visa master american_express discover jcb maestro elo naranja cabal unionpay patagonia_365 tarjeta_sol] + 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_payment_method_is_nt_credit_card_and_not_encrypted + payment_method = @nt_credit_card + payment_method.stubs(source: nil, payment_cryptogram: nil, eci: nil, payment_data: 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_purchase_checking_idempotency_header + headers_list = [] + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge!({ idempotency_key: 'test123' })) + end.check_request do |_endpoint, _data, headers| + headers_list << headers + end.respond_with(successful_authorize_response, successful_capture_response) + assert_not_equal headers_list[0]['Idempotency-Key'], headers_list[1]['Idempotency-Key'] + assert_success response end def test_successful_authorize response = stub_comms do @gateway.authorize(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + 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 end + def test_successful_authorize_encrypted_apple_pay + apple_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ + source: :apple_pay, + payment_data: { + version: 'EC_v1', + data: 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9', + signature: 'signature', + header: { + ephemeralPublicKey: 'ephemeralPublicKey', + publicKeyHash: 'publicKeyHash', + transactionId: 'transactionId' + } + } + }) + + stub_comms do + @gateway.authorize(@amount, apple_pay, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/APPLEPAY-SSL/, data) + assert_match(%r(<version>EC_v1</version>), data) + assert_match(%r(<transactionId>transactionId</transactionId>), data) + assert_match(%r(<data>QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9</data>), data) + end.respond_with(successful_authorize_response) + end + + def test_successful_authorize_encrypted_google_pay + google_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ + source: :google_pay, + payment_data: { + version: 'EC_v1', + data: 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9', + signature: 'signature', + header: { + ephemeralPublicKey: 'ephemeralPublicKey', + publicKeyHash: 'publicKeyHash', + transactionId: 'transactionId' + } + } + }) + + stub_comms do + @gateway.authorize(@amount, google_pay, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/PAYWITHGOOGLE-SSL/, data) + assert_match(%r(<protocolVersion>EC_v1</protocolVersion>), data) + assert_match(%r(<signature>signature</signature>), data) + end.respond_with(successful_authorize_response) + end + + def test_encrypted_payment_with_invalid_source + apple_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ + source: :android_pay, + payment_data: { + version: 'EC_v1', + data: 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9', + signature: 'signature', + header: { + ephemeralPublicKey: 'ephemeralPublicKey', + publicKeyHash: 'publicKeyHash', + transactionId: 'transactionId' + } + } + }) + error = assert_raises(ArgumentError) do + @gateway.authorize(@amount, apple_pay, @options) + end + + assert_equal 'Invalid encrypted wallet source', error.message + end + def test_successful_authorize_by_reference response = stub_comms do @gateway.authorize(@amount, @options[:order_id].to_s, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/payAsOrder/, data) end.respond_with(successful_authorize_response) assert_success response assert_equal 'R50704213207145707', response.authorization end + def test_exemption_in_request + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ exemption_type: 'LV', exemption_placement: 'AUTHENTICATION' })) + end.check_request do |_endpoint, data, _headers| + assert_match(/exemption/, data) + assert_match(/AUTHENTICATION/, data) + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_risk_data_in_request + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(risk_data:)) + end.check_request do |_endpoint, data, _headers| + doc = Nokogiri::XML(data) + + authentication_risk_data = doc.at_xpath('//riskData//authenticationRiskData') + assert_equal(risk_data[:authentication_risk_data][:authentication_method], authentication_risk_data.attribute('authenticationMethod').value) + + timestamp = doc.at_xpath('//riskData//authenticationRiskData//authenticationTimestamp//date') + assert_equal(risk_data[:authentication_risk_data][:authentication_date][:day_of_month], timestamp.attribute('dayOfMonth').value) + assert_equal(risk_data[:authentication_risk_data][:authentication_date][:month], timestamp.attribute('month').value) + assert_equal(risk_data[:authentication_risk_data][:authentication_date][:year], timestamp.attribute('year').value) + assert_equal(risk_data[:authentication_risk_data][:authentication_date][:hour], timestamp.attribute('hour').value) + assert_equal(risk_data[:authentication_risk_data][:authentication_date][:minute], timestamp.attribute('minute').value) + assert_equal(risk_data[:authentication_risk_data][:authentication_date][:second], timestamp.attribute('second').value) + + shopper_account_risk_data_xml = doc.at_xpath('//riskData//shopperAccountRiskData') + shopper_account_risk_data = risk_data[:shopper_account_risk_data] + assert_equal(shopper_account_risk_data[:transactions_attempted_last_day], shopper_account_risk_data_xml.attribute('transactionsAttemptedLastDay').value) + assert_equal(shopper_account_risk_data[:transactions_attempted_last_year], shopper_account_risk_data_xml.attribute('transactionsAttemptedLastYear').value) + assert_equal(shopper_account_risk_data[:purchases_completed_last_six_months], shopper_account_risk_data_xml.attribute('purchasesCompletedLastSixMonths').value) + assert_equal(shopper_account_risk_data[:add_card_attempts_last_day], shopper_account_risk_data_xml.attribute('addCardAttemptsLastDay').value) + assert_equal(shopper_account_risk_data[:previous_suspicious_activity], shopper_account_risk_data_xml.attribute('previousSuspiciousActivity').value) + assert_equal(shopper_account_risk_data[:shipping_name_matches_account_name], shopper_account_risk_data_xml.attribute('shippingNameMatchesAccountName').value) + assert_equal(shopper_account_risk_data[:shopper_account_age_indicator], shopper_account_risk_data_xml.attribute('shopperAccountAgeIndicator').value) + assert_equal(shopper_account_risk_data[:shopper_account_change_indicator], shopper_account_risk_data_xml.attribute('shopperAccountChangeIndicator').value) + assert_equal(shopper_account_risk_data[:shopper_account_password_change_indicator], shopper_account_risk_data_xml.attribute('shopperAccountPasswordChangeIndicator').value) + assert_equal(shopper_account_risk_data[:shopper_account_shipping_address_usage_indicator], shopper_account_risk_data_xml.attribute('shopperAccountShippingAddressUsageIndicator').value) + assert_equal(shopper_account_risk_data[:shopper_account_payment_account_indicator], shopper_account_risk_data_xml.attribute('shopperAccountPaymentAccountIndicator').value) + assert_date_element(shopper_account_risk_data[:shopper_account_creation_date], shopper_account_risk_data_xml.at_xpath('//shopperAccountCreationDate//date')) + assert_date_element(shopper_account_risk_data[:shopper_account_modification_date], shopper_account_risk_data_xml.at_xpath('//shopperAccountModificationDate//date')) + assert_date_element(shopper_account_risk_data[:shopper_account_password_change_date], shopper_account_risk_data_xml.at_xpath('//shopperAccountPasswordChangeDate//date')) + assert_date_element(shopper_account_risk_data[:shopper_account_shipping_address_first_use_date], shopper_account_risk_data_xml.at_xpath('//shopperAccountShippingAddressFirstUseDate//date')) + assert_date_element(shopper_account_risk_data[:shopper_account_payment_account_first_use_date], shopper_account_risk_data_xml.at_xpath('//shopperAccountPaymentAccountFirstUseDate//date')) + + transaction_risk_data_xml = doc.at_xpath('//riskData//transactionRiskData') + transaction_risk_data = risk_data[:transaction_risk_data] + assert_equal(transaction_risk_data[:shipping_method], transaction_risk_data_xml.attribute('shippingMethod').value) + assert_equal(transaction_risk_data[:delivery_timeframe], transaction_risk_data_xml.attribute('deliveryTimeframe').value) + assert_equal(transaction_risk_data[:delivery_email_address], transaction_risk_data_xml.attribute('deliveryEmailAddress').value) + assert_equal(transaction_risk_data[:reordering_previous_purchases], transaction_risk_data_xml.attribute('reorderingPreviousPurchases').value) + assert_equal(transaction_risk_data[:pre_order_purchase], transaction_risk_data_xml.attribute('preOrderPurchase').value) + assert_equal(transaction_risk_data[:gift_card_count], transaction_risk_data_xml.attribute('giftCardCount').value) + + amount_xml = doc.at_xpath('//riskData//transactionRiskData//transactionRiskDataGiftCardAmount//amount') + amount_data = transaction_risk_data[:transaction_risk_data_gift_card_amount] + assert_equal(amount_data[:value], amount_xml.attribute('value').value) + assert_equal(amount_data[:currency], amount_xml.attribute('currencyCode').value) + assert_equal(amount_data[:exponent], amount_xml.attribute('exponent').value) + assert_equal(amount_data[:debit_credit_indicator], amount_xml.attribute('debitCreditIndicator').value) + + assert_date_element(transaction_risk_data[:transaction_risk_data_pre_order_date], transaction_risk_data_xml.at_xpath('//transactionRiskDataPreOrderDate//date')) + end.respond_with(successful_authorize_response) + assert_success response + end + def test_successful_reference_transaction_authorize_with_merchant_code response = stub_comms do - @gateway.authorize(@amount, @options[:order_id].to_s, @options.merge({ merchant_code: 'testlogin2'})) - end.check_request do |endpoint, data, headers| + @gateway.authorize(@amount, @options[:order_id].to_s, @options.merge({ merchant_code: 'testlogin2' })) + end.check_request do |_endpoint, data, _headers| assert_match(/testlogin2/, data) end.respond_with(successful_authorize_response) assert_success response @@ -47,17 +453,125 @@ def test_successful_reference_transaction_authorize_with_merchant_code def test_authorize_passes_ip_and_session_id response = stub_comms do @gateway.authorize(@amount, @credit_card, @options.merge(ip: '127.0.0.1', session_id: '0215ui8ib1')) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/<session shopperIPAddress="127.0.0.1" id="0215ui8ib1"\/>/, data) end.respond_with(successful_authorize_response) assert_success response end + def test_authorize_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, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<storedCredentials usage\=\"USED\" merchantInitiatedReason\=\"UNSCHEDULED\"\>/, data) + assert_match(/<schemeTransactionIdentifier\>000000000000020005060720116005060\<\/schemeTransactionIdentifier\>/, data) + end.respond_with(successful_authorize_response) + 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(/<storedCredentials usage\=\"USED\" merchantInitiatedReason\=\"UNSCHEDULED\"\>/, data) + assert_match(/<schemeTransactionIdentifier\>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(/<storedCredentials usage\=\"USED\" merchantInitiatedReason\=\"UNSCHEDULED\"\>/, data) + assert_match(/<schemeTransactionIdentifier\>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(/<storedCredentials usage\=\"FIRST\" merchantInitiatedReason\=\"RECURRING\"\>/, 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(/<storedCredentials usage\=\"USED\" merchantInitiatedReason\=\"RECURRING\"\>/, data) + assert_match(/<schemeTransactionIdentifier\>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(/<storedCredentials usage\=\"FIRST\" merchantInitiatedReason\=\"INSTALMENT\"\>/, data) + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_authorize_passes_correct_stored_credentials_for_first_unscheduled + options = @options.merge( + stored_credential_usage: 'FIRST', + stored_credential_initiated_reason: 'UNSCHEDULED' + ) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<storedCredentials usage="FIRST" merchantInitiatedReason="UNSCHEDULED">/, 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 + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match %r(<pfId>12345678901</pfId>), data + assert_match %r(<subName>Example Shop</subName>), data + assert_match %r(<subId>1234567</subId>), data + end.respond_with(successful_authorize_response) + assert_success response + end + def test_failed_authorize response = stub_comms do @gateway.authorize(@amount, @credit_card, @options) end.respond_with(failed_authorize_response) assert_equal '7', response.error_code + assert_match 'Invalid payment details', response.message assert_failure response end @@ -68,20 +582,163 @@ def test_successful_purchase assert_success response end + def test_transaction_with_level_two_data + options = @options.merge(@level_two_data) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match %r(<invoiceReferenceNumber>INV12233565</invoiceReferenceNumber>), data + assert_match %r(<customerReference>CUST00000101</customerReference>), data + assert_match %r(<cardAcceptorTaxId>VAT1999292</cardAcceptorTaxId>), data + assert_match %r(<salesTax><amountvalue="20"currencyCode="GBP"exponent="2"/></salesTax>), data.gsub(/\s+/, '') + assert_match %r(<shipFromPostalCode>43245</shipFromPostalCode>), data + assert_match %r(<destinationPostalCode>54545</destinationPostalCode>), data + assert_match %r(<destinationCountryCode>CO</destinationCountryCode>), data + assert_match %r(<taxExempt>false</taxExempt>), data + assert_match %r(<orderDate><datedayOfMonth="#{Date.today.day}"month="#{Date.today.month}"year="#{Date.today.year}"/></orderDate>), 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(<invoiceReferenceNumber>INV12233565</invoiceReferenceNumber>), data + assert_match %r(<customerReference>CUST00000101</customerReference>), data + assert_match %r(<cardAcceptorTaxId>VAT1999292</cardAcceptorTaxId>), data + assert_match %r(<salesTax><amountvalue="0"currencyCode="GBP"exponent="2"/></salesTax>), data.gsub(/\s+/, '') + assert_match %r(<shipFromPostalCode>43245</shipFromPostalCode>), data + assert_match %r(<destinationPostalCode>54545</destinationPostalCode>), data + assert_match %r(<destinationCountryCode>CO</destinationCountryCode>), data + assert_match %r(<taxExempt>true</taxExempt>), data + assert_match %r(<orderDate><datedayOfMonth="#{Date.today.day}"month="#{Date.today.month}"year="#{Date.today.year}"/></orderDate>), data.gsub(/\s+/, '') + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_transaction_with_level_three_data + options = @options.merge(@level_three_data) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match %r(<customerReference>CUST00000102</customerReference>), data + assert_match %r(<cardAcceptorTaxId>VAT1999285</cardAcceptorTaxId>), data + assert_match %r(<salesTax><amountvalue="20"currencyCode="GBP"exponent="2"/></salesTax>), data.gsub(/\s+/, '') + assert_match %r(<discountAmount><amountvalue="1"currencyCode="GBP"exponent="2"/></discountAmount>), data.gsub(/\s+/, '') + assert_match %r(<shippingAmount><amountvalue="50"currencyCode="GBP"exponent="2"/></shippingAmount>), data.gsub(/\s+/, '') + assert_match %r(<dutyAmount><amountvalue="20"currencyCode="GBP"exponent="2"/></dutyAmount>), data.gsub(/\s+/, '') + assert_match %r(<item><description>Laptop14</description><productCode>LP00125</productCode><commodityCode>COM00125</commodityCode><quantity>2</quantity><unitCost><amountvalue=\"1500\"currencyCode=\"GBP\"exponent=\"2\"/></unitCost><unitOfMeasure>each</unitOfMeasure><itemTotal><amountvalue=\"3000\"currencyCode=\"GBP\"exponent=\"2\"/></itemTotal><itemTotalWithTax><amountvalue=\"4000\"currencyCode=\"GBP\"exponent=\"2\"/></itemTotalWithTax><itemDiscountAmount><amountvalue=\"0\"currencyCode=\"GBP\"exponent=\"2\"/></itemDiscountAmount><taxAmount><amountvalue=\"500\"currencyCode=\"GBP\"exponent=\"2\"/></taxAmount></item><item><description>Laptop15</description><productCode>LP00120</productCode><commodityCode>COM00125</commodityCode><quantity>2</quantity><unitCost><amountvalue=\"1000\"currencyCode=\"GBP\"exponent=\"2\"/></unitCost><unitOfMeasure>each</unitOfMeasure><itemTotal><amountvalue=\"2000\"currencyCode=\"GBP\"exponent=\"2\"/></itemTotal><itemTotalWithTax><amountvalue=\"3000\"currencyCode=\"GBP\"exponent=\"2\"/></itemTotalWithTax><itemDiscountAmount><amountvalue=\"0\"currencyCode=\"GBP\"exponent=\"2\"/></itemDiscountAmount><taxAmount><amountvalue=\"500\"currencyCode=\"GBP\"exponent=\"2\"/></taxAmount></item>), data.gsub(/\s+/, '') + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_successful_purchase_with_custom_string_fields + options = @options.merge(custom_string_fields: { custom_string_field_1: 'testvalue1', custom_string_field_2: 'testvalue2' }) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match %r(<FraudSightData>\n), data + assert_match %r(<customStringFields>\n), data + assert_match %r(<customStringField1>testvalue1</customStringField1>), data + assert_match %r(<customStringField2>testvalue2</customStringField2>), data + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_successful_purchase_with_sub_merchant_data + options = @options.merge(@sub_merchant_options) + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.respond_with(successful_authorize_response, successful_capture_response) + assert_success response + end + + def test_successful_purchase_skipping_capture + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(skip_capture: true)) + end.respond_with(successful_authorize_response, successful_capture_response) + assert response.responses.length == 1 + assert_success response + end + + def test_successful_purchase_with_network_token + response = stub_comms do + @gateway.purchase(@amount, @nt_credit_card, @options) + end.respond_with(successful_authorize_response, successful_capture_response) + assert_success response + end + + def test_successful_purchase_with_network_token_with_stored_credentials + response = stub_comms do + @gateway.purchase(@amount, @nt_credit_card, @options.merge(stored_credential_usage: 'FIRST', + stored_credential_transaction_id: '123', stored_credential: { initiator: 'merchant' })) + end.respond_with(successful_authorize_response, successful_capture_response) + assert_success response + end + + def test_success_purchase_with_network_token_with_stored_credentials_with_cit + response = stub_comms do + @gateway.purchase(@amount, @nt_credit_card, @options.merge(stored_credential_usage: 'FIRST', + stored_credential_transaction_id: '123', stored_credential: { initiator: 'cardholder' })) + end.check_request do |_endpoint, data, _headers| + element = Nokogiri::XML(data) + scheme_transaction_identifier = element.xpath('//schemeTransactionIdentifier') + assert_empty(scheme_transaction_identifier, 'XML should not contain <schemeTransactionIdentifier> element') + end.respond_with(successful_authorize_response, successful_capture_response) + assert_success response + end + + def test_successful_authorize_with_network_token_with_eci + response = stub_comms do + @gateway.authorize(@amount, @nt_credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match %r(<eciIndicator>05</eciIndicator>), data + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_successful_authorize_with_network_token_with_shopper_ip_address + response = stub_comms do + @gateway.authorize(@amount, @nt_credit_card, @options.merge(ip: '127.0.0.1', email: 'wow@example.com')) + end.check_request do |_endpoint, data, _headers| + assert_match(/<session shopperIPAddress=\"127.0.0.1\"\/>/, 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')) + end.respond_with(successful_authorize_with_elo_response, successful_capture_with_elo_response) + assert_success response + end + def test_purchase_passes_correct_currency response = stub_comms do @gateway.purchase(@amount, @credit_card, @options.merge(currency: 'CAD')) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/CAD/, data) end.respond_with(successful_authorize_response, successful_capture_response) assert_success response end + def test_successful_purchase_with_two_digits_year + response = stub_comms do + @gateway.purchase(@amount, @credit_card_with_two_digits_year, @options) + end.respond_with(successful_authorize_response, successful_capture_response) + assert_success response + end + def test_purchase_authorize_fails response = stub_comms do @gateway.purchase(@amount, @credit_card, @options) end.respond_with(failed_authorize_response) assert_failure response + assert_equal '7', response.error_code + assert_match 'Invalid payment details', response.message assert_equal 1, response.responses.size end @@ -97,7 +754,27 @@ def test_purchase_does_not_run_inquiry end.respond_with(successful_capture_response) assert_success response - assert_equal %w(authorize capture), response.responses.collect{|e| e.params['action']} + assert_equal(%w(authorize capture), response.responses.collect { |e| e.params['action'] }) + end + + def test_failed_purchase_with_issuer_response_code + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.respond_with(failed_purchase_response_with_issuer_response_code) + + assert_failure response + assert_equal('51', response.params['issuer_response_code']) + assert_equal('Insufficient funds/over credit limit', response.params['issuer_response_description']) + end + + def test_failed_purchase_without_active_merchant_generated_response_message + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.respond_with(failed_purchase_response_without_useful_error_from_gateway) + + assert_failure response + assert_equal('61', response.params['issuer_response_code']) + assert_equal('Exceeds withdrawal amount limit', response.message) end def test_successful_void @@ -109,6 +786,15 @@ def test_successful_void assert_equal '924e810350efc21a989e0ac7727ce43b', response.params['cancel_received_order_code'] end + def test_successful_void_with_elo + response = stub_comms do + @gateway.void(@options[:order_id], @options) + end.respond_with(successful_void_inquiry_with_elo_response, successful_void_with_elo_response) + assert_success response + assert_equal 'SUCCESS', response.message + assert_equal '3a10f83fb9bb765488d0b3eb153879d7', response.params['cancel_received_order_code'] + end + def test_void_fails_unless_status_is_authorized response = stub_comms do @gateway.void(@options[:order_id], @options) @@ -117,6 +803,23 @@ def test_void_fails_unless_status_is_authorized assert_equal "A transaction status of 'AUTHORISED' is required.", response.message end + def test_supports_network_tokenization + assert_instance_of TrueClass, @gateway.supports_network_tokenization? + end + + def test_void_using_order_id_embedded_with_token + response = stub_comms do + authorization = "#{@options[:order_id]}|99411111780163871111|shopper|59424549c291397379f30c5c082dbed8" + @gateway.void(authorization, @options) + end.check_request do |_endpoint, data, _headers| + assert_tag_with_attributes('orderInquiry', { 'orderCode' => @options[:order_id].to_s }, data) if %r(<orderInquiry .*?>).match?(data) + assert_tag_with_attributes('orderModification', { 'orderCode' => @options[:order_id].to_s }, data) if %r(<orderModification .*?>).match?(data) + end.respond_with(successful_void_inquiry_response, successful_void_response) + assert_success response + assert_equal 'SUCCESS', response.message + assert_equal '924e810350efc21a989e0ac7727ce43b', response.params['cancel_received_order_code'] + end + def test_successful_refund_for_captured_payment response = stub_comms do @gateway.refund(@amount, @options[:order_id], @options) @@ -145,7 +848,7 @@ def test_refund_fails_unless_status_is_captured @gateway.refund(@amount, @options[:order_id], @options) end.respond_with(failed_refund_inquiry_response, successful_refund_response) assert_failure response - assert_equal "A transaction status of 'CAPTURED' or 'SETTLED' or 'SETTLED_BY_MERCHANT' is required.", response.message + assert_equal "A transaction status of 'CAPTURED' or 'SETTLED' or 'SETTLED_BY_MERCHANT' or 'SENT_FOR_REFUND' is required.", response.message end def test_full_refund_for_unsettled_payment_forces_void @@ -156,6 +859,25 @@ def test_full_refund_for_unsettled_payment_forces_void assert 'cancel', response.responses.last.params['action'] end + def test_refund_failure_with_force_full_refund_if_unsettled_does_not_force_void + response = stub_comms do + @gateway.refund(@amount, @options[:order_id], @options.merge(force_full_refund_if_unsettled: true)) + end.respond_with('total garbage') + + assert_failure response + end + + def test_refund_using_order_id_embedded_with_token + response = stub_comms do + authorization = "#{@options[:order_id]}|99411111780163871111|shopper|59424549c291397379f30c5c082dbed8" + @gateway.refund(@amount, authorization, @options) + end.check_request do |_endpoint, data, _headers| + assert_tag_with_attributes('orderInquiry', { 'orderCode' => @options[:order_id].to_s }, data) if %r(<orderInquiry .*?>).match?(data) + assert_tag_with_attributes('orderModification', { 'orderCode' => @options[:order_id].to_s }, data) if %r(<orderModification .*?>).match?(data) + end.respond_with(successful_refund_inquiry_response('CAPTURED'), successful_refund_response) + assert_success response + end + def test_capture response = stub_comms do response = @gateway.authorize(@amount, @credit_card, @options) @@ -164,12 +886,76 @@ def test_capture assert_success response end - def test_successful_credit + def test_capture_using_order_id_embedded_with_token + response = stub_comms do + response = @gateway.authorize(@amount, @credit_card, @options) + authorization = "#{response.authorization}|99411111780163871111|shopper|59424549c291397379f30c5c082dbed8" + @gateway.capture(@amount, authorization, @options) + end.check_request do |_endpoint, data, _headers| + assert_tag_with_attributes('orderModification', { 'orderCode' => response.authorization }, data) if %r(<orderModification .*?>).match?(data) + end.respond_with(successful_authorize_response, successful_capture_response) + assert_success response + end + + def test_successful_fast_fund_credit + options = @options.merge({ fast_fund_credit: true, email: 'test@email.com' }) + + stub_comms do + @gateway.credit(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<FF_DISBURSE-SSL>/, data) + assert_match(/<shopperEmailAddress>/, data) + end.respond_with(successful_visa_credit_response) + end + + def test_successful_visa_credit + response = stub_comms do + @gateway.credit(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<paymentDetails action="REFUND">/, data) + end.respond_with(successful_visa_credit_response) + assert_success response + assert_equal '3d4187536044bd39ad6a289c4339c41c', response.authorization + end + + def test_successful_mastercard_credit response = stub_comms do @gateway.credit(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/<paymentDetails action="REFUND">/, data) - end.respond_with(successful_credit_response) + end.respond_with(successful_mastercard_credit_response) + assert_success response + assert_equal 'f25257d251b81fb1fd9c210973c941ff', response.authorization + end + + def test_successful_visa_account_funding_transaction + response = stub_comms do + @gateway.credit(@amount, @credit_card, @options.merge(@aft_options)) + end.check_request do |_endpoint, data, _headers| + assert_match(/<fundingTransfer type="A" category="PULL_FROM_CARD">/, data) + end.respond_with(successful_visa_credit_response) + assert_success response + assert_equal '3d4187536044bd39ad6a289c4339c41c', response.authorization + end + + def test_successful_authorize_visa_account_funding_transaction + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(@aft_options)) + end.check_request do |_endpoint, data, _headers| + assert_match(/<fundingTransfer type="A" category="PULL_FROM_CARD">/, data) + end.respond_with(successful_visa_credit_response) + assert_success response + assert_equal '3d4187536044bd39ad6a289c4339c41c', response.authorization + end + + def test_successful_authorize_visa_aft_not_include_address2_or_middle_name + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(@aft_less_options)) + end.check_request do |_endpoint, data, _headers| + refute data.include?('middle') + refute data.include?('address2') + assert_match(/<fundingTransfer type="A" category="PULL_FROM_CARD">/, data) + end.respond_with(successful_visa_credit_response) assert_success response assert_equal '3d4187536044bd39ad6a289c4339c41c', response.authorization end @@ -177,13 +963,13 @@ def test_successful_credit def test_description stub_comms do @gateway.authorize(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match %r(<description>Purchase</description>), data end.respond_with(successful_authorize_response) stub_comms do @gateway.authorize(@amount, @credit_card, @options.merge(description: 'Something cool.')) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match %r(<description>Something cool.</description>), data end.respond_with(successful_authorize_response) end @@ -191,13 +977,13 @@ def test_description def test_order_content stub_comms do @gateway.authorize(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_no_match %r(orderContent), data end.respond_with(successful_authorize_response) stub_comms do @gateway.authorize(@amount, @credit_card, @options.merge(order_content: "Lots 'o' crazy <data> stuff.")) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match %r(<orderContent>\s*<!\[CDATA\[Lots 'o' crazy <data> stuff\.\]\]>\s*</orderContent>), data end.respond_with(successful_authorize_response) end @@ -205,12 +991,10 @@ def test_order_content def test_capture_time stub_comms do @gateway.capture(@amount, 'bogus', @options) - end.check_request do |endpoint, data, headers| - if data =~ /capture/ + 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 @@ -218,35 +1002,29 @@ def test_capture_time 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 + end.check_request do |_endpoint, data, _headers| + assert_tag_with_attributes 'amount', { 'value' => '100', 'exponent' => '2', 'currencyCode' => 'GBP' }, data end.respond_with(successful_authorize_response) end 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 + end.check_request do |_endpoint, data, _headers| + 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 + end.check_request do |_endpoint, data, _headers| + assert_tag_with_attributes 'amount', { 'value' => '10000', 'exponent' => '3', 'currencyCode' => 'OMR' }, data end.respond_with(successful_authorize_response) end def test_address_handling stub_comms do @gateway.authorize(100, @credit_card, @options.merge(billing_address: address)) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match %r(<firstName>Jim</firstName>), data assert_match %r(<lastName>Smith</lastName>), data assert_match %r(<address1>456 My Street</address1>), data @@ -260,7 +1038,7 @@ def test_address_handling stub_comms do @gateway.authorize(100, @credit_card, @options.merge(billing_address: address.with_indifferent_access)) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match %r(<firstName>Jim</firstName>), data assert_match %r(<lastName>Smith</lastName>), data assert_match %r(<address1>456 My Street</address1>), data @@ -273,8 +1051,8 @@ def test_address_handling end.respond_with(successful_authorize_response) stub_comms do - @gateway.authorize(100, @credit_card, @options.merge(address: address)) - end.check_request do |endpoint, data, headers| + @gateway.authorize(100, @credit_card, @options.merge(address:)) + end.check_request do |_endpoint, data, _headers| assert_match %r(<firstName>Jim</firstName>), data assert_match %r(<lastName>Smith</lastName>), data assert_match %r(<address1>456 My Street</address1>), data @@ -288,14 +1066,14 @@ def test_address_handling stub_comms do @gateway.authorize(100, @credit_card, @options.merge(billing_address: { phone: '555-3323' })) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_no_match %r(firstName), data assert_no_match %r(lastName), data assert_no_match %r(address2), data assert_match %r(<address1>N/A</address1>), data assert_match %r(<city>N/A</city>), data assert_match %r(<postalCode>0000</postalCode>), data - assert_match %r(<state>N/A</state>), data + assert_match %r(<state/>), data assert_match %r(<countryCode>US</countryCode>), data assert_match %r(<telephoneNumber>555-3323</telephoneNumber>), data end.respond_with(successful_authorize_response) @@ -304,7 +1082,7 @@ def test_address_handling def test_no_address_specified stub_comms do @gateway.authorize(100, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_no_match %r(cardAddress), data assert_no_match %r(address), data assert_no_match %r(firstName), data @@ -325,80 +1103,142 @@ def test_address_with_parts_unspecified stub_comms do @gateway.authorize(100, @credit_card, @options.merge(billing_address: address_with_nils)) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_no_match %r(firstName), data assert_no_match %r(lastName), data assert_no_match %r(address2), data assert_match %r(<address1>N/A</address1>), data assert_match %r(<city>N/A</city>), data assert_match %r(<postalCode>0000</postalCode>), data - assert_match %r(<state>N/A</state>), data + assert_match %r(<state/>), data assert_match %r(<countryCode>US</countryCode>), data assert_match %r(<telephoneNumber>555-3323</telephoneNumber>), data end.respond_with(successful_authorize_response) end - def test_email - stub_comms do - @gateway.authorize(100, @credit_card, @options.merge(email: 'eggcellent@example.com')) - end.check_request do |endpoint, data, headers| - assert_match %r(<shopperEmailAddress>eggcellent@example.com</shopperEmailAddress>), data - end.respond_with(successful_authorize_response) - + def test_state_sent_for_3ds_transactions_in_us_country + us_billing_address = address.merge(country: 'US') stub_comms do - @gateway.authorize(100, @credit_card, @options) - end.check_request do |endpoint, data, headers| - assert_no_match %r(shopperEmailAddress), data + @gateway.authorize(100, @credit_card, @options.merge(billing_address: us_billing_address, execute_threed: true)) + end.check_request do |_endpoint, data, _headers| + assert_match %r(firstName), data + assert_match %r(lastName), data + assert_match %r(<address1>456 My Street</address1>), data + assert_match %r(<address2>Apt 1</address2>), data + assert_match %r(<city>Ottawa</city>), data + assert_match %r(<postalCode>K1C2N6</postalCode>), data + assert_match %r(<state>ON</state>), data + assert_match %r(<countryCode>US</countryCode>), data + assert_match %r(<telephoneNumber>\(555\)555-5555</telephoneNumber>), data end.respond_with(successful_authorize_response) end - def test_ip + def test_state_not_sent_for_3ds_transactions_in_non_us_country + stub_comms do + @gateway.authorize(100, @credit_card, @options.merge(billing_address: address, execute_threed: true)) + end.check_request do |_endpoint, data, _headers| + assert_match %r(firstName), data + assert_match %r(lastName), data + assert_match %r(<address1>456 My Street</address1>), data + assert_match %r(<address2>Apt 1</address2>), data + assert_match %r(<city>Ottawa</city>), data + assert_match %r(<postalCode>K1C2N6</postalCode>), data + assert_no_match %r(<state>ON</state>), data + assert_match %r(<countryCode>CA</countryCode>), data + assert_match %r(<telephoneNumber>\(555\)555-5555</telephoneNumber>), data + end.respond_with(successful_authorize_response) + end + + def test_email + stub_comms do + @gateway.authorize(100, @credit_card, @options.merge(email: 'eggcellent@example.com')) + end.check_request do |_endpoint, data, _headers| + assert_match %r(<shopperEmailAddress>eggcellent@example.com</shopperEmailAddress>), data + end.respond_with(successful_authorize_response) + + stub_comms do + @gateway.authorize(100, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_no_match %r(shopperEmailAddress), data + end.respond_with(successful_authorize_response) + end + + def test_statement_narrative_and_truncation + stub_comms do + @gateway.authorize(100, @credit_card, @options.merge(statement_narrative: 'Merchant Statement Narrative The Story Of Your Purchase')) + end.check_request do |_endpoint, data, _headers| + assert_match %r(<statementNarrative>Merchant Statement Narrative The Story Of Your Pur</statementNarrative>), data + assert_no_match %r(<statementNarrative>Merchant Statement Narrative The Story Of Your Purchase</statementNarrative>), data + end.respond_with(successful_authorize_response) + end + + def test_instalments + stub_comms do + @gateway.purchase(100, @credit_card, @options.merge(instalments: 3)) + end.check_request do |_endpoint, data, _headers| + unless /<capture>/.match?(data) + assert_match %r(<instalments>3</instalments>), data + assert_no_match %r(cpf), data + end + end.respond_with(successful_authorize_response, successful_capture_response) + + stub_comms do + @gateway.purchase(100, @credit_card, @options.merge(instalments: 3, cpf: 12341234)) + end.check_request do |_endpoint, data, _headers| + unless /<capture>/.match?(data) + assert_match %r(<instalments>3</instalments>), data + assert_match %r(<cpf>12341234</cpf>), data + end + end.respond_with(successful_authorize_response, successful_capture_response) + end + + def test_ip stub_comms do @gateway.authorize(100, @credit_card, @options.merge(ip: '192.137.11.44')) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match %r(<session shopperIPAddress="192.137.11.44"/>), data end.respond_with(successful_authorize_response) stub_comms do @gateway.authorize(100, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_no_match %r(<session), data end.respond_with(successful_authorize_response) end def test_parsing response = stub_comms do - @gateway.authorize(100, @credit_card, @options.merge(address: {address1: '123 Anystreet', country: 'US'})) + @gateway.authorize(100, @credit_card, @options.merge(address: { address1: '123 Anystreet', country: 'US' })) end.respond_with(successful_authorize_response) assert_equal({ - 'action'=>'authorize', - 'amount_currency_code'=>'HKD', - 'amount_debit_credit_indicator'=>'credit', - 'amount_exponent'=>'2', - 'amount_value'=>'15000', - 'avs_result_code_description'=>'UNKNOWN', - 'balance'=>true, - 'balance_account_type'=>'IN_PROCESS_AUTHORISED', - 'card_number'=>'4111********1111', - 'cvc_result_code_description'=>'UNKNOWN', - 'last_event'=>'AUTHORISED', - 'order_status'=>true, - 'order_status_order_code'=>'R50704213207145707', - 'payment'=>true, - 'payment_method'=>'VISA-SSL', - 'payment_service'=>true, - 'payment_service_merchant_code'=>'XXXXXXXXXXXXXXX', - 'payment_service_version'=>'1.4', - 'reply'=>true, - 'risk_score_value'=>'1', - }, response.params) + 'action' => 'authorize', + 'amount_currency_code' => 'HKD', + 'amount_debit_credit_indicator' => 'credit', + 'amount_exponent' => '2', + 'amount_value' => '15000', + 'avs_result_code_description' => 'UNKNOWN', + 'balance' => true, + 'balance_account_type' => 'IN_PROCESS_AUTHORISED', + 'card_number' => '4111********1111', + 'cvc_result_code_description' => 'UNKNOWN', + 'last_event' => 'AUTHORISED', + 'order_status' => true, + 'order_status_order_code' => 'R50704213207145707', + 'payment' => true, + 'payment_method' => 'VISA-SSL', + 'payment_service' => true, + 'payment_service_merchant_code' => 'XXXXXXXXXXXXXXX', + 'payment_service_version' => '1.4', + 'reply' => true, + 'risk_score_value' => '1' + }, response.params) end def test_auth stub_comms do @gateway.authorize(100, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, _data, headers| assert_equal 'Basic dGVzdGxvZ2luOnRlc3RwYXNzd29yZA==', headers['Authorization'] end.respond_with(successful_authorize_response) end @@ -407,17 +1247,16 @@ def test_request_respects_test_mode_on_gateway_instance ActiveMerchant::Billing::Base.mode = :production @gateway = WorldpayGateway.new( - :login => 'testlogin', - :password => 'testpassword', - :test => true + login: 'testlogin', + password: 'testpassword', + test: true ) stub_comms do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |endpoint, _data, _headers| assert_equal WorldpayGateway.test_url, endpoint end.respond_with(successful_authorize_response, successful_capture_response) - ensure ActiveMerchant::Billing::Base.mode = :test end @@ -425,20 +1264,49 @@ def test_request_respects_test_mode_on_gateway_instance def test_refund_amount_contains_debit_credit_indicator response = stub_comms do @gateway.refund(@amount, @options[:order_id], @options) - end.check_request do |endpoint, data, headers| - if data =~ /<refund>/ + end.check_request do |_endpoint, data, _headers| + if /<refund>/.match?(data) request_hash = Hash.from_xml(data) assert_equal 'credit', request_hash['paymentService']['modify']['orderModification']['refund']['amount']['debitCreditIndicator'] end - end.respond_with(successful_refund_inquiry_response, successful_refund_response) + end.respond_with(successful_refund_inquiry_response('CAPTURED'), successful_refund_response) assert_success response end - def assert_tag_with_attributes(tag, attributes, string) - assert(m = %r(<#{tag}([^>]+)/>).match(string)) - attributes.each do |attribute, value| - assert_match %r(#{attribute}="#{value}"), m[1] - end + def test_cancel_or_refund + stub_comms do + @gateway.refund(@amount, @options[:order_id], @options) + end.check_request do |_endpoint, data, _headers| + next if data =~ /<inquiry>/ + + refute_match(/<cancelOrRefund\/>/, data) + end.respond_with(successful_refund_inquiry_response, successful_refund_response) + + stub_comms do + @gateway.refund(@amount, @options[:order_id], @options.merge(cancel_or_refund: true)) + end.check_request do |_endpoint, data, _headers| + next if data =~ /<inquiry>/ + + assert_match(/<cancelOrRefund\/>/, data) + end.respond_with(successful_refund_inquiry_response('SENT_FOR_REFUND'), successful_cancel_or_refund_response) + end + + def test_cancel_or_refund_with_void + stub_comms do + @gateway.void(@options[:order_id], @options) + end.check_request do |_endpoint, data, _headers| + next if data =~ /<inquiry>/ + + refute_match(/<cancelOrRefund\/>/, data) + end.respond_with(successful_refund_inquiry_response, successful_refund_response) + + stub_comms do + @gateway.void(@options[:order_id], @options.merge(cancel_or_refund: true)) + end.check_request do |_endpoint, data, _headers| + next if data =~ /<inquiry>/ + + assert_match(/<cancelOrRefund\/>/, data) + end.respond_with(successful_refund_inquiry_response('SENT_FOR_REFUND'), successful_cancel_or_refund_response) end def test_successful_verify @@ -448,6 +1316,29 @@ def test_successful_verify assert_success response end + def test_successful_verify_with_0_auth + stub_comms do + @gateway.verify(@credit_card, @options.merge(zero_dollar_auth: true)) + end.check_request do |_endpoint, data, _headers| + assert_match(/amount value="0"/, data) if /<submit>/.match?(data) + end.respond_with(successful_authorize_response, successful_void_response) + end + + def test_successful_verify_with_0_auth_and_ineligible_card + stub_comms do + @gateway.verify(@elo_credit_card, @options.merge(zero_dollar_auth: true)) + end.check_request do |_endpoint, data, _headers| + refute_match(/amount value="0"/, data) + end.respond_with(successful_authorize_response, successful_void_response) + end + + def test_successful_verify_with_elo + @gateway.expects(:ssl_post).times(2).returns(successful_authorize_with_elo_response, successful_void_with_elo_response) + + response = @gateway.verify(@elo_credit_card, @options.merge(currency: 'BRL')) + assert_success response + end + def test_successful_verify_with_failed_void @gateway.expects(:ssl_post).times(2).returns(successful_authorize_response, failed_void_response) @@ -462,31 +1353,835 @@ def test_failed_verify assert_failure response end - def test_transcript_scrubbing - assert_equal scrubbed_transcript, @gateway.scrub(transcript) + def test_empty_inst_id_is_stripped + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ inst_id: '' })) + end.check_request do |_, data, _| + assert_not_match(/installationId/, data) + end.respond_with(successful_authorize_response) + end + + def test_3ds_name_coersion_for_testing + @options[:execute_threed] = true + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match %r{<cardHolderName>3D</cardHolderName>}, data if /<submit>/.match?(data) + end.respond_with(successful_authorize_response, successful_capture_response) + assert_success response + end + + def test_3ds_name_coersion_based_on_version_for_testing + @options[:execute_threed] = true + @options[:three_ds_version] = '2.0' + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match %r{<cardHolderName>Longbob Longsen</cardHolderName>}, data if /<submit>/.match?(data) + end.respond_with(successful_authorize_response, successful_capture_response) + assert_success response + + @options[:three_ds_version] = '2' + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match %r{<cardHolderName>Longbob Longsen</cardHolderName>}, data if /<submit>/.match?(data) + end.respond_with(successful_authorize_response, successful_capture_response) + assert_success response + + @options[:three_ds_version] = '1.0.2' + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match %r{<cardHolderName>3D</cardHolderName>}, data if /<submit>/.match?(data) + end.respond_with(successful_authorize_response, successful_capture_response) + assert_success response + end + + def test_3ds_name_not_coerced_in_production + ActiveMerchant::Billing::Base.mode = :production + + @options[:execute_threed] = true + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_not_match %r{<cardHolderName>3D</cardHolderName>}, data + end.respond_with(successful_authorize_response, successful_capture_response) + ensure + ActiveMerchant::Billing::Base.mode = :test + end + + def test_3ds_additional_information + browser_size = '390x400' + session_id = '0215ui8ib1' + df_reference_id = '1326vj9jc2' + + options = @options.merge( + session_id:, + df_reference_id:, + browser_size:, + execute_threed: true, + three_ds_version: '2.0.1' + ) + + stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_tag_with_attributes 'additional3DSData', { 'dfReferenceId' => df_reference_id, 'challengeWindowSize' => browser_size }, data + end.respond_with(successful_authorize_response) + end + + def test_transcript_scrubbing + assert_equal scrubbed_transcript, @gateway.scrub(transcript) + end + + def test_transcript_scrubbing_on_network_token + assert_equal network_token_transcript_scrubbed, @gateway.scrub(network_token_transcript) + end + + def test_transcript_scrubbing_on_aft + assert_equal aft_transcript_scrubbed, @gateway.scrub(aft_transcript) + end + + def test_3ds_version_1_request + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(three_d_secure_option(version: '1.0.2', xid: 'xid'))) + end.check_request do |_endpoint, data, _headers| + assert_match %r{<paymentService version="1.4" merchantCode="testlogin">}, data + assert_match %r{<eci>eci</eci>}, data + assert_match %r{<cavv>cavv</cavv>}, data + assert_match %r{<xid>xid</xid>}, data + assert_match %r{<threeDSVersion>1.0.2</threeDSVersion>}, data + end.respond_with(successful_authorize_response) + end + + def test_3ds_version_2_request + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(three_d_secure_option(version: '2.1.0', ds_transaction_id: 'ds_transaction_id'))) + end.check_request do |_endpoint, data, _headers| + assert_match %r{<paymentService version="1.4" merchantCode="testlogin">}, data + assert_match %r{<eci>eci</eci>}, data + assert_match %r{<cavv>cavv</cavv>}, data + assert_match %r{<dsTransactionId>ds_transaction_id</dsTransactionId>}, data + assert_match %r{<threeDSVersion>2.1.0</threeDSVersion>}, data + end.respond_with(successful_authorize_response) + end + + def test_3ds_version_2_request_nt + stub_comms do + @gateway.authorize(@amount, @nt_credit_card, @options.merge(three_d_secure_option(version: '2.1.0', ds_transaction_id: 'ds_transaction_id'))) + end.check_request do |_endpoint, data, _headers| + assert_match %r{<paymentService version="1.4" merchantCode="testlogin">}, data + assert_match %r{<eci>eci</eci>}, data + assert_match %r{<cavv>cavv</cavv>}, data + assert_match %r{<dsTransactionId>ds_transaction_id</dsTransactionId>}, data + assert_match %r{<threeDSVersion>2.1.0</threeDSVersion>}, data + assert_match %r(<EMVCO_TOKEN-SSL type="NETWORKTOKEN">), data + end.respond_with(successful_authorize_response) + end + + def test_failed_authorize_with_unknown_card + response = stub_comms do + @gateway.authorize(@amount, @sodexo_voucher, @options) + end.respond_with(failed_with_unknown_card_response) + assert_failure response + assert_equal '5', response.error_code + end + + def test_failed_purchase_with_unknown_card + response = stub_comms do + @gateway.purchase(@amount, @sodexo_voucher, @options) + end.respond_with(failed_with_unknown_card_response) + assert_failure response + assert_equal '5', response.error_code + end + + def test_failed_verify_with_unknown_card + @gateway.expects(:ssl_post).returns(failed_with_unknown_card_response) + + response = @gateway.verify(@sodexo_voucher, @options) + assert_failure response + assert_equal '5', response.error_code + end + + def test_successful_store + response = stub_comms do + @gateway.store(@credit_card, @store_options) + end.check_request do |_endpoint, data, _headers| + assert_match %r(<paymentTokenCreate>), data + assert_match %r(<createToken/?>), data + assert_match %r(<authenticatedShopperID>59424549c291397379f30c5c082dbed8</authenticatedShopperID>), data + assert_match %r(4242424242424242), data + assert_no_match %r(<order>), data + assert_no_match %r(<paymentDetails>), data + assert_no_match %r(<CARD-SSL>), data + end.respond_with(successful_store_response) + + assert_success response + assert_equal 'SUCCESS', response.message + assert_equal @token, response.authorization + end + + def test_successful_authorize_using_token + response = stub_comms do + @gateway.authorize(@amount, @token, @options) + end.check_request do |_endpoint, data, _headers| + assert_tag_with_attributes('order', { 'orderCode' => @options[:order_id].to_s }, data) + assert_match %r(<authenticatedShopperID>59424549c291397379f30c5c082dbed8</authenticatedShopperID>), data + assert_tag_with_attributes 'TOKEN-SSL', { 'tokenScope' => 'shopper' }, data + assert_match %r(<paymentTokenID>99411111780163871111</paymentTokenID>), data + end.respond_with(successful_authorize_response) + + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_authorize_with_token_includes_shopper_using_minimal_options + stub_comms do + @gateway.authorize(@amount, @token, @options) + end.check_request do |_endpoint, data, _headers| + assert_match %r(<authenticatedShopperID>59424549c291397379f30c5c082dbed8</authenticatedShopperID>), data + end.respond_with(successful_authorize_response) + end + + def test_successful_purchase_using_token + response = stub_comms do + @gateway.purchase(@amount, @token, @options) + end.check_request do |_endpoint, data, _headers| + assert_tag_with_attributes('order', { 'orderCode' => @options[:order_id].to_s }, data) if %r(<order .*?>).match?(data) + end.respond_with(successful_authorize_response, successful_capture_response) + + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_verify_using_token + response = stub_comms do + @gateway.verify(@token, @options) + end.check_request do |_endpoint, data, _headers| + assert_tag_with_attributes('order', { 'orderCode' => @options[:order_id].to_s }, data) if %r(<order .*?>).match?(data) + end.respond_with(successful_authorize_response, successful_void_response) + + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_credit_using_token + response = stub_comms do + @gateway.credit(@amount, @token, @options) + end.check_request do |_endpoint, data, _headers| + assert_tag_with_attributes('order', { 'orderCode' => @options[:order_id].to_s }, data) + assert_match(/<paymentDetails action="REFUND">/, data) + assert_match %r(<authenticatedShopperID>59424549c291397379f30c5c082dbed8</authenticatedShopperID>), data + assert_tag_with_attributes 'TOKEN-SSL', { 'tokenScope' => 'shopper' }, data + assert_match '<paymentTokenID>99411111780163871111</paymentTokenID>', data + end.respond_with(successful_visa_credit_response) + + assert_success response + assert_equal 'SUCCESS', response.message + assert_equal '3d4187536044bd39ad6a289c4339c41c', response.authorization + end + + def test_optional_idempotency_key_header + response = stub_comms do + @gateway.authorize(@amount, @token, @options.merge({ idempotency_key: 'test123' })) + end.check_request do |_endpoint, _data, headers| + headers && headers['Idempotency-Key'] == 'test123' + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_failed_store + response = stub_comms do + @gateway.store(@credit_card, @store_options.merge(customer: '_invalidId')) + end.respond_with(failed_store_response) + + assert_failure response + assert_equal '2', response.error_code + assert_equal 'authenticatedShopperID cannot start with an underscore', response.message + end + + def test_store_should_raise_when_customer_not_present + assert_raises(ArgumentError) do + @gateway.store(@credit_card) + end + end + + def test_failed_authorize_using_token + response = stub_comms do + @gateway.authorize(@amount, @token, @options) + end.respond_with(failed_authorize_response_2) + + assert_failure response + assert_equal '5', response.error_code + assert_match %r{XML failed validation: Invalid payment details : Card number not recognised:}, response.message + end + + def test_failed_verify_using_token + response = stub_comms do + @gateway.verify(@token, @options) + end.respond_with(failed_authorize_response_2) + + assert_failure response + assert_equal '5', response.error_code + assert_match %r{XML failed validation: Invalid payment details : Card number not recognised:}, response.message + end + + def test_authorize_order_id_not_overridden_by_order_id_of_token + @token = 'wrong_order_id|99411111780163871111|shopper|59424549c291397379f30c5c082dbed8' + response = stub_comms do + @gateway.authorize(@amount, @token, @options) + end.check_request do |_endpoint, data, _headers| + assert_tag_with_attributes('order', { 'orderCode' => @options[:order_id].to_s }, data) + assert_match %r(<authenticatedShopperID>59424549c291397379f30c5c082dbed8</authenticatedShopperID>), data + assert_tag_with_attributes 'TOKEN-SSL', { 'tokenScope' => 'shopper' }, data + assert_match %r(<paymentTokenID>99411111780163871111</paymentTokenID>), data + end.respond_with(successful_authorize_response) + + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_purchase_order_id_not_overridden_by_order_id_of_token + @token = 'wrong_order_id|99411111780163871111|shopper|59424549c291397379f30c5c082dbed8' + response = stub_comms do + @gateway.purchase(@amount, @token, @options) + end.check_request do |_endpoint, data, _headers| + assert_tag_with_attributes('order', { 'orderCode' => @options[:order_id].to_s }, data) if %r(<order .*?>).match?(data) + end.respond_with(successful_authorize_response, successful_capture_response) + + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_verify_order_id_not_overridden_by_order_id_of_token + @token = 'wrong_order_id|99411111780163871111|shopper|59424549c291397379f30c5c082dbed8' + response = stub_comms do + @gateway.verify(@token, @options) + end.check_request do |_endpoint, data, _headers| + assert_tag_with_attributes('order', { 'orderCode' => @options[:order_id].to_s }, data) if %r(<order .*?>).match?(data) + end.respond_with(successful_authorize_response, successful_void_response) + + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_credit_order_id_not_overridden_by_order_if_of_token + @token = 'wrong_order_id|99411111780163871111|shopper|59424549c291397379f30c5c082dbed8' + response = stub_comms do + @gateway.credit(@amount, @token, @options) + end.check_request do |_endpoint, data, _headers| + assert_tag_with_attributes('order', { 'orderCode' => @options[:order_id].to_s }, data) + assert_match(/<paymentDetails action="REFUND">/, data) + assert_match %r(<authenticatedShopperID>59424549c291397379f30c5c082dbed8</authenticatedShopperID>), data + assert_tag_with_attributes 'TOKEN-SSL', { 'tokenScope' => 'shopper' }, data + assert_match '<paymentTokenID>99411111780163871111</paymentTokenID>', data + end.respond_with(successful_visa_credit_response) + + assert_success response + assert_equal 'SUCCESS', response.message + assert_equal '3d4187536044bd39ad6a289c4339c41c', response.authorization + end + + def test_handles_plain_text_response + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.respond_with('Temporary Failure, please Retry') + assert_failure response + assert_match "Unparsable response received from Worldpay. Please contact Worldpay if you continue to receive this message. \(The raw response returned by the API was: \"Temporary Failure, please Retry\"\)", response.message + end + + def test_successful_authorize_synchronous_response + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/4242424242424242/, data) + end.respond_with(successful_authorize_synchronous_response) + assert_success response + assert_equal 'fbe493442977787ea2fadabfb23c2574', response.authorization + end + + def test_successful_capture_synchronous_response + response = stub_comms do + response = @gateway.authorize(@amount, @credit_card, @options) + @gateway.capture(@amount, response.authorization, @options) + end.respond_with(successful_authorize_synchronous_response, successful_capture_synchronous_response) + assert_success response + end + + def test_failed_capture_synchronous_response + response = stub_comms do + response = @gateway.authorize(@amount, @credit_card, @options) + @gateway.capture(@amount, response.authorization, @options) + end.respond_with(successful_authorize_synchronous_response, failed_capture_synchronous_response) + assert_failure response + end + + def test_successful_refund_synchronous_response + response = stub_comms do + @gateway.refund(@amount, @options[:order_id], @options) + end.respond_with(successful_refund_synchronous_response) + assert_success response + end + + def test_failed_refund_synchronous_response + response = stub_comms do + @gateway.refund(@amount, @options[:order_id], @options) + end.respond_with(failed_refund_synchronous_response) + assert_failure response + end + + def test_network_token_type_assignation_when_apple_token + stub_comms do + @gateway.authorize(@amount, @apple_play_network_token, @options) + end.check_request(skip_response: true) do |_endpoint, data, _headers| + assert_match %r(<EMVCO_TOKEN-SSL type="APPLEPAY">), data + end + end + + def test_network_token_type_assignation_when_network_token + stub_comms do + @gateway.authorize(@amount, @nt_credit_card, @options) + end.check_request(skip_response: true) do |_endpoint, data, _headers| + assert_match %r(<EMVCO_TOKEN-SSL type="NETWORKTOKEN">), data + end + end + + 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(<EMVCO_TOKEN-SSL type="GOOGLEPAY">), data + assert_match %r(<eciIndicator>05</eciIndicator>), 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(<EMVCO_TOKEN-SSL type="GOOGLEPAY">), 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(<EMVCO_TOKEN-SSL type="GOOGLEPAY">), data + assert_match %r(<eciIndicator>07</eciIndicator>), 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(<EMVCO_TOKEN-SSL type="GOOGLEPAY">), data + end + end + + def test_order_id_crop_and_clean + @options[:order_id] = "abc1234 abc1234 'abc1234' <abc1234> \"abc1234\" | abc1234 abc1234 abc1234 abc1234 abc1234" + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match %r(<order orderCode="abc1234abc1234abc1234abc1234abc1234abc1234abc1234abc1234abc1234ab">), data + end.respond_with(successful_authorize_response) + 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(/<storedCredentials usage\=\"USED\" merchantInitiatedReason\=\"RECURRING\"\>/, data) + assert_match(/<schemeTransactionIdentifier\>000000000000020005060720116005060\<\/schemeTransactionIdentifier\>/, data) + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_authorize_stored_credentials_for_initial_customer_transaction + options = @options.merge!( + { + stored_credential: { + initial_transaction: true, + reason_type: 'unscheduled', + initiator: 'cardholder' + } + } + ) + + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<storedCredentials usage\=\"FIRST\" customerInitiatedReason\=\"UNSCHEDULED\"\>/, data) + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_authorize_stored_credentials_for_subsequent_customer_transaction + options = @options.merge!({ + stored_credential: { + initial_transaction: false, + reason_type: 'recurring', + initiator: 'cardholder' + } + }) + + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/<storedCredentials usage\=\"USED\"\>/, data) + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_authorize_recurring_apple_pay_with_ntid + stored_credential_params = stored_credential(:used, :recurring, :merchant, network_transaction_id: '3812908490218390214124') + + @options.merge({ + stored_credential: stored_credential_params, + stored_credential_transaction_id: '000000000000020005060720116005060', + wallet_type: :apple_pay + }) + response = stub_comms do + @gateway.authorize(@amount, @apple_play_network_token, @options) + end.check_request do |_endpoint, data, _headers| + assert_match %r(<EMVCO_TOKEN-SSL type="APPLEPAY">), data + refute_match %r(<cryptogram>), 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 }) + end.check_request do |_endpoint, data, _headers| + assert_tag_with_attributes('orderInquiry', { 'orderCode' => @options[:order_id].to_s }, data) + end.respond_with(successful_authorize_response) + assert_success response + assert_equal 'R50704213207145707', response.authorization + end + + def test_successful_inquire_with_authorization + response = stub_comms do + @gateway.inquire(@options[:order_id].to_s, {}) + end.check_request do |_endpoint, data, _headers| + assert_tag_with_attributes('orderInquiry', { 'orderCode' => @options[:order_id].to_s }, data) + end.respond_with(successful_authorize_response) + assert_success response + assert_equal 'R50704213207145707', response.authorization + end + + private + + def assert_date_element(expected_date_hash, date_element) + assert_equal(expected_date_hash[:day_of_month], date_element.attribute('dayOfMonth').value) + assert_equal(expected_date_hash[:month], date_element.attribute('month').value) + assert_equal(expected_date_hash[:year], date_element.attribute('year').value) + end + + def assert_tag_with_attributes(tag, attributes, string) + assert(m = %r(<#{tag}([^>]+)/?>).match(string)) + attributes.each do |attribute, value| + assert_match %r(#{attribute}="#{value}"), m[1] + end + end + + def three_d_secure_option(version:, xid: nil, ds_transaction_id: nil) + { + three_d_secure: { + eci: 'eci', + cavv: 'cavv', + xid:, + ds_transaction_id:, + version: + } + } + end + + def risk_data + return @risk_data if defined?(@risk_data) + + authentication_time = Time.now + shopper_account_creation_date = Date.today + shopper_account_modification_date = Date.today - 1.day + shopper_account_password_change_date = Date.today - 2.days + shopper_account_shipping_address_first_use_date = Date.today - 3.day + shopper_account_payment_account_first_use_date = Date.today - 4.day + transaction_risk_data_pre_order_date = Date.today + 1.day + + @risk_data = { + authentication_risk_data: { + authentication_method: 'localAccount', + authentication_date: { + day_of_month: authentication_time.strftime('%d'), + month: authentication_time.strftime('%m'), + year: authentication_time.strftime('%Y'), + hour: authentication_time.strftime('%H'), + minute: authentication_time.strftime('%M'), + second: authentication_time.strftime('%S') + } + }, + shopper_account_risk_data: { + transactions_attempted_last_day: '1', + transactions_attempted_last_year: '2', + purchases_completed_last_six_months: '3', + add_card_attempts_last_day: '4', + previous_suspicious_activity: 'false', # Boolean (true or false) + shipping_name_matches_account_name: 'true', # Boolean (true or false) + shopper_account_age_indicator: 'lessThanThirtyDays', # Possible Values: noAccount, createdDuringTransaction, lessThanThirtyDays, thirtyToSixtyDays, moreThanSixtyDays + shopper_account_change_indicator: 'thirtyToSixtyDays', # Possible values: changedDuringTransaction, lessThanThirtyDays, thirtyToSixtyDays, moreThanSixtyDays + shopper_account_password_change_indicator: 'noChange', # Possible Values: noChange, changedDuringTransaction, lessThanThirtyDays, thirtyToSixtyDays, moreThanSixtyDays + shopper_account_shipping_address_usage_indicator: 'moreThanSixtyDays', # Possible Values: thisTransaction, lessThanThirtyDays, thirtyToSixtyDays, moreThanSixtyDays + shopper_account_payment_account_indicator: 'thirtyToSixtyDays', # Possible Values: noAccount, duringTransaction, lessThanThirtyDays, thirtyToSixtyDays, moreThanSixtyDays + shopper_account_creation_date: { + day_of_month: shopper_account_creation_date.strftime('%d'), + month: shopper_account_creation_date.strftime('%m'), + year: shopper_account_creation_date.strftime('%Y') + }, + shopper_account_modification_date: { + day_of_month: shopper_account_modification_date.strftime('%d'), + month: shopper_account_modification_date.strftime('%m'), + year: shopper_account_modification_date.strftime('%Y') + }, + shopper_account_password_change_date: { + day_of_month: shopper_account_password_change_date.strftime('%d'), + month: shopper_account_password_change_date.strftime('%m'), + year: shopper_account_password_change_date.strftime('%Y') + }, + shopper_account_shipping_address_first_use_date: { + day_of_month: shopper_account_shipping_address_first_use_date.strftime('%d'), + month: shopper_account_shipping_address_first_use_date.strftime('%m'), + year: shopper_account_shipping_address_first_use_date.strftime('%Y') + }, + shopper_account_payment_account_first_use_date: { + day_of_month: shopper_account_payment_account_first_use_date.strftime('%d'), + month: shopper_account_payment_account_first_use_date.strftime('%m'), + year: shopper_account_payment_account_first_use_date.strftime('%Y') + } + }, + transaction_risk_data: { + shipping_method: 'digital', + delivery_timeframe: 'electronicDelivery', + delivery_email_address: 'abe@lincoln.gov', + reordering_previous_purchases: 'false', + pre_order_purchase: 'false', + gift_card_count: '0', + transaction_risk_data_gift_card_amount: { + value: '123', + currency: 'EUR', + exponent: '2', + debit_credit_indicator: 'credit' + }, + transaction_risk_data_pre_order_date: { + day_of_month: transaction_risk_data_pre_order_date.strftime('%d'), + month: transaction_risk_data_pre_order_date.strftime('%m'), + year: transaction_risk_data_pre_order_date.strftime('%Y') + } + } + } + end + + def successful_authorize_response + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE paymentService PUBLIC "-//Bibit//DTD Bibit PaymentService v1//EN" + "http://dtd.bibit.com/paymentService_v1.dtd"> + <paymentService version="1.4" merchantCode="XXXXXXXXXXXXXXX"> + <reply> + <orderStatus orderCode="R50704213207145707"> + <payment> + <paymentMethod>VISA-SSL</paymentMethod> + <amount value="15000" currencyCode="HKD" exponent="2" debitCreditIndicator="credit"/> + <lastEvent>AUTHORISED</lastEvent> + <CVCResultCode description="UNKNOWN"/> + <AVSResultCode description="UNKNOWN"/> + <balance accountType="IN_PROCESS_AUTHORISED"> + <amount value="15000" currencyCode="HKD" exponent="2" debitCreditIndicator="credit"/> + </balance> + <cardNumber>4111********1111</cardNumber> + <riskScore value="1"/> + </payment> + </orderStatus> + </reply> + </paymentService> + RESPONSE + end + + def successful_authorize_synchronous_response + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE paymentService PUBLIC "-//WorldPay//DTD WorldPay PaymentService v1//EN" + "http://dtd.worldpay.com/paymentService_v1.dtd"> + <paymentService version="1.4" merchantCode="SPREEDLYCFT"> + <reply> + <orderStatus orderCode="fbe493442977787ea2fadabfb23c2574"> + <payment> + <paymentMethod>VISA_CREDIT-SSL</paymentMethod> + <amount value="100" currencyCode="GBP" exponent="2" debitCreditIndicator="credit"/> + <lastEvent>AUTHORISED</lastEvent> + <CVCResultCode description="NOT SENT TO ACQUIRER"/> + <AVSResultCode description="NOT SUPPLIED BY SHOPPER"/> + <balance accountType="IN_PROCESS_AUTHORISED"> + <amount value="100" currencyCode="GBP" exponent="2" debitCreditIndicator="credit"/> + </balance> + <cardNumber>4111********1111</cardNumber> + <riskScore value="1"/> + </payment> + </orderStatus> + </reply> + </paymentService> + RESPONSE + end + + def failed_authorize_response + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE paymentService PUBLIC "-//Bibit//DTD Bibit PaymentService v1//EN" + "http://dtd.bibit.com/paymentService_v1.dtd"> + <paymentService version="1.4" merchantCode="XXXXXXXXXXXXXXX"> + <reply> + <orderStatus orderCode="R12538568107150952"> + <error code="7"> + <![CDATA[Invalid payment details : Card number : 4111********1111]]> + </error> + </orderStatus> + </reply> + </paymentService> + RESPONSE + end + + # main variation is that CDATA is nested inside <error> w/o newlines; also a + # more recent captured response from remote tests where the reply is + # contained the error directly (no <orderStatus>) + def failed_authorize_response_2 + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE paymentService PUBLIC "-//WorldPay//DTD WorldPay PaymentService v1//EN" "http://dtd.worldpay.com/paymentService_v1.dtd"> + <paymentService version="1.4" merchantCode="SPREEDLY"> + <reply> + <error code="5"><![CDATA[XML failed validation: Invalid payment details : Card number not recognised: 606070******4400]]></error> + </reply> + </paymentService> + RESPONSE + end + + def failed_purchase_response_with_issuer_response_code + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE paymentService PUBLIC "-//Bibit//DTD Bibit PaymentService v1//EN" + "http://dtd.bibit.com/paymentService_v1.dtd"> + <paymentService version="1.4" merchantCode="XXXXXXXXXXXXXXX"> + <reply> + <orderStatus orderCode="R50704213207145707"> + <payment> + <paymentMethod>VISA-SSL</paymentMethod> + <amount value="15000" currencyCode="USD" exponent="2" debitCreditIndicator="credit"/> + <lastEvent>REFUSED</lastEvent> + <IssuerResponseCode code="51" description="Insufficient funds/over credit limit"/> + <CVCResultCode description="C"/> + <AVSResultCode description="H"/> + <AAVAddressResultCode description="B"/> + <AAVPostcodeResultCode description="B"/> + <AAVCardholderNameResultCode description="B"/> + <AAVTelephoneResultCode description="B"/> + <AAVEmailResultCode description="B"/> + <cardHolderName><![CDATA[Test McTest]]></cardHolderName> + <issuerCountryCode>US</issuerCountryCode> + <issuerName>TEST BANK</issuerName> + <riskScore value="0"/> + </payment> + </orderStatus> + </reply> + </paymentService> + RESPONSE + end + + def successful_capture_response + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE paymentService PUBLIC "-//Bibit//DTD Bibit PaymentService v1//EN" + "http://dtd.bibit.com/paymentService_v1.dtd"> + <paymentService version="1.4" merchantCode="SPREEDLY"> + <reply> + <ok> + <captureReceived orderCode="33955f6bb4524813b51836de76228983"> + <amount value="100" currencyCode="GBP" exponent="2" debitCreditIndicator="credit"/> + </captureReceived> + </ok> + </reply> + </paymentService> + RESPONSE + end + + def successful_capture_synchronous_response + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE paymentService PUBLIC "-//WorldPay//DTD WorldPay PaymentService v1//EN" + "http://dtd.worldpay.com/paymentService_v1.dtd"> + <paymentService version="1.4" merchantCode="SPREEDLYCFT"> + <reply> + <orderStatus orderCode="fbe493442977787ea2fadabfb23c2574"> + <payment> + <paymentMethod>VISA_CREDIT-SSL</paymentMethod> + <amount value="100" currencyCode="GBP" exponent="2" debitCreditIndicator="credit"/> + <lastEvent>CAPTURED</lastEvent> + <CVCResultCode description="NOT SENT TO ACQUIRER"/> + <AVSResultCode description="NOT SUPPLIED BY SHOPPER"/> + <balance accountType="IN_PROCESS_CAPTURED"> + <amount value="100" currencyCode="GBP" exponent="2" debitCreditIndicator="credit"/> + </balance> + <riskScore value="1"/> + </payment> + </orderStatus> + </reply> + </paymentService> + RESPONSE + end + + def failed_capture_synchronous_response + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE paymentService PUBLIC "-//WorldPay//DTD WorldPay PaymentService v1//EN" + "http://dtd.worldpay.com/paymentService_v1.dtd"> + <paymentService version="1.4" merchantCode="SPREEDLYCFT"> + <reply> + <orderStatus orderCode="bb2e156f3eb9e210fe11777c1102ea4b"> + <error code="5"><![CDATA[Requested capture amount (GBP 1.01) exceeds the authorised balance for this payment (GBP 1.00)]]></error> + </orderStatus> + </reply> + </paymentService> + RESPONSE end - private - - def successful_authorize_response - <<-RESPONSE + def successful_authorize_with_elo_response + <<~RESPONSE <?xml version="1.0" encoding="UTF-8"?> - <!DOCTYPE paymentService PUBLIC "-//Bibit//DTD Bibit PaymentService v1//EN" - "http://dtd.bibit.com/paymentService_v1.dtd"> - <paymentService version="1.4" merchantCode="XXXXXXXXXXXXXXX"> + <!DOCTYPE paymentService PUBLIC "-//WorldPay//DTD WorldPay PaymentService v1//EN" "http://dtd.worldpay.com/paymentService_v1.dtd"> + <paymentService version="1.4" merchantCode="SPREEDLY"> <reply> - <orderStatus orderCode="R50704213207145707"> + <orderStatus orderCode="9fe31a79de5f6aa3ce1ed7bea7edbf42"> <payment> - <paymentMethod>VISA-SSL</paymentMethod> - <amount value="15000" currencyCode="HKD" exponent="2" debitCreditIndicator="credit"/> + <paymentMethod>ELO-SSL</paymentMethod> + <amount value="100" currencyCode="BRL" exponent="2" debitCreditIndicator="credit" /> <lastEvent>AUTHORISED</lastEvent> - <CVCResultCode description="UNKNOWN"/> - <AVSResultCode description="UNKNOWN"/> + <CVCResultCode description="C" /> + <AVSResultCode description="H" /> <balance accountType="IN_PROCESS_AUTHORISED"> - <amount value="15000" currencyCode="HKD" exponent="2" debitCreditIndicator="credit"/> + <amount value="100" currencyCode="BRL" exponent="2" debitCreditIndicator="credit" /> </balance> - <cardNumber>4111********1111</cardNumber> - <riskScore value="1"/> + <cardNumber>4514********0008</cardNumber> + <riskScore value="21" /> </payment> </orderStatus> </reply> @@ -494,34 +2189,55 @@ def successful_authorize_response RESPONSE end - def failed_authorize_response - <<-RESPONSE + def successful_capture_with_elo_response + <<~RESPONSE <?xml version="1.0" encoding="UTF-8"?> - <!DOCTYPE paymentService PUBLIC "-//Bibit//DTD Bibit PaymentService v1//EN" - "http://dtd.bibit.com/paymentService_v1.dtd"> - <paymentService version="1.4" merchantCode="XXXXXXXXXXXXXXX"> + <!DOCTYPE paymentService PUBLIC "-//WorldPay//DTD WorldPay PaymentService v1//EN" "http://dtd.worldpay.com/paymentService_v1.dtd"> + <paymentService version="1.4" merchantCode="SPREEDLY"> <reply> - <orderStatus orderCode="R12538568107150952"> - <error code="7"> - <![CDATA[Invalid payment details : Card number : 4111********1111]]> - </error> + <ok> + <captureReceived orderCode="9fe31a79de5f6aa3ce1ed7bea7edbf42"> + <amount value="100" currencyCode="BRL" exponent="2" debitCreditIndicator="credit" /> + </captureReceived> + </ok> + </reply> + </paymentService> + RESPONSE + end + + def successful_void_inquiry_with_elo_response + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE paymentService PUBLIC "-//WorldPay//DTD WorldPay PaymentService v1//EN" "http://dtd.worldpay.com/paymentService_v1.dtd"> + <paymentService version="1.4" merchantCode="SPREEDLY"> + <reply> + <orderStatus orderCode="eda0b101428892fdb32e2fc617a7f5e0"> + <payment> + <paymentMethod>ELO-SSL</paymentMethod> + <amount value="100" currencyCode="BRL" exponent="2" debitCreditIndicator="credit" /> + <lastEvent>AUTHORISED</lastEvent> + <CVCResultCode description="C" /> + <AVSResultCode description="H" /> + <balance accountType="IN_PROCESS_AUTHORISED"> + <amount value="100" currencyCode="BRL" exponent="2" debitCreditIndicator="credit" /> + </balance> + <cardNumber>4514********0008</cardNumber> + <riskScore value="21" /> + </payment> </orderStatus> </reply> </paymentService> RESPONSE end - def successful_capture_response - <<-RESPONSE + def successful_void_with_elo_response + <<~RESPONSE <?xml version="1.0" encoding="UTF-8"?> - <!DOCTYPE paymentService PUBLIC "-//Bibit//DTD Bibit PaymentService v1//EN" - "http://dtd.bibit.com/paymentService_v1.dtd"> + <!DOCTYPE paymentService PUBLIC "-//WorldPay//DTD WorldPay PaymentService v1//EN" "http://dtd.worldpay.com/paymentService_v1.dtd"> <paymentService version="1.4" merchantCode="SPREEDLY"> <reply> <ok> - <captureReceived orderCode="33955f6bb4524813b51836de76228983"> - <amount value="100" currencyCode="GBP" exponent="2" debitCreditIndicator="credit"/> - </captureReceived> + <cancelReceived orderCode="3a10f83fb9bb765488d0b3eb153879d7" /> </ok> </reply> </paymentService> @@ -529,7 +2245,7 @@ def successful_capture_response end def successful_inquiry_response - <<-RESPONSE + <<~RESPONSE <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE paymentService PUBLIC "-//Bibit//DTD Bibit PaymentService v1//EN" "http://dtd.bibit.com/paymentService_v1.dtd"> @@ -556,7 +2272,7 @@ def successful_inquiry_response end def successful_void_inquiry_response - <<-RESPONSE + <<~RESPONSE <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE paymentService PUBLIC "-//WorldPay//DTD WorldPay PaymentService v1//EN" "http://dtd.worldpay.com/paymentService_v1.dtd"> @@ -583,7 +2299,7 @@ def successful_void_inquiry_response end def successful_void_response - <<-RESPONSE + <<~RESPONSE <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE paymentService PUBLIC "-//WorldPay//DTD WorldPay PaymentService v1//EN" "http://dtd.worldpay.com/paymentService_v1.dtd"> @@ -598,7 +2314,7 @@ def successful_void_response end def failed_void_inquiry_response - <<-RESPONSE + <<~RESPONSE <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE paymentService PUBLIC "-//WorldPay//DTD WorldPay PaymentService v1//EN" "http://dtd.worldpay.com/paymentService_v1.dtd"> @@ -625,35 +2341,64 @@ def failed_void_inquiry_response RESPONSE end - def successful_refund_inquiry_response(last_event='CAPTURED') - <<-RESPONSE -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE paymentService PUBLIC "-//Bibit//DTD Bibit PaymentService v1//EN" - "http://dtd.bibit.com/paymentService_v1.dtd"> -<paymentService version="1.4" merchantCode="SPREEDLY"> - <reply> - <orderStatus orderCode="d192c159d5730d339c03fa1a8dc796eb"> - <payment> - <paymentMethod>VISA-SSL</paymentMethod> - <amount value="100" currencyCode="GBP" exponent="2" debitCreditIndicator="credit"/> - <lastEvent>#{ last_event }</lastEvent> - <CVCResultCode description="UNKNOWN"/> - <AVSResultCode description="NOT SUPPLIED BY SHOPPER"/> - <balance accountType="IN_PROCESS_AUTHORISED"> - <amount value="100" currencyCode="GBP" exponent="2" debitCreditIndicator="credit"/> - </balance> - <cardNumber>4111********1111</cardNumber> - <riskScore value="1"/> - </payment> - <date dayOfMonth="20" month="04" year="2011" hour="22" minute="24" second="0"/> - </orderStatus> - </reply> -</paymentService> + def failed_purchase_response_without_useful_error_from_gateway + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <paymentService version="1.4" merchantCode="ACMECORP"> + <reply> + <orderStatus orderCode="2119303"> + <payment> + <paymentMethod>ECMC_DEBIT-SSL</paymentMethod> + <amount value="2000" currencyCode="USD" exponent="2" debitCreditIndicator="credit"/> + <lastEvent>REFUSED</lastEvent> + <IssuerResponseCode code="61" description="Exceeds withdrawal amount limit"/> + <CVCResultCode description="A"/> + <AVSResultCode description="H"/> + <AAVAddressResultCode description="B"/> + <AAVPostcodeResultCode description="B"/> + <AAVCardholderNameResultCode description="B"/> + <AAVTelephoneResultCode description="B"/> + <AAVEmailResultCode description="B"/> + <cardHolderName>Snuffy Smith</cardHolderName> + <issuerCountryCode>US</issuerCountryCode> + <issuerName>PRETEND BANK</issuerName> + <riskScore value="95"/> + </payment> + </orderStatus> + </reply> + </paymentService> + RESPONSE + end + + def successful_refund_inquiry_response(last_event = 'CAPTURED') + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE paymentService PUBLIC "-//Bibit//DTD Bibit PaymentService v1//EN" + "http://dtd.bibit.com/paymentService_v1.dtd"> + <paymentService version="1.4" merchantCode="SPREEDLY"> + <reply> + <orderStatus orderCode="d192c159d5730d339c03fa1a8dc796eb"> + <payment> + <paymentMethod>VISA-SSL</paymentMethod> + <amount value="100" currencyCode="GBP" exponent="2" debitCreditIndicator="credit"/> + <lastEvent>#{last_event}</lastEvent> + <CVCResultCode description="UNKNOWN"/> + <AVSResultCode description="NOT SUPPLIED BY SHOPPER"/> + <balance accountType="IN_PROCESS_AUTHORISED"> + <amount value="100" currencyCode="GBP" exponent="2" debitCreditIndicator="credit"/> + </balance> + <cardNumber>4111********1111</cardNumber> + <riskScore value="1"/> + </payment> + <date dayOfMonth="20" month="04" year="2011" hour="22" minute="24" second="0"/> + </orderStatus> + </reply> + </paymentService> RESPONSE end def successful_refund_response - <<-RESPONSE + <<~RESPONSE <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE paymentService PUBLIC "-//WorldPay//DTD WorldPay PaymentService v1//EN" "http://dtd.worldpay.com/paymentService_v1.dtd"> @@ -669,8 +2414,70 @@ def successful_refund_response RESPONSE end + def successful_cancel_or_refund_response + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE paymentService PUBLIC "-//WorldPay//DTD WorldPay PaymentService v1//EN" + "http://dtd.worldpay.com/paymentService_v1.dtd"> + <paymentService version="1.4" merchantCode="SPREEDLY"> + <reply> + <ok> + <voidReceived orderCode="afd85a0de932d5b7111b3eda78945544"></voidReceived> + </ok> + </reply> + </paymentService> + RESPONSE + end + + def successful_refund_synchronous_response + <<~RESPONSE + <paymentService version="1.4" merchantCode="MERCHANT-CODE"> + <reply> + <orderStatus orderCode="testcentralcell0008"> + <payment> + <paymentMethod>ECMC_CREDIT-SSL</paymentMethod> + <amount value="1000" currencyCode="ARS" exponent="2" debitCreditIndicator="credit"/> + <lastEvent>SENT_FOR_REFUND</lastEvent> + <AuthorisationId id="999999"/> + <CVCResultCode description="C"/> + <cardHolderName> + <![CDATA[CARDHOLDER_NAME]]> + </cardHolderName> + <issuerCountryCode>AR</issuerCountryCode> + <issuerName>ISSUER-NAME</issuerName> + <localAcquirer>WA</localAcquirer> + <schemeResponse> + <transactionIdentifier>999999999</transactionIdentifier> + </schemeResponse> + </payment> + <orderModification orderCode="testcentralcell0008"> + <refund> + <amount value="1000" currencyCode="ARS" exponent="2" debitCreditIndicator="credit"/> + </refund> + </orderModification> + </orderStatus> + </reply> + </paymentService> + RESPONSE + end + + def failed_refund_synchronous_response + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE paymentService PUBLIC "-//WorldPay//DTD WorldPay PaymentService v1//EN" + "http://dtd.worldpay.com/paymentService_v1.dtd"> + <paymentService version="1.4" merchantCode="SPREEDLYCFT"> + <reply> + <orderStatus orderCode="49a9d4e8a52bccbd3a3a6ac228ae0998"> + <error code="5"><![CDATA[Refund amount too high]]></error> + </orderStatus> + </reply> + </paymentService> + RESPONSE + end + def failed_refund_inquiry_response - <<-RESPONSE + <<~RESPONSE <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE paymentService PUBLIC "-//WorldPay//DTD WorldPay PaymentService v1//EN" "http://dtd.worldpay.com/paymentService_v1.dtd"> @@ -699,7 +2506,7 @@ def failed_refund_inquiry_response end def failed_void_response - <<-REQUEST + <<~REQUEST <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE paymentService PUBLIC "-//WorldPay//DTD WorldPay PaymentService v1//EN" "http://dtd.worldpay.com/paymentService_v1.dtd"> <paymentService version="1.4" merchantCode="CHARGEBEEM1"> @@ -714,8 +2521,8 @@ def failed_void_response REQUEST end - def successful_credit_response - <<-RESPONSE + def successful_visa_credit_response + <<~RESPONSE <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE paymentService PUBLIC "-//WorldPay//DTD WorldPay PaymentService v1//EN" "http://dtd.worldpay.com/paymentService_v1.dtd"> @@ -731,8 +2538,31 @@ def successful_credit_response RESPONSE end + def successful_mastercard_credit_response + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE paymentService PUBLIC "-//WorldPay//DTD WorldPay PaymentService v1//EN" + "http://dtd.worldpay.com/paymentService_v1.dtd"> + <paymentService version="1.4" merchantCode="YOUR_MERCHANT_CODE"> + <reply> + <orderStatus orderCode="f25257d251b81fb1fd9c210973c941ff\"> + <payment> + <paymentMethod>ECMC_DEBIT-SSL</paymentMethod> + <amount value="1110" currencyCode="GBP" exponent="2" debitCreditIndicator="credit"/> + <lastEvent>SENT_FOR_REFUND</lastEvent> + <AuthorisationId id="987654"/> + <balance accountType="IN_PROCESS_CAPTURED"> + <amount value="1110" currencyCode="GBP" exponent="2" debitCreditIndicator="debit"/> + </balance> + </payment> + </orderStatus> + </reply> + </paymentService> + RESPONSE + end + def sample_authorization_request - <<-REQUEST + <<~REQUEST <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE paymentService PUBLIC "-//RBS WorldPay//DTD RBS WorldPay PaymentService v1//EN" "http://dtd.wp3.rbsworldpay.com/paymentService_v1.dtd"> <paymentService merchantCode="XXXXXXXXXXXXXXX" version="1.4"> @@ -742,7 +2572,7 @@ def sample_authorization_request <amount value="100" exponent="2" currencyCode="HKD"/> <orderContent>Products Products Products</orderContent> <paymentDetails> - <VISA-SSL> + <CARD-SSL> <cardNumber>4242424242424242</cardNumber> <expiryDate> <date month="09" year="2011"/> @@ -762,7 +2592,7 @@ def sample_authorization_request <telephoneNumber>(555)555-5555</telephoneNumber> </address> </cardAddress> - </VISA-SSL> + </CARD-SSL> <session id="asfasfasfasdgvsdzvxzcvsd" shopperIPAddress="127.0.0.1"/> </paymentDetails> <shopper> @@ -778,72 +2608,373 @@ def sample_authorization_request end def transcript - <<-TRANSCRIPT - <paymentService version="1.4" merchantCode="CHARGEBEEM1"> - <submit> - <order orderCode="4efd348dbe6708b9ec9c118322e0954f"> - <description>Purchase</description> - <amount value="100" currencyCode="GBP" exponent="2"/> - <paymentDetails> - <VISA-SSL> - <cardNumber>4111111111111111</cardNumber> - <expiryDate> - <date month="09" year="2016"/> - </expiryDate> - <cardHolderName>Longbob Longsen</cardHolderName> - <cvc>123</cvc> - <cardAddress> - <address> - <address1>N/A</address1> - <postalCode>0000</postalCode> - <city>N/A</city> - <state>N/A</state> - <countryCode>US</countryCode> - </address> - </cardAddress> - </VISA-SSL> - </paymentDetails> - <shopper> - <shopperEmailAddress>wow@example.com</shopperEmailAddress> - </shopper> - </order> - </submit> - </paymentService> + <<~TRANSCRIPT + <paymentService version="1.4" merchantCode="CHARGEBEEM1"> + <submit> + <order orderCode="4efd348dbe6708b9ec9c118322e0954f"> + <description>Purchase</description> + <amount value="100" currencyCode="GBP" exponent="2"/> + <paymentDetails> + <CARD-SSL> + <cardNumber>4111111111111111</cardNumber> + <expiryDate> + <date month="09" year="2016"/> + </expiryDate> + <cardHolderName>Longbob Longsen</cardHolderName> + <cvc>123</cvc> + <cardAddress> + <address> + <address1>N/A</address1> + <postalCode>0000</postalCode> + <city>N/A</city> + <state>N/A</state> + <countryCode>US</countryCode> + </address> + </cardAddress> + </CARD-SSL> + </paymentDetails> + <shopper> + <shopperEmailAddress>wow@example.com</shopperEmailAddress> + </shopper> + </order> + </submit> + </paymentService> TRANSCRIPT end def scrubbed_transcript - <<-TRANSCRIPT - <paymentService version="1.4" merchantCode="CHARGEBEEM1"> - <submit> - <order orderCode="4efd348dbe6708b9ec9c118322e0954f"> - <description>Purchase</description> - <amount value="100" currencyCode="GBP" exponent="2"/> - <paymentDetails> - <VISA-SSL> - <cardNumber>[FILTERED]</cardNumber> - <expiryDate> - <date month="09" year="2016"/> - </expiryDate> - <cardHolderName>Longbob Longsen</cardHolderName> - <cvc>[FILTERED]</cvc> - <cardAddress> - <address> - <address1>N/A</address1> - <postalCode>0000</postalCode> - <city>N/A</city> - <state>N/A</state> + <<~TRANSCRIPT + <paymentService version="1.4" merchantCode="CHARGEBEEM1"> + <submit> + <order orderCode="4efd348dbe6708b9ec9c118322e0954f"> + <description>Purchase</description> + <amount value="100" currencyCode="GBP" exponent="2"/> + <paymentDetails> + <CARD-SSL> + <cardNumber>[FILTERED]</cardNumber> + <expiryDate> + <date month="09" year="2016"/> + </expiryDate> + <cardHolderName>Longbob Longsen</cardHolderName> + <cvc>[FILTERED]</cvc> + <cardAddress> + <address> + <address1>N/A</address1> + <postalCode>0000</postalCode> + <city>N/A</city> + <state>N/A</state> + <countryCode>US</countryCode> + </address> + </cardAddress> + </CARD-SSL> + </paymentDetails> + <shopper> + <shopperEmailAddress>wow@example.com</shopperEmailAddress> + </shopper> + </order> + </submit> + </paymentService> + TRANSCRIPT + end + + def aft_transcript + <<~TRANSCRIPT + <paymentService version="1.4" merchantCode="SPREEDLY"> + <submit> + <order orderCode="24602b5855e3edf2f7821f6e86694b7f"> + <description>Account Funding Transaction</description> + <amount value="100" currencyCode="GBP" exponent="2"/> + <paymentDetails> + <CARD-SSL> + <cardNumber>4111111111111111</cardNumber> + <expiryDate> + <date month="09" year="2025"/> + </expiryDate> + <cardHolderName>Longbob Longsen</cardHolderName> + <cvc>123</cvc> + </CARD-SSL> + </paymentDetails> + <shopper> + <shopperEmailAddress>wow@example.com</shopperEmailAddress> + <browser> + <acceptHeader/> + <userAgentHeader/> + </browser> + </shopper> + <fundingTransfer type="A" category="PULL_FROM_CARD"> + <paymentPurpose>01</paymentPurpose> + <fundingParty type="sender"> + <accountReference accountType="02">4111111111111112</accountReference> + <fullName> + <first>First</first> + <middle>Middle</middle> + <last>Sender</last> + </fullName> + <fundingAddress> + <address1>123 Sender St</address1> + <address2>Apt 1</address2> + <postalCode>12345</postalCode> + <city>Senderville</city> + <state>NC</state> <countryCode>US</countryCode> - </address> - </cardAddress> - </VISA-SSL> - </paymentDetails> - <shopper> - <shopperEmailAddress>wow@example.com</shopperEmailAddress> - </shopper> - </order> - </submit> - </paymentService> + </fundingAddress> + </fundingParty> + <fundingParty type="recipient"> + <accountReference accountType="03">4111111111111111</accountReference> + <fullName> + <first>First</first> + <middle>Middle</middle> + <last>Recipient</last> + </fullName> + <fundingAddress> + <address1>123 Recipient St</address1> + <address2>Apt 1</address2> + <postalCode>12345</postalCode> + <city>Recipientville</city> + <state>NC</state> + <countryCode>US</countryCode> + </fundingAddress> + <fundingData> + <birthDate> + <date dayOfMonth="01" month="01" year="1980"/> + </birthDate> + <telephoneNumber>123456789</telephoneNumber> + </fundingData> + </fundingParty> + </fundingTransfer> + </order> + </submit> + </paymentService> + TRANSCRIPT + end + + def aft_transcript_scrubbed + <<~TRANSCRIPT + <paymentService version="1.4" merchantCode="SPREEDLY"> + <submit> + <order orderCode="24602b5855e3edf2f7821f6e86694b7f"> + <description>Account Funding Transaction</description> + <amount value="100" currencyCode="GBP" exponent="2"/> + <paymentDetails> + <CARD-SSL> + <cardNumber>[FILTERED]</cardNumber> + <expiryDate> + <date month="09" year="2025"/> + </expiryDate> + <cardHolderName>Longbob Longsen</cardHolderName> + <cvc>[FILTERED]</cvc> + </CARD-SSL> + </paymentDetails> + <shopper> + <shopperEmailAddress>wow@example.com</shopperEmailAddress> + <browser> + <acceptHeader/> + <userAgentHeader/> + </browser> + </shopper> + <fundingTransfer type="A" category="PULL_FROM_CARD"> + <paymentPurpose>01</paymentPurpose> + <fundingParty type="sender"> + <accountReference accountType="02">[FILTERED]</accountReference> + <fullName> + <first>First</first> + <middle>Middle</middle> + <last>Sender</last> + </fullName> + <fundingAddress> + <address1>123 Sender St</address1> + <address2>Apt 1</address2> + <postalCode>12345</postalCode> + <city>Senderville</city> + <state>NC</state> + <countryCode>US</countryCode> + </fundingAddress> + </fundingParty> + <fundingParty type="recipient"> + <accountReference accountType="03">[FILTERED]</accountReference> + <fullName> + <first>First</first> + <middle>Middle</middle> + <last>Recipient</last> + </fullName> + <fundingAddress> + <address1>123 Recipient St</address1> + <address2>Apt 1</address2> + <postalCode>12345</postalCode> + <city>Recipientville</city> + <state>NC</state> + <countryCode>US</countryCode> + </fundingAddress> + <fundingData> + <birthDate> + <date dayOfMonth="01" month="01" year="1980"/> + </birthDate> + <telephoneNumber>123456789</telephoneNumber> + </fundingData> + </fundingParty> + </fundingTransfer> + </order> + </submit> + </paymentService> TRANSCRIPT end + + def network_token_transcript + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE paymentService PUBLIC "-//WorldPay//DTD WorldPay PaymentService v1//EN" "http://dtd.worldpay.com/paymentService_v1.dtd"> + <paymentService version="1.4" merchantCode="SPREEDLY"> + <submit> + <order orderCode="c293b34a70aee391193a1c08168b6c91"> + <description>Purchase</description> + <amount value="100" currencyCode="GBP" exponent="2" /> + <paymentDetails> + <EMVCO_TOKEN-SSL type="APPLEPAY"> + <tokenNumber>4895370015293175</tokenNumber> + <expiryDate> + <date month="10" year="2024" /> + </expiryDate> + <cardHolderName>PedroPerez</cardHolderName> + <cryptogram>axxxxxxxxx</cryptogram> + <eciIndicator>07</eciIndicator> + </EMVCO_TOKEN-SSL> + </paymentDetails> + <shopper> + <shopperEmailAddress>wow@ example.com</shopperEmailAddress> + <browser> + <acceptHeader /> + <userAgentHeader /> + </browser> + </shopper> + </order> + </submit> + </paymentService> + RESPONSE + end + + def network_token_transcript_scrubbed + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE paymentService PUBLIC "-//WorldPay//DTD WorldPay PaymentService v1//EN" "http://dtd.worldpay.com/paymentService_v1.dtd"> + <paymentService version="1.4" merchantCode="SPREEDLY"> + <submit> + <order orderCode="c293b34a70aee391193a1c08168b6c91"> + <description>Purchase</description> + <amount value="100" currencyCode="GBP" exponent="2" /> + <paymentDetails> + <EMVCO_TOKEN-SSL type="APPLEPAY"> + <tokenNumber>[FILTERED]</tokenNumber> + <expiryDate> + <date month="10" year="2024" /> + </expiryDate> + <cardHolderName>PedroPerez</cardHolderName> + <cryptogram>[FILTERED]</cryptogram> + <eciIndicator>07</eciIndicator> + </EMVCO_TOKEN-SSL> + </paymentDetails> + <shopper> + <shopperEmailAddress>wow@ example.com</shopperEmailAddress> + <browser> + <acceptHeader /> + <userAgentHeader /> + </browser> + </shopper> + </order> + </submit> + </paymentService> + RESPONSE + end + + def failed_with_unknown_card_response + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE paymentService PUBLIC "-//WorldPay//DTD WorldPay PaymentService v1//EN" "http://dtd.worldpay.com/paymentService_v1.dtd"> + <paymentService version="1.4" merchantCode="SPREEDLY"> + <reply> + <error code="5"> + <![CDATA[XML failed validation: Invalid payment details : Card number not recognised: 606070******4400]]> + </error> + </reply> + </paymentService> + RESPONSE + end + + def successful_store_response + <<~RESPONSE + <?xml version="1.0"?> + <!DOCTYPE paymentService PUBLIC "-//WorldPay//DTD WorldPay PaymentService v1//EN" "http://dtd.worldpay.com/paymentService_v1.dtd"> + <paymentService version="1.4" merchantCode="SPREEDLY"> + <reply> + <token> + <authenticatedShopperID>59424549c291397379f30c5c082dbed8</authenticatedShopperID> + <tokenDetails tokenEvent="NEW"> + <paymentTokenID>99411111780163871111</paymentTokenID> + <paymentTokenExpiry> + <date dayOfMonth="30" month="05" year="2019" hour="22" minute="54" second="47"/> + </paymentTokenExpiry> + <tokenReason>Created token without payment on 2019-05-23</tokenReason> + </tokenDetails> + <paymentInstrument> + <cardDetails> + <expiryDate> + <date month="09" year="2020"/> + </expiryDate> + <cardHolderName><![CDATA[Longbob Longsen]]></cardHolderName> + <derived> + <cardBrand>VISA</cardBrand> + <cardSubBrand>VISA_CREDIT</cardSubBrand> + <issuerCountryCode>N/A</issuerCountryCode> + <issuerName>TARGOBANK AG & CO. KGAA</issuerName> + <obfuscatedPAN>4111********1111</obfuscatedPAN> + </derived> + </cardDetails> + </paymentInstrument> + </token> + </reply> + </paymentService> + RESPONSE + end + + def failed_store_response + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE paymentService PUBLIC "-//WorldPay//DTD WorldPay PaymentService v1//EN" "http://dtd.worldpay.com/paymentService_v1.dtd"> + <paymentService version="1.4" merchantCode="SPREEDLY"> + <reply> + <error code="2"><![CDATA[authenticatedShopperID cannot start with an underscore]]></error> + </reply> + </paymentService> + RESPONSE + end + + def successful_aft_response + <<~RESPONSE + <?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE paymentService PUBLIC "-//WorldPay//DTD WorldPay PaymentService v1//EN" "http://dtd.worldpay.com/paymentService_v1.dtd"> + <paymentService version="1.4" merchantCode="SPREEDLY"> + <reply> + <orderStatus orderCode="d493bbdf45239ef244316bba986f5196"> + <payment> + <paymentMethod>VISA_CREDIT-SSL</paymentMethod> + <amount value="100" currencyCode="GBP" exponent="2" debitCreditIndicator="credit"/> + <lastEvent>AUTHORISED</lastEvent> + <CVCResultCode description="C"/> + <AVSResultCode description="H"/> + <cardHolderName><![CDATA[Longbob Longsen]]></cardHolderName> + <issuerCountryCode>N/A</issuerCountryCode> + <balance accountType="IN_PROCESS_AUTHORISED"> + <amount value="100" currencyCode="GBP" exponent="2" debitCreditIndicator="credit"/> + </balance> + <cardNumber>4111********1111</cardNumber> + <riskScore value="1"/> + <schemeResponse> + <transactionIdentifier>060720116005062</transactionIdentifier> + </schemeResponse> + <fundingLinkId></fundingLinkId> + </payment> + </orderStatus> + </reply> + </paymentService> + RESPONSE + end end diff --git a/test/unit/gateways/worldpay_us_test.rb b/test/unit/gateways/worldpay_us_test.rb index ee576daa591..65040db2915 100644 --- a/test/unit/gateways/worldpay_us_test.rb +++ b/test/unit/gateways/worldpay_us_test.rb @@ -67,7 +67,7 @@ def test_authorize_and_capture capture = stub_comms do @gateway.capture(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/postonly=354275517/, data) end.respond_with(successful_capture_response) @@ -84,7 +84,7 @@ def test_refund refund = stub_comms do @gateway.refund(@amount, response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/historykeyid=353583515/, data) assert_match(/orderkeyid=252889136/, data) end.respond_with(successful_refund_response) @@ -102,7 +102,7 @@ def test_void refund = stub_comms do @gateway.void(response.authorization) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/historykeyid=353583515/, data) assert_match(/orderkeyid=252889136/, data) end.respond_with(successful_refund_response) @@ -136,15 +136,15 @@ def test_unsuccessful_verify def test_passing_cvv stub_comms do @gateway.purchase(@amount, @credit_card) - end.check_request do |endpoint, data, headers| + end.check_request do |_endpoint, data, _headers| assert_match(/#{@credit_card.verification_value}/, data) end.respond_with(successful_purchase_response) end def test_passing_billing_address stub_comms do - @gateway.purchase(@amount, @credit_card, :billing_address => address) - end.check_request do |endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, billing_address: address) + end.check_request do |_endpoint, data, _headers| assert_match(/ci_billaddr1=456\+My\+Street/, data) assert_match(/ci_billzip=K1C2N6/, data) end.respond_with(successful_purchase_response) @@ -152,16 +152,16 @@ def test_passing_billing_address def test_passing_phone_number stub_comms do - @gateway.purchase(@amount, @credit_card, :billing_address => address) - end.check_request do |endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, billing_address: address) + end.check_request do |_endpoint, data, _headers| assert_match(/ci_phone=%28555%29555-5555/, data) end.respond_with(successful_purchase_response) end def test_passing_billing_address_without_phone stub_comms do - @gateway.purchase(@amount, @credit_card, :billing_address => address(:phone => nil)) - end.check_request do |endpoint, data, headers| + @gateway.purchase(@amount, @credit_card, billing_address: address(phone: nil)) + end.check_request do |_endpoint, data, _headers| assert_no_match(/udf3/, data) end.respond_with(successful_purchase_response) end @@ -178,7 +178,7 @@ def test_empty_response_fails def test_backup_url response = stub_comms(@gateway) do @gateway.purchase(@amount, @credit_card, use_backup_url: true) - end.check_request do |endpoint, data, headers| + end.check_request do |endpoint, _data, _headers| assert_equal WorldpayUsGateway.backup_url, endpoint end.respond_with(successful_purchase_response) assert_success response @@ -436,94 +436,94 @@ def failed_void_response end def pre_scrubbed - <<-EOS -opening connection to trans.worldpay.us:443... -opened -starting SSL for trans.worldpay.us:443... -SSL established -<- "POST /cgi-bin/process.cgi 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: trans.worldpay.us\r\nContent-Length: 425\r\n\r\n" -<- "acctid=MPNAB&action=ns_quicksale_cc&amount=1.00&ccname=Longbob+Longsen&ccnum=4446661234567892&ci_billaddr1=456+My+Street&ci_billaddr2=Apt+1&ci_billcity=Ottawa&ci_billcountry=CA&ci_billstate=ON&ci_billzip=K1C2N6&ci_companyname=Widgets+Inc&ci_email=&ci_ipaddress=&ci_phone=%28555%29555-5555&currencycode=USD&cvv2=987&expmon=09&expyear=2019&merchantordernumber=67f4f20082e79684f036f25dafe96304&merchantpin=1234567890&subid=SPREE" --> "HTTP/1.1 200 OK\r\n" --> "Date: Tue, 13 Feb 2018 19:28:27 GMT\r\n" --> "Server: Apache\r\n" --> "X-Frame-Options: SAMEORIGIN\r\n" --> "Content-Type: text/html;charset=ISO-8859-1\r\n" --> "Content-Length: 962\r\n" --> "Connection: close\r\n" --> "\r\n" -reading 962 bytes... --> "<html><body><plaintext>\r\nAccepted=SALE:036586:477::919067116:N::N\r\nhistoryid=919067116\r\norderid=722189706\r\nAccepted=SALE:036586:477::919067116:N::N\r\nACCOUNTNUMBER=************7892\r\nACCTID=MPNAB\r\nauthcode=036586\r\nAuthNo=SALE:036586:477::919067116:N::N\r\nAVS_RESULT=N\r\nBATCHNUMBER=\r\nCVV2_RESULT=N\r\nDEBIT_TRACE_NUMBER=\r\nENTRYMETHOD=M\r\nhistoryid=919067116\r\nMERCHANT_DBA_ADDR=11121 Willows Road NE\r\nMERCHANT_DBA_CITY=Redmond\r\nMERCHANT_DBA_NAME=Merchant Partners\r\nMERCHANT_DBA_PHONE=4254979909\r\nMERCHANT_DBA_STATE=WA\r\nMERCHANTID=542929804946788\r\nMERCHANTORDERNUMBER=67f4f20082e79684f036f25dafe96304\r\norderid=722189706\r\nPAYTYPE=Visa\r\nPRODUCT_DESCRIPTION=\r\nReason=\r\nRECEIPT_FOOTER=Thank You\r\nrecurid=0\r\nrefcode=919067116-036586\r\nresult=1\r\nSEQUENCE_NUMBER=370609730\r\nStatus=Accepted\r\nSUBID=SPREE\r\nSYSTEMAUDITTRACENUMBER=477\r\nTERMINALID=160551\r\nTRANSGUID=d5701d57-9147-4ded-b596-6805581f081c:266\r\ntransid=370609730\r\ntransresult=APPROVED\r\nVISATRANSACTIONID=088044000036586\r\n" -read 962 bytes -Conn close - EOS + <<~REQUEST + opening connection to trans.worldpay.us:443... + opened + starting SSL for trans.worldpay.us:443... + SSL established + <- "POST /cgi-bin/process.cgi 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: trans.worldpay.us\r\nContent-Length: 425\r\n\r\n" + <- "acctid=MPNAB&action=ns_quicksale_cc&amount=1.00&ccname=Longbob+Longsen&ccnum=4446661234567892&ci_billaddr1=456+My+Street&ci_billaddr2=Apt+1&ci_billcity=Ottawa&ci_billcountry=CA&ci_billstate=ON&ci_billzip=K1C2N6&ci_companyname=Widgets+Inc&ci_email=&ci_ipaddress=&ci_phone=%28555%29555-5555&currencycode=USD&cvv2=987&expmon=09&expyear=2019&merchantordernumber=67f4f20082e79684f036f25dafe96304&merchantpin=1234567890&subid=SPREE" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Tue, 13 Feb 2018 19:28:27 GMT\r\n" + -> "Server: Apache\r\n" + -> "X-Frame-Options: SAMEORIGIN\r\n" + -> "Content-Type: text/html;charset=ISO-8859-1\r\n" + -> "Content-Length: 962\r\n" + -> "Connection: close\r\n" + -> "\r\n" + reading 962 bytes... + -> "<html><body><plaintext>\r\nAccepted=SALE:036586:477::919067116:N::N\r\nhistoryid=919067116\r\norderid=722189706\r\nAccepted=SALE:036586:477::919067116:N::N\r\nACCOUNTNUMBER=************7892\r\nACCTID=MPNAB\r\nauthcode=036586\r\nAuthNo=SALE:036586:477::919067116:N::N\r\nAVS_RESULT=N\r\nBATCHNUMBER=\r\nCVV2_RESULT=N\r\nDEBIT_TRACE_NUMBER=\r\nENTRYMETHOD=M\r\nhistoryid=919067116\r\nMERCHANT_DBA_ADDR=11121 Willows Road NE\r\nMERCHANT_DBA_CITY=Redmond\r\nMERCHANT_DBA_NAME=Merchant Partners\r\nMERCHANT_DBA_PHONE=4254979909\r\nMERCHANT_DBA_STATE=WA\r\nMERCHANTID=542929804946788\r\nMERCHANTORDERNUMBER=67f4f20082e79684f036f25dafe96304\r\norderid=722189706\r\nPAYTYPE=Visa\r\nPRODUCT_DESCRIPTION=\r\nReason=\r\nRECEIPT_FOOTER=Thank You\r\nrecurid=0\r\nrefcode=919067116-036586\r\nresult=1\r\nSEQUENCE_NUMBER=370609730\r\nStatus=Accepted\r\nSUBID=SPREE\r\nSYSTEMAUDITTRACENUMBER=477\r\nTERMINALID=160551\r\nTRANSGUID=d5701d57-9147-4ded-b596-6805581f081c:266\r\ntransid=370609730\r\ntransresult=APPROVED\r\nVISATRANSACTIONID=088044000036586\r\n" + read 962 bytes + Conn close + REQUEST end def post_scrubbed - <<-EOS -opening connection to trans.worldpay.us:443... -opened -starting SSL for trans.worldpay.us:443... -SSL established -<- "POST /cgi-bin/process.cgi 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: trans.worldpay.us\r\nContent-Length: 425\r\n\r\n" -<- "acctid=MPNAB&action=ns_quicksale_cc&amount=1.00&ccname=Longbob+Longsen&ccnum=[FILTERED]&ci_billaddr1=456+My+Street&ci_billaddr2=Apt+1&ci_billcity=Ottawa&ci_billcountry=CA&ci_billstate=ON&ci_billzip=K1C2N6&ci_companyname=Widgets+Inc&ci_email=&ci_ipaddress=&ci_phone=%28555%29555-5555&currencycode=USD&cvv2=[FILTERED]&expmon=09&expyear=2019&merchantordernumber=67f4f20082e79684f036f25dafe96304&merchantpin=[FILTERED]&subid=SPREE" --> "HTTP/1.1 200 OK\r\n" --> "Date: Tue, 13 Feb 2018 19:28:27 GMT\r\n" --> "Server: Apache\r\n" --> "X-Frame-Options: SAMEORIGIN\r\n" --> "Content-Type: text/html;charset=ISO-8859-1\r\n" --> "Content-Length: 962\r\n" --> "Connection: close\r\n" --> "\r\n" -reading 962 bytes... --> "<html><body><plaintext>\r\nAccepted=SALE:036586:477::919067116:N::N\r\nhistoryid=919067116\r\norderid=722189706\r\nAccepted=SALE:036586:477::919067116:N::N\r\nACCOUNTNUMBER=************7892\r\nACCTID=MPNAB\r\nauthcode=036586\r\nAuthNo=SALE:036586:477::919067116:N::N\r\nAVS_RESULT=N\r\nBATCHNUMBER=\r\nCVV2_RESULT=N\r\nDEBIT_TRACE_NUMBER=\r\nENTRYMETHOD=M\r\nhistoryid=919067116\r\nMERCHANT_DBA_ADDR=11121 Willows Road NE\r\nMERCHANT_DBA_CITY=Redmond\r\nMERCHANT_DBA_NAME=Merchant Partners\r\nMERCHANT_DBA_PHONE=4254979909\r\nMERCHANT_DBA_STATE=WA\r\nMERCHANTID=542929804946788\r\nMERCHANTORDERNUMBER=67f4f20082e79684f036f25dafe96304\r\norderid=722189706\r\nPAYTYPE=Visa\r\nPRODUCT_DESCRIPTION=\r\nReason=\r\nRECEIPT_FOOTER=Thank You\r\nrecurid=0\r\nrefcode=919067116-036586\r\nresult=1\r\nSEQUENCE_NUMBER=370609730\r\nStatus=Accepted\r\nSUBID=SPREE\r\nSYSTEMAUDITTRACENUMBER=477\r\nTERMINALID=160551\r\nTRANSGUID=d5701d57-9147-4ded-b596-6805581f081c:266\r\ntransid=370609730\r\ntransresult=APPROVED\r\nVISATRANSACTIONID=088044000036586\r\n" -read 962 bytes -Conn close - EOS + <<~REQUEST + opening connection to trans.worldpay.us:443... + opened + starting SSL for trans.worldpay.us:443... + SSL established + <- "POST /cgi-bin/process.cgi 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: trans.worldpay.us\r\nContent-Length: 425\r\n\r\n" + <- "acctid=MPNAB&action=ns_quicksale_cc&amount=1.00&ccname=Longbob+Longsen&ccnum=[FILTERED]&ci_billaddr1=456+My+Street&ci_billaddr2=Apt+1&ci_billcity=Ottawa&ci_billcountry=CA&ci_billstate=ON&ci_billzip=K1C2N6&ci_companyname=Widgets+Inc&ci_email=&ci_ipaddress=&ci_phone=%28555%29555-5555&currencycode=USD&cvv2=[FILTERED]&expmon=09&expyear=2019&merchantordernumber=67f4f20082e79684f036f25dafe96304&merchantpin=[FILTERED]&subid=SPREE" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Tue, 13 Feb 2018 19:28:27 GMT\r\n" + -> "Server: Apache\r\n" + -> "X-Frame-Options: SAMEORIGIN\r\n" + -> "Content-Type: text/html;charset=ISO-8859-1\r\n" + -> "Content-Length: 962\r\n" + -> "Connection: close\r\n" + -> "\r\n" + reading 962 bytes... + -> "<html><body><plaintext>\r\nAccepted=SALE:036586:477::919067116:N::N\r\nhistoryid=919067116\r\norderid=722189706\r\nAccepted=SALE:036586:477::919067116:N::N\r\nACCOUNTNUMBER=************7892\r\nACCTID=MPNAB\r\nauthcode=036586\r\nAuthNo=SALE:036586:477::919067116:N::N\r\nAVS_RESULT=N\r\nBATCHNUMBER=\r\nCVV2_RESULT=N\r\nDEBIT_TRACE_NUMBER=\r\nENTRYMETHOD=M\r\nhistoryid=919067116\r\nMERCHANT_DBA_ADDR=11121 Willows Road NE\r\nMERCHANT_DBA_CITY=Redmond\r\nMERCHANT_DBA_NAME=Merchant Partners\r\nMERCHANT_DBA_PHONE=4254979909\r\nMERCHANT_DBA_STATE=WA\r\nMERCHANTID=542929804946788\r\nMERCHANTORDERNUMBER=67f4f20082e79684f036f25dafe96304\r\norderid=722189706\r\nPAYTYPE=Visa\r\nPRODUCT_DESCRIPTION=\r\nReason=\r\nRECEIPT_FOOTER=Thank You\r\nrecurid=0\r\nrefcode=919067116-036586\r\nresult=1\r\nSEQUENCE_NUMBER=370609730\r\nStatus=Accepted\r\nSUBID=SPREE\r\nSYSTEMAUDITTRACENUMBER=477\r\nTERMINALID=160551\r\nTRANSGUID=d5701d57-9147-4ded-b596-6805581f081c:266\r\ntransid=370609730\r\ntransresult=APPROVED\r\nVISATRANSACTIONID=088044000036586\r\n" + read 962 bytes + Conn close + REQUEST end def pre_scrubbed_check - <<-EOS -opening connection to trans.worldpay.us:443... -opened -starting SSL for trans.worldpay.us:443... -SSL established -<- "POST /cgi-bin/process.cgi 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: trans.worldpay.us\r\nContent-Length: 412\r\n\r\n" -<- "acctid=MPNAB&action=ns_quicksale_check&amount=1.00&ci_billaddr1=456+My+Street&ci_billaddr2=Apt+1&ci_billcity=Ottawa&ci_billcountry=CA&ci_billstate=ON&ci_billzip=K1C2N6&ci_companyname=Widgets+Inc&ci_email=&ci_ipaddress=&ci_phone=%28555%29555-5555&ckaba=244183602&ckacct=15378535&ckaccttype=1&ckno=12345654321&currencycode=USD&merchantordernumber=5ec80ff8210dc9d24248ac2777d6b4f3&merchantpin=1234567890&subid=SPREE" --> "HTTP/1.1 200 OK\r\n" --> "Date: Tue, 13 Feb 2018 19:29:38 GMT\r\n" --> "Server: Apache\r\n" --> "X-Frame-Options: SAMEORIGIN\r\n" --> "Content-Type: text/html;charset=ISO-8859-1\r\n" --> "Content-Length: 414\r\n" --> "Connection: close\r\n" --> "\r\n" -reading 414 bytes... --> "<html><body><plaintext>\r\nDeclined=DECLINED:1103180001:Invalid Bank:\r\nhistoryid=919060608\r\norderid=722196666\r\nACCOUNTNUMBER=****8535\r\nDeclined=DECLINED:1103180001:Invalid Bank:\r\nENTRYMETHOD=KEYED\r\nhistoryid=919060608\r\nMERCHANTORDERNUMBER=5ec80ff8210dc9d24248ac2777d6b4f3\r\norderid=722196666\r\nPAYTYPE=Check\r\nrcode=1103180001\r\nReason=DECLINED:1103180001:Invalid Bank:\r\nrecurid=0\r\nresult=0\r\nStatus=Declined\r\ntransid=0\r\n" -read 414 bytes -Conn close - EOS + <<~REQUEST + opening connection to trans.worldpay.us:443... + opened + starting SSL for trans.worldpay.us:443... + SSL established + <- "POST /cgi-bin/process.cgi 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: trans.worldpay.us\r\nContent-Length: 412\r\n\r\n" + <- "acctid=MPNAB&action=ns_quicksale_check&amount=1.00&ci_billaddr1=456+My+Street&ci_billaddr2=Apt+1&ci_billcity=Ottawa&ci_billcountry=CA&ci_billstate=ON&ci_billzip=K1C2N6&ci_companyname=Widgets+Inc&ci_email=&ci_ipaddress=&ci_phone=%28555%29555-5555&ckaba=244183602&ckacct=15378535&ckaccttype=1&ckno=12345654321&currencycode=USD&merchantordernumber=5ec80ff8210dc9d24248ac2777d6b4f3&merchantpin=1234567890&subid=SPREE" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Tue, 13 Feb 2018 19:29:38 GMT\r\n" + -> "Server: Apache\r\n" + -> "X-Frame-Options: SAMEORIGIN\r\n" + -> "Content-Type: text/html;charset=ISO-8859-1\r\n" + -> "Content-Length: 414\r\n" + -> "Connection: close\r\n" + -> "\r\n" + reading 414 bytes... + -> "<html><body><plaintext>\r\nDeclined=DECLINED:1103180001:Invalid Bank:\r\nhistoryid=919060608\r\norderid=722196666\r\nACCOUNTNUMBER=****8535\r\nDeclined=DECLINED:1103180001:Invalid Bank:\r\nENTRYMETHOD=KEYED\r\nhistoryid=919060608\r\nMERCHANTORDERNUMBER=5ec80ff8210dc9d24248ac2777d6b4f3\r\norderid=722196666\r\nPAYTYPE=Check\r\nrcode=1103180001\r\nReason=DECLINED:1103180001:Invalid Bank:\r\nrecurid=0\r\nresult=0\r\nStatus=Declined\r\ntransid=0\r\n" + read 414 bytes + Conn close + REQUEST end def post_scrubbed_check - <<-EOS -opening connection to trans.worldpay.us:443... -opened -starting SSL for trans.worldpay.us:443... -SSL established -<- "POST /cgi-bin/process.cgi 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: trans.worldpay.us\r\nContent-Length: 412\r\n\r\n" -<- "acctid=MPNAB&action=ns_quicksale_check&amount=1.00&ci_billaddr1=456+My+Street&ci_billaddr2=Apt+1&ci_billcity=Ottawa&ci_billcountry=CA&ci_billstate=ON&ci_billzip=K1C2N6&ci_companyname=Widgets+Inc&ci_email=&ci_ipaddress=&ci_phone=%28555%29555-5555&ckaba=244183602&ckacct=[FILTERED]&ckaccttype=1&ckno=12345654321&currencycode=USD&merchantordernumber=5ec80ff8210dc9d24248ac2777d6b4f3&merchantpin=[FILTERED]&subid=SPREE" --> "HTTP/1.1 200 OK\r\n" --> "Date: Tue, 13 Feb 2018 19:29:38 GMT\r\n" --> "Server: Apache\r\n" --> "X-Frame-Options: SAMEORIGIN\r\n" --> "Content-Type: text/html;charset=ISO-8859-1\r\n" --> "Content-Length: 414\r\n" --> "Connection: close\r\n" --> "\r\n" -reading 414 bytes... --> "<html><body><plaintext>\r\nDeclined=DECLINED:1103180001:Invalid Bank:\r\nhistoryid=919060608\r\norderid=722196666\r\nACCOUNTNUMBER=****8535\r\nDeclined=DECLINED:1103180001:Invalid Bank:\r\nENTRYMETHOD=KEYED\r\nhistoryid=919060608\r\nMERCHANTORDERNUMBER=5ec80ff8210dc9d24248ac2777d6b4f3\r\norderid=722196666\r\nPAYTYPE=Check\r\nrcode=1103180001\r\nReason=DECLINED:1103180001:Invalid Bank:\r\nrecurid=0\r\nresult=0\r\nStatus=Declined\r\ntransid=0\r\n" -read 414 bytes -Conn close - EOS + <<~REQUEST + opening connection to trans.worldpay.us:443... + opened + starting SSL for trans.worldpay.us:443... + SSL established + <- "POST /cgi-bin/process.cgi 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: trans.worldpay.us\r\nContent-Length: 412\r\n\r\n" + <- "acctid=MPNAB&action=ns_quicksale_check&amount=1.00&ci_billaddr1=456+My+Street&ci_billaddr2=Apt+1&ci_billcity=Ottawa&ci_billcountry=CA&ci_billstate=ON&ci_billzip=K1C2N6&ci_companyname=Widgets+Inc&ci_email=&ci_ipaddress=&ci_phone=%28555%29555-5555&ckaba=244183602&ckacct=[FILTERED]&ckaccttype=1&ckno=12345654321&currencycode=USD&merchantordernumber=5ec80ff8210dc9d24248ac2777d6b4f3&merchantpin=[FILTERED]&subid=SPREE" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Tue, 13 Feb 2018 19:29:38 GMT\r\n" + -> "Server: Apache\r\n" + -> "X-Frame-Options: SAMEORIGIN\r\n" + -> "Content-Type: text/html;charset=ISO-8859-1\r\n" + -> "Content-Length: 414\r\n" + -> "Connection: close\r\n" + -> "\r\n" + reading 414 bytes... + -> "<html><body><plaintext>\r\nDeclined=DECLINED:1103180001:Invalid Bank:\r\nhistoryid=919060608\r\norderid=722196666\r\nACCOUNTNUMBER=****8535\r\nDeclined=DECLINED:1103180001:Invalid Bank:\r\nENTRYMETHOD=KEYED\r\nhistoryid=919060608\r\nMERCHANTORDERNUMBER=5ec80ff8210dc9d24248ac2777d6b4f3\r\norderid=722196666\r\nPAYTYPE=Check\r\nrcode=1103180001\r\nReason=DECLINED:1103180001:Invalid Bank:\r\nrecurid=0\r\nresult=0\r\nStatus=Declined\r\ntransid=0\r\n" + read 414 bytes + Conn close + REQUEST end end 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/multi_response_test.rb b/test/unit/multi_response_test.rb index 82e8735e962..ed7c1935fd2 100644 --- a/test/unit/multi_response_test.rb +++ b/test/unit/multi_response_test.rb @@ -12,8 +12,8 @@ def test_processes_sub_requests r1 = Response.new(true, '1', {}) r2 = Response.new(true, '2', {}) m = MultiResponse.run do |r| - r.process{r1} - r.process{r2} + r.process { r1 } + r.process { r2 } end assert_equal [r1, r2], m.responses end @@ -22,8 +22,8 @@ def test_run_convenience_method r1 = Response.new(true, '1', {}) r2 = Response.new(true, '2', {}) m = MultiResponse.run do |r| - r.process{r1} - r.process{r2} + r.process { r1 } + r.process { r2 } end assert_equal [r1, r2], m.responses end @@ -34,16 +34,16 @@ def test_proxies_last_request r1 = Response.new( true, '1', - {'one' => 1}, - :test => true, - :authorization => 'auth1', - :avs_result => {:code => 'AVS1'}, - :cvv_result => 'CVV1', - :error_code => :card_declined, - :fraud_review => true + { 'one' => 1 }, + test: true, + authorization: 'auth1', + avs_result: { code: 'AVS1' }, + cvv_result: 'CVV1', + error_code: :card_declined, + fraud_review: true ) - m.process{r1} - assert_equal({'one' => 1}, m.params) + m.process { r1 } + assert_equal({ 'one' => 1 }, m.params) assert_equal '1', m.message assert m.test assert_equal 'auth1', m.authorization @@ -56,15 +56,15 @@ def test_proxies_last_request r2 = Response.new( true, '2', - {'two' => 2}, - :test => false, - :authorization => 'auth2', - :avs_result => {:code => 'AVS2'}, - :cvv_result => 'CVV2', - :fraud_review => false + { 'two' => 2 }, + test: false, + authorization: 'auth2', + avs_result: { code: 'AVS2' }, + cvv_result: 'CVV2', + fraud_review: false ) - m.process{r2} - assert_equal({'two' => 2}, m.params) + m.process { r2 } + assert_equal({ 'two' => 2 }, m.params) assert_equal '2', m.message assert !m.test assert_equal 'auth2', m.authorization @@ -80,15 +80,15 @@ def test_proxies_first_request_if_marked r1 = Response.new( true, '1', - {'one' => 1}, - :test => true, - :authorization => 'auth1', - :avs_result => {:code => 'AVS1'}, - :cvv_result => 'CVV1', - :fraud_review => true + { 'one' => 1 }, + test: true, + authorization: 'auth1', + avs_result: { code: 'AVS1' }, + cvv_result: 'CVV1', + fraud_review: true ) - m.process{r1} - assert_equal({'one' => 1}, m.params) + m.process { r1 } + assert_equal({ 'one' => 1 }, m.params) assert_equal '1', m.message assert m.test assert_equal 'auth1', m.authorization @@ -100,15 +100,15 @@ def test_proxies_first_request_if_marked r2 = Response.new( true, '2', - {'two' => 2}, - :test => false, - :authorization => 'auth2', - :avs_result => {:code => 'AVS2'}, - :cvv_result => 'CVV2', - :fraud_review => false + { 'two' => 2 }, + test: false, + authorization: 'auth2', + avs_result: { code: 'AVS2' }, + cvv_result: 'CVV2', + fraud_review: false ) - m.process{r2} - assert_equal({'one' => 1}, m.params) + m.process { r2 } + assert_equal({ 'one' => 1 }, m.params) assert_equal '1', m.message assert m.test assert_equal 'auth1', m.authorization @@ -124,9 +124,9 @@ def test_primary_response_always_returns_the_last_response_on_failure r1 = Response.new(true, '1', {}, {}) r2 = Response.new(false, '2', {}, {}) r3 = Response.new(false, '3', {}, {}) - m.process{r1} - m.process{r2} - m.process{r3} + m.process { r1 } + m.process { r2 } + m.process { r3 } assert_equal r2, m.primary_response assert_equal '2', m.message end @@ -135,8 +135,8 @@ def test_stops_processing_upon_failure r1 = Response.new(false, '1', {}) r2 = Response.new(true, '2', {}) m = MultiResponse.run do |r| - r.process{r1} - r.process{r2} + r.process { r1 } + r.process { r2 } end assert !m.success? assert_equal [r1], m.responses @@ -147,12 +147,12 @@ def test_merges_sub_multi_responses r2 = Response.new(true, '2', {}) r3 = Response.new(true, '3', {}) m1 = MultiResponse.run do |r| - r.process{r1} - r.process{r2} + r.process { r1 } + r.process { r2 } end m = MultiResponse.run do |r| - r.process{m1} - r.process{r3} + r.process { m1 } + r.process { r3 } end assert_equal [r1, r2, r3], m.responses end @@ -161,15 +161,105 @@ def test_handles_ignores_optional_request_result m = MultiResponse.new r1 = Response.new(true, '1') - m.process{r1} + m.process { r1 } assert_equal '1', m.message assert_equal [r1], m.responses r2 = Response.new(false, '2') - m.process(:ignore_result){r2} + m.process(:ignore_result) { r2 } assert_equal '1', m.message assert_equal [r1, r2], m.responses assert m.success? end + + def test_handles_responses_with_only_one_with_avs_and_cvv_result + r1 = Response.new(true, '1', {}, { avs_result: AVSResult.new(code: 'Y'), cvv_result: 'M' }) + r2 = Response.new(true, '2', {}) + m = MultiResponse.run do |r| + r.process { r1 } + r.process { r2 } + end + assert_equal [r1, r2], m.responses + assert_equal m.avs_result, { 'code' => 'Y', 'message' => 'Street address and 5-digit postal code match.', 'street_match' => 'Y', 'postal_match' => 'Y' } + assert_equal m.cvv_result, { 'code' => 'M', 'message' => 'CVV matches' } + end + + def test_handles_responses_using_last_response_cvv_and_avs_result + r1 = Response.new(true, '1', {}, { avs_result: AVSResult.new(code: 'Y'), cvv_result: 'M' }) + r2 = Response.new(true, '1', {}, { avs_result: AVSResult.new(code: 'B'), cvv_result: 'N' }) + m = MultiResponse.run do |r| + r.process { r1 } + r.process { r2 } + end + assert_equal [r1, r2], m.responses + assert_equal m.avs_result, { 'code' => 'B', 'message' => 'Street address matches, but postal code not verified.', 'street_match' => 'Y', 'postal_match' => nil } + assert_equal m.cvv_result, { 'code' => 'N', 'message' => 'CVV does not match' } + end + + def test_handles_responses_using_first_response_cvv_and_avs_result + r1 = Response.new(true, '1', {}, { avs_result: AVSResult.new(code: 'Y'), cvv_result: 'M' }) + r2 = Response.new(true, '1', {}, { avs_result: AVSResult.new(code: 'B'), cvv_result: 'N' }) + m = MultiResponse.run(:use_first_response) do |r| + r.process { r1 } + r.process { r2 } + end + assert_equal [r1, r2], m.responses + assert_equal m.avs_result, { 'code' => 'Y', 'message' => 'Street address and 5-digit postal code match.', 'street_match' => 'Y', 'postal_match' => 'Y' } + assert_equal m.cvv_result, { 'code' => 'M', 'message' => 'CVV matches' } + end + + def test_handles_responses_using_first_response_cvv_that_no_has_cvv_and_avs_result + r1 = Response.new(true, '1', {}) + r2 = Response.new(true, '1', {}, { avs_result: AVSResult.new(code: 'B'), cvv_result: 'N' }) + m = MultiResponse.run(:use_first_response) do |r| + r.process { r1 } + r.process { r2 } + end + assert_equal [r1, r2], m.responses + assert_equal m.avs_result, { 'code' => nil, 'message' => nil, 'street_match' => nil, 'postal_match' => nil } + assert_equal m.cvv_result, { 'code' => nil, 'message' => nil } + end + + def test_handles_response_with_avs_and_without_cvv_result + r1 = Response.new(true, '1', {}, { avs_result: AVSResult.new(code: 'X'), cvv_result: CVVResult.new(nil) }) + r2 = Response.new(true, '2', {}) + m = MultiResponse.run do |r| + r.process { r1 } + r.process { r2 } + end + assert_equal [r1, r2], m.responses + assert_equal m.avs_result, { 'code' => 'X', 'message' => 'Street address and 9-digit postal code match.', 'street_match' => 'Y', 'postal_match' => 'Y' } + assert_equal m.cvv_result, { 'code' => nil, 'message' => nil } + end + + def test_handles_response_avs_and_cvv_result_with_wrong_values_avs_and_cvv_code + r1 = Response.new(true, '1', {}, { avs_result: AVSResult.new(code: '1234567'), cvv_result: CVVResult.new('987654') }) + r2 = Response.new(true, '2', {}) + m = MultiResponse.run do |r| + r.process { r1 } + r.process { r2 } + end + assert_equal [r1, r2], m.responses + assert_equal m.avs_result, { 'code' => '1234567', 'message' => nil, 'street_match' => nil, 'postal_match' => nil } + assert_equal m.cvv_result, { 'code' => '987654', 'message' => nil } + end + + def test_handles_response_without_avs_and_cvv_result + r1 = Response.new(true, '1', {}) + r2 = Response.new(true, '2', {}) + m = MultiResponse.run do |r| + r.process { r1 } + r.process { r2 } + end + assert_equal [r1, r2], m.responses + assert_equal m.avs_result, { 'code' => nil, 'message' => nil, 'street_match' => nil, 'postal_match' => nil } + assert_equal m.cvv_result, { 'code' => nil, 'message' => nil } + end + + def test_handles_responses_avs_and_cvv_result_with_no_responses_provideds + m = MultiResponse.new + assert_equal m.avs_result, nil + assert_equal m.cvv_result, nil + end end diff --git a/test/unit/network_connection_retries_test.rb b/test/unit/network_connection_retries_test.rb index 84fbfac2c08..97cb306e233 100644 --- a/test/unit/network_connection_retries_test.rb +++ b/test/unit/network_connection_retries_test.rb @@ -9,7 +9,7 @@ class MyNewError < StandardError def setup @logger = stubs(:logger) @requester = stubs(:requester) - @ok = stub(:code => 200, :message => 'OK', :body => 'success') + @ok = stub(code: 200, message: 'OK', body: 'success') end def test_eoferror_raises_correctly @@ -67,7 +67,6 @@ def test_ssl_errors_raise_correctly end end - def test_invalid_response_error assert_raises(ActiveMerchant::InvalidResponseError) do retry_exceptions do @@ -79,7 +78,7 @@ def test_invalid_response_error def test_unrecoverable_exception_logged_if_logger_provided @logger.expects(:info).once assert_raises(ActiveMerchant::ConnectionError) do - retry_exceptions :logger => @logger do + retry_exceptions logger: @logger do raise EOFError end end @@ -108,7 +107,7 @@ def test_failure_limit_reached_logs_final_error @requester.expects(:post).times(ActiveMerchant::NetworkConnectionRetries::DEFAULT_RETRIES).raises(Errno::ECONNREFUSED) assert_raises(ActiveMerchant::ConnectionError) do - retry_exceptions(:logger => @logger) do + retry_exceptions(logger: @logger) do @requester.post end end @@ -117,7 +116,7 @@ def test_failure_limit_reached_logs_final_error def test_failure_then_success_with_retry_safe_enabled @requester.expects(:post).times(2).raises(EOFError).then.returns(@ok) - retry_exceptions :retry_safe => true do + retry_exceptions retry_safe: true do @requester.post end end @@ -127,18 +126,21 @@ def test_failure_then_success_logs_success @logger.expects(:info).with(regexp_matches(/success/)) @requester.expects(:post).times(2).raises(EOFError).then.returns(@ok) - retry_exceptions(:logger => @logger, :retry_safe => true) do + retry_exceptions(logger: @logger, retry_safe: true) do @requester.post end end def test_mixture_of_failures_with_retry_safe_enabled - @requester.expects(:post).times(3).raises(Errno::ECONNRESET). - raises(Errno::ECONNREFUSED). - raises(EOFError) + @requester. + expects(:post). + times(3). + raises(Errno::ECONNRESET). + raises(Errno::ECONNREFUSED). + raises(EOFError) assert_raises(ActiveMerchant::ConnectionError) do - retry_exceptions :retry_safe => true do + retry_exceptions retry_safe: true do @requester.post end end @@ -159,7 +161,7 @@ def test_failure_with_ssl_certificate_logs_error_if_logger_specified @requester.expects(:post).raises(OpenSSL::X509::CertificateError) assert_raises(ActiveMerchant::ClientCertificateError) do - retry_exceptions :logger => @logger do + retry_exceptions logger: @logger do @requester.post end end @@ -169,7 +171,7 @@ def test_failure_with_additional_exceptions_specified @requester.expects(:post).raises(MyNewError) assert_raises(ActiveMerchant::ConnectionError) do - retry_exceptions :connection_exceptions => {MyNewError => 'my message'} do + retry_exceptions connection_exceptions: { MyNewError => 'my message' } do @requester.post end end diff --git a/test/unit/network_tokenization_credit_card_test.rb b/test/unit/network_tokenization_credit_card_test.rb index a5ac80e822d..a5c881cc0b6 100644 --- a/test/unit/network_tokenization_credit_card_test.rb +++ b/test/unit/network_tokenization_credit_card_test.rb @@ -1,25 +1,36 @@ require 'test_helper' class NetworkTokenizationCreditCardTest < Test::Unit::TestCase - def setup - @tokenized_card = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ - number: '4242424242424242', :brand => 'visa', + @tokenized_card = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new( + number: '4242424242424242', brand: 'visa', month: default_expiration_date.month, year: default_expiration_date.year, - payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', eci: '05' - }) - @tokenized_apple_pay_card = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', eci: '05', + 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 - }) - @tokenized_android_pay_card = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ + ) + @tokenized_android_pay_card = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new( source: :android_pay - }) - @tokenized_google_pay_card = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ + ) + @tokenized_google_pay_card = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new( source: :google_pay - }) - @tokenized_bogus_pay_card = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ + ) + @existing_network_token = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new( + source: :network_token + ) + @tokenized_bogus_pay_card = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new( source: :bogus_pay - }) + ) end def test_type @@ -44,5 +55,6 @@ def test_source assert_equal @tokenized_android_pay_card.source, :android_pay assert_equal @tokenized_google_pay_card.source, :google_pay assert_equal @tokenized_bogus_pay_card.source, :apple_pay + assert_equal @existing_network_token.source, :network_token end end diff --git a/test/unit/post_data_test.rb b/test/unit/post_data_test.rb index dd22426f862..5d6b265b28b 100644 --- a/test/unit/post_data_test.rb +++ b/test/unit/post_data_test.rb @@ -1,7 +1,7 @@ require 'test_helper' class MyPost < ActiveMerchant::PostData - self.required_fields = [ :ccnumber, :ccexp, :firstname, :lastname, :username, :password, :order_id, :key, :time ] + self.required_fields = %i[ccnumber ccexp firstname lastname username password order_id key time] end class PostDataTest < Test::Unit::TestCase @@ -29,7 +29,7 @@ def test_ignore_blank_fields end def test_dont_ignore_required_blank_fields - ActiveMerchant::PostData.required_fields = [ :name ] + ActiveMerchant::PostData.required_fields = [:name] post = ActiveMerchant::PostData.new assert_equal 0, post.keys.size @@ -45,6 +45,6 @@ def test_dont_ignore_required_blank_fields def test_subclass post = MyPost.new - assert_equal [ :ccnumber, :ccexp, :firstname, :lastname, :username, :password, :order_id, :key, :time ], post.required_fields + assert_equal %i[ccnumber ccexp firstname lastname username password order_id key time], post.required_fields end end diff --git a/test/unit/posts_data_test.rb b/test/unit/posts_data_test.rb index 960992cd0d4..81f5197c602 100644 --- a/test/unit/posts_data_test.rb +++ b/test/unit/posts_data_test.rb @@ -1,12 +1,11 @@ require 'test_helper' class PostsDataTests < Test::Unit::TestCase - def setup @url = 'http://example.com' @gateway = SimpleTestGateway.new - @ok = stub(:body => '', :code => '200', :message => 'OK') - @error = stub(:code => 500, :message => 'Internal Server Error', :body => 'failure') + @ok = stub(body: '', code: '200', message: 'OK') + @error = stub(code: 500, message: 'Internal Server Error', body: 'failure') end def teardown @@ -71,9 +70,13 @@ def test_setting_timeouts def test_setting_proxy_settings @gateway.class.proxy_address = 'http://proxy.com' @gateway.class.proxy_port = 1234 + @gateway.class.proxy_user = 'user' + @gateway.class.proxy_password = 'password' ActiveMerchant::Connection.any_instance.expects(:request).returns(@ok) ActiveMerchant::Connection.any_instance.expects(:proxy_address=).with('http://proxy.com') ActiveMerchant::Connection.any_instance.expects(:proxy_port=).with(1234) + ActiveMerchant::Connection.any_instance.expects(:proxy_user=).with('user') + ActiveMerchant::Connection.any_instance.expects(:proxy_password=).with('password') assert_nothing_raised do @gateway.ssl_post(@url, '') diff --git a/test/unit/rails_compatibility_test.rb b/test/unit/rails_compatibility_test.rb index addcb3ec701..ebcbaaf4a72 100644 --- a/test/unit/rails_compatibility_test.rb +++ b/test/unit/rails_compatibility_test.rb @@ -2,7 +2,7 @@ class RailsCompatibilityTest < Test::Unit::TestCase def test_should_be_able_to_access_errors_indifferently - cc = credit_card('4779139500118580', :first_name => '') + cc = credit_card('4779139500118580', first_name: '') silence_deprecation_warnings do assert !cc.valid? diff --git a/test/unit/response_test.rb b/test/unit/response_test.rb index 716d14d7619..a5ed17e5d47 100644 --- a/test/unit/response_test.rb +++ b/test/unit/response_test.rb @@ -2,35 +2,35 @@ class ResponseTest < Test::Unit::TestCase def test_response_success - assert Response.new(true, 'message', :param => 'value').success? - assert !Response.new(false, 'message', :param => 'value').success? + assert Response.new(true, 'message', param: 'value').success? + assert !Response.new(false, 'message', param: 'value').success? end def test_response_without_avs - response = Response.new(true, 'message', :param => 'value') + response = Response.new(true, 'message', param: 'value') assert response.avs_result.has_key?('code') end def test_response_without_cvv - response = Response.new(true, 'message', :param => 'value') + response = Response.new(true, 'message', param: 'value') assert response.cvv_result.has_key?('code') end def test_get_params - response = Response.new(true, 'message', :param => 'value') + response = Response.new(true, 'message', param: 'value') assert_equal ['param'], response.params.keys end def test_avs_result - response = Response.new(true, 'message', {}, :avs_result => { :code => 'A', :street_match => 'Y', :zip_match => 'N' }) + response = Response.new(true, 'message', {}, avs_result: { code: 'A', street_match: 'Y', zip_match: 'N' }) avs_result = response.avs_result assert_equal 'A', avs_result['code'] assert_equal AVSResult.messages['A'], avs_result['message'] end def test_cvv_result - response = Response.new(true, 'message', {}, :cvv_result => 'M') + response = Response.new(true, 'message', {}, cvv_result: 'M') cvv_result = response.cvv_result assert_equal 'M', cvv_result['code'] assert_equal CVVResult.messages['M'], cvv_result['message'] diff --git a/test/unit/three_d_secure_eci_mapper_test.rb b/test/unit/three_d_secure_eci_mapper_test.rb new file mode 100644 index 00000000000..e7890dd3e4d --- /dev/null +++ b/test/unit/three_d_secure_eci_mapper_test.rb @@ -0,0 +1,30 @@ +require 'test_helper' + +class ThreeDSecureEciMapperTest < Test::Unit::TestCase + ThreeDSecureEciMapper = ActiveMerchant::Billing::ThreeDSecureEciMapper + + [ + { eci: '00', brands: %i[master maestro], expected_eci_mapping: ThreeDSecureEciMapper::NON_THREE_D_SECURE_TRANSACTION }, + { eci: '01', brands: %i[master maestro], expected_eci_mapping: ThreeDSecureEciMapper::ATTEMPTED_AUTHENTICATION_TRANSACTION }, + { eci: '02', brands: %i[master maestro], expected_eci_mapping: ThreeDSecureEciMapper::FULLY_AUTHENTICATED_TRANSACTION }, + { eci: '05', brands: %i[visa american_express discover diners_club jcb dankort elo], expected_eci_mapping: ThreeDSecureEciMapper::FULLY_AUTHENTICATED_TRANSACTION }, + { eci: '06', brands: %i[visa american_express discover diners_club jcb dankort elo], expected_eci_mapping: ThreeDSecureEciMapper::ATTEMPTED_AUTHENTICATION_TRANSACTION }, + { eci: '07', brands: %i[visa american_express discover diners_club jcb dankort elo], expected_eci_mapping: ThreeDSecureEciMapper::NON_THREE_D_SECURE_TRANSACTION } + ].each do |test_spec| + test_spec[:brands].each do |brand| + eci = test_spec[:eci] + expected_eci_mapping = test_spec[:expected_eci_mapping] + test "#map for #{brand} and '#{eci}' returns :#{expected_eci_mapping}" do + assert_equal expected_eci_mapping, ThreeDSecureEciMapper.map(brand, eci) + end + end + end + + test "#map for :unknown_brand and '05' returns nil" do + assert_nil ThreeDSecureEciMapper.map(:unknown_brand, '05') + end + + test "#map for :visa and 'unknown_eci' returns nil" do + assert_nil ThreeDSecureEciMapper.map(:visa, 'unknown_eci') + end +end diff --git a/test/unit/transcripts/alelo_purchase b/test/unit/transcripts/alelo_purchase new file mode 100644 index 00000000000..6cd426f43a4 --- /dev/null +++ b/test/unit/transcripts/alelo_purchase @@ -0,0 +1,69 @@ +<- "POST /alelo/sandbox/captura-oauth-provider/oauth/token HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept: application/json\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nHost: sandbox-api.alelo.com.br\r\nContent-Length: 158\r\n\r\n" +<- "grant_type=client_credentials&client_id=ed702c5c-117c-4bc6-989a-055ede547b6d&client_secret=uI3cG0nO5cI0lQ4mY2aF1eN4kV0jA4vC1bJ1rJ0gV2pW7aU4uC&scope=%2Fcapture" +-> "HTTP/1.1 200 OK\r\n" +-> "X-Backside-Transport: FAIL FAIL\r\n" +-> "Connection: close\r\n" +-> "Transfer-Encoding: chunked\r\n" +-> "Content-Type: application/json\r\n" +-> "Cache-Control: private, no-store, no-cache, must-revalidate\r\n" +-> "Pragma: no-cache\r\n" +-> "Content-Security-Policy: default-src 'self'; style-src 'unsafe-inline'\r\n" +-> "Access-Control-Expose-Headers: APIm-Debug-Trans-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-Global-Transaction-ID\r\n" +-> "Access-Control-Allow-Origin: *\r\n" +-> "Access-Control-Allow-Methods: POST\r\n" +-> "Content-Encoding: gzip\r\n" +-> "\r\n" +-> xxxxxx some binary string xxxxx +-> Decoded Response Body: +-> { "token_type":"Bearer", "access_token":"AAIkODFiYjJlYWYtM2Q1OS00OTM5LTk4ZTctOTRiZjllZTQ2MzljYzd8XKy3u3vEEWIl6xiQGwMiCSkebtYZzHEZX8N8h4pXS1RtqPrxc9Vz6KszCIITgA0tiGkfjj1yl4n3Z9F4-zic5Va0EvvbHLCBzYiQJCmE5ezh9d_1I5I4ncDnKZa5", "expires_in":86400, "consented_on":1666785813, "scope":"/capture" }opening connection to api.alelo.com.br:443... +starting SSL for sandbox-api.alelo.com.br:443... +SSL established, protocol: TLSv1.2, cipher: DHE-RSA-AES256-GCM-SHA384 +<- "GET /alelo/sandbox/capture/key?format=json HTTP/1.1\r\nAccept: application/json\r\nX-Ibm-Client-Id: ed702c5c-117c-4bc6-989a-055ede547b6d\r\nX-Ibm-Client-Secret: uI3cG0nO5cI0lQ4mY2aF1eN4kV0jA4vC1bJ1rJ0gV2pW7aU4uC\r\nAuthorization: Bearer AAIkZWQ3MDJjNWMtMTE3Yy00YmM2LTk4OWEtMDU1ZWRlNTQ3YjZkT5y20AlJqCMxbP0m6e7NnLCghHw7E50NsrAopbwLbtZ7zbDeSVOfVdxkIMbT6XnQrOAN65uFJ_ZH9FLh4dIUV9KOzJyBYnf4YND493nATHFhQpepNvo41qu7rykjBkEw\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nHost: sandbox-api.alelo.com.br\r\n\r\n" +-> "HTTP/1.1 200 OK\r\n" +-> "X-Backside-Transport: OK OK\r\n" +-> "Connection: close\r\n" +-> "Transfer-Encoding: chunked\r\n" +-> "Content-Type: application/json\r\n" +-> "Date: Wed, 24 Aug 2022 22:20:34 GMT\r\n" +-> "X-Global-Transaction-ID: 379298106306a42155f1a5f2\r\n" +-> "x-content-type-options: nosniff\r\n" +-> "x-xss-protection: 1; mode=block\r\n" +-> "cache-control: no-cache, no-store, max-age=0, must-revalidate\r\n" +-> "pragma: no-cache\r\n" +-> "expires: 0\r\n" +-> "x-frame-options: DENY\r\n" +-> "Access-Control-Expose-Headers: APIm-Debug-Trans-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-Global-Transaction-ID\r\n" +-> "Access-Control-Allow-Origin: *\r\n" +-> "Access-Control-Allow-Methods: GET\r\n" +-> "Set-Cookie: 20af66d9029740e40c713b05f443f86d=2bdc6d7099faf31789e088a0d77ea5cc; path=/; HttpOnly; Secure; SameSite=None\r\n" +-> "X-RateLimit-Limit: name=rate-limit,400;\r\n" +-> "X-RateLimit-Remaining: name=rate-limit,399;\r\n" +-> "Content-Encoding: gzip\r\n" +-> xxxxxx some binary string xxxxx +-> Decoded Response Body: +-> {"uuid":"81346b5b-7ce0-4e89-b049-5488b7c8a2b6","format":"BASE64","publicKey":"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnWPNWBkwWKDzJmRxxue2fCTrTSMJAoPNn32fIhbw9rNO5GErDOKg5rWzGcIthcH9F2BKIOVZ9YAc0kRVZlsSef2anOxwhFsA6gvj4H4R6lkb8lM9ee9YKw+MrWebKvj78g7o/z7roQkzS6iGcV4/rg4jL819TAz9kHY3Vf2tJi5wW3lPoXawUpvBXGLU1ZPe1RGekFtzBHyFNWniiY7pXF2qRFdeGJ4vcVxfIcYEAV/Pz9vUyLCsscRVBxA24sgYKt8giIp2Ymr2tpsjNwI+bj3jhsej+vdI/CG5klooLW84MSn6LEcCpaG71OB9KN5dDsZ3yKCCLuhmljEX/Wza8QIDAQAB"}opening connection to api.alelo.com.br:443... +<- "POST /alelo/sandbox/capture/transaction HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept: application/json\r\nX-Ibm-Client-Id: ed702c5c-117c-4bc6-989a-055ede547b6d\r\nX-Ibm-Client-Secret: uI3cG0nO5cI0lQ4mY2aF1eN4kV0jA4vC1bJ1rJ0gV2pW7aU4uC\r\nAuthorization: Bearer AAIkZWQ3MDJjNWMtMTE3Yy00YmM2LTk4OWEtMDU1ZWRlNTQ3YjZkkyzyfNOK8fm61YM_NgGTTp09udTkhoeCdjHA7nMfXwCbTaiU3BJ6NwNkLqJ7ogfDG2cOhwnhVOmdCQ4wwLRwChUZJ__RHzxbt-MlhtQbGqUkF0i1ZwlNUrO7ElCXsyW0\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nHost: sandbox-api.alelo.com.br\r\nContent-Length: 913\r\n\r\n" +<- "{\"token\":\"eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.Sy8e0y387LVWcv0P7nwKtu7DHKWww4fCnCx68OnjhmM9sbhmZ9FTQwznGfnSljWFYxXmQPy2PnvtsdvMsEPp5jUANMKTpp4aAGfS_1x31UBuMeRQT5NMlFG8lhCwbPmNtrEOyB2fZUQBsmePpTtdzzAn1tj_hY7XC2bqGkPJ2zS6K2jXqdtkGWeLSMjEDRdyjDuQ_ybFx6uHfYcN_ioXcetUU_MJ1Ai3snfBU-150fCKTmY0SJ09tWMLCyYmvL416L_ha2UmY3tZm1zvpkyRQ612t88ZCEeUK-ZXBYp7FJT-KAAogG49G-Nadj3PSe8jQdxoIZTP46knT3vYp6OmxQ._5Y3c8IjGVJEpkvk4rXwNA.H7LHUpASrZB7zf4Wj5og2l7OBIvgLb2zEdpEC2NVcQPW612oS5jMnUucd58NGDoNoQssxfpjJXse5K-2V7KYEkRlYoVN39gqrIYNesnqPqTmU2VvpQsarjPVO62DgOes3R-qAKkytR3uB3VqNdYmgzdhWf-5tKc8XwLRa41kIsWo6cL7KvKzNUNS_a083X5IUje7Gh4yuH21RzAYVH3diuWDX9QcrdFMZ0BQxE1SpddG8QBcyGgQGvMsfju3Q2kEDrXuJZUSURRiOHdGOpHcYaTjHiGv-Q-NNVNtlwcMhGguMW93_YG8m5gI8VyW8Nq_2epq9YqZwK2YC7XO9CAMxQCaak1ol3ZqR5eo_RO6_ZIUe5NY6NjSWCLqijrAnARbpBUnaXY8CJN4_xRpg3zgqg.gFksDAHH--hQMyWZtXyOfQ\",\"uuid\":\"53141521-afc8-4a08-af0c-f0382aef43c1\"}" +-> "HTTP/1.1 200 OK\r\n" +-> "X-Backside-Transport: OK OK\r\n" +-> "Connection: close\r\n" +-> "Transfer-Encoding: chunked\r\n" +-> "Content-Type: application/json\r\n" +-> "Date: Wed, 24 Aug 2022 22:20:37 GMT\r\n" +-> "X-Global-Transaction-ID: 379298106306a42455f1a6b2\r\n" +-> "x-content-type-options: nosniff\r\n" +-> "x-xss-protection: 1; mode=block\r\n" +-> "cache-control: no-cache, no-store, max-age=0, must-revalidate\r\n" +-> "pragma: no-cache\r\n" +-> "expires: 0\r\n" +-> "x-frame-options: DENY\r\n" +-> "Access-Control-Expose-Headers: APIm-Debug-Trans-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-Global-Transaction-ID\r\n" +-> "Access-Control-Allow-Origin: *\r\n" +-> "Access-Control-Allow-Methods: POST\r\n" +-> "Set-Cookie: 20af66d9029740e40c713b05f443f86d=2bdc6d7099faf31789e088a0d77ea5cc; path=/; HttpOnly; Secure; SameSite=None\r\n" +-> "X-RateLimit-Limit: name=rate-limit,400;\r\n" +-> "X-RateLimit-Remaining: name=rate-limit,399;\r\n" +-> "Content-Encoding: gzip\r\n" +-> "\r\n" +-> xxxxxx some binary string xxxxx +Conn close diff --git a/test/unit/transcripts/alelo_purchase_scrubbed b/test/unit/transcripts/alelo_purchase_scrubbed new file mode 100644 index 00000000000..52290d93893 --- /dev/null +++ b/test/unit/transcripts/alelo_purchase_scrubbed @@ -0,0 +1,69 @@ +<- "POST /alelo/sandbox/captura-oauth-provider/oauth/token HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept: application/json\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nHost: sandbox-api.alelo.com.br\r\nContent-Length: 158\r\n\r\n" +<- "grant_type=client_credentials&client_id=[FILTERED]&client_secret=[FILTERED]&scope=%2Fcapture" +-> "HTTP/1.1 200 OK\r\n" +-> "X-Backside-Transport: FAIL FAIL\r\n" +-> "Connection: close\r\n" +-> "Transfer-Encoding: chunked\r\n" +-> "Content-Type: application/json\r\n" +-> "Cache-Control: private, no-store, no-cache, must-revalidate\r\n" +-> "Pragma: no-cache\r\n" +-> "Content-Security-Policy: default-src 'self'; style-src 'unsafe-inline'\r\n" +-> "Access-Control-Expose-Headers: APIm-Debug-Trans-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-Global-Transaction-ID\r\n" +-> "Access-Control-Allow-Origin: *\r\n" +-> "Access-Control-Allow-Methods: POST\r\n" +-> "Content-Encoding: gzip\r\n" +-> "\r\n" +-> xxxxxx some binary string xxxxx +-> Decoded Response Body: +-> { "token_type":"Bearer", "access_token":"[FILTERED]", "expires_in":86400, "consented_on":1666785813, "scope":"/capture" }opening connection to api.alelo.com.br:443... +starting SSL for sandbox-api.alelo.com.br:443... +SSL established, protocol: TLSv1.2, cipher: DHE-RSA-AES256-GCM-SHA384 +<- "GET /alelo/sandbox/capture/key?format=json HTTP/1.1\r\nAccept: application/json\r\nX-Ibm-Client-Id:[FILTERED]\r\nX-Ibm-Client-Secret:[FILTERED]\r\nAuthorization: Bearer [FILTERED]\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nHost: sandbox-api.alelo.com.br\r\n\r\n" +-> "HTTP/1.1 200 OK\r\n" +-> "X-Backside-Transport: OK OK\r\n" +-> "Connection: close\r\n" +-> "Transfer-Encoding: chunked\r\n" +-> "Content-Type: application/json\r\n" +-> "Date: Wed, 24 Aug 2022 22:20:34 GMT\r\n" +-> "X-Global-Transaction-ID: 379298106306a42155f1a5f2\r\n" +-> "x-content-type-options: nosniff\r\n" +-> "x-xss-protection: 1; mode=block\r\n" +-> "cache-control: no-cache, no-store, max-age=0, must-revalidate\r\n" +-> "pragma: no-cache\r\n" +-> "expires: 0\r\n" +-> "x-frame-options: DENY\r\n" +-> "Access-Control-Expose-Headers: APIm-Debug-Trans-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-Global-Transaction-ID\r\n" +-> "Access-Control-Allow-Origin: *\r\n" +-> "Access-Control-Allow-Methods: GET\r\n" +-> "Set-Cookie: 20af66d9029740e40c713b05f443f86d=2bdc6d7099faf31789e088a0d77ea5cc; path=/; HttpOnly; Secure; SameSite=None\r\n" +-> "X-RateLimit-Limit: name=rate-limit,400;\r\n" +-> "X-RateLimit-Remaining: name=rate-limit,399;\r\n" +-> "Content-Encoding: gzip\r\n" +-> xxxxxx some binary string xxxxx +-> Decoded Response Body: +-> {"uuid":"81346b5b-7ce0-4e89-b049-5488b7c8a2b6","format":"BASE64","publicKey":"[FILTERED]"}opening connection to api.alelo.com.br:443... +<- "POST /alelo/sandbox/capture/transaction HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept: application/json\r\nX-Ibm-Client-Id:[FILTERED]\r\nX-Ibm-Client-Secret:[FILTERED]\r\nAuthorization: Bearer [FILTERED]\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nHost: sandbox-api.alelo.com.br\r\nContent-Length: 913\r\n\r\n" +<- "{\"token\":\"eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.Sy8e0y387LVWcv0P7nwKtu7DHKWww4fCnCx68OnjhmM9sbhmZ9FTQwznGfnSljWFYxXmQPy2PnvtsdvMsEPp5jUANMKTpp4aAGfS_1x31UBuMeRQT5NMlFG8lhCwbPmNtrEOyB2fZUQBsmePpTtdzzAn1tj_hY7XC2bqGkPJ2zS6K2jXqdtkGWeLSMjEDRdyjDuQ_ybFx6uHfYcN_ioXcetUU_MJ1Ai3snfBU-150fCKTmY0SJ09tWMLCyYmvL416L_ha2UmY3tZm1zvpkyRQ612t88ZCEeUK-ZXBYp7FJT-KAAogG49G-Nadj3PSe8jQdxoIZTP46knT3vYp6OmxQ._5Y3c8IjGVJEpkvk4rXwNA.H7LHUpASrZB7zf4Wj5og2l7OBIvgLb2zEdpEC2NVcQPW612oS5jMnUucd58NGDoNoQssxfpjJXse5K-2V7KYEkRlYoVN39gqrIYNesnqPqTmU2VvpQsarjPVO62DgOes3R-qAKkytR3uB3VqNdYmgzdhWf-5tKc8XwLRa41kIsWo6cL7KvKzNUNS_a083X5IUje7Gh4yuH21RzAYVH3diuWDX9QcrdFMZ0BQxE1SpddG8QBcyGgQGvMsfju3Q2kEDrXuJZUSURRiOHdGOpHcYaTjHiGv-Q-NNVNtlwcMhGguMW93_YG8m5gI8VyW8Nq_2epq9YqZwK2YC7XO9CAMxQCaak1ol3ZqR5eo_RO6_ZIUe5NY6NjSWCLqijrAnARbpBUnaXY8CJN4_xRpg3zgqg.gFksDAHH--hQMyWZtXyOfQ\",\"uuid\":\"53141521-afc8-4a08-af0c-f0382aef43c1\"}" +-> "HTTP/1.1 200 OK\r\n" +-> "X-Backside-Transport: OK OK\r\n" +-> "Connection: close\r\n" +-> "Transfer-Encoding: chunked\r\n" +-> "Content-Type: application/json\r\n" +-> "Date: Wed, 24 Aug 2022 22:20:37 GMT\r\n" +-> "X-Global-Transaction-ID: 379298106306a42455f1a6b2\r\n" +-> "x-content-type-options: nosniff\r\n" +-> "x-xss-protection: 1; mode=block\r\n" +-> "cache-control: no-cache, no-store, max-age=0, must-revalidate\r\n" +-> "pragma: no-cache\r\n" +-> "expires: 0\r\n" +-> "x-frame-options: DENY\r\n" +-> "Access-Control-Expose-Headers: APIm-Debug-Trans-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-Global-Transaction-ID\r\n" +-> "Access-Control-Allow-Origin: *\r\n" +-> "Access-Control-Allow-Methods: POST\r\n" +-> "Set-Cookie: 20af66d9029740e40c713b05f443f86d=2bdc6d7099faf31789e088a0d77ea5cc; path=/; HttpOnly; Secure; SameSite=None\r\n" +-> "X-RateLimit-Limit: name=rate-limit,400;\r\n" +-> "X-RateLimit-Remaining: name=rate-limit,399;\r\n" +-> "Content-Encoding: gzip\r\n" +-> "\r\n" +-> xxxxxx some binary string xxxxx +Conn close diff --git a/test/unit/versionable_test.rb b/test/unit/versionable_test.rb new file mode 100644 index 00000000000..798645e423d --- /dev/null +++ b/test/unit/versionable_test.rb @@ -0,0 +1,89 @@ +require 'test_helper' + +class VersionableTest < Test::Unit::TestCase + class ParentClass + include ActiveMerchant::Versionable + end + + class DummyClass < ParentClass + end + + class FakeClass < ParentClass + end + + class DummyChildClass < DummyClass + end + + def setup + @dummy_instance = DummyClass.new + @fake_instance = FakeClass.new + @dummy_child_instance = DummyChildClass.new + end + + def test_class_can_set_and_fetch_default_version + DummyClass.version('1.0') + assert_equal '1.0', DummyClass.fetch_version, 'Class should return the correct version' + end + + def test_class_can_set_and_fetch_custom_feature_version + DummyClass.version('2.0', :custom_api) + DummyClass.version('v2', :some_feature) + assert_equal '2.0', DummyClass.fetch_version(:custom_api), 'Class should return the correct version' + assert_equal 'v2', DummyClass.fetch_version(:some_feature), 'Class should return the correct version' + end + + def test_instance_can_fetch_default_version + DummyClass.version('v3') + assert_equal 'v3', @dummy_instance.fetch_version, 'Instance should return the correct version' + end + + def test_instance_can_fetch_custom_feature_version + DummyClass.version('v4', :custom_api) + DummyClass.version('4.0', :some_feature) + assert_equal 'v4', @dummy_instance.fetch_version(:custom_api), 'Instance should return the correct version' + assert_equal '4.0', @dummy_instance.fetch_version(:some_feature), 'Instance should return the correct version' + end + + def test_fetch_version_returns_nil_for_unset_feature + assert_nil DummyClass.fetch_version(:nonexistent_feature), 'Class should return nil for an unset feature' + assert_nil @dummy_instance.fetch_version(:nonexistent_feature), 'Instance should return nil for an unset feature' + end + + def test_classes_dont_share_versions + # Default key + DummyClass.version('1.0') + FakeClass.version('2.0') + DummyChildClass.version('3.0') + + # Custom key :some_feature + DummyChildClass.version('v5', :some_feature) + FakeClass.version('v3', :some_feature) + DummyClass.version('v4', :some_feature) + + assert_equal '1.0', DummyClass.fetch_version, 'Class should return the correct version' + assert_equal 'v4', DummyClass.fetch_version(:some_feature), 'Class should return the correct version' + assert_equal '2.0', FakeClass.fetch_version, 'Class should return the correct version' + assert_equal 'v3', FakeClass.fetch_version(:some_feature), 'Class should return the correct version' + assert_equal '3.0', DummyChildClass.fetch_version, 'Class should return the correct version' + assert_equal 'v5', DummyChildClass.fetch_version(:some_feature), 'Class should return the correct version' + end + + def test_instances_dont_share_versions + # Default key + DummyClass.version('1.0') + FakeClass.version('2.0') + DummyChildClass.version('3.0') + + # Custom key :some_feature + DummyChildClass.version('v5', :some_feature) + FakeClass.version('v3', :some_feature) + DummyClass.version('v4', :some_feature) + + assert_equal '1.0', @dummy_instance.fetch_version, 'Instance should return the correct version' + assert_equal 'v4', @dummy_instance.fetch_version(:some_feature), 'Instance should return the correct version' + assert_equal '2.0', @fake_instance.fetch_version, 'Instance should return the correct version' + assert_equal 'v3', @fake_instance.fetch_version(:some_feature), 'Instance should return the correct version' + assert_equal '3.0', @dummy_child_instance.fetch_version, 'Instance should return the correct version' + assert_equal 'v5', @dummy_child_instance.fetch_version(:some_feature), 'Instance should return the correct version' + end +end